Système de fichiers Squashfs
Le Squashfs est un système de fichiers compressé en lecture seule qui se présente sous le forme d'un fichier. La somme de contrôle du fichier Squashfs permet a elle seule la vérification de l'intégrité de l'ensemble des fichiers du système de fichiers. De plus, son accès exclusif en lecture assure la non modification des fichiers.
L'outil mksquashfs permet de créer un Squashfs à partir d'un dossier source :
rootfs.squashfs: Squashfs filesystem, little endian, version 1024.0, compressed [...]
Le fichier Squashfs rootfs.squashfs
contient un système de fichiers. Étant un
lui-même un fichier, il est contenu dans un système de fichiers.
Montage d'un Squashfs en loop device
L'accès au système de fichiers contenu dans un Squashfs nécessite un étape intermédiare.
Pour rappel, un système de fichiers est l'organisation des fichiers au sein d'un volume physique ou logique.
Un volume physique, tel qu'un périphérique de stockage, peut se diviser en
partition pour
faire cohabiter plusieurs systèmes de fichiers.
Sous Linux les périphériques et leurs partitions sont accessibles par bloc de mémoire via des block devices présent dans /dev
.
Pour accéder aux fichiers présents dans un système de fichiers il nécessaire de monter le block device associé à sa partition dans un répertoire appelé le point de montage.
À partir de ce point de montage le kernel Linux est capable de réaliser des
lectures / écritures sur les fichiers.
Le Squashfs étant un fichier, le montage de son système de fichiers doit alors
passer par un élément intermédiaire : le loop device.
Un loop device est un pseudo-périphérique qui permet de rendre accessible un fichier
comme un block device.
En d'autres termes, cela permet de manipuler le fichier comme un périphérique physique.
Les loop devices sont représentés par les pseudo-périphérique /dev/loop*
sous Linux.
Le binaire losetup permet l'association
de fichier à un loop device. Pour associer un loop device à notre fichier rootfs.squashfs
il faut d'abord connaître un pseudo-périphérique non utilisé :
/dev/loop0
loop.ko
soit inséré.
Ce dernier est souvent compilé avec l'image kernel Linux, la commande suivante
permet de le vérifier :
kernel/drivers/block/loop.ko
modprobe loop
.
Vous pouvez alors observer avec lsblk
la présence du loop device :
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop0 7:15 0 4K 0 loop
...
Le fichier étant maintenant accessible par bloc de mémoire, nous pouvons le monter
dans un dossier via la commande mount
:
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop0 7:15 0 4K 0 loop ~/boot-squashfs/mnt
...
Bonjour !
Pour dissocier le loop device au fichier Squashfs il faut utiliser l'option -d
ou --detach
:
La dissociation ne démonte cependant pas le dossier, il faut le faire manuellement (ou d'abord démonter puis dissocier) :
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop0 7:15 0 4K 0 loop ~/boot-squashfs/mnt
...
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
...
L'association d'un loop device pour un Squashfs peut être réalisé directement par
la commande mount
avec les options suivantes :
-t
, peut être
détecté par mount
impliquant automatiquement l'option -o loop
pour les Squashfs.
Ainsi, la commande mount rootfs.squashfs mnt
est aussi valable.
Création d'un rootfs
Un Squashfs peut contenir n'importe quel type de fichier. Nous souhaitons
dans cet article l'utiliser commme le système de fichiers racine, dit rootfs,
de notre kernel Linux.
Il est ainsi nécessaire que le rootfs contiennent un certains nombre de
fichier essentiel tel que le processus init.
Par simplicité nous allons construire un rootfs basé sur la distribution Debian
pour l'architecture x86 64-bits à partir de l'outil debootstrap
:
include
permet l'installation de paquet Debian durant la génération
du rootfs. Par praticité installons le système d'initialisation (i.e. le processus init)
systemd sur notre rootfs.
Nous avons à présent un rootfs minimal que nous utiliserons pour notre Squashfs. Vous pouvez configurer celui-ci à votre convenance. Par exemple, nous pouvons définir son hostname :
Nous allons aussi ajouter un utilisateur admin
avec les droits root (uid 0 et gid 0)
sans mot de passe en exécutant
la commande useradd
et passwd
via chroot
:
Démarrage via un initramfs
Durant sa phase d'initialisation, le kernel monte le premier système de fichiers
qu'il trouve comme son rootfs et tente d'exécuter son processus init.
En cas d'échec du montage, le kernel passe en "kernel panic : Unable to mount root fs" et s'arrête.
Le rootfs à monter peut être spécifié via le paramètre kernel
root=<value>
.
La valeur <value>
permet d'identifier le périphérique où se trouve le système de fichiers,
tel que root=PARTUUID=<uuid>
, root=PARTLABEL=<label>
ou directement le block device associé
avec root=/dev/sda1
par exemple. Cette analyse du paramètre root=
s'observe dans
le code source du kernel dans le fichier init/do_mounts.c
avec les fonctions root_dev_setup
et name_to_dev_t
.
Il convient alors de constater qu'il n'est pas possible d'indiquer
au kernel de monter notre fichier Squashfs comme le rootfs avec le paramètre root
.
En effet, notre Squashfs étant un fichier il est stocké dans un
système de fichiers dit parent, il n'est donc pas accessible par le kernel au démarrage.
Nous pouvons alors décider de monter dans un premier temps le système de fichiers parent
via root=
, puis une fois le kernel démarré exécuter le montage de notre Squashfs.
Cependant, dans ce cas de figure c'est le système de fichiers parent qui sera
le rootfs et non celui contenu dans notre Squashfs.
Ainsi, pour monter notre Squashfs comme le rootfs il est nécessaire
de réaliser des opérations en amont par l'intermédiare d'un initramfs.
Un initramfs est un système de fichiers temporaire chargé au démarrage en mémoire vive par
le kernel Linux pour préparer le montage du rootfs et y basculer son exécution en
exécutant son processus init.
/dev/ram0
puis monté tel le rootfs temporaire.
L'initramfs est une archive cpio
décompressée par le kernel dans un système de fichiers temporaire tmpfs en mémoire vive.
Le terme "initrd" reste souvent utilisé pour faire référence à l'usage d'un rootfs
d'initialisation en mémoire vive bien qu'il peut s'agir en réalité d'une archive cpio.
Outil initramfs-tools
L'utilisation d'un initramfs étant trés courant dans la plus part des systèmes, des outils permettent d'en générer facilement. Nous utiliserons l'outil initramfs-tools notamment utilisé sous les distributions Debian. Initramfs-tools permet de générer un initramfs pour le rootfs et son kernel Linux sur lequel il s'exécute. En fonction des fichiers présents dans le rootfs il détermine les opérations que doit réaliser l'initramfs pour préparer le rootfs avant d'y basculer l'exécution du kernel. Par exemple, si l'utilisateur configure un outil de chiffrement de disque sur son rootfs il sera nécessaire que l'initramfs effectue le déchiffrement pour rendre accessible le rootfs au kernel. En regénérant un initramfs, initramfs-tools prendra en compte les opérations de déchiffrement à réaliser.
Nous pouvons installer le paquet initramfs-tools via APT dans notre rootfs Debian :
Plutôt que d'analyser l'intégralité du rootfs à la recherche d'élément permettant
de déterminer les opérations, initramfs-tools possède deux répertoires
de configuration : /etc/initramfs-tools
et /usr/share/initramfs-tools
.
Par convention, la configuration présente dans /etc/
est statique, celle dans
/usr/share
peut être ajoutée et modifiée.
Ainsi, les outils nécessitant des opérations durant l'exécution de l'initramfs vont déposer
des fichiers dans le dossier /usr/share/initramfs-tools
et imposer
la regénération de l'initramfs via initramfs-tools.
Le dossier /usr/share/initramfs-tools
se décompose en :
- Un script shell
init
qui sera le processus init de l'initramfs. - Un dossier
conf.d
pour inclure des variables shell au processus init par fichier individuel. - Un dossier
modules.d
pour ajouter et insérer au démarrage des modules kernel par fichier individuel. - Un dossier
hooks
pour ajouter des hooks scripts shell exécutés durant la génération de l'initramfs. - Un dossier
scripts
pour ajouter des boots scripts shell exécutés par le processus init de l'initramfs.
Le processus init /usr/share/initramfs-tools/init
analyse les arguments de
ligne de commande kernel pour définir des variables shell puis exécute de manière
ordonné des boots scripts avant et après
le montage du rootfs dans /root
. Enfin, le processus bascule l'exécution du kernel en
exécutant le processus init se trouvant dans /root
.
Le périphérique du rootfs à monter est déterminé via l'argument root=
de la
ligne de commande.
Vous pouvez l'observer dans la fonction local_mount_root
du fichier /usr/share/initramfs-tools/scripts/local
qui se charge de monter le rootfs dans le dossier /root
:
local_mount_root()
{
# ...
# Mount root
if ! mount ${roflag} ${FSTYPE:+-t "${FSTYPE}"} ${ROOTFLAGS} "${ROOT}" "${rootmnt?}"; then
panic "Failed to mount ${ROOT} as root file system."
fi
}
La variable $ROOT
correspond à la valeur de l'argument root=
et $rootmnt
à la valeur /root
.
Configuration
Le démarrage sur un Squashfs nécessite l'ajout d'opérations dans l'initramfs,
à savoir le montage du système de fichiers parent du Squashfs puis
son association au loop device défini par l'argument root=
de la ligne de commande.
Ces opérations peuvent être réalisées en ajoutant un boot script dans le
répertoire de configuration /usr/share/initramfs-tools/scripts
d'initramfs-tools.
Vous remarquerez que le dossier est décomposé en plusieurs sous-dossiers, chacun d'eux
correspond à une étape dans le fil d'exécution du processus init avant et après
le montage du rootfs.
Boot scripts
À partir des indications de la documentation
de Debian nous allons ajouter notre boot script dans le dossier local-premount
pour
s'exécuter juste avant le montage du rootfs.
Notre boot script doit commencer par une entête permettant à initramfs-tools de gérer un ordre d'exécution entre les boots scripts :
#!/bin/sh
PREREQ=""
prereqs()
{
echo "$PREREQ"
}
case $1 in
prereqs)
prereqs
exit 0
;;
esac
# Fonctions initramfs-tools
. /scripts/functions
# Variable $ROOT définie par le processus init, correspondant à l'argument
# root= de la ligne de commande kernel
log_begin_msg "Attach Squashfs to ${ROOT}"
La première opération consiste à monter la partition du système de fichiers parent.
Il faut alors déterminer son block device associé.
L'attribution des block devices pouvant varier en fonction du partitionnement
et du peuplement du dossier /dev/
par le kernel, il est préférable de se baser
sur les nommages persistants
disponibles dans /dev/disk
.
Les fichiers étant des liens symboliques, la commande realpath
permet de récupérer
le block device associé.
Nous choisirons d'identifier la partition par son label via le dossier dev/disk/by-partlabel
.
Pour rendre le script générique, nous attendrons un argument squashfs.partlabel=
dans la ligne de commande kernel ― accessible via le pseudo-fichier /proc/cmdline
―
pour spécifier le label de la partition contenant le fichier Squashfs :
#!/bin/sh
# ...
# Récupère l'argument squashfs.partlabel pour idenfitier la partition
partlabel=$(cat /proc/cmdline | sed -nr -e 's/.*squashfs\.partlabel=([^= ]+).*/\1/p')
if [ -z "$partlabel" ] ; then
panic "Squashfs partlabel not specified. Boot arguments must include a squashfs.partlabel= parameter"
fi
# Récupère le device associé à la partition
dev="$(realpath "/dev/disk/by-partlabel/${partlabel}")"
if [ $? -ne 0 ] ; then
panic "Partlabel '${partlabel}' not found"
fi
# Montage de la partition
mkdir /mnt
mount $dev /mnt -t ext4
La seconde opération est d'associer le fichier Squashfs se trouvant dans /mnt
au loop device défini par l'argument root=
accessible par la variable $ROOT
.
Dans le même principe que l'identification de la partition à monter, nous
pouvons attendre un argument squashfs.file=
pour déterminer le chemin d'accès
du fichier Squashfs dans le dossier /mnt
.
#!/bin/sh
# ...
# Récupère l'argument squashfs.file pour le nom du fichier Squashfs
file=$(cat /proc/cmdline | sed -nr -e 's/.*squashfs\.file=([^= ]+).*/\1/p')
if [ -z "$file" ] ; then
panic "Squashfs file not specified. Boot arguments must include a squashfs.file= parameter"
fi
if [ ! -f "/mnt/${file}" ] ; then
panic "Could not find the Squashfs file ${file}"
fi
# Associe le fichier Squashfs au loop device passé en argument root
if losetup $ROOT /mnt/${file} ; then
log_success_msg "Squashfs attached to ${ROOT} with success"
else
panic "Could not attach the Squashfs to ${ROOT}"
fi
log_end_msg
exit 0
Voici le fichier ~/boot-squashfs/debian/usr/share/initramfs-tools/scripts/local-premount/squashfs
dans son intégralité. Donnez les droits d'exécution à votre boot script via chmod +x
.
Modules
Pour fonctionner correctement certains binaires nécessitent l'insertion de modules
dans le kernel. C'est le cas pour losetup
avec le driver loop
mais aussi pour
mount
avec le driver squashfs
pour le montage de Squashfs.
Le driver loop
est présent par défaut dans l'initramfs mais n'est pas inséré,
tandis que le driver squashfs
n'est pas présent.
Pour ajouter ces modules et les insérer dans l'initramfs il est nécessaire de
modifier l'initramfs via initramfs-tools.
Deux options s'offrent à nous.
La première est la création d'un hook script pour indiquer à la génération
de l'initramfs l'insertion au démarrage des deux modules via la fonction force_load
du fichier
/usr/share/initramfs-tools/hook-functions
.
La seconde est de créer un fichier dans le dossier /usr/share/initramfs-tools/modules.d
et de lister les modules.
Nous allons opter pour la seconde solution avec le fichier ~/boot-squashfs/debian/usr/share/initramfs-tools/modules.d/modules
:
loop
squashfs
Génération
La génération d'un initramfs avec initramfs-tools peut se réaliser
avec la commande update-initramfs
.
Cependant, pour générer un initramfs il est nécessaire que le rootfs possède une
image kernel Linux. En effet, l'initramfs étant exécuté par un kernel ― en général
celui présent sur le rootfs où il a été généré ― l'usage des modules
doit correspondre à la même version.
Il faut donc installer le paquet linux-image-amd64
dans notre rootfs :
Vous remarquerez la génération automatique de l'initramfs durant l'installation du paquet. Vous pouvez cependant générer un initramfs manuellement :
Le fichier initramfs /boot/initrd.img
suffixé par la version du kernel a été généré.
Extraction
Il peut être nécessaire d'extraire l'initramfs pour valider les modifications apportées.
Nous pouvons dédier un dossier temporaire dans /tmp
pour en extraire son contenu :
167487 blocs
Vous pouvez retrouver notre boot script dans /tmp/initramfs/scripts/local-premount/squashfs
.
La commande lsinitramfs
permet quant à elle de lister les fichiers présent
dans l'initramfs :
Émulation du démarrage sur le Squashfs
Nous allons créer notre Squashfs à partir de notre rootfs Debian. Puis nous émulerons le démarrage du kernel Linux via QEMU avec l'interface UEFI en partant d'une image disque ― représentant le volume physique ― préalablement partitionnée et peuplée avec le Squashfs.
Image disque
Une image disque de 500Mo sera suffisant pour l'exercice de cet article.
Nous créerons deux partitions boot
et primary
.
La première sera une partition EFI
avec un système de fichiers FAT
qui contiendra notre initramfs et l'image kernel présent dans le dossier /boot
de notre rootfs ainsi qu'un bootloader.
La seconde sera une partition avec un système de fichiers ext4 contenant le fichier
de notre Squashfs.
Fichier Squashfs
L'initramfs et l'image kernel devant être dans la partition boot
, le dossier /boot
doit être exclu du Squashfs pour éviter les doublons.
L'option -e
de mksquashfs
permet d'exclure un fichier ou dossier.
Nous voulons cependant garder le dossier /boot
vide afin d'y monter par la suite
la partition boot
, ainsi nous n'excluons que son contenu avec un motif en regex :
Bootloader GRUB
Il est nécessaire d'utiliser un bootloader pour charger en mémoire notre initramfs et notre kernel avant d'exécuter ce dernier. Nous utiliserons le bootloader GRUB.
L'outil grub-mkimage
permet de générer un bootloader GRUB.
Sous Debian, l'outil est présent dans le paquet grub-common.
Peuplement des partitions
La dernière étape consiste à monter les partitions de notre image disque et d'y
copier nos éléments. Nous allons créer un dossier disk
avec deux sous-dossiers
boot
et primary
:
De la même manière que le Squashfs, associons un loop device à notre image disque afin de monter ses partitions dans les sous-dossiers :
L'option -P
ou --partscan
est nécessaire lorsque que le fichier est partitionné
pour créer un loop device par partition. Vous pouvez observer avec lsblk
deux
sous loop device p1
et p2
étant respctivement la partition boot
et primary
de notre image disque.
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop0 7:34 0 500M 0 loop
├─loop0p1 259:7 0 121M 0 part
└─loop0p2 259:8 0 377M 0 part
...
Nous pouvons désormais monter ces deux loop device dans nos sous-dossiers :
Copions l'ensemble des éléments présent dans le dossier boot
de notre rootfs
Debian dans le dossier disk/boot
et renommons par simplicité l'image kernel
et l'initramfs :
Reste à ajouter notre bootloader bootx64.efi
. Ce dernier doit se situer dans
le dossier EFI/Boot
de la partition boot
afin d'être détecté par l'UEFI.
Nous allons l'accompagner d'un fichier de configuration GRUB
afin d'automatiser le chargement en mémoire et le démarrage de notre kernel avec
nos paramètres :
menuentry 'Linux' {
linux /vmlinuz console=ttyS0 squashfs.file=rootfs.squashfs squashfs.partlabel=primary root=/dev/loop0
initrd /initrd.img
}
Copions ces deux fichiers dans le dossier disk/boot/EFI/Boot
:
disk/boot
avec la permission 755.
Pour finir, copions le Squashfs dans le sous-dossier disk/primary
:
Exécutez la commande sync
afin d'assurer que
la copie des fichiers est effective sur l'image disque.
Émulation avec QEMU
Pour tester notre solution nous allons émuler l'exécution avec QEMU à partir de
notre image disque. QEMU utilise par défaut l'interface BIOS, l'option -bios
permet d'utiliser une autre interface tel que l'UEFI.
Le projet OVMF,
installable via le paquet Debian ovmf
, permet d'utiliser l'UEFI sous QEMU
en passant à l'option -bios
le fichier /usr/share/ovmf/OVMF.fd
.
Le menu GRUB s'affiche, appuyez sur "Entrée". En analysant les traces du kernel
nous observons la ligne Unpacking initramfs...
qui témoigne de l'utilisation
de notre initramfs. Puis plus loin la ligne Run /init as init process
qui montre
le démarrage du processus init de l'initramfs. Par la suite nous observons alors
les logs de notre boot script avec Begin: Attach Squashfs rootfs.squashfs from /dev/sda2 to /dev/loop0 ...
et Success: Squashfs attached to /dev/loop0 with success
. Une fois le Squashfs
monté l'initramfs pivote l'exécution dessus en exécutant le processus init, ici
systemd.
Le login apparaît, authentifiez vous avec l'utilisateur admin
créé en amont.
La commande lsblk
affiche les périphériques suivant :
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
loop0 7:0 0 256.4M 0 loop /
sda 8:0 0 500M 0 disk
├─sda1 8:1 0 121M 0 part
└─sda2 8:2 0 377M 0 part
...
Le périphérique loop0
correspond au loop device de notre Squashfs, monté à la
racine par notre initramfs.
Le périphérique sda
et ses deux partitions correspond à notre image disque.
Vous pouvez monter la partition boot
dans /boot
:
Lecture / écriture avec un Overlayfs
Le Squashfs étant en lecture seule, si vous tentez une écriture ― en créant un fichier par exemple ― vous serez confronté au message suivant :
touch: cannot touch 'toto': Read-only file system
Cela peut être problématique si vous désirez avoir le droit d'écriture tout en gardant les avantages du Squashfs. Nous allons voir dans cette partie comment palier ce problème avec la mise en place d'un Overlayfs.
L'Overlayfs est lui aussi un système de fichiers. Son mécanisme permet de fusionner l'accès à un dossier dont les fichiers sont présents dans deux autres dossiers. On parle alors de lowerdir et upperdir pour distinguer les deux dossiers qui peuvent provenir du même système de fichiers. Les modifications sur le dossier sont appliquées dans le upperdir, laissant intact le lowerdir. Un troisième dossier workdir est alors nécessaire pour rendre les modifications atomiques sur le upperdir, il doit être sur le même système de fichier que ce dernier.
Ainsi, afin d'avoir les droits d'écriture dans notre home
il faut monter un overlay dans /home/admin
avec en lowerdir le même dossier, de manière à
garder les éventuels dossier déjà présent, et en upperdir
et workdir deux dossiers présents dans un système de fichiers possédant l'accès
en écriture, par exemple un tmpfs.
Si vous exécutez la commande mount
depuis le dossier destination, ici /home/admin
,
vous remarquerez que l'erreur d'écriture Read-only file system
persiste.
En effet, le processus du shell fait toujours référence au /home/admin
avant le montage
de l'overlay, c'est-à-dire à celui présent sur le Squashfs, en lecture seule.
La commande cd
, permettant de changer le répertoire du shell, peut alors être
utilisée pour rafraîchir le dossier /home/admin
et prendre en compte l'overlay.
La commande mount
affiche le montage de l'overlay et ses paramètres :
overlayfs on /home/admin type overlay (rw,relatime,lowerdir=/home/admin,upperdir=/tmp/upper,workdir=/tmp/work)
Il est désormais possible d'écrire dans le dossier /home/admin
. Vous observerez
que vos fichiers sont en réalité écrits dans l'upperdir /tmp/upper
.
Tel un système de fichiers classique, vous pouvez démonter l'overlay via umount /home/admin
.
Si vous avez l'erreur target is busy
cela signifie probablement que vous essayez
de démonter l'overlay tout en étant dans le dossier destination, déplacez-vous
alors dans un autre dossier.
Le dossier /home/admin
redevient vide mais vos fichiers sont toujours présents
dans l'upperdir.
Montage dans l'initramfs
Dans le cas où vous souhaitez rendre l'ensemble du rootfs en lecture/écriture il
est nécessaire de monter un Overlayfs sur la racine.
De la même manière que le montage du dossier /home/admin
, nous pourrions
envisager le montage suivant :
La commande ne retourne pas d'erreur, cependant vous observerez que l'erreur
d'écriture persiste même en changeant de dossier avec cd
.
Le montage d'un overlay à la racine du rootfs doit s'effectuer en amont, dans
l'initramfs, pour s'assurer que tout les processus utilisent la référence du /
de l'overlay et non celui du Squashfs.
Nous allons donc ajouter un nouveau boot script pour monter un Overlayfs.
Cette nouvelle opération ne peut s'ajouter dans notre boot script actuel.
En effet, ce dernier est réalisé à l'étape local-premount (i.e. avant le montage
du rootfs) tandis que le montage de l'overlay doit se réaliser après.
Nous opterons donc pour ajouter notre boot script overlay
à l'étape init-bottom,
c'est-à-dire juste avant la bascule sur le rootfs.
À l'exécution du boot script, le Squashfs est monté dans /root
,
ce dossier est donc en lecture seule. Nous souhaitons monter l'overlay sur le
dossier /root
avec des dossiers upper et work accessible en lecture / écriture
et le dossier lower contenant le Squashfs.
Afin de s'assurrer que ces trois dossiers soient accessibles une fois la bascule
sur /root
, il est préférable de les créer dans /root
et non pas dans /
qui
est le rootfs de l'initramfs.
Cependant, le Squashfs étant monté dans /root
il est nécessaire de déplacer
son point de montage temporairement pour créer les trois dossiers /root/lower
,
/root/upper
et /root/work
. Puis, de replacer le Squashfs dans /root/lower
cette fois-ci avant de monter l'overlay. Voici le boot script dans son intégralité :
#!/bin/sh
PREREQ=""
prereqs()
{
echo "$PREREQ"
}
case $1 in
prereqs)
prereqs
exit 0
;;
esac
. /scripts/functions
log_begin_msg "Setup the overlay"
# Création du dossier read-only pour déplacer temporairement le Squashfs
mkdir /squashfs
mount -o move /root /squashfs
# Il est désormais possible d'écrire dans /root.
# On y créer un dossier read-only pour le Squashfs
mkdir /root/lower
mount -o move /squashfs /root/lower
# Création des dossiers upper et work
mkdir /root/upper
mkdir /root/work
# Monte un Overlayfs dans /root (le dossier où l'on va basculer) avec en
# lower les fichiers du Squashfs en lecture seule et en upper / work
# des dossiers en lecture / écriture
mount overlayfs -t overlay -o lowerdir=/root/lower,upperdir=/root/upper,workdir=/root/work /root
log_end_msg
exit 0
L'usage de l'Overlayfs nécessite l'insertion du module overlay
. De la même manière
que les modules loop
et squashfs
, il est nécessaire d'ajouter ce module
à liste déjà existante dans le fichier ~/boot-squashfs/debian/usr/share/initramfs-tools/modules.d/modules
:
loop
squashfs
overlay
Il ne reste plus qu'à regénérer l'initramfs, le copier la partition boot
de l'image
disque toujours monter et de relancer QEMU :
overlayfs on / type overlay (rw,relatime,lowerdir=/root/lower,upperdir=/root/upper,workdir=/root/work)
Les dossiers /root/lower
, /root/upper
et /root/work
ne sont cependant pas
accessible en l'état, le dossier /root
étant vide.
En effet, le dossier /root
actuel est celui du Squashfs, tandis que les
dossiers lower, upper et work correspondent aux dossiers créés depuis le dossier
/root
de l'initramfs. Ainsi, les dossiers sont comme cachés mais existent,
vous avez désormais les droits d'écriture sur tout le rootfs.
Vous aurez cependant observé le message overlayfs: upper fs does not support tmpfile
dans les traces du kernel au moment de l'exécution de notre boot script overlay.
En effet, l'initramfs étant chargé en mémoire vive, les dossiers lower, upper
et work sont donc volatiles : un redémarrage du système provoquera la perte des
écritures réalisées.
Persistance des modifications
Pour rendre les écritures persistantes il est nécessaire que le dossier upper provienne d'un système de fichiers non volatile. Nous avons à notre disposition une image disque avec une partition primary, contenant notre fichier Squashfs, avec de l'espace libre. Ainsi, nous pouvons créer les dossiers upper et work sur cette partition.
Nous allons modifier le boot script overlay. Afin de garder la possibilité de
démarrer le système sans persistante, nous activons la persistance si l'argument
persist
est fourni dans la ligne de commande kernel.
#!/bin/sh
PREREQ=""
prereqs()
{
echo "$PREREQ"
}
case $1 in
prereqs)
prereqs
exit 0
;;
esac
. /scripts/functions
persist=$(cat /proc/cmdline | grep -q "persist" && echo "true" || echo "false")
if $persist ; then
log_begin_msg "Setup a persistent overlay"
else
log_begin_msg "Setup a volatile overlay"
fi
# Création du dossier read-only pour déplacer temporairement le Squashfs
mkdir /squashfs
mount -o move /root /squashfs
# Il est désormais possible d'écrire dans /root.
# On y créer un dossier read-only pour le Squashfs
mkdir /root/lower
mount -o move /squashfs /root/lower
upper=/root/upper
work=/root/work
if $persist ; then
# Création des dossiers upper et work persistant dans le point de
# montage /mnt réalisé dans le boot script squashfs
upper=/mnt/upper
work=/mnt/work
fi
mkdir "$upper"
mkdir "$work"
# Monte un Overlayfs dans /root (le dossier où l'on va basculer) avec en
# lower les fichiers du Squashfs en lecture seule et en upper / work
# des dossiers en lecture / écriture
mount overlayfs -t overlay -o lowerdir=/root/lower,upperdir=${upper},workdir=${work} /root
log_end_msg
exit 0
Nous pouvons désormais ajouter une nouvelle entrée dans le menu GRUB grub.cfg
pour démarrer
avec ou sans persistance :
menuentry 'Linux with persistent memory' {
linux /vmlinuz console=ttyS0 persist squashfs.file=rootfs.squashfs squashfs.partlabel=primary root=/dev/loop0
initrd /initrd.img
}
menuentry 'Linux with volatile memory' {
linux /vmlinuz console=ttyS0 squashfs.file=rootfs.squashfs squashfs.partlabel=primary root=/dev/loop0
initrd /initrd.img
}
Regénérez l'initramfs et mettez-le à jour dans la partition boot ainsi que la
configuration GRUB. Notre overlay étant maintenant monté sur la partition primary
il est nécessaire de démonter les partitions avant de lancer QEMU afin d'éviter
des incohérences sur les opérations d'écritures.
overlayfs on / type overlay (rw,relatime,lowerdir=/root/lower,upperdir=/mnt/upper,workdir=/mnt/work)
Vous observerez que le dossier /mnt
est cependant vide. De la même manière qu'avec les
dossiers tmp, l'overlay ayant été monté depuis l'initramfs les chemins /mnt/upper
et /mnt/work
sont du point de vue de l'initramfs lorsque nous avons monté
la partition primary
dans le dossier /mnt
.
Rien n'empêche, tel l'initramfs, de monter cette partition dans /mnt
:
lost+found rootfs.squashfs upper work
Les dossiers upper et work deviennent accessible. En analysant le dossier upper vous
pouvez déterminer les fichiers qui ont été modifiés depuis le démarrage du
système. Notamment dans les dossiers /etc
et /var
.
Essayez d'écrire un fichier dans votre home, puis quittez QEMU. En montant la partition
primary
vous retrouverez votre fichier :
hello world !
En exécutant à nouveau QEMU avec la mémoire peristante le fichier sera toujours présent.
Conclusion
L'usage d'un Squashfs peut être un bon choix pour construire un système sécurisé en protégeant et vérifiant l'intégrité du Squashfs. En effet, dans l'état actuel de notre démonstration rien n'empêche un utilisateur de modifier le Squashfs directement depuis la mémoire. Pour contrer cette faille, nous pouvons chiffrer la mémoire pour protéger celle-ci de toute modification sans le mot de passe. Cependant, une fois le système démarré et le Squashfs dechiffré, un utilisateur ayant les droits priviligiés (root) peut décompresser le Squashfs, y appliquer des modifications, le recompresser puis redémarrer le système. Ainsi, sans vérification de l'intégrité du Squashfs au démarrage, par l'initramfs, rien ne permet d'assurer que celui-ci n'a pas été altéré. Le Squashfs étant un fichier, un simple checksum permet de valider son intégrité. Le checksum à comparer peut être calculé en amont et utilisé par l'initramfs pour la vérification. Un système trés sécurisé peut mettre en place une chaîne de confiance avec Trusted Plateform Module combiné avec le Secure Boot de l'UEFI afin de sauvegarder de manière chiffré et sécurisé le checksum utilisé par l'initramfs.
Dans cette article nous n'avons aussi pas abordé la configuration SQUASHFS du kernel Linux. Notamment l'option SQUASHFS 4K DEVBLK SIZE permettant d'améliorer les performances lecture / écriture du Squashfs.