Files
RedBear-OS/local/docs/INIT-NAMESPACE-MANAGER-SCALABILITY-PLAN.md
T
vasilito 0ddc032202 acpid hardening + initnsmgr concurrency design
- Bump submodule/base to the acpid AML-handler hardening (bounded stall,
  mutex owner-check, static _PSS/_PSD/_CST/_CPC cache, panic-free scheme
  path, observability). Proven NOT a regression: a mutex-only baseline
  wedges identically under load in a 3/3 framebuffer-ground-truth test,
  so the residual under-load boot wedge is head-of-line blocking in
  initnsmgr, not acpid.

- Add local/docs/INITNSMGR-CONCURRENCY-DESIGN.md: the concrete
  worker-offload design (Design A) that decouples the blocking openat
  from the initnsmgr dispatch loop, plus Design B (kernel O_NONBLOCK +
  deferred single-thread), a staged plan, and validation rules. Key
  finding baked in: redox_rt's Mutex is a spinlock, so the cap_fd must be
  resolved under a short lock and the openat run with the lock released.

- Update INIT-NAMESPACE-MANAGER-SCALABILITY-PLAN.md with the acpid
  hardening section (done vs the still-deferred #1 transport decoupling).

- Add local/patches/wip-initnsmgr/step1-send-refactor.patch: the
  compiled-but-not-yet-boot-validated Step 1 (Rc<RefCell> -> Arc<Mutex>
  Send refactor) of initnsmgr, saved durably. It is intentionally NOT in
  the base gitlink: bootstrap is the earliest-boot component and this
  must be boot-validated on an idle host (the current host is under heavy
  external load) before landing. Steps 2-5 (worker bring-up) likewise
  need an idle host.
2026-07-21 08:34:22 +09:00

14 KiB

Red Bear OS Init Namespace Manager Scalability Plan

Purpose

This document is the canonical analysis and execution plan for a systemic reliability defect in the boot/console path: the single-threaded, blocking design of the init namespace manager (initnsmgr) turns any transient slowness in one scheme provider into a whole-system wedge.

It was written after root-causing the long-standing "redbear-mini never reaches a usable brush login" symptom (16+ prior investigation sessions with shifting, incorrect theories). The immediate trigger was a concrete acpid bug (fixed — see below), but the reason a single stuck daemon could freeze the entire system is the architecture described here. That amplifier is still present.

When another document discusses mini/console login reliability, boot slowness, "flaky boot hangs", or the namespace/scheme-open path, prefer this file for the current robustness judgment and the execution order.

Ground state (2026-07-21)

  • redbear-mini logs into brush and executes commands. Verified by framebuffer ground truth: Red Bear login: user → MOTD → user@redbear: $echo RB=$((21*2))=OKRB=42=OK. When it wins the race, it is fast (login prompt ~12s, brush prompt ~16s in QEMU q35/KVM).
  • The dominant blocker — an acpid AML-mutex self-deadlock — is fixed (local/sources/base commit d78fd44a). See "Trigger 1" below.
  • A residual, architectural flakiness remains: under load, boot occasionally wedges at the fbcond→vesad display handoff (display.vesa:v2/2). This is not a vesad bug; it is the namespace manager amplifying vesad's transient unreadiness. See "The amplifier" and "Execution plan".

The two layers

Trigger 1 — acpid AML mutex (FIXED, d78fd44a)

cpufreqd reads /scheme/acpi/processor/CPUn/pss (ACPI P-state data). Evaluating that AML runs _ACQ on an ACPI mutex. The acquire handler in local/sources/base/drivers/acpid/src/aml_physmem.rs had two bugs:

  1. Timeout units. ACPI _ACQ timeouts are milliseconds; 0xFFFF is the spec's "wait forever" (ACPICA ACPI_WAIT_FOREVER, include/acpi/actypes.h:459). The code multiplied the value by 1000, treating it as seconds — 0xFFFF became ~18 hours.
  2. No ownership / no recursion. ACPI mutexes are recursive: the owning thread may re-acquire, each Acquire paired with a Release (ACPICA acpi_ex_acquire_mutex_object, drivers/acpi/acpica/exmutex.c:140, acquisition_depth). The code tracked only a "held" set with no owner, so a nested acquire by the single AML thread waited for a release that only the waiting thread itself could perform — a self-deadlock.

Fixed by tracking (owner ThreadId, recursion depth) per mutex, treating a nested acquire by the owner as a depth bump, interpreting the timeout as milliseconds, and bounding any wait to 5s so a misbehaving AML method can never freeze the scheme-serving thread. Verified against local/reference/linux-7.1 ACPICA.

The amplifier — initnsmgr head-of-line blocking (NOT fixed)

local/sources/base/bootstrap/src/initnsmgr.rs runs the init namespace manager. Every process in a restricted login namespace (i.e. every login shell, and much of init's own spawn path) resolves paths through it: the kernel routes an open of scheme:path in that namespace to the manager, which proxies to the real provider. Two properties make this fragile:

  • It is single-threaded: one run() loop, next_requesthandle_syncwrite_response.
  • Its open_scheme_resource does a blocking syscall::openat on the provider's capability fd, inside the dispatch loop.

So when any provider is briefly not servicing its socket (descheduled under load, mid-tick, in a one-shot init window), the manager's openat blocks, the loop stops, and every open in the system queues behind it forever. This is textbook head-of-line blocking. It also explains mini's historical slowness: ~2452 namespace opens serialize through that one thread per boot.

acpid was merely the trigger with ~100% hit probability. With acpid fixed, the next-most-likely trigger (vesad's display handoff) surfaces at a lower rate. Fixing individual daemons is a whack-a-mole; the amplifier is the real defect.

Diagnostic proof (reusable): the console/log stack is itself a victim of the wedge, so a diagnostic that writes /scheme/debug or re-opens a scheme goes silent exactly when the wedge starts. Cut through it with (1) the QEMU debug console (I/O port 0xE9, qemu -debugcon file:) after acquiring port-IO rights via the pcid handshake, and (2) pre-opening the kernel scheme fds once and re-reading via lseek(0)+read. A per-open trace in initnsmgr (to a debug fd from UPPER_FDTBL_TAG + GlobalSchemes::Debug) pinpointed the wedged open as acpi:processor/CPU3/pss (then, post-fix, display.vesa:v2/2).

Co-victim audit (2026-07-21): other daemons on the same chain

A systematic audit of all 18 scheme-serving daemons for the acpid failure class (a single-threaded daemon that can wait unboundedly in its serving thread) found:

  • acpid was the only software unbounded wait in the boot-critical path (the AML mutex bug — fixed). No other boot-path scheme handler contains an unbounded software wait or a reentrant blocking open/read/call of another scheme.

  • Hardware busy-waits (rtcd RTC UIP bit, ps2d PS/2 OUTPUT_FULL, sb16d/ihdad DSP/RIRB, ihdgd/ahcid/nvmed/bcm2835 register polls) are either hardware-bounded (the bit clears in microseconds) or live in daemons that pcid does not spawn without the matching device (no audio on QEMU q35, etc.). Not the acpid class. (They are still a robustness item under the project WARNING POLICY — unbounded on paper — but adding timeouts to ~15 sites blind, without a reproducing hang, risks regressions and is out of scope here.)

  • ucsid is the one remaining co-victim worth noting. It consumes /scheme/acpi/* in build_state() before it publishes its scheme, and in config/redbear-mini.toml it is the only boot-critical, acpi-dependent unit with a blocking init type (type = { scheme = "ucsi" }, while every other acpi consumer there is oneshot_async). Before the acpid fix, a stalled acpid would have hung ucsid's init and thus boot. It already degrades gracefully on EAGAIN (discover_ucsi_devices returns empty), but EAGAIN only covers "acpid is busy", not "acpid is wedged" (open blocks, per the kernel corollary below). With acpid fixed it works.

    Do not simply flip ucsid to oneshot_async to satisfy the "optional hardware must not block boot" rule (bare-boot-blocking-init-units): ready_sync_scheme hands the scheme cap fd to init via the INIT_NOTIFY pipe (daemon/src/lib.rs:130ready_with_fd), so init registers the ucsi scheme on the daemon's behalf — only the {scheme=…} init type does that. oneshot_async would leave ucsi unregistered (consumers get ENODEV). The correct hardening is a source refactor: publish the scheme immediately with a lazy/empty state, then run the heavy ACPI/i2c discovery in the background after ready_sync_scheme. It is defense-in-depth against an already-fixed trigger, not an active bug, so it must be done with runtime validation, not blind at session end.

The kernel corollary — open ignores O_NONBLOCK

The manager cannot even defer a slow open today, because in Redox an open() with O_NONBLOCK still blocks: O_NONBLOCK governs later reads, not the open itself. This is documented in-tree at local/sources/base/drivers/inputd/src/lib.rs:139 ("this can block indefinitely") and enforced by local/sources/kernel/src/scheme/user.rs UserInner::call_inner, which unconditionally .block()s the caller until the provider replies (no O_NONBLOCK check on the request/response path).

Notably, fbcond already has handoff-retry logic that would work if open returned EAGAIN — its retry never fires today only because the first open blocks instead of returning.

Execution plan (fix EITHER to break the chain; both are worth doing)

Ordered by blast-radius / safety. Neither was attempted at the end of the discovering session: both live in the most critical components (kernel, no_std bootstrap), where an error means "the system does not boot at all", and the discovering host was under heavy uncontrolled external load, making clean validation impossible. They must be done deliberately, with framebuffer-ground-truth validation on an idle host or real hardware.

Option A — make initnsmgr not block the dispatch loop (preferred)

Two shapes, in increasing order of change:

  1. Deferred/event-driven open. Attempt the provider openat non-blocking; on would-block, park (tag, cap_fd, reference, flags) and return without responding, then complete and write_response when the provider becomes ready. Requires the kernel corollary (Option C) so the open can actually report would-block, plus a redox-scheme deferred-response path.
  2. Worker offload. Keep the dispatch loop responsive by handing the blocking openat to a small worker pool; respond from the worker via socket.write_response(Response::…, tag) (Socket is an fd wrapper → Send; CallRequestTag is Send). Cost: bootstrap is #![no_std], so this needs a real thread primitive (redox_rt::thread::rlct_clone_impl — raw stack/TCB/TLS setup), which is the risky part.

This is the direct answer to the operator's SMP question: the parallelism that matters here is fault isolation of the namespace-open path, not throughput. AML execution itself must stay serialized (ACPI requires it; Linux does the same with a global interpreter mutex) — the thing that must not serialize is the transport: serving the scheme socket must not stall behind one slow open.

Option B — bound provider unreadiness at the source

Ensure boot-critical providers (notably vesad) are servicing their scheme socket before anything can open them (e.g. tighten the inputd→driver handoff so the handoff signal follows, not precedes, the provider entering its event loop). Narrower, but only closes known triggers; it does not remove the amplifier for the next slow daemon.

Option C — honor O_NONBLOCK on open in the kernel

Make UserInner::call_inner (open opcode) return EAGAIN instead of blocking when O_NONBLOCK is set and the provider has not yet taken/answered the request (with the existing cancellation protocol). This is the cleanest enabler for Option A.1 and immediately activates fbcond's existing retry, but it changes scheme-open semantics system-wide — the widest blast radius, so it needs the most care and the strongest validation.

acpid hardening (2026-07-21): the daemon-local half of the same problem

acpid is the same shape as initnsmgr in miniature: single-threaded, serving a scheme (acpi) on the very thread that evaluates AML. A slow or stuck AML method stops it answering scheme requests, which is what let the mutex bug wedge the whole system. Six hardening items were identified; five are done (safe, verifiable, reduce or bound the ways acpid can stall the transport), one is designed-but-deferred (the same deferred-response rewrite as Option A above, in a boot-critical daemon).

Done (local/sources/base/drivers/acpid):

  • AML mutex recursion + ownership + timeout units (aml_physmem.rs, the original fix): a nested acquire by the owning thread bumps a depth instead of self-deadlocking; timeout read as milliseconds; any wait bounded to 5s. Mirrors ACPICA exmutex.c:140.
  • stall() bounded per ACPICA (aml_physmem.rs): refuse >255us and warn >100us instead of an unbounded CPU busy-spin on the serving thread (ACPICA exsystem.c:129-147).
  • release owner-check (aml_physmem.rs): a release of a not-currently-held mutex, or by a non-owning thread, is logged (ACPICA AE_NOT_ACQUIRED / owner-mismatch, exmutex.c:287,376) rather than silently corrupting depth.
  • Static processor-method cache (scheme.rs): _PSS/_PSD/_CST/_CPC are fixed after boot but cpufreqd polls them; cache the first evaluation so later reads never re-run the AML interpreter under the global lock. This removes acpid's dominant recurring head-of-line source.
  • Panic-free scheme path (acpi.rs): every release_global_lock().expect(...) on the AML-evaluation path became log-and-continue, and a result.ok()?.unwrap() that panicked on Ok(None) (absent method) became ?. A scheme-daemon panic kills the acpi scheme and wedges every consumer, so the serving path must not panic on odd firmware.
  • Observability (aml_physmem.rs, acpi.rs): log AML evaluations ≥50ms and mutex-acquire timeouts — the early-warning signal before a stall becomes a wedge.

Deferred (#1 — decouple AML execution from the transport): make acpid keep answering the acpi scheme while a slow AML method runs, by rewriting its loop from process_requests_nonblocking onto the low-level next_request → park the CallRequest Tag → run AML on a single serialized worker → write_response(tag) when done. AML must stay serialized (ACPI requires it); only the transport is parallelized. This is exactly Option A's deferred-response shape, in a boot-critical daemon, and cannot be cleanly validated under the current external load — so it is designed here, not done blind. With the cache and bounds above, acpid's remaining exposure is a single slow/first AML evaluation (bounded to seconds, not infinite), which is the tail this rewrite would close.

Validation expectations

  • Framebuffer screendump (QMP) is ground truth; the serial mirror is racy and often silent on a healthy boot. Harness pattern: QMP send-key login + periodic screendump.
  • Because the failure is a race, one boot proves nothing — always compare rates across N≥5 boots on an idle host (external load, e.g. a background opencode at ~290% CPU, contaminates every measurement and inflates the failure rate).
  • Success criterion for this plan: N≥10 consecutive clean boots to a working brush login on an idle host, and no display.vesa:v2/*-class wedge under induced provider delay.
  • local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md — canonical desktop path; console/login is its floor.
  • local/docs/IRQ-AND-LOWLEVEL-CONTROLLERS-ENHANCEMENT-PLAN.md — acpid/ACPI robustness context.
  • Memory: mini-login-acpid-mutex-rootcause, fbcond-handoff-console-wedge.