Journal Linux et BusyBox, un duo parfait pour s'amuser

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
54
31
jan.
2022

Bonjour Nal,

Depuis peu, le weekend j'aime bien m'amuser sur des petits projets que je ne compte pas continuer.

Ce weekend, c'était concevoir rapidement un OS bootable basé sur Linux. Mais sans suivre LFS.

Le disque virtuel

Je commence par me créer une petite image disque :

$ qemu-img create ./hd0.img 2G

Puis une table des partitions (à la va vite, une seule partition qui fait tout le disque):

$ cfdisk ./hd0.img

Enfin, je formate le tout en EXT4 et le monte :

$ losetup -P /dev/loop0 ./hd0.img
$ mkfs.ext4 /dev/loop0p1
$ mkdir ./mnt
$ mount /dev/loop0p1 ./mnt

Booter avec Linux et GRUB

L'étape suivante, c'est installer le noyau et le bootloader. J'ai pas envie de compiler moi même par flemme, je prend donc le noyau et l'initrd d'une Debian :

$ mkdir ./mnt/boot
$ cp /boot/vmlinuz-* ./mnt/boot/kernel.bin
$ cp /boot/initrd* ./mnt/boot/kernel.initrd
$ mkdir ./mnt/boot/grub
$ touch ./mnt/boot/grub.cfg

J'édite ce dernier fichier avec le contenu suivant :

set timeout=0
set default=0

menuentry "Mini Linux" {
  set root=(hd0,msdos1)
  echo "Loading kernel..."
  linux /boot/kernel.bin noresume quiet root=/dev/sda1
  echo "Loading initial ramdisk..."
  initrd /boot/kernel.initrd
}

L'option noresume est obligatoire vu que j'ai pas de partition SWAP et que le initrd de Debian est configuré avec l'UUID de la partition de ma distribution. J'aurais une belle erreur en boucle au démarrage (cf cet article).

Je pourrais recréer l'initrd, mais cela me prendrais plus que 2min. Non c'est pas le but.

On finalise l'installation de Grub avec la commande suivante :

$ echo "(hd0) /dev/loop0" > ./grub-device-map
$ grub-install \
    --no-floppy \
    --grub-mkdevicemap=./grub-device-map \
    --modules="biosdisk part_msdos ext2 configfile" \
    --root-directory ./mnt \
    /dev/loop0

On créé quelques dossiers supplémentaires :

$ mkdir ./mnt/{dev,run,sys,proc}

Et hop, plus qu'à démonter et tester tout ça :

$ umount ./mnt
$ losetup -d
$ qemu-system-x86_64 -hda ./hd0.img -m 1G

Ça boot ! Et je me retrouve dans l'initramfs car il ne trouve pas le programme init (PID 1).

Un système minimal avec BusyBox

Pour le coup, rien de plus simple :

  • on se rend à cette adresse
  • on télécharge les binaires statiques
  • on les place dans ./mnt/bin (pas oublier de remonter le disque)

Un petit changement du fichier ./mnt/grub/grub.cfg :

- linux /boot/kernel.bin noresume quiet root=/dev/sda1
+ linux /boot/kernel.bin noresume quiet root=/dev/sda1 init=/bin/init

Le programme init va lire le fichier /etc/inittab, et s'il n'existe pas, il va assumer une configuration par défaut, c'est pas vraiment ce que je veux, du coup, vite fait bien fait :

$ mkdir ./mnt/etc
$ cat > ./mnt/etc/inittab <<EOF
::sysinit:/etc/rc.sh init

::ctrlaltdel:/bin/reboot

::shutdown:/etc/rc.sh shutdown
::shutdown:/bin/umount -a -r

::restart:/bin/init

::once:/bin/chvt 2
tty2::respawn:-/bin/getty 38400 tty2 -l /bin/sh -n
EOF
$ cat > ./mnt/etc/rc.sh <<EOF
#!/bin/sh

echo "All done"
EOF
$ chmod +x ./mnt/etc/rc.sh

Pour plus d'info sur ce fichier, vous pouvez consulter ce lien.

Donc qu'est-ce qu'il se passe ici ?

  • après le boot, on va appeler notre script /etc/rc.sh
  • lors d'un restart on va simplement réappeler /bin/init
  • lors de l'appui sur les touches CTRL+ALT+DEL, on va appeler le programme /bin/reboot
  • lors du shutdown, on va appeler notre script /etc/rc.sh puis la commande umount

Une fois que le système est initialisé, on va appeler une seule fois la commande /bin/chvt 2, c'est l'équivalent de CTRL+ALT+F2. J'ai pas envie de mettre les futurs TTY sur le même que la sortie du kernel.

Enfin, j'en créé un nouveau, tty2 avec la commande getty. Et au lieu de lancer le programme /bin/login, je lui demande de lancer /bin/sh.

Un dernier petit détail :

$ cat > ./mnt/etc/passwd <<EOF
root:x:0:0:root:/root:/bin/sh
EOF
$ mkdir ./mnt/root

On démonte la partition, et on relance QEMU. Ça boot et on a un shell!

Conclusion

J'ai maintenant une image disque bootable avec un kernel, et un système de base ultra minimal, le tout en moins de temps qu'il n'en faut pour dire "linux".

Le vrai fun commence. Au lieu d'avoir un /etc/rc.sh, j'ai commencé à écrire un petit binaire statique en C++ qui embarque Lua 5.4. L'idée est d'avoir un /etc/rc.lua qui ressemblerait à :

local ssh = rc.service.define{
  name = "sshd",
  command = "/bin/sshd"
}

function rc.phase.init()
  ssh:start()
end

function rc.phase.shutdown()
  ssh:stop()
end

Le tout avec création de cgroups pour chaque service. Pourquoi ? Parce que je trouve ça fun le temps d'un weekend.

Qui sait, peut être que le weekend prochain je regarderais comment ajouter uutils et Wayland. Pourquoi pas Flatpak ou Snap aussi dans le futur ?

Tout plein de choses à tester, pour une inutilité maximale !

  • # Besoin d'expliciter le init ?

    Posté par  (Mastodon) . Évalué à 5.

    Génial cette procédure de boostrap, merci !

    Un petit changement du fichier ./mnt/grub/grub.cfg :

    Il me semble bien que tu n'as pas à préciser le chemin de init si il fait partie des quelques chemins "standards" : https://elixir.bootlin.com/linux/latest/source/init/main.c#L1555

    En théorie, la théorie et la pratique c'est pareil. En pratique c'est pas vrai.

    • [^] # Re: Besoin d'expliciter le init ?

      Posté par  (site web personnel) . Évalué à 3. Dernière modification le 31 janvier 2022 à 10:20.

      Yes, pas besoin de le préciser, mais je l'ai mis de manière explicite pour faire découvrir l'option du noyau, au cas ou tu veux t'écarter du FHS.

      Je me suis amusé à faire un /apps/busybox/bin/* par exemple.

      https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg

      • [^] # Re: Besoin d'expliciter le init ?

        Posté par  (site web personnel) . Évalué à 3.

        Quelle taille au final ?

        Si tu fait du go qui embarque toute lib au final, tu arrives a démarrer l'appli a la place d'init ?

        C'est possible de faire une app dans une vm avec rien d'autres ?

        "La première sécurité est la liberté"

        • [^] # Re: Besoin d'expliciter le init ?

          Posté par  (site web personnel) . Évalué à 6.

          Mon RootFS utilise actuellement environ 70Mo.

          Ce que tu décris est parfaitement possible, c'est le principe même des unikernel.

          Il faut cependant noter que le processus init a un rôle particulier :

          • il doit toujours tourner, si il s'arrête, kernel panic
          • il doit écouter le signal SIGCHLD de tout ses enfants, et le traiter correctement, sinon les processus zombies de sont pas supprimé de la liste des processus
          • il doit gérer les signaux ACPI pour éteindre la machine, rebooter, etc…

          C'est la raison pour laquelle j'ai choisi BusyBox au lieu de développer le mien.

          https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg

          • [^] # Re: Besoin d'expliciter le init ?

            Posté par  (site web personnel) . Évalué à 3. Dernière modification le 01 février 2022 à 08:44.

            Oui, c'est le principe des unikernel, sauf que je n'ai jamais vu personne tenté le coup avec Linux complet.

            Dans le cas d'une VM, on est sur des méthodes "crash only" pour l'arret. Cela veut dire que l'application tourne ou crash. Donc le kernel panic n'est pas vraiment un problème (sauf peut être pour le debug ou les log), le soft est relancé. concernant la gestion de signaux, je ne sais pas si le runtime go le gère.

            Concernant l'acpi, dans le cadre d'une application unikernel, on s'en fout un peu, je pense.

            D'ailleurs 70 Mo me parait beaucoup. Un kernel, c'est une dizaine de mega, la libc 6, l'application une dizaine aussi. J'imagine que l'image n'est pas optimisé pour ne tourner qu'en VM et emporte plein de drivers et de dépendance.

            "La première sécurité est la liberté"

            • [^] # Re: Besoin d'expliciter le init ?

              Posté par  (site web personnel) . Évalué à 3.

              Le kernel : 6.6 Mo
              Le initrd : ~40 Mo

              Et les bins de busybox ~20Mo. J'aurais pu télécharger uniquement le binaire busybox et faire des hard links (cc ln) pour économiser de l'espace.

              Mais oui j'ai pris celui de Debian tel quel, pour pas me prendre la tête, il y a beaucoup d'optimisations possibles.

              https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg

              • [^] # Re: Besoin d'expliciter le init ?

                Posté par  . Évalué à 4.

                Le binaire de busybox, c'est 2Mo sur ma debian. Tu as dupliqué autant de fois le binaire que nécessaire pour arriver à tous ces Mo?

                Parceque oui, fait des liens symboliques pour les bins essentiels: init, sh, pour les autres tu peux t'en passer.
                Si tu lances le shell de busybox et que tu appelles un binaire, busybox vérifie s'il ne s'agit pas d'une de ses applets et la lance le cas échéant (enfin si t'as configuré l'option qui va bien).

                • [^] # Re: Besoin d'expliciter le init ?

                  Posté par  . Évalué à 2.

                  Des liens symboliques, c'est pas mieux de faire des liens en dur pour ça ?

                  https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll

              • [^] # Re: Besoin d'expliciter le init ?

                Posté par  (site web personnel) . Évalué à 3. Dernière modification le 02 février 2022 à 16:46.

                J'ai 1.2Mo pour un busybox lié statiquement avec musl sur x86_64, avec toutes les options activées.

            • [^] # Re: Besoin d'expliciter le init ?

              Posté par  . Évalué à 2.

              Je suis pas sûr que ce soit très intéressant d'utiliser linux pour ça. D'une part car il y a pleins de trucs inutiles dans Linux et je ne suis pas certains qu'il soit modulaire à ce point.

              D'autre part parce que de ce que je comprends quand tu pars sur ce type de déploiement tu ne veux pas de découpage userland/kernelland. Tout est ton appli et si tu ajoute une gestion des droits et les commutations de contexte tu perds beaucoup de performance pour un truc dont tu n'a pas besoin.

              C'est pour ça il me semble que quand on parle des unikernel c'est qu'on se place du point de vue de l'hyperviseur et que pour construire des unikernels on parle plutôt de système d'exploitation bibliothèques pour construire des unikernels. Dis autrement et pour faire le parallèle avec les conteneurs, l'hyperviseur, c'est le runtime runc, l'unikernel, c'est ton image oci, et le système d'exploitation bibliothèque c'est le dockerfile ou le buildpack.

              J'imagine que packager linux en bibliothèque est un travail qui paraît trop gros par rapport à l'intérêt et faire accepter ça à la lkml (c'est impossible à maintenir sans leur participation) soit pas être gagné.

              https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll

              • [^] # Re: Besoin d'expliciter le init ?

                Posté par  (site web personnel) . Évalué à 5.

                D'une part car il y a pleins de trucs inutiles dans Linux et je ne suis pas certains qu'il soit modulaire à ce point.

                De mémoire, make menuconfig donnait une monstrueuse liste d'option de compilation du noyau. Dans le cas d'un noyau pour VM, une grande partie des drivers sont inutiles ainsi que les modules, etc… bref, si, Linux est extrêmement configurable.

                D'autre part parce que de ce que je comprends quand tu pars sur ce type de déploiement tu ne veux pas de découpage userland/kernelland.

                si tu veux ça tu as tort. Un des principes de la sécurité est la réduction de la surface d'attaque et la réduction de privilège des applications. Comme on ne fait plus tourner de serveur en root, on ne vaut pas d'application en mode kernel ! Tu n'as pas envie de refaire Heartbleed.

                Tout est ton appli et si tu ajoute une gestion des droits et les commutations de contexte tu perds beaucoup de performance pour un truc dont tu n'a pas besoin.

                C'est vrai en théorie. Mais avoir à la fois le niveau de performance de Linux et son niveau de sécurité est très complexe, et pas prêt d'arriver. Il existe des piles IP coté application très rapide, mais pas sécurisé. Bref, faire mieux que Linux est illusoire à court terme.

                L'idée n'est pas de modifier Linux, mais de le configurer "à poil" dans une VM, pour ne faire tourner qu'une seul application, tout en bénéficiant de ses perfs et de sa sécurité.

                "La première sécurité est la liberté"

                • [^] # Re: Besoin d'expliciter le init ?

                  Posté par  . Évalué à 4.

                  si tu veux ça tu as tort. Un des principes de la sécurité est la réduction de la surface d'attaque et la réduction de privilège des applications. Comme on ne fait plus tourner de serveur en root, on ne vaut pas d'application en mode kernel ! Tu n'as pas envie de refaire Heartbleed.

                  Heartbleed a eu lieu malgré cette ségrégation et le bug n'était pas dans le noyau. Le concept même de ce genre de déploiement est de remplacer le processus par une machine virtuelle ta séparation kernelland/userland par la commutation entre ta vm et ton hyperviseur. Multiplier les couches pour multiplier multiplier les couches n'a pas grand intérêt. Ça m'a plus l'air d'un cargo cult.

                  https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll

                  • [^] # Re: Besoin d'expliciter le init ?

                    Posté par  (site web personnel) . Évalué à 4.

                    C'est pas faux. Mais j'ai l'impression que compromettre le service a bien plus d'impact si il est kernel, même en mono-application. L'application peut avoir un nombre de capacité réduite, qui limiterait beaucoup l'effet d'injection de code.

                    "La première sécurité est la liberté"

                    • [^] # Re: Besoin d'expliciter le init ?

                      Posté par  . Évalué à 3.

                      C'est parce que tu vois le kernel comme le maître incontesté de la machine, celui qui manipule directement le matériel et à qui personne ne peut dire non. Mais ça c'est vrai pour le matériel que le quel tu le démarre et personne dans son système ne peut lui dire non. Mais là le matériel sur lequel il démarre est virtuel et n'a de sens que pour lui. Il peut casser son filesystem par exemple ou corrompre sa mémoire, mais ça n'aura d'impact que pour lui.

                      Bien sûr cela dépend de la qualité de la virtualisation, mais de ce que je comprends les primitives à implémenter pour sont relativement simples (plus que pour faire du sandboxing).

                      https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll

          • [^] # Re: Besoin d'expliciter le init ?

            Posté par  . Évalué à 6.

            Dans un unikernel, il n'y a pas de séparation entre le noyau et l'application (pas de userland en qque sorte). Les appels systèmes, qui nécessitent de passer du userland au noyau sont remplacés par des appels de fonctions ce qui est beaucoup plus rapide.

            Ici on aurai 1 noyau + 1 application PID=1. Je ne sais pas si ça porte un nom si ce n'est un système dépouillé :)

        • [^] # Re: Besoin d'expliciter le init ?

          Posté par  . Évalué à 4.

          Genre un unikernel ?

  • # bref, j'ai écrit un init qui se configure en Lua

    Posté par  . Évalué à 10.

    Si tu te méfies pas, tu vas te retrouver avec une distrib perso :)

    • [^] # Re: bref, j'ai écrit un init qui se configure en Lua

      Posté par  (site web personnel) . Évalué à 4.

      bref, j'ai écrit un init qui se configure en Lua

      Je suis un DevOps, le "Infrastructure as Code" c'est mon quotidien :D

      Si tu te méfies pas, tu vas te retrouver avec une distrib perso :)

      Là, on en est encore loin quand même :

      • pas de gestion des users, il y a que root pour l'instant
      • pas de gestionnaire de paquet
      • pas une chaîne de compilation indépendante d'un système hôte (faire gaffe donc au chemin des .so, ou alors tout compiler en statique, ou alors tout plein de lien symboliques)

      https://link-society.com - https://kubirds.com - https://github.com/link-society/flowg

      • [^] # Re: bref, j'ai écrit un init qui se configure en Lua

        Posté par  (site web personnel) . Évalué à 3. Dernière modification le 01 février 2022 à 08:47.

        Il y a une niche pour des unikernel basé sur Linux : tu n'as besoin que d'un seul utilisateur, pas de paquet, etc…

        J'imagine qu'il doit y avoir une autre niche pour un concept d'unikernel sous docker (sans le kernel donc).

        "La première sécurité est la liberté"

    • [^] # Re: bref, j'ai écrit un init qui se configure en Lua

      Posté par  (site web personnel) . Évalué à 2.

      Si ça se trouve, c’est comme ça qu’a commencé TinyCoreLinux ;-)

  • # un peu pareil

    Posté par  . Évalué à 6. Dernière modification le 01 février 2022 à 23:02.

    Dans un objectif un peu différent, j'avais aussi fait un peu la même expérience: 1/ créer l'image root fs 2/ lancer qemu avec le kernel de mon poste.

    Et il se trouve que l'on a même pas besoin de copier le noyau (vmlinuz + initrd) dans /boot. Il y a des options magiques parmis les 185 proposées.

    kernel=$(uname -r)
    qemu-system-x86_64 \
      ... \
      -kernel /boot/vmlinuz-${kernel} \
      -initrd /boot/initrd.img-${kernel} \
      ... \
      -append "... init=/bin/bash"

    Ici, je fais un peu le bourrin avec un bash comme init. Cela fonction mais fait un kernel panic quand le process se termine et ne gère pas l'ACPI comme noté par l'OP et surement plein d'autre truc.

    Mais ça marche :) On a le prompt en une fraction moins de deux secondes.

  • # qq. soucis...

    Posté par  . Évalué à 1.

    Bonjour / bonsoir,

    J'ai rencontré qq. problèmes (sur Ubuntu 20.04) :
    1. grub-install --no-floppy --grub-mkdevicemap=..
    Installing for x86_64-efi platform.
    grub-install: error: ./mnt doesn't look like an EFI partition.

    2. lors du lancement par qemu : qemu-system-x86_64: warning: host doesn't support requested feature: CPUID.80000001H:ECX.svm [bit 2]

    Quoi faire ?

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.