logo

bootstrap-initrd

Linux initrd generator to bootstrap a POSIX-ish system from a reasonably small binary seed git clone https://hacktivis.me/git/make-initrd.git
commit: b3171e7b01912ed27f62911c8a9beee57ef1d976
parent e4ea48246c6fcb7d12a8bbc1b7db50e3b99d55ac
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date:   Sat, 27 Apr 2024 12:19:08 +0200

Initial Commit for bootstrap-initrd

Diffstat:

A.gitignore4++++
MREADME.md51++++++++++++++++++++++++++++++++++++++-------------
Dinit88-------------------------------------------------------------------------------
Ainit.c126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainit.sh102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Als-stub.c53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmake-initrd.sh93+++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Amv-stub.c39+++++++++++++++++++++++++++++++++++++++
8 files changed, 424 insertions(+), 132 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2022 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me> +# SPDX-License-Identifier: ISC +/initramfs-tcc +/initramfs-tcc.cpio.xz diff --git a/README.md b/README.md @@ -1,21 +1,46 @@ -# make-initrd +# bootstrap-initrd ``` -Copyright 2017-2022 Haelwenn (lanodan) Monnier <contact@hacktivis.me> +Copyright 2017 Haelwenn (lanodan) Monnier <contact@hacktivis.me> SPDX-License-Identifier: ISC ``` -A barebones initrd (aka initramfs) generator for Linux, intended for systems with simple but special needs like an mirrored LUKS+ZFS root pool. +A initrd generator for Linux to bootstrap a POSIX-ish system from a reasonably small binary seed, full source bootstrap isn't intended as a goal. -Write your own ``init`` executable (my own is provided as example) and declare it's required commands in ``make-initrd.sh`` which will generate the initramfs. +Status: Very early experiments, doesn't yet builds everything, generator very likely doesn't runs on your machine. -## Dependencies -* POSIX environment -* (optionnal) busybox -* (optionnal) lddtree command from pax-utils, required for glibc systems +## Rationales +### TCC (binary) +Need to start somewhere and TCC allows to interpret C, allowing to avoid seeding another interpreter. +The reason to not pick Guile+MesCC instead is because I find Guile Scheme to be less well-known than amd64 itself. -## Tested systems -All are using ZFS on LUKS. +### musl (binary) +Need a libc to start somewhere, tryhards could maybe compile it from source with TCC to reduce the seed further but this hasn't been tried. -* Gentoo Linux amd64 musl -* Gentoo Linux amd64 glibc -* Alpine Linux amd64 (musl) +### mksh (binary) +Temporary workaround to mrsh lack of working interactivity in the console (Due to some missing TTY initialization?). + +### mrsh +emersion POSIX shell, somewhat incomplete but buildable with only a C Compiler. + +### yacc +The reference implementation of yacc, only needs a C Compiler to build. (Unlike byacc and bison) + +### utils-std +My own software, which explicitly allows to bootstrap with an incomplete POSIX environment. + +### minised +Incomplete implementation of `sed(1)` (notably missing `-i`), but has minimal dependencies. +Likely to be replaced with a `sed(1)` from a \*BSD. + +### (One True) awk +Reference implementation and AFAIK the one used in BSDs. + +### bmake +NetBSD make made portable, uses an autotools configure script but much simpler than the GNU Make one. + +## Launching in QEMU +``` +$ qemu-system-x86_64 -enable-kvm -cpu host -m 1536 -kernel /boot/vmlinuz-6.6.21-gentoo -initrd /tmp/initramfs-tcc.cpio.xz -append 'init=/init console=ttyS0 panic=1' -nographic -no-reboot +``` + +* Combination of `panic=1` and `-no-reboot` allows to exit+relaunch diff --git a/init b/init @@ -1,88 +0,0 @@ -#!/bin/sh -# Copyright 2017-2022 Haelwenn (lanodan) Monnier <contact@hacktivis.me> -# SPDX-License-Identifier: ISC - -export PATH=/bin:/sbin:/usr/bin:/usr/sbin -export init=/sbin/init newroot=/newroot root=zroot/ROOT/gentoo sh=/bin/sh level=3\ -a dev_hotplug=mdev - - -rescueshell() { - export PS1='rsh:$(tty | cut -c6-):$PWD # ' - if command -v setsid >/dev/null; then - # shellcheck disable=SC2094 - setsid "$sh" -i -0<"$console" 1>"$console" 2>&1 - else - # shellcheck disable=SC2094 - "$sh" -i 0<"$console" 1>"$console" 2>&1 - fi -} - -die() { - echo "Dropping into a rescueshell..." - rescueshell || exec $sh -i -} - -getdev() { - blkid | grep "$1" | cut -d: -f1 -} - -set -v - -/bin/busybox --install /usr/bin - -umask 0077 -mount -t proc proc /proc -mount -t sysfs sysfs /sys -if grep -q devtmpfs /proc/filesystems; then - mount -t devtmpfs devtmpfs /dev -else - mount -t tmpfs tmpfs /dev -fi - -# shellcheck disable=SC2013 -for arg in $(cat /proc/cmdline); do - case $arg in - rescue*) - export rescue=1 - ;; - single) - export level=2 - ;; - level*|init*|root*|crypt_root*|sh*|dev_hotplug*) - # shellcheck disable=SC2163 - export "$arg" - ;; - esac -done - -root="${root/#ZFS=}" - -"$dev_hotplug" -s || die - -command -v "$dev_hotplug" > /proc/sys/kernel/hotplug - -[ -h /dev/fd ] || ln -fs /proc/self/fd /dev/fd -[ -h /dev/stderr ] || ln -fs /proc/self/stderr /dev/stderr -[ -h /dev/stdin ] || ln -fs /proc/self/stdin /dev/stdin -[ -h /dev/stdout ] || ln -fs /proc/self/stdout /dev/stdout -: "${console:=/dev/console}" -# shellcheck disable=SC2094 -exec 0<"$console" 1>"$console" 2>&1 - -#root=$(getdev $crypt_root) -#[ $root ] || root=/dev/sda2 -#cryptsetup open $root root || die -# -#mount /dev/mapper/root $newroot || die - -modprobe zfs || die - -cryptsetup open /dev/sda2 cryptrpool || die - -zpool import -d /dev/mapper -d /dev -N rpool || die - -mount -t zfs -o rw,zfsutil "$root" "$newroot" || die - -for d in /proc /sys /dev; do umount -l "$d"; done - -exec switch_root "$newroot" "${init:-/sbin/init}" "$level" || die diff --git a/init.c b/init.c @@ -0,0 +1,126 @@ +// environ +#define _GNU_SOURCE + +#include <spawn.h> // posix_spawn +#include <unistd.h> // chdir, environ +#include <stdlib.h> // setenv +#include <stdio.h> // fprintf +#include <string.h> // strerror +#include <errno.h> +#include <sys/wait.h> // waitpid +#include <sys/stat.h> // mknod +#include <sys/sysmacros.h> // makedev +#include <sys/mount.h> +#include <fcntl.h> // O_RDONLY + +#include "mrsh_tcc.h" + +static int +exec_wait(char *args[]) +{ + pid_t child = 0; + // Allows to avoid a fork+exec + int ret = posix_spawn(&child, args[0], NULL, NULL, args, environ); + if(ret != 0) { + fprintf(stderr, "Failed spawning command with argv[0] = '%s': %s\n", args[0], strerror(errno)); + return -1; + } + + if(waitpid(child, NULL, 0) == -1) { + fprintf(stderr, "Failed waiting for child of pid %d: %s\n", child, strerror(errno)); + return -1; + } + + return 0; +} + +static int +build_mrsh() +{ + chdir("/mrsh-a858396b79ba217760b0982dd6f45c91c5192c3b"); + + fprintf(stderr, "Compiling mrsh\n"); + + if(exec_wait(mrsh_tcc_cmd) < 0) return -1; + + fprintf(stderr, "Compiled mrsh !\n"); + + return 0; +} + +static void +e_mknod(const char *path, mode_t mode, dev_t dev) +{ + if(mknod(path, mode, dev) < 0) + fprintf(stderr, "Failed creating device '%s': %s\n", path, strerror(errno)); +} + +int +main(int argc, char *argv[]) +{ + setenv("PATH", "/bin", 1); + if(build_mrsh() < 0) return 1; + + //e_mknod("/dev/null", S_IFCHR|0666, makedev(0x1, 0x3)); + //e_mknod("/dev/tty", S_IFCHR|0666, makedev(0x5, 0x0)); + //e_mknod("/dev/tty1", S_IFCHR|0620, makedev(0x4, 0x1)); + //e_mknod("/dev/console", S_IFCHR|0660, makedev(0x5, 0x1)); + + if(mount("sysfs", "/sys", "sysfs", MS_NOSUID|MS_NODEV|MS_NOEXEC|MS_RELATIME, NULL) < 0) + fprintf(stderr, "Failed to mount /sys: %s\n", strerror(errno)); + + if(mount("proc", "/proc", "proc", MS_NOSUID|MS_NODEV|MS_NOEXEC|MS_RELATIME, "hidepid=2") < 0) + fprintf(stderr, "Failed to mount /proc: %s\n", strerror(errno)); + + if(mount("devtmpfs", "/dev", "devtmpfs", MS_NOSUID|MS_NOEXEC, NULL) < 0) + fprintf(stderr, "Failed to mount /proc: %s\n", strerror(errno)); + +#if 0 + int console_ro = open("/dev/console", O_RDONLY); + if(console_ro < 0) + { + fprintf(stderr, "Failed ro-opening /dev/console: %s\n", strerror(errno)); + return 1; + } + + // turn stdin into /dev/console + if(dup2(console_ro, 0) < 0) + { + fprintf(stderr, "Failed setting /dev/console as stdin: %s\n", strerror(errno)); + return 1; + } + + int console_wo = open("/dev/console", O_WRONLY); + if(console_wo < 0) + { + fprintf(stderr, "Failed wo-opening /dev/console: %s\n", strerror(errno)); + return 1; + } + + // turn stdout into /dev/console + if(dup2(console_wo, 1) < 0) + { + fprintf(stderr, "Failed setting /dev/console as stdout: %s\n", strerror(errno)); + return 1; + } + + // turn stderr into /dev/console + if(dup2(console_wo, 2) < 0) + { + fprintf(stderr, "Failed setting /dev/console as stderr: %s\n", strerror(errno)); + return 1; + } +#endif + + fprintf(stderr, "Launching: /bin/mrsh /init.sh\n"); + + setenv("PS1", "> ", 1); + + if(execl("/bin/mrsh", "/bin/mrsh", "/init.sh", NULL) < 0) + { + fprintf(stderr, "Failed to execute: %s\n", strerror(errno)); + return 1; + } + + fprintf(stderr, "Unreachable\n"); +} diff --git a/init.sh b/init.sh @@ -0,0 +1,102 @@ +#!/bin/mrsh +profile_export() { + export "$@" + echo "export '$@'" >>/etc/profile +} + +build_awk() { + cd /awk-*/ + yacc -d -b awkgram awkgram.y + $CC -o maketab maketab.c + ./maketab awkgram.tab.h >proctab.c + $CC -o /bin/awk awkgram.tab.c b.c main.c parse.c proctab.c tran.c lib.c run.c lex.c +} + +build_stubs() { + $CC $CFLAGS -o /bin/ls /ls-stub.c + $CC $CFLAGS -o /bin/mv /mv-stub.c +} + +build_bmake() { + cd /bmake/ + ./configure + sh ./make-bootstrap.sh + link bmake /bin/bmake +} + +set -ex -o pipefail + +export CC=tcc +export CFLAGS="-Os -Wall -Wextra" + +cd /yacc-*/ +$CC $CFLAGS -o /bin/yacc *.c + +cd /utils-std-*/ +mrsh ./makeless.sh + +profile_export PATH="$PATH:$PWD/cmd/" +profile_export YACC="yacc" +# re-export due to prior lack of echo +profile_export CC="$CC" +profile_export CFLAGS="$CFLAGS" + +export SHELL="/bin/mksh" +# Note: This is a hardlink, no ln -s yet +link $SHELL /bin/sh + +cd /minised-1.16 +$CC $CFLAGS sedcomp.c sedexec.c -o /bin/sed + +build_awk + +# stubs for utilities currently missing from utils-std + +build_stubs + +cat >/bin/hostname <<'EOF' +#!/bin/sh +exec uname -n +EOF + +chmod +x /bin/hostname + +cat >/bin/grep <<'EOF' +#!/bin/sh +set -e +while [ "$1" = "-*" ] +do + args="$args $1" + shift +done + +re="$1" +shift + +exec sed $args -n -e "/${re}/p" "$@" +EOF + +chmod +x /bin/grep + +cat >/bin/egrep <<'EOF' +#!/bin/sh +set -e +exec grep -E "$@" +EOF + +chmod +x /bin/egrep + +cat >/bin/fgrep <<'EOF' +#!/bin/sh +set -e +exec grep -F "$@" +EOF + +chmod +x /bin/fgrep + +build_bmake + +cd / + +# Note: Strip out -l on switching back to mrsh +exec $SHELL -l diff --git a/ls-stub.c b/ls-stub.c @@ -0,0 +1,53 @@ +#include <sys/stat.h> +#include <stdio.h> // fprintf +#include <unistd.h> // getopt +#include <stdbool.h> +#include <fcntl.h> // AT_SYMLINK_NOFOLLOW +#include <string.h> // strerror +#include <errno.h> + +int +main(int argc, char *argv[]) +{ + bool opt_d = false, print_inode = false; + int stat_opts = 0; + + int c = -1; + while((c = getopt(argc, argv, ":di")) != -1) + { + switch(c) + { + case 'd': + stat_opts |= AT_SYMLINK_NOFOLLOW; + opt_d = true; + break; + case 'i': + print_inode = true; + break; + case '?': + fprintf(stderr, "ls: Unknown option '-%c'\n", optopt); + break; + } + } + + argc -= optind; + argv += optind; + + if(!opt_d) fprintf(stderr, "ls: Warning, readdir not implemented yet\n"); + + for(int i = 0; i<argc; i++) + { + char *file = argv[i]; + struct stat status; + if(fstatat(AT_FDCWD, file, &status, stat_opts) < 0) + { + fprintf(stderr, "ls: Error getting status for file '%s': %s\n", file, strerror(errno)); + return 1; + } + + if(print_inode) printf("%lu ", status.st_ino); + printf("%s\n", file); + } + + return 0; +} diff --git a/make-initrd.sh b/make-initrd.sh @@ -1,59 +1,90 @@ #!/bin/sh -# Copyright 2017-2022 Haelwenn (lanodan) Monnier <contact@hacktivis.me> +# Copyright 2017 Haelwenn (lanodan) Monnier <contact@hacktivis.me> # SPDX-License-Identifier: ISC -out_base=initramfs-all -elves="busybox blkid lsblk cryptsetup mksh zfs zpool" +elves="mksh" +tarballs=" + utils-std-ff0b40a.tar.gz + bmake-20230909.tar.gz + minised-1.16.tar.gz + make-4.4.1.tar.gz + yacc-1.9.1.tar.Z + byacc-20240109.tgz + nawk-20230909.tar.gz + mrsh-a858396b79ba217760b0982dd6f45c91c5192c3b.tar.gz +" WORKDIR="$(pwd)" +name_base="initramfs-tcc" +out_base="${WORKDIR}/${name_base}" die() { echo "[Error] die: ${*}" exit 1 } +gen_mrsh_tcc_h() { + cd mrsh-a858396b79ba217760b0982dd6f45c91c5192c3b || die + printf 'char *mrsh_tcc_cmd[] = {' + printf -- '"%s", ' /bin/tcc -Iinclude -o /bin/mrsh + find -name '*.c' | grep -v readline | grep -v example | sed -e 's;^./;";' -e 's;$;",;' | tr '\n' ' ' | sed 's;$;NULL;' + printf '};' + cd "$out_base" || die "Failed: cd $out_base" +} + +set -e + if test -e "$out_base"; then - rm -fr "$out_base" || die "Failed: rm -fr $out_base" + rm -fr "$out_base" fi mkdir -p "$out_base" || die "Failed: mkdir $out_base" cd "$out_base" || die "Failed: cd $out_base" -mkdir -p bin lib dev proc sys newroot etc || die "Failed creating base directories" + +for i in $tarballs; do + tar xf "/var/cache/distfiles/$i" || die "Failed extracting $i" +done +rm yacc-1.9.1/yacc || die + +deblob + +mkdir -p bin dev proc sys etc tmp || die "Failed creating base directories" ln -s /proc/mounts etc/mtab || die "Failed symlink for /etc/mtab" ln -s . usr -ln -s lib lib64 -ln -s lib lib32 - -if test -d "/lib/modules" -then - cp -pr "/lib/modules" lib || die "Failed copying kernel modules" -else - echo "[Warning] '/lib/modules' doesn’t exist" -fi - -mknod -m 660 dev/console c 5 1 -mknod -m 666 dev/urandom c 1 9 -mknod -m 666 dev/random c 1 8 -#mknod -m 640 dev/mem c 1 1 -mknod -m 666 dev/null c 1 3 -mknod -m 666 dev/tty c 5 0 -mknod -m 666 dev/zero c 1 5 -mknod -m 620 dev/tty1 c 4 1 +chmod 777 tmp for elf in $elves; do origin=$(command -v "$elf") cp "$origin" bin/ || die done -cp "${WORKDIR}/init" . || die "copying init" -chmod 755 init || die "init chmod" -ln bin/mksh bin/sh || die "default shell link" - for lib in $(find bin -type f -exec lddtree -l {} + | grep -v bin/ | sort | uniq); do cp "$lib" lib/ || die done -if find . -print0 | cpio --null -ov --format=newc | xz -9 --check=crc32 > "../${out_base}.cpio.xz"; then - test -e "/boot/${out_base}.cpio.xz" && mv "/boot/${out_base}.cpio.xz" "/boot/${out_base}.cpio.xz.old" - cp "../${out_base}.cpio.xz" /boot -fi +for i in fd stderr stdin stdout; do + ln -fs proc/self/$i dev/$i +done + +cp "${WORKDIR}/init.c" ./init || die "copying init" +sed -i '1i#!/bin/tcc -run' ./init || die "failed adding tcc shebang to init" +chmod 755 init || die "init chmod" + +for i in init.sh ls-stub.c mv-stub.c +do + cp -p "${WORKDIR}/$i" ./ || die "failed copying $i" +done + +QMERGE=1 qmerge -yOKv --root $PWD dev-lang/tcc || die "Failed qmerge'ing tcc" + +qlist musl | grep -v -e /bin/ -e /etc/ -e /sbin/ | while read f +do + mkdir -p "$(dirname "./$f")" || die + cp -pr "$f" "./$f" || die +done + +gen_mrsh_tcc_h > mrsh_tcc.h + +deblob -n + +find . -print0 | cpio --null -o --format=newc | xz -9 --check=crc32 > "${out_base}.cpio.xz" cd "${WORKDIR}" diff --git a/mv-stub.c b/mv-stub.c @@ -0,0 +1,39 @@ +#include <stdio.h> // fprintf, rename +#include <unistd.h> // getopt +#include <string.h> // strerror +#include <errno.h> + +int +main(int argc, char *argv[]) +{ + int c = -1; + while((c = getopt(argc, argv, ":f")) != -1) + { + switch(c) + { + case 'f': + // ignored + break; + case '?': + fprintf(stderr, "mv: Unknown option '-%c'\n", optopt); + break; + } + } + + argc -= optind; + argv += optind; + + if(argc != 2) + { + fprintf(stderr, "Usage: mv [-f] src dest\n"); + return 1; + } + + if(rename(argv[0], argv[1]) != 0) + { + fprintf(stderr, "mv: Failed renaming: %s\n", strerror(errno)); + return 1; + } + + return 0; +}