diff --git a/Buildroot/board/FOG/FOS/rootfs_overlay/etc/fstab b/Buildroot/board/FOG/FOS/rootfs_overlay/etc/fstab index 16cf1f4..2262914 100644 --- a/Buildroot/board/FOG/FOS/rootfs_overlay/etc/fstab +++ b/Buildroot/board/FOG/FOS/rootfs_overlay/etc/fstab @@ -3,7 +3,10 @@ proc /proc proc defaults 0 0 devpts /dev/pts devpts defaults,gid=5,mode=620 0 0 tmpfs /dev/shm tmpfs mode=0777 0 0 -tmpfs /tmp tmpfs mode=1777 0 0 +# size= is the default only: /etc/init.d/S00tmpsize resizes /tmp from +# ramdisk_size= (FOG_KERNEL_RAMDISK_SIZE). It caps anything only because the +# kernel builds CONFIG_TMPFS; without it "tmpfs" is ramfs and ignores size=. +tmpfs /tmp tmpfs mode=1777,size=256m 0 0 tmpfs /run tmpfs mode=0755,nosuid,nodev 0 0 sysfs /sys sysfs defaults 0 0 # Type is efivarfs, NOT efivars: "efivars" was the old CONFIG_EFI_VARS sysfs diff --git a/Buildroot/board/FOG/FOS/rootfs_overlay/etc/init.d/S00tmpsize b/Buildroot/board/FOG/FOS/rootfs_overlay/etc/init.d/S00tmpsize new file mode 100755 index 0000000..0190f37 --- /dev/null +++ b/Buildroot/board/FOG/FOS/rootfs_overlay/etc/init.d/S00tmpsize @@ -0,0 +1,50 @@ +#!/bin/bash +# +# Caps /tmp at the size the FOG server asks for (ADR-0021). +# +# FOS's root is in RAM, and so is /tmp. A runaway write to /tmp must end in +# "No space left on device", not in the client running out of memory and +# hanging. fstab mounts /tmp as tmpfs with a 256 MiB default; this resizes it +# from ramdisk_size= on the kernel command line, which the server fills from +# the FOG_KERNEL_RAMDISK_SIZE setting (in KB). That setting sized the old ext2 +# ram disk; an initramfs has no ram disk, so the same knob now sizes /tmp, and +# every FOG server already sends it. +# +# The size is clamped: never below 64 MiB, never above half of physical +# memory -- a tmpfs larger than RAM caps nothing. A missing or non-numeric +# value keeps the 256 MiB default. +# +# S00tmpsize start resize /tmp (what rcS runs) +# S00tmpsize print print the size in KB it would use, change nothing +# +# FOG_CMDLINE and FOG_MEMINFO override /proc/cmdline and /proc/meminfo, for +# tests/checks/tmp-size.sh. + +DEFAULT_KB=262144 +MIN_KB=65536 + +tmpSizeKB() { + local cmdline="${FOG_CMDLINE:-/proc/cmdline}" meminfo="${FOG_MEMINFO:-/proc/meminfo}" + local arg kb="" memkb half + for arg in $(cat "$cmdline" 2>/dev/null); do + [[ $arg == ramdisk_size=* ]] && kb="${arg#ramdisk_size=}" + done + [[ $kb =~ ^[0-9]{1,10}$ ]] || kb=$DEFAULT_KB + [[ $kb -lt $MIN_KB ]] && kb=$MIN_KB + memkb=$(awk '/^MemTotal:/ {print $2; exit}' "$meminfo" 2>/dev/null) + if [[ $memkb =~ ^[0-9]+$ ]]; then + half=$(( memkb / 2 )) + [[ $kb -gt $half ]] && kb=$half + fi + echo "$kb" +} + +case $1 in + start) + kb=$(tmpSizeKB) + mount -o remount,size="${kb}k" /tmp || echo " * Could not resize /tmp to ${kb} KB; it keeps the fstab default" + ;; + print) + tmpSizeKB + ;; +esac diff --git a/CLAUDE.md b/CLAUDE.md index ad79e9f..150d197 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -179,6 +179,7 @@ tests/checks/mdraid-gate.sh # md arrays auto-assemble only with mdraid= tests/checks/multicast-receiver.sh # udp-receiver gets --start-timeout from mcastMaxWait; an empty stream says no sender answered (fos#48) tests/checks/fixparts-binary-present.sh # fs.patch builds fixparts, which runFixparts() calls (fos#194) +tests/checks/tmp-size.sh # /tmp is real tmpfs, sized from ramdisk_size= and clamped (ADR-0021) tests/checks/perftest.sh # Performance Test task: rates, sampled image, dev/ share, bottleneck (fos#55) tests/checks/package-mirrors.sh # build.sh's package mirror fallback and hash enforcement @@ -405,6 +406,18 @@ and add a new ADR for any similarly hard-to-reverse decision: wherever `web=` comes from. The `fogpin=` parser drops base64 padding, so `fog.curl` restores it. Guarded by `tests/checks/tls-trust.sh`. +- **0021 — initramfs for every arch.** x64/x86 now ship an xz cpio initramfs + (same names, `init.xz`/`init_32.xz`) instead of an ext2 image booted as a + classic initrd: mainline is removing `root=/dev/ram0` initrd support + (January 2027), and fos#75 panics inside that loader. The server's + `root=`/`ramdisk_size=`/`rootfstype=` are ignored once `/init` exists, so no + server change is needed. Two traps: customizing the init is now an appended + overlay cpio, not a loop mount (`xz --check=crc32` still required); and + `CONFIG_SHMEM` was off, which made every "tmpfs" a size-ignoring ramfs, so + `/tmp` was never capped. SHMEM/TMPFS are now on and `S00tmpsize` sizes `/tmp` + from `ramdisk_size=` (FOG_KERNEL_RAMDISK_SIZE), clamped to half of RAM. + Guarded by `tests/checks/initrd-format.sh` and `tests/checks/tmp-size.sh`. + General conventions to preserve when editing `funcs.sh`/`partition-funcs.sh`: - Both libraries hardcode `/usr/share/fog/lib` as their own path and expect to diff --git a/build.sh b/build.sh index 79f69bc..8fbfe8c 100755 --- a/build.sh +++ b/build.sh @@ -301,11 +301,11 @@ function buildFilesystem() { cd dist || { echo "Couldn't change directory to dist"; exit 1; } case "${arch}" in x64) - compiledfile="../fssource$arch/output/images/rootfs.ext2.xz" + compiledfile="../fssource$arch/output/images/rootfs.cpio.xz" initfile='init.xz' ;; x86) - compiledfile="../fssource$arch/output/images/rootfs.ext2.xz" + compiledfile="../fssource$arch/output/images/rootfs.cpio.xz" initfile='init_32.xz' ;; arm64) diff --git a/configs/fsx64.config b/configs/fsx64.config index b82d3c9..0d8e88d 100644 --- a/configs/fsx64.config +++ b/configs/fsx64.config @@ -2943,7 +2943,18 @@ BR2_PACKAGE_PARTIMAGE=y # # BR2_TARGET_ROOTFS_BTRFS is not set # BR2_TARGET_ROOTFS_CLOOP is not set -# BR2_TARGET_ROOTFS_CPIO is not set +BR2_TARGET_ROOTFS_CPIO=y +BR2_TARGET_ROOTFS_CPIO_FULL=y +# BR2_TARGET_ROOTFS_CPIO_DRACUT is not set +# BR2_TARGET_ROOTFS_CPIO_NONE is not set +# BR2_TARGET_ROOTFS_CPIO_GZIP is not set +# BR2_TARGET_ROOTFS_CPIO_BZIP2 is not set +# BR2_TARGET_ROOTFS_CPIO_LZ4 is not set +# BR2_TARGET_ROOTFS_CPIO_LZMA is not set +# BR2_TARGET_ROOTFS_CPIO_LZO is not set +BR2_TARGET_ROOTFS_CPIO_XZ=y +# BR2_TARGET_ROOTFS_CPIO_ZSTD is not set +# BR2_TARGET_ROOTFS_CPIO_UIMAGE is not set # BR2_TARGET_ROOTFS_CRAMFS is not set # BR2_TARGET_ROOTFS_EROFS is not set BR2_TARGET_ROOTFS_EXT2=y diff --git a/configs/fsx86.config b/configs/fsx86.config index d2665ac..9cc0121 100755 --- a/configs/fsx86.config +++ b/configs/fsx86.config @@ -3050,7 +3050,18 @@ BR2_PACKAGE_PARTIMAGE=y # # BR2_TARGET_ROOTFS_BTRFS is not set # BR2_TARGET_ROOTFS_CLOOP is not set -# BR2_TARGET_ROOTFS_CPIO is not set +BR2_TARGET_ROOTFS_CPIO=y +BR2_TARGET_ROOTFS_CPIO_FULL=y +# BR2_TARGET_ROOTFS_CPIO_DRACUT is not set +# BR2_TARGET_ROOTFS_CPIO_NONE is not set +# BR2_TARGET_ROOTFS_CPIO_GZIP is not set +# BR2_TARGET_ROOTFS_CPIO_BZIP2 is not set +# BR2_TARGET_ROOTFS_CPIO_LZ4 is not set +# BR2_TARGET_ROOTFS_CPIO_LZMA is not set +# BR2_TARGET_ROOTFS_CPIO_LZO is not set +BR2_TARGET_ROOTFS_CPIO_XZ=y +# BR2_TARGET_ROOTFS_CPIO_ZSTD is not set +# BR2_TARGET_ROOTFS_CPIO_UIMAGE is not set # BR2_TARGET_ROOTFS_CRAMFS is not set # BR2_TARGET_ROOTFS_EROFS is not set BR2_TARGET_ROOTFS_EXT2=y diff --git a/configs/kernelarm64.config b/configs/kernelarm64.config index 927a354..c295725 100644 --- a/configs/kernelarm64.config +++ b/configs/kernelarm64.config @@ -194,7 +194,8 @@ CONFIG_EPOLL=y CONFIG_SIGNALFD=y CONFIG_TIMERFD=y CONFIG_EVENTFD=y -# CONFIG_SHMEM is not set +CONFIG_SHMEM=y +CONFIG_TMPFS=y CONFIG_AIO=y CONFIG_IO_URING=y # CONFIG_IO_URING_MOCK_FILE is not set diff --git a/configs/kernelx64.config b/configs/kernelx64.config index bf3030f..2cd72c8 100644 --- a/configs/kernelx64.config +++ b/configs/kernelx64.config @@ -218,7 +218,8 @@ CONFIG_EPOLL=y CONFIG_SIGNALFD=y CONFIG_TIMERFD=y CONFIG_EVENTFD=y -# CONFIG_SHMEM is not set +CONFIG_SHMEM=y +CONFIG_TMPFS=y CONFIG_AIO=y CONFIG_IO_URING=y # CONFIG_IO_URING_MOCK_FILE is not set diff --git a/configs/kernelx86.config b/configs/kernelx86.config index 0e6070d..843448c 100755 --- a/configs/kernelx86.config +++ b/configs/kernelx86.config @@ -213,7 +213,8 @@ CONFIG_EPOLL=y CONFIG_SIGNALFD=y CONFIG_TIMERFD=y CONFIG_EVENTFD=y -# CONFIG_SHMEM is not set +CONFIG_SHMEM=y +CONFIG_TMPFS=y CONFIG_AIO=y CONFIG_IO_URING=y # CONFIG_IO_URING_MOCK_FILE is not set diff --git a/docs/adr/0021-initramfs-for-every-arch.md b/docs/adr/0021-initramfs-for-every-arch.md new file mode 100644 index 0000000..e6ad79b --- /dev/null +++ b/docs/adr/0021-initramfs-for-every-arch.md @@ -0,0 +1,106 @@ +# An initramfs for every arch + +The x64 and x86 inits were ext2 filesystem images (`rootfs.ext2.xz`). The +kernel boots such an image through the classic initrd path: it copies the +bytes to `/initrd.image`, identifies the format at offset 0, writes the image +into `/dev/ram0`, and mounts that as `root=/dev/ram0`. arm64 already shipped a +cpio initramfs (`arm_init.cpio.gz`). + +We decided **every arch ships a cpio initramfs. x64 and x86 now build +`rootfs.cpio.xz`, published under the same names, `init.xz` and `init_32.xz`.** + +## Why + +Mainline is removing the classic initrd path: + +- v7.0 removed its `linuxrc` half + ([vfs initrd pull](https://lkml.iu.edu/hypermail/linux/kernel/2602.0/09468.html), + [series](https://lkml.iu.edu/2511.2/11639.html)). +- 7.2 logs "using deprecated initrd support, will be removed in January 2027; + use initramfs instead" on every boot (`init/do_mounts_initrd.c`). 6.18.59 + logs "will be removed soon". +- Initramfs is not affected. + +A kernel after the removal cannot boot an ext2 init at all. The 6.18 LTS line +keeps the path, so stable FOS is not broken today. Any move to a 7.x kernel, +such as the fos#110 experiment, needs this first. + +fos#75 is the second reason. Its panic, "RAMDISK: Couldn't find valid RAM disk +image starting at 0", comes from `init/do_mounts_rd.c`, the classic loader. +An initramfs is unpacked by different code (`init/initramfs.c`) and never +reaches that function. Whether this fixes fos#75 is **not yet known**: the +reporters' machines are the only test. + +## Compatibility + +- **Server:** nothing changes. With an initramfs that contains `/init`, the + kernel runs it and ignores `root=/dev/ram0`, `ramdisk_size=` and + `rootfstype=ext4`, which FOG's boot menus and `create-usb-image.sh` still + send. Buildroot adds that `/init`; it execs `/sbin/init`. +- **Kernel:** the 6.18 kernels FOS ships unpack an xz initramfs + (`CONFIG_RD_XZ=y`, `CONFIG_BLK_DEV_INITRD=y`). An older FOS kernel paired + with a new init also works, for the same reason. +- **The root filesystem** is now the kernel's rootfs (ramfs while `root=` is + on the command line, `init/do_mounts.c` `init_rootfs()`) instead of a + 256 MiB ext2 ram disk. Nothing large is written there: logs rotate, and + the scripts write a few small files under `/etc` and `/usr/share/fog/lib`. + `inittab`'s `remount,rw /` still succeeds. + +## /tmp is capped, from the setting the server already sends + +`/tmp` was never on the ram disk; fstab mounts it as tmpfs. But every kernel +config had `CONFIG_SHMEM` off, and without it the kernel registers "tmpfs" as +ramfs, which ignores `size=`. So `/tmp` had no limit before this ADR either: +a runaway write grew until the client ran out of memory. `df` printing 0 for +`/tmp` on a live boot is how this was found. + +- All three kernel configs now build `CONFIG_SHMEM` and `CONFIG_TMPFS`. +- fstab gives `/tmp` `size=256m` as a default. +- `etc/init.d/S00tmpsize` resizes `/tmp` from `ramdisk_size=` on the kernel + command line. The server fills that from **FOG_KERNEL_RAMDISK_SIZE** (KB, + default 275000). That setting sized the ext2 ram disk; an initramfs has no + ram disk, so the same setting now sizes `/tmp`, and every FOG server, old + or new, already sends it. An admin who wants 512 MiB or 1 GiB sets it there. +- The size is clamped to at least 64 MiB and at most half of physical + memory. A larger tmpfs would cap nothing. A missing or non-numeric value + keeps 256 MiB. + +Guarded by `tests/checks/tmp-size.sh`. + +## Customizing the init + +The long-standing forum method -- `xz -d`, loop-mount the ext2 image, edit, +unmount, `xz -C crc32` -- does not work on a cpio: there is no filesystem to +mount. Two methods replace it: + +- **Append an overlay (preferred).** The kernel unpacks concatenated cpio + archives in order, and a later file replaces an earlier one. Put only the + changed files in a small cpio and append it: + + ``` + cd overlay && find . | cpio -o -H newc -R 0:0 | xz -9 --check=crc32 > ../overlay.cpio.xz + cat init.xz overlay.cpio.xz > init-custom.xz + ``` + + No root, no repack of the whole init, and the same overlay re-applies to + every new release unchanged. Verified in QEMU on 6.18.59: a script added + this way ran at boot. +- **Unpack and repack**, for removing files: `xz -dc init.xz | cpio -idm` as + root, edit, then `find . | cpio -o -H newc | xz -9 --check=crc32`. + +`--check=crc32` is required either way, as it was before: the kernel's xz +decoder supports only CRC32 or no check, not xz's default CRC64 +(`Documentation/staging/xz.rst`). + +Postinit scripts on the storage node (`/images/postinitscripts`) need no +change; they never touched the init file. + +## What would make us revisit this + +A machine that boots the ext2 init but not the initramfs. The experimental +release exists to find one before this reaches Latest. + +## Guard + +`tests/checks/initrd-format.sh` fails when any arch ships a non-cpio artifact, +and still pairs each artifact's compression with the kernel's decompressor. diff --git a/tests/README.md b/tests/README.md index dd4ec3e..4e6750c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -308,6 +308,12 @@ tests/checks/fixparts-binary-present.sh # it. A symbol with no Kconfig definition is # dropped silently, which is how every init from # 2023-12 on shipped without it. See fos#194 +tests/checks/tmp-size.sh + # /tmp is a real, capped tmpfs: SHMEM/TMPFS in + # every kernel config (-b: post-oldconfig), a + # size= default in fstab, and S00tmpsize's size + # from ramdisk_size= clamped to 64 MiB..half of + # RAM. See ADR-0021 tests/checks/perftest.sh # fog.perftest (Performance Test task): MB/s and # GB/min arithmetic, the dev/ share mapped to the diff --git a/tests/checks/initrd-format.sh b/tests/checks/initrd-format.sh index 2070a06..98f12d7 100755 --- a/tests/checks/initrd-format.sh +++ b/tests/checks/initrd-format.sh @@ -8,7 +8,7 @@ # # in any kernelsource/ present # # Why this exists. build.sh publishes a *different* init image per arch -- -# rootfs.ext2.xz for x64/x86, rootfs.cpio.gz for arm64 -- and each of those +# rootfs.cpio.xz for x64/x86, rootfs.cpio.gz for arm64 -- and each of those # compressions needs its own CONFIG_RD_* symbol in the kernel that has to unpack # it. Nothing in the tree tied the two ends together, and they drifted: the # arm64 kernel shipped with `# CONFIG_RD_GZIP is not set` while the arm64 init @@ -164,6 +164,14 @@ for arch in x64 x86 arm64; do brsuffix="${row%%|*}" rdsuffix="${row#*|}" echo "arch $arch ships $artifact" + # A filesystem image (ext2) is booted through the classic initrd path, + # root=/dev/ram0, which mainline deprecated and schedules for removal in + # January 2027; 7.2 warns on every boot. It is also the code path fos#75 + # panics in. Every arch ships an initramfs (cpio) instead -- ADR-0021. + if [[ $imgtype != cpio ]]; then + echo "FAIL [build.sh] arch $arch ships $artifact, a classic initrd image; ship a cpio initramfs (ADR-0021)" + fails=$((fails + 1)) + fi checkBuildroot "$arch" "$imgtype" "$brsuffix" checkKernel "configs/kernel${arch}.config" \ "$REPO/configs/kernel${arch}.config" "$rdsuffix" diff --git a/tests/checks/tmp-size.sh b/tests/checks/tmp-size.sh new file mode 100644 index 0000000..2f66358 --- /dev/null +++ b/tests/checks/tmp-size.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# +# Assertion harness for the /tmp size cap (ADR-0021). +# +# tests/checks/tmp-size.sh # check the committed configs +# tests/checks/tmp-size.sh -b # also check post-oldconfig .config +# # in any kernelsource/ present +# +# Why this exists. Every kernel config had CONFIG_SHMEM off, and without it +# the kernel registers "tmpfs" as plain ramfs: fstab's tmpfs /tmp accepted a +# size= and enforced nothing, so a runaway write to /tmp grew until the client +# ran out of memory. df printed 0 for /tmp, which is what gave it away. The +# ext2 ram disk never bounded /tmp either; it only bounded the root. +# +# Three things have to hold together, and each is checked here: the kernel +# builds real tmpfs (SHMEM and TMPFS, and -b proves they survived oldconfig); +# fstab gives /tmp a size= default; and S00tmpsize turns ramdisk_size= (the +# server's FOG_KERNEL_RAMDISK_SIZE) into a size that is clamped to sane +# bounds. The last is run as shipped, through its own "print" mode. + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO="$HERE/../.." +OVERLAY="$REPO/Buildroot/board/FOG/FOS/rootfs_overlay" +SCRIPT="$OVERLAY/etc/init.d/S00tmpsize" + +checkBuilt=0 +[[ ${1:-} == -b ]] && checkBuilt=1 + +fails=0 +checked=0 +ck() { + checked=$((checked + 1)) + if [[ $2 != 1 ]]; then + echo "FAIL $1" + fails=$((fails + 1)) + fi +} +eq() { [[ $1 == "$2" ]] && echo 1 || echo 0; } + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +# size +size() { + echo "$1" > "$tmp/cmdline" + if [[ $2 == none ]]; then + rm -f "$tmp/meminfo" + else + printf 'MemTotal: %s kB\nMemFree: 1 kB\n' "$2" > "$tmp/meminfo" + fi + FOG_CMDLINE="$tmp/cmdline" FOG_MEMINFO="$tmp/meminfo" "$SCRIPT" print +} + +BASE="loglevel=4 initrd=init.xz root=/dev/ram0 rw" +ck "1 FOG's default 275000 on a 4 GiB client" "$(eq "$(size "$BASE ramdisk_size=275000 web=x" 4000000)" 275000)" +ck "2 no ramdisk_size keeps 256 MiB" "$(eq "$(size "$BASE web=x" 4000000)" 262144)" +ck "3 a 1 GiB setting on a 1 GiB client is half of RAM" "$(eq "$(size "$BASE ramdisk_size=1048576" 990264)" 495132)" +ck "4 a 512 MiB setting on 8 GiB is honored" "$(eq "$(size "$BASE ramdisk_size=524288" 8000000)" 524288)" +ck "5 below 64 MiB is raised to 64 MiB" "$(eq "$(size "$BASE ramdisk_size=1000" 4000000)" 65536)" +ck "6 non-numeric keeps the default" "$(eq "$(size "$BASE ramdisk_size=abc" 4000000)" 262144)" +ck "7 trailing garbage keeps the default" "$(eq "$(size "$BASE ramdisk_size=275000x" 4000000)" 262144)" +ck "8 empty value keeps the default" "$(eq "$(size "$BASE ramdisk_size=" 4000000)" 262144)" +ck "9 unreadable meminfo: no clamp, setting kept" "$(eq "$(size "$BASE ramdisk_size=524288" none)" 524288)" +ck "10 a name that merely ends in ramdisk_size is ignored" "$(eq "$(size "$BASE xramdisk_size=999999" 4000000)" 262144)" + +ck "11 S00tmpsize is executable (rcS runs it as \$i start)" "$([[ -x $SCRIPT ]] && echo 1 || echo 0)" +grep -qE '^tmpfs[[:space:]]+/tmp[[:space:]]+tmpfs[[:space:]]+[^[:space:]]*size=' "$OVERLAY/etc/fstab" +ck "12 fstab gives /tmp a size= default" "$(eq $? 0)" + +# The kernel half: without these, every size= above is ignored. +kernelHasTmpfs() { + local label="$1" file="$2" sym + [[ -f $file ]] || return 0 + for sym in CONFIG_SHMEM=y CONFIG_TMPFS=y; do + grep -qxF "$sym" "$file" + ck "13 [$label] carries $sym" "$(eq $? 0)" + done +} +for arch in x64 x86 arm64; do + kernelHasTmpfs "configs/kernel${arch}.config" "$REPO/configs/kernel${arch}.config" + [[ $checkBuilt -eq 1 ]] && kernelHasTmpfs "kernelsource${arch}/.config" "$REPO/kernelsource${arch}/.config" +done + +echo +echo "passed: $((checked - fails)) failed: $fails" +[[ $fails -eq 0 ]]