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.
This commit is contained in:
2026-07-16 12:20:14 +09:00
parent 2b64199007
commit d5561184f0
10 changed files with 170 additions and 82 deletions
+3 -2
View File
@@ -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"
"""
+8 -2
View File
@@ -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"
"""
+9 -2
View File
@@ -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" }
"""
+119
View File
@@ -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
+26 -71
View File
@@ -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 \