Environnement u-boot
L'environnement u-boot, dit u-env, contient une liste de variable permettant de contrôler le processus de démarrage vers le kernel.
Au format key = value
, ces variables peuvent contenir de simple valeur comme définir une suite d'instruction à exécuter.
Par exemple, prenons ce petit environnement text-based :
variable=toto
print_variable=if [ -n ${variable} ] ; then echo ${variable} ; fi
run print_variable
affichera "toto". On dit alors que print_variable
est une commande.
Certaines variables sont utilisées par u-boot,
par exemple bootcmd
est la variable (commande) exécutée automatiquement au démarrage.
Avant d'aborder la sécurisation de l'u-env, voyons dans un premier temps comment celui-ci est stocké et chargé en mémoire.
Emplacement de l'u-env
L'u-env ne prend pas beaucoup d'espace mémoire, en général quelques kilo-octet suffisent, permettant ainsi à l'environnement d'être stocké là où il y a de la place pour peu qu'u-boot le supporte.
L'ensemble des stockages supportés, dit des locations, sont déclarés dans le dossier env d'u-boot.
On retrouve dans le fichier Kconfig les variables ENV_IS_IN_*
des locations supportées ainsi que leur driver .c associé.
Par exemple, le fichier nand.c déclare l'env driver de la location NAND :
#include <env_internal.h>
U_BOOT_ENV_LOCATION(nand) = {
.location = ENVL_NAND,
ENV_NAME("NAND")
.load = env_nand_load,
#if defined(CMD_SAVEENV)
.save = env_save_ptr(env_nand_save),
#endif
.init = env_nand_init,
};
L'header include/env_internal.h énumère les locations supportées par u-boot et leur structure :
enum env_location {
ENVL_UNKNOWN,
ENVL_EEPROM,
ENVL_EXT4,
ENVL_NAND,
[...]
ENVL_NOWHERE,
};
struct env_driver {
const char *name;
enum env_location location;
int (*load)(void);
int (*save)(void);
int (*erase)(void);
int (*init)(void);
}
/* Declare a new environment location driver */
#define U_BOOT_ENV_LOCATION(__name) \
ll_entry_declare(struct env_driver, __name, env_driver)
À la compilation, les locations supportées par défaut sont énumérées par ordre de priorité dans le tableau env_locations
présent le fichier env.c :
static enum env_location env_locations[] = {
#ifdef CONFIG_ENV_IS_IN_EEPROM
ENVL_EEPROM,
#endif
#ifdef CONFIG_ENV_IS_IN_EXT4
ENVL_EXT4,
#endif
#ifdef CONFIG_ENV_IS_IN_NAND
ENVL_NAND,
#endif
[...]
#ifdef CONFIG_ENV_IS_NOWHERE
ENVL_NOWHERE,
#endif
};
Nous verrons par la suite comment u-boot charge l'u-env à partir de ces drivers.
Environnement par défaut
U-boot posséde un u-env par défaut intégré au binaire. Il existe deux façons de définir cet environnement par défaut :
Old-style C environment : la macro
CFG_EXTRA_ENV_SETTINGS
déclare l'environnement, chaque ligne doit se terminer par "\0". Chaque board déclare généralement sa config dans un header dans le dossier include/configs.Text-based environment : utilise un fichier .env comme environnement par défaut si
CONFIG_USE_DEFAULT_ENV_FILE
est activé, le nom du fichier peut être défini avecCONFIG_ENV_SOURCE_FILE
.
Dans les deux cas, l'environnement par défaut se retrouve déclaré dans la variable default_environment
dans le header env_default.h :
const char default_environment[] = {
#ifndef CONFIG_USE_DEFAULT_ENV_FILE
#ifdef CONFIG_USE_BOOTARGS
"bootargs=" CONFIG_BOOTARGS "\0"
#endif
[...]
#ifdef CFG_EXTRA_ENV_SETTINGS
CFG_EXTRA_ENV_SETTINGS
#endif
#else /* CONFIG_USE_DEFAULT_ENV_FILE */
#include "generated/defaultenv_autogenerated.h"
#endif
};
Cet environnement par défaut peut-être chargé par la location ENVL_NOWHERE
.
Exemple de configuration
Pour illustrer les notions présentées prenons le cas d'une board avec une eMMC.
L'eMMC possède 4 partitions hardware : deux partitions boot, une partition RPMB et une partition user.
Nous souhaitons placer notre u-env dans la partition user, juste après la table de partition GPT.
Pour être large nous choisissons la position 0x5000
(20480) soit le secteur 40 (512 bytes * 40).
Notre environnement fera 8Kb (0x2000
) et nous positionnons une copie à la suite.
À partir de ces informations, voici la configuration u-boot :
CONFIG_ENV_IS_IN_MMC=y /* Supporte la location MMC */
CONFIG_SYS_MMC_ENV_DEV=1 /* Numéro du device MMC (équivalent au /dev/mmcblk1 sous Linux) */
CONFIG_SYS_MMC_ENV_PART=0 /* Partition HARDWARE de l'eMMC : 0 = partition user, 1 = première partition boot, 2 = deuxième partition boot */
CONFIG_ENV_SIZE=0x2000 /* Taille de l'u-env */
CONFIG_ENV_OFFSET=0x5000 /* Emplacement dans la partition hardware*/
CONFIG_SYS_REDUNDAND_ENVIRONMENT=y /* Utilisation d'une copie */
CONFIG_ENV_OFFSET_REDUND=0x7000 /* Emplacement de la copie, en général à la suite (OFFSET + SIZE) */
Chargement de l'u-env
Au démarrage, u-boot cherche à charger son environnement.
Le point d'entrée est la fonction env_load
présent dans le fichier env.c :
int env_load(void)
{
[...]
for (prio = 0; (drv = env_driver_lookup(ENVOP_LOAD, prio)); prio++) {
printf("Loading Environment from %s... ", drv->name);
ret = drv->load();
if (!ret) {
printf("OK\n");
return 0;
}
[...]
}
[...]
return -ENODEV;
}
Cette fonction passe en revue les drivers par ordre de priorité croissant et s'arrête dès qu'un environnement
est chargé.
La fonction env_driver_lookup
fait appel à la fonction weak env_get_location
qui retourne la location du tableau
env_locations
présent à l'index prio
:
__weak enum env_location env_get_location(enum env_operation op, int prio)
{
return arch_env_get_location(op, prio);
}
__weak enum env_location arch_env_get_location(enum env_operation op, int prio)
{
if (prio >= ARRAY_SIZE(env_locations))
return ENVL_UNKNOWN;
return env_locations[prio];
}
Un contructeur peut définir sa propre gestion des locations. Prenons le cas de STM, la fonction env_get_location
est redéfinie
dans le fichier stm32mp1.c et remplace la fonction weak :
enum env_location env_get_location(enum env_operation op, int prio)
{
u32 bootmode = get_bootmode();
if (prio)
return ENVL_UNKNOWN;
switch (bootmode & TAMP_BOOT_DEVICE_MASK) {
case BOOT_FLASH_SD:
case BOOT_FLASH_EMMC:
if (CONFIG_IS_ENABLED(ENV_IS_IN_MMC))
return ENVL_MMC;
else
return ENVL_NOWHERE;
case BOOT_FLASH_NAND:
case BOOT_FLASH_SPINAND:
if (IS_ENABLED(CONFIG_ENV_IS_IN_UBI))
return ENVL_UBI;
else
return ENVL_NOWHERE;
case BOOT_FLASH_NOR:
if (CONFIG_IS_ENABLED(ENV_IS_IN_SPI_FLASH))
return ENVL_SPI_FLASH;
else
return ENVL_NOWHERE;
default:
return ENVL_NOWHERE;
}
}
Ici, aucun tableau de location n'est utilisé. La location dépend du device surlequel la board démarre, si la location n'est pas supportée alors l'environnement par défaut (ENVL_NOWHERE) est utilisé.
Sécuriser l'u-env
Il est facile de constater que l'u-env est modifiable si celui-ci est présent sur un stockage persistant.
Si nous reprenons notre exemple d'u-env dans l'eMMC, un binaire tel que fw_setenv
du paquet fw-utils, permet de modifier l'u-env depuis l'userspace Linux.
Lorsqu'il s'agit de sécuriser notre système cela représente une faille de sécurité non négligeable.
L'article Securing U-boot, qui présente entre autres des vecteurs d'attaque, aborbe le sujet de l'u-env. Par le non support d'u-env signé par u-boot, la solution proposée est de n'utiliser que l'u-env par défaut, la modification à chaud depuis la RAM peut alors être bloquée en désactivant le shell u-boot et en retirant le JTAG sur la board.
Cependant, comment faire lorsqu'il est nécessaire d'avoir des variables persistantes ? Notamment pour les systèmes de mise à jour tel que SWUpdate ou RAUC, de plus en plus utilisés dans les systèmes embarqués. C'est ce que nous allons voir en combinant plusieurs fonctionnalités d'u-boot.
Writeable list
U-boot offre une fonctionnalité intéressante : la writeable list. Cette fonctionnalité repose sur les flags des variables u-boot et permet de définir une liste de variable accessible en écriture dans l'u-env.
Flags
Les flags permettent de spécifier le type de donnée des variables et leur accès, dont :
- s : string
- d : decimal
- x : hexadecimal
- b : boolean
- r : read-only
- o : write-once
- w : writeable
La syntaxe "variable:flag" attribue le ou les flag à la variable, par exemple "foo:s" n'autorise que les chaînes de caractère pour la variable foo
.
Les structures sont déclarées dans le header env_flags.h.
Il existe deux méthodes pour définir les flags :
- Soit en définissant une variable
.flags
dans l'u-env, à chaud avecsetenv .flags bar:sr
ou bien à la compilation avecCONFIG_ENV_FLAGS_LIST_DEFAULT
. - Soit en définissant
CFG_ENV_FLAGS_LIST_STATIC
, intégré à la macroENV_FLAGS_LIST_STATIC
.
C'est la fonction env_flags_lookup
présente dans le fichier flags.c
qui retourne la liste des flags pour une variable donnée :
/*
* Look for flags in a provided list and failing that the static list
*/
static inline int env_flags_lookup(const char *flags_list, const char *name,
char *flags)
{
int ret = 1;
if (!flags)
/* bad parameter */
return -1;
/* try the env first */
if (flags_list)
ret = env_attr_lookup(flags_list, name, flags);
if (ret != 0)
/* if not found in the env, look in the static list */
ret = env_attr_lookup(ENV_FLAGS_LIST_STATIC, name, flags);
return ret;
}
La fonction regarde en premier lieu dans la variable flags_list
puis dans notre liste compilée ENV_FLAGS_LIST_STATIC
.
CONFIG_ENV_WRITEABLE_LIST
L'activation de la writeable list avec CONFIG_ENV_WRITEABLE_LIST
implique des modifications à plusieurs endroits.
config ENV_WRITEABLE_LIST
bool "Permit write access only to listed variables"
select ENV_APPEND
help
If defined, only environment variables which explicitly set the 'w'
writeable flag can be written and modified at runtime. No variables
can be otherwise created, written or imported into the environment.
Flags par défaut
Les flags viennent de ENV_FLAGS_LIST_STATIC
et non plus de la variable .flags
comme nous le montre la fonction
env_flags_init
du fichier flags.c :
void env_flags_init(struct env_entry *var_entry)
{
[...]
if (first_call) {
#ifdef CONFIG_ENV_WRITEABLE_LIST
flags_list = ENV_FLAGS_LIST_STATIC;
#else
flags_list = env_get(ENV_FLAGS_VAR);
#endif
first_call = 0;
}
/* look in the ".flags" and static for a reference to this variable */
ret = env_flags_lookup(flags_list, var_name, flags);
[...]
}
Environnement par défaut
La fonction env_load
présentée précédemment charge en premier lieu l'environnement par défaut :
int env_load(void)
{
[...]
if (CONFIG_IS_ENABLED(ENV_WRITEABLE_LIST)) {
/*
* When using a list of writeable variables, the baseline comes
* from the built-in default env. So load this first.
*/
env_set_default(NULL, 0);
}
for (prio = 0; (drv = env_driver_lookup(ENVOP_LOAD, prio)); prio++) {
[...]
}
}
CONFIG_ENV_APPEND
L'option CONFIG_ENV_APPEND
est activée par dépendance dans le Kconfig.
Cette option permet de superposer plusieurs environnements dans le hashtable de l'u-env :
l'environnement par défaut et l'environnement de la location.
Comportement de l'u-env
Les changements apportées par l'activation de la writeable list contraint de définir
dans la macro CONFIG_ENV_FLAGS_LIST_STATIC
les variables, et leur flag, pouvant être
écrit dans l'environnement persistant.
En réalité, la vérification est aussi réalisée à la lecture de l'environnement.
En effet, bien qu'à partir des flags u-boot est capable d'interdire l'écriture d'une valeur qui n'est pas conforme,
cela n'empêche pas un tiers malveillant d'écriture par un autre moyen dans l'environnement (fw_setenv par exemple).
Ainsi, la writeable list agit aussi comme un filtre lors de la lecture de l'environnement et refuse les variables
qui non conforme à leur flag. Par exemple, une variable toto
définie dans l'environnement par défaut avec le flag d
ne pourra pas être écrasée par une variable toto="string"
venant de l'eMMC, u-boot l'ignora tout simplement.
Exemple avec SWUpdate
Poursuivons avec notre exemple précédent. Nous décidons d'utiliser SWUpdate pour mettre à jour à distance notre distribution Linux
avec la méthode A/B.
On utilise la fonctionnalité de bootcount d'u-boot
avec CONFIG_BOOTCOUNT_LIMIT
et CONFIG_BOOTCOUNT_ENV
:
menuconfig BOOTCOUNT_LIMIT
bool "Enable support for checking boot count limit"
help
Enable checking for exceeding the boot count limit.
More information: https://docs.u-boot.org/en/latest/api/bootcount.html
[...]
config BOOTCOUNT_ENV
bool "Boot counter in environment"
help
If no softreset save registers are found on the hardware
"bootcount" is stored in the environment. To prevent a
saveenv on all reboots, the environment variable
"upgrade_available" is used. If "upgrade_available" is
0, "bootcount" is always 0. If "upgrade_available" is 1,
"bootcount" is incremented in the environment.
So the Userspace Application must set the "upgrade_available"
and "bootcount" variables to 0, if the system booted successfully.
Cela nécessite d'écrire deux variables : upgrade_available
et bootcount
, ce à quoi on ajoute une variable slot
pour
définir quel rootfs démarrer.
Voici le code à ajouter dans la config de notre board :
#define CFG_ENV_FLAGS_LIST_STATIC
"slot:dw," \
"upgrade_available:dw," \
"bootcount:dw"
#define SWUPDATE_ENV_SETTINGS \
"slot=0\0" \
"upgrade_available=0\0" \
"bootcount=0\0" \
"altbootcmd=" \
"echo Rollback to previous rootfs; " \
"if test \"x${slot}\" = \"x0\"; then " \
"setenv slot 1; " \
"else " \
"setenv slot 0; " \
"fi; setenv bootcount 0; saveenv; saveenv; run bootcmd\0"
#define CFG_EXTRA_ENV_SETTINGS \
SWUPDATE_ENV_SETTINGS
Conclusion
L'usage de la writeable list avec la combinaison d'un environnement par défaut et persistant permet de sécuriser l'u-env. Vous noterez cependant qu'il est nécessaire de désactiver l'accès au shell et ainsi que le JTAG pour bloquer toutes modifications à chaud.