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

216 lines
14 KiB
Markdown

# 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))=OK``RB=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_request``handle_sync``write_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:130``ready_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`; `CallRequest``Tag` 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.
## Related
- `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`.