Faire fonctionner qemu-user-static avec Podman

Récemment, j’ai eu à faire tourner des conteneurs ARM64 sur le laptop x86 d’un client. Facile me direz-vous : tu n’as qu’à installer qemu-user-static sur le laptop ! Et t’as aussi un conteneur tout prêt sur Docker Hub, au cas où !

La messe est dite ? Pas si sûr…

État des lieux

En effet, s’il est facile d’installer qemu-user-static sur Fedora, ce n’est pas le cas de CentOS Stream ou Red Hat Enterprise Linux. On peut toujours récupérer le paquet d’un repository Fedora et l’installer sur ces downstream de Fedora. Ça marche mais ça reste un paquet orphelin qui ne sera jamais mis à jour…

Et l’image docker.io/multiarch/qemu-user-static que l’on peut trouver sur le Docker Hub ? Elle n’a pas été mise à jour depuis 2 ans et la dernière version disponible de qemu est la 7.2. Quand on sait que la version 10 de qemu a été publiée récemment, ça fait désordre…

TL;DR

Deux one-liners, un pour la construction de l’image et un pour son exécution.

Construire l’image de conteneur localhost/qemu-user-static :

sudo podman build -f https://red.ht/qemu-user-static -t localhost/qemu-user-static /tmp

Exécuter qemu-user-static :

sudo podman run --rm --privileged --security-opt label=filetype:container_file_t --security-opt label=level:s0 --security-opt label=type:spc_t localhost/qemu-user-static

Ça y est ! Vous pouvez maintenant exécuter une image de conteneur qui est dans une architecture matérielle différente de celle de votre machine.

Exemple :

$ arch
x86_64

$ podman run -it --rm --platform linux/arm64/v8 docker.io/library/alpine     
/ # arch
aarch64

Construction de l’image

J’ai choisi de baser mon image sur les images officielles Fedora (version 42 lors de l’écriture de cet article). Si vous avez déjà travaillé avec l’image docker.io/multiarch/qemu-user-static, vous verrez que j’ai un peu modernisé tout ça :

Le résultat est dépouillé :

FROM quay.io/fedora/fedora:42

RUN dnf install -y qemu-user-static \
 && dnf clean all

ADD container-entrypoint /

ENTRYPOINT ["/container-entrypoint"]
CMD []

Le script container-entrypoint est lui aussi réduit à son strict nécessaire :

#!/bin/sh

set -Eeuo pipefail

if [ ! -d /proc/sys/fs/binfmt_misc ]; then
    echo "No binfmt support in the kernel."
    echo "  Try: '/sbin/modprobe binfmt_misc' from the host"
    exit 1
fi

if [ ! -f /proc/sys/fs/binfmt_misc/register ]; then
    echo "Mounting /proc/sys/fs/binfmt_misc..."
    mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
fi

echo "Cleaning up..."
find /proc/sys/fs/binfmt_misc -type f -name 'qemu-*' -exec sh -c 'echo -1 > {}' \;

echo "Registering..."
exec /usr/lib/systemd/systemd-binfmt

Exécution

Une fois l’image construite, il faut l’exécuter pour que les binaires qemu-*-static soient enregistrés auprès du système binfmt. Mais il y a une petite subtilité : sur une distribution Linux type Fedora, CentOS Stream ou RHEL, le système SELinux veille au grain ! Et si quelque chose tente de percer l’étanchéité du moteur de conteneurisation, SELinux le détecte et l’action est interdite.

Et c’est exactement ce qui se produit quand on exécute naïvement l’image construite précédemment : les binaires qemu-*-static ont une étiquette SELinux qui interdit leur exécution par le moteur de conteneurisation au moment où il s’apprête à lancer le PID 1 du conteneur à émuler.

La solution ? Lancer le conteneur avec les options SELinux qui donne au conteneur les bonnes étiquettes SELinux afin que l’action soit autorisée. C’est le rôle des options --security-opt :

Ces deux options, vous l’avez peut-être reconnu, font que les fichiers de notre image localhost/qemu-user-static ont l’étiquette SELinux des volumes partagés. Le partage de ces fichiers est donc autorisé entre les conteneurs.

Les options --security-opt label=type:spc_t et --privileged permettent au conteneur de s’exécuter en root, de pouvoir procéder au montage du pseudo système de fichier binfmt_misc et d’enregistrer les binaires qemu-*-static.

La ligne de commande complète est :

sudo podman run --rm --privileged --security-opt label=filetype:container_file_t --security-opt label=level:s0 --security-opt label=type:spc_t localhost/qemu-user-static

Si vous oubliez ces options de sécurité, lorsque vous voudrez exécuter un conteneur dans une architecture différente, podman se terminera sans erreur mais sans rien exécuter…

Avec un peu de chance, vous repèrerez alors dans vos logs le message d’erreur suivant :

[10051.131634] audit: type=1400 audit(1749488918.807:3124): avc:  denied  { read execute } for  pid=32544 comm="dumb-init" path="/usr/bin/qemu-x86_64-static" dev="overlay" ino=434591 scontext=system_u:system_r:container_t:s0:c53,c117 tcontext=system_u:object_r:container_file_t:s0:c1022,c1023 tclass=file permissive=0
[10051.131656] audit: type=1701 audit(1749488918.807:3125): auid=10018 uid=1000 gid=1000 ses=1 subj=system_u:system_r:container_t:s0:c53,c117 pid=32544 comm="dumb-init" exe="/usr/bin/qemu-x86_64-static" sig=11 res=1

Même si ça peut sembler cryptique au premier abord, le message dit l’essentiel : tu essayes d’éxécuter ({ read execute }) un binaire (/usr/bin/qemu-x86_64-static) qui a l’étiquette system_u:object_r:container_file_t:s0:c1022,c1023 depuis un processus qui a l’étiquette system_u:system_r:container_t:s0:c53,c117 (notez la différence sur la fin de l’étiquette !) et cet accès est refusé (avc: denied).

Conclusion

Ce sujet a été le caillou dans ma chaussure durant ces dernières semaines, je suis content d’être parvenu à trouver une solution ! Et j’espère que la dite solution vous sera autant utile à vous qu’elle l’a été pour moi.