From d5561184f083e389e4db0e92874b85b7c47fcf49 Mon Sep 17 00:00:00 2001 From: vasilito Date: Thu, 16 Jul 2026 12:20:14 +0900 Subject: [PATCH] boot: restore mini ISO to a working login prompt + build-system fixes Mini now boots (q35/KVM) all the way to the getty "Red Bear login:" prompt on VT2 with zero kernel panics. Root-cause fixes across submodules + config: - config: 00_base.service zsh->mkdir (a full shell startup hangs an init oneshot; the earlier 'echo/mkdir also hang' result was a silent config override running zsh). 00_ptyd.service notify->{scheme=pty} (ptyd signals readiness via its scheme fd; notify made init block on a byte forever). - kernel: wrmsr_safe/rdmsr_safe #GP recovery so /scheme/sys/msr can't panic the kernel (cpufreqd's P-state MSR write #GP'd on KVM, halting the boot before login). base: ptyd scheme type + hwd ACPI enum -> debug (noise). installer: warn on config [[files]] override. relibc/bootloader: prior getrlimit + warning-demotion pointers. - build-redbear.sh: fix host-fstools staleness (installer/redoxfs never rebuilt on source change), prefix rebuild-every-build (touch libc.a), add concurrent-build flock. Submodule pointers: base 8aa8616d, installer 460d9530, kernel 155d01b1, relibc a7663b3a, bootloader 6e119641. --- config/base.toml | 5 +- config/redbear-legacy-base.toml | 10 ++- config/redbear-mini.toml | 11 ++- local/scripts/build-redbear.sh | 119 +++++++++++++++++++++++++++ local/sources/base | 2 +- local/sources/bootloader | 2 +- local/sources/installer | 2 +- local/sources/kernel | 2 +- local/sources/relibc | 2 +- recipes/core/base-initfs/recipe.toml | 97 ++++++---------------- 10 files changed, 170 insertions(+), 82 deletions(-) diff --git a/config/base.toml b/config/base.toml index 499462d718..beba4180db 100644 --- a/config/base.toml +++ b/config/base.toml @@ -29,8 +29,9 @@ data = """ description = "Base environment setup (tmpdir)" [service] -cmd = "zsh" -args = ["-c", "rm -rf /tmp; mkdir -m a=rwxt /tmp"] +# Create the world-writable, sticky /tmp with mkdir (uutils, Rust). +cmd = "mkdir" +args = ["-p", "-m", "a=rwxt", "/tmp"] type = "oneshot" """ diff --git a/config/redbear-legacy-base.toml b/config/redbear-legacy-base.toml index 57f902994c..1d8f24834f 100644 --- a/config/redbear-legacy-base.toml +++ b/config/redbear-legacy-base.toml @@ -22,8 +22,14 @@ data = """ description = "Base environment setup (tmpdir)" [service] -cmd = "zsh" -args = ["-c", "rm -rf /tmp; mkdir -m a=rwxt /tmp"] +# Create the world-writable, sticky /tmp with mkdir (uutils, Rust). This must +# NOT use zsh: a full interactive shell startup hangs the init oneshot here +# (before the login stack even starts), whereas a simple dynamically-linked +# /usr binary like mkdir/echo completes cleanly after switchroot. See the +# usr-dynamic-service-spawn-hang investigation — the hang was zsh-specific, +# not a general /usr spawn failure. +cmd = "mkdir" +args = ["-p", "-m", "a=rwxt", "/tmp"] type = "oneshot" """ diff --git a/config/redbear-mini.toml b/config/redbear-mini.toml index eac7439c12..abc70059ef 100644 --- a/config/redbear-mini.toml +++ b/config/redbear-mini.toml @@ -597,9 +597,16 @@ type = "oneshot" path = "/etc/init.d/00_ptyd.service" data = """ [unit] -description = "Pseudo-terminal daemon (non-blocking on live-mini)" +description = "Pseudo-terminal daemon" [service] cmd = "ptyd" -type = "notify" +# ptyd registers the "pty" scheme and signals readiness by sending its scheme +# cap fd back to init (redox_scheme ready_sync_scheme / ready_with_fd). init's +# scheme-service startup does a call_ro(FD) to receive that fd and register the +# scheme. It MUST therefore be type={scheme="pty"} — NOT "notify". With "notify" +# init does a plain readiness-byte read_exact that ptyd never sends, so init +# blocks forever here (the observed post-switchroot boot hang before the console +# stack). See ptyd/src/main.rs: it is on the SchemeDaemon/ready_with_fd path. +type = { scheme = "pty" } """ diff --git a/local/scripts/build-redbear.sh b/local/scripts/build-redbear.sh index b0be86ca40..3a05be17e8 100755 --- a/local/scripts/build-redbear.sh +++ b/local/scripts/build-redbear.sh @@ -3,6 +3,22 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# Concurrent-build guard. Two builds running at once corrupt each other's +# cookbook caches, repo/ artefacts and prefix, and produce non-deterministic +# ISOs. Take an exclusive, non-blocking flock on a lockfile; if another build +# already holds it, refuse rather than silently racing. Set REDBEAR_ALLOW_ +# CONCURRENT=1 to override (e.g. builds targeting isolated output dirs). +if [ -z "${REDBEAR_ALLOW_CONCURRENT:-}" ] && command -v flock >/dev/null 2>&1; then + REDBEAR_BUILD_LOCK="${TMPDIR:-/tmp}/redbear-build.lock" + exec 9>"$REDBEAR_BUILD_LOCK" + if ! flock -n 9; then + echo ">>> ERROR: another RedBear build is already running (lock: $REDBEAR_BUILD_LOCK)." >&2 + echo ">>> Wait for it to finish, or set REDBEAR_ALLOW_CONCURRENT=1 to override." >&2 + exit 1 + fi +fi + source "$SCRIPT_DIR/lib/relibc-surface.sh" # ============================================================================= @@ -564,6 +580,7 @@ echo ">>> This may take 30-60 minutes on first build..." # packages that don't use autotools. if [ "$NO_CACHE" != "1" ]; then STALE_DETECTED=0 + RELIBC_INVALIDATED=0 for src in relibc kernel base bootloader installer; do src_dir="$PROJECT_ROOT/local/sources/$src" pkgar="$PROJECT_ROOT/repo/x86_64-unknown-redox/$src.pkgar" @@ -594,6 +611,7 @@ if [ "$NO_CACHE" != "1" ]; then rm -f "$PROJECT_ROOT/repo/x86_64-unknown-redox/$src".* find "$PROJECT_ROOT/recipes" -path "*/$src/target" -type d -exec rm -rf {} + 2>/dev/null || true STALE_DETECTED=1 + [ "$src" = "relibc" ] && RELIBC_INVALIDATED=1 fi fi done @@ -605,6 +623,97 @@ if [ "$NO_CACHE" != "1" ]; then -o -path "*/target/x86_64-unknown-redox/sysroot" \) \ -type d -exec rm -rf {} + 2>/dev/null || true fi + + # Downstream ABI-staleness prevention (relibc consumers). + # + # The loop above rebuilds relibc/kernel/base when THEIR OWN sources + # change, but statically-linked consumers keep their pkgars as long as + # THEIR sources are unchanged. relibc is both the C runtime AND the + # Redox scheme/syscall protocol shim, so a consumer built against an + # older relibc bakes in the OLD protocol. For boot-critical binaries + # copied into the initfs from the sysroot by + # recipes/core/base-initfs/recipe.toml this skew is FATAL, not cosmetic: + # - a stale getty issued the pre-"userspace fd allocation" two-step + # scheme-open and got EACCES opening /scheme/rand (login dead); + # - a stale redoxfs panicked "failed to create pipe". + # Both shipped in an ISO whose freshly-built daemons (randd/ptyd/...) + # already spoke the new protocol — a silent, hard-to-trace mismatch. + # + # Rule: invalidate an ABI-critical consumer when it would link an older + # relibc than the one this build will ship. That happens either because + # relibc is about to rebuild (RELIBC_INVALIDATED=1, or its pkgar is + # already gone) or because a prior run left relibc.pkgar newer than the + # consumer's pkgar (the exact getty/redoxfs case: relibc rebuilt hours + # after userutils/redoxfs). + # + # NB: if you add a binary to the base-initfs sysroot copy-set, add its + # recipe name to ABI_CRITICAL_PKGS below. + ABI_CRITICAL_PKGS="redoxfs userutils ion driver-manager" + RELIBC_PKGAR="$PROJECT_ROOT/repo/x86_64-unknown-redox/relibc.pkgar" + relibc_rebuilding=0 + if [ "$RELIBC_INVALIDATED" = "1" ] || [ ! -f "$RELIBC_PKGAR" ]; then + relibc_rebuilding=1 + fi + relibc_ts=0 + [ -f "$RELIBC_PKGAR" ] && relibc_ts=$(stat -c %Y "$RELIBC_PKGAR" 2>/dev/null || echo "0") + ABI_STALE=0 + for pkg in $ABI_CRITICAL_PKGS; do + pkg_pkgar="$PROJECT_ROOT/repo/x86_64-unknown-redox/$pkg.pkgar" + [ -f "$pkg_pkgar" ] || continue + pkg_ts=$(stat -c %Y "$pkg_pkgar" 2>/dev/null || echo "0") + if [ "$relibc_rebuilding" = "1" ] || { [ "$relibc_ts" != "0" ] && [ "$relibc_ts" -gt "$pkg_ts" ]; }; then + echo ">>> ABI-stale $pkg detected (would link an older relibc than this build ships); invalidating..." + rm -f "$PROJECT_ROOT/repo/x86_64-unknown-redox/$pkg".* + find "$PROJECT_ROOT/recipes" "$PROJECT_ROOT/local/recipes" \ + -path "*/$pkg/target" -type d -exec rm -rf {} + 2>/dev/null || true + ABI_STALE=1 + fi + done + # base-initfs bundles the (now-invalidated) binaries into the initfs + # image, so it must re-cook to pick up the freshly relinked copies. + if [ "$ABI_STALE" = "1" ]; then + echo ">>> Invalidating base-initfs so it re-bundles the refreshed initfs binaries..." + rm -f "$PROJECT_ROOT/repo/x86_64-unknown-redox/base-initfs".* + find "$PROJECT_ROOT/recipes" -path "*/base-initfs/target" -type d -exec rm -rf {} + 2>/dev/null || true + fi +fi + +# Host fstools staleness (redox_installer / redoxfs FUSE used by `make live`). +# +# `make live` assembles the image with the HOST-compiled redox_installer from +# build/fstools/bin, built once by the `fstools` make target via +# `cargo install --path local/sources/installer`. That target is a DIRECTORY +# target (build/fstools) with NO prerequisite on the installer/redoxfs sources, +# so once build/fstools exists make never re-runs the cargo install — a source +# change to either fork silently never reaches image assembly. This bit us: an +# installer change to config-override handling appeared to "do nothing" because +# make live kept running a 6-day-old installer binary. Mirror the recipe +# staleness logic: if either fork's committed HEAD or working tree is newer +# than build/fstools, remove it so make rebuilds the host tools. Runs +# unconditionally (the NO_CACHE repo clean above does not touch build/fstools). +FSTOOLS_DIR="$PROJECT_ROOT/build/fstools" +if [ -d "$FSTOOLS_DIR" ]; then + fstools_ts=$(stat -c %Y "$FSTOOLS_DIR" 2>/dev/null || echo "0") + fstools_stale=0 + for src in installer redoxfs; do + src_dir="$PROJECT_ROOT/local/sources/$src" + [ -d "$src_dir/.git" ] || continue + src_ts=$(git -C "$src_dir" log -1 --format=%ct HEAD 2>/dev/null || echo "0") + src_dirty=0 + if ! git -C "$src_dir" diff --quiet HEAD 2>/dev/null \ + || ! git -C "$src_dir" diff --cached --quiet HEAD 2>/dev/null \ + || [ -n "$(git -C "$src_dir" ls-files --others --exclude-standard 2>/dev/null)" ]; then + src_dirty=1 + fi + if { [ "$src_ts" != "0" ] && [ "$fstools_ts" != "0" ] && [ "$src_ts" -gt "$fstools_ts" ]; } \ + || [ "$src_dirty" = "1" ]; then + echo ">>> Stale host fstools detected ($src newer than build/fstools); rebuilding host installer/redoxfs..." + fstools_stale=1 + fi + done + if [ "$fstools_stale" = "1" ]; then + rm -rf "$FSTOOLS_DIR" + fi fi PREFIX_LIBC="$PROJECT_ROOT/prefix/x86_64-unknown-redox/sysroot/x86_64-unknown-redox/lib/libc.a" @@ -657,6 +766,16 @@ if [ "$STALE_PREFIX" = "1" ] && [ -z "${REDBEAR_SKIP_PREFIX_REBUILD:-}" ]; then exit 1 fi echo ">>> Prefix rebuilt successfully." + # Stamp libc.a to now so the staleness check above does not re-fire on + # every subsequent build. `make prefix` extracts a prebuilt toolchain + # tarball and is a no-op when the prefix is already present, so without + # this touch libc.a keeps its old mtime, stays "older" than the fork's + # git commit timestamp, and the (expensive) prefix step runs on every + # build forever. Touching after a successful rebuild means we only rebuild + # again when the fork actually advances past this point. The uncommitted- + # changes (src_dirty) path above is unaffected: it intentionally rebuilds + # whenever the working tree is dirty, regardless of mtime. + touch "$PREFIX_LIBC" STALE_PREFIX=0 fi diff --git a/local/sources/base b/local/sources/base index db724cda70..8aa8616d5e 160000 --- a/local/sources/base +++ b/local/sources/base @@ -1 +1 @@ -Subproject commit db724cda700a299c69823359445271b80522b5f3 +Subproject commit 8aa8616d5e594d4afac04a7f48fa6e7a8af32045 diff --git a/local/sources/bootloader b/local/sources/bootloader index c78c3a63c0..6e11964121 160000 --- a/local/sources/bootloader +++ b/local/sources/bootloader @@ -1 +1 @@ -Subproject commit c78c3a63c032ef1a989eaff1a91cc112f4313c0f +Subproject commit 6e11964121615bfabadf9f3c5cf6faafad6c5186 diff --git a/local/sources/installer b/local/sources/installer index 1e9dcdee98..460d9530a5 160000 --- a/local/sources/installer +++ b/local/sources/installer @@ -1 +1 @@ -Subproject commit 1e9dcdee9809bba3338ec8f66f1a21d788838340 +Subproject commit 460d9530a57e2d9f3ff07e1a9166564e4960110b diff --git a/local/sources/kernel b/local/sources/kernel index b5a7bf5581..155d01b11e 160000 --- a/local/sources/kernel +++ b/local/sources/kernel @@ -1 +1 @@ -Subproject commit b5a7bf5581cc31b61fd444bbe8798d5fadfac4d4 +Subproject commit 155d01b11e43a1bebeee7bbc54a15835c684ddfa diff --git a/local/sources/relibc b/local/sources/relibc index f46e2a31a7..a7663b3adf 160000 --- a/local/sources/relibc +++ b/local/sources/relibc @@ -1 +1 @@ -Subproject commit f46e2a31a7a23f2fa71b4b57c2224e901f739271 +Subproject commit a7663b3adfe865ed7447e91353c43fa9e04198b5 diff --git a/recipes/core/base-initfs/recipe.toml b/recipes/core/base-initfs/recipe.toml index 13127e5b09..50198889a6 100644 --- a/recipes/core/base-initfs/recipe.toml +++ b/recipes/core/base-initfs/recipe.toml @@ -10,6 +10,11 @@ dependencies = [ "redoxfs", "ion", "driver-manager", + # userutils provides `getty`, copied into the bare initfs from the + # sysroot below. Without this dependency getty is absent from the + # sysroot at cook time, the copy is silently skipped, and the bare + # ISO has no login (init fails to exec /scheme/initfs/bin/getty). + "userutils", ] script = """ set -eo pipefail @@ -60,17 +65,19 @@ aarch64_bins() bare_initfs() { - BINS=( - init - logd - ramfs - randd - zerod - ptyd - ) - # `getty` is not a member of the base workspace — it lives in - # userutils. We copy the prebuilt binary from the sysroot after - # the cargo build step (see the EXTRA_SYSROOT_BINS copy below). + # Per user directive, the bare initfs ships the FULL driver set — + # identical to the standard x86 initfs (pcid, pcid-spawner, ahcid, ided, + # nvmed, virtio-*, ps2d, usb*, e1000d, rtl8168d, acpid, hwd, rtcd, lived) + # plus the framebuffer console stack (inputd, vesad, fbcond, fbbootlogd). + # Every driver initialises at boot and redoxfs can find/mount a real disk. + # + # The interactive login is NOT run from the initfs: after the initfs + # brings up the drivers + framebuffer console it switchroots to /usr, + # where the rootfs config (config/minimal.toml, included by redbear-bare) + # provides 00_ptyd.service + 29_activate_console + 30_console.service + # (getty -> login -> zsh) with a real /etc/passwd and shell. So the bare + # initfs matches the mainline initfs exactly — no getty/login/ptyd here. + x86_common_bins } case "${TARGET}" in @@ -94,61 +101,11 @@ esac rm -rf "${COOKBOOK_BUILD}/initfs" mkdir -p "${COOKBOOK_BUILD}/initfs/lib/init.d" -if [ "${REDBEAR_BARE_INITFS:-0}" = "1" ]; then - # Bare initfs boots the absolute minimum: logd, ramfs, randd, - # zerod, ptyd, getty. Only copy the init.d units that reference - # binaries we actually built (no fbcond, vesad, pcid, acpid, - # fbbootlogd, hwd, inputd, etc. — those daemons aren't in the - # bare BINS, and the init runtime would otherwise try to start - # them and hit "binary not found" or busy-loop on the debug - # console scheme). - for unit in "${COOKBOOK_SOURCE}/init.initfs.d"/*; do - case "$(basename "$unit")" in - 00_logd.service|00_randd.service|00_zerod.service|00_runtime.target|ramfs@.service|90_initfs.target|50_rootfs.service) - cp -v "$unit" "${COOKBOOK_BUILD}/initfs/lib/init.d/" - ;; - esac - done - # Bare initfs also needs ptyd (built from BINS) and getty (copied - # from sysroot below). Provide minimal unit files for both. - cat > "${COOKBOOK_BUILD}/initfs/lib/init.d/00_ptyd.service" <<'EOF' -[unit] -description = "Pseudo-terminal daemon" -default_dependencies = false -[service] -cmd = "ptyd" -type = { scheme = "pty" } -EOF - cat > "${COOKBOOK_BUILD}/initfs/lib/init.d/30_getty.service" <<'EOF' -[unit] -description = "Console getty (login on /scheme/console/2)" -default_dependencies = false -requires_weak = ["00_ptyd.service"] -[service] -cmd = "getty" -args = ["2"] -type = "oneshot_async" -EOF - # Bare runtime.target — ptyd and getty are not pulled in by the - # mainline 00_runtime.target (which references nulld/rtcd that we - # filtered out). Add them as weak requires so init schedules them - # before reaching 90_initfs.target. - cat > "${COOKBOOK_BUILD}/initfs/lib/init.d/00_runtime.target" <<'EOF' -[unit] -description = "Services that relibc needs to function" -default_dependencies = false -requires_weak = [ - "00_logd.service", - "00_randd.service", - "00_zerod.service", - "00_ptyd.service", - "30_getty.service", - "ramfs@logging.service", -] -EOF -else - cp "${COOKBOOK_SOURCE}/init.initfs.d"/* "${COOKBOOK_BUILD}/initfs/lib/init.d/" -fi +# The bare initfs now uses the full mainline unit graph (all driver + +# console + rootfs units). The interactive login runs from /usr after +# switchroot (see bare_initfs() above), so no initfs-specific getty/ptyd +# units are injected — bare and non-bare copy the identical unit set. +cp "${COOKBOOK_SOURCE}/init.initfs.d"/* "${COOKBOOK_BUILD}/initfs/lib/init.d/" mkdir -pv "${COOKBOOK_BUILD}/initfs/lib/drivers.d" cp -v "${COOKBOOK_SOURCE}/drivers/initfs-storage.toml" "${COOKBOOK_BUILD}/initfs/lib/drivers.d/00-storage.toml" @@ -178,11 +135,9 @@ done cp "${COOKBOOK_SYSROOT}/usr/bin/redoxfs" "${COOKBOOK_BUILD}/initfs/bin" cp "${COOKBOOK_SYSROOT}/usr/bin/ion" "${COOKBOOK_BUILD}/initfs/bin" cp "${COOKBOOK_SYSROOT}/usr/bin/driver-manager" "${COOKBOOK_BUILD}/initfs/bin" -# Bare initfs needs `getty` (built by userutils, not the base -# workspace) so the initfs shell can launch a login prompt. -if [ "${REDBEAR_BARE_INITFS:-0}" = "1" ] && [ -f "${COOKBOOK_SYSROOT}/usr/bin/getty" ]; then - cp "${COOKBOOK_SYSROOT}/usr/bin/getty" "${COOKBOOK_BUILD}/initfs/bin" -fi +# NB: getty/login are intentionally NOT copied into the initfs. The +# interactive login runs from /usr after switchroot (config/minimal.toml's +# 30_console.service), where a real /etc/passwd, login and shell exist. ARCH="$(echo "${GNU_TARGET}" | cut -d - -f1)" RUSTFLAGS="$RUSTFLAGS -Ctarget-feature=+crt-static -Clink-arg=-nostartfiles -Clink-arg=-nostdlib" cargo \