ADR-0020 v1 contract freeze — pre-freeze corrections (guest syscall ABI)
Groundwork before the golden-diff ABI freeze gate lands, so v1 pins a correct
surface (user/usys.h):
- Corrected the
field_info_errno docstring: it cited Linux-14 EFAULT/-22 EINVAL; the syscall actually returns QuantumOSSYSCALL_EFAULT(-2) /
SYSCALL_EINVAL(-1). - Added 5 symmetric user-side twin-ABI
_Static_asserts (udp_req_t=24,
field_imprint_req_t=76,field_recall_req_t=76,field_recall_out_t=136,
user_args_t=516) so a struct-size drift fails BOTH ring builds, not just the
kernel's. - Annotated the intentional errno-band overlaps as frozen-v1 decisions:
svc_restarts()returns-1(not-a-service) sharingSYSCALL_EINVAL, and
qseed_value()returns a raw u64 not partitioned from the negative-errno band. - Split the 6 kernel-side ring-crossing
_k_ttwins (udp_req_k_t,
cap_derive_req_k_t, the fourqpu_*_k_t) out ofkernel/src/syscall.cinto a
new kernel-internal headerkernel/include/kernel/syscall_abi.h, so the
upcoming ABI freeze probe can#includeand compiler-measure them. ABI-neutral
(identical structs,_Static_asserts moved with them).
Added — v1 golden ABI freeze gate (ADR-0020)
contracts/abi/v1.golden freezes the guest+kernel ABI contract — the 35 syscall
numbers, the sub-op namespaces, the errno table, capability permission bits, and
the ring-crossing struct sizes — and CI diffs it on every push/PR. The values are
compiler-measured, not parsed from source: two probe TUs (user/abi_probe.c,
kernel/src/abi_probe_kern.c) emit the contract into a .abi_ents section under
the real build flags, and scripts/extract-abi.py reads it back with objcopy.
The gate cross-checks that the user/kernel twins agree, diffs against the
committed golden (an intended change is a visible golden diff, never silently
regenerated — make regen-abi-golden is human-only), and a teeth-check compiles
a mutated SYS_QPU and asserts the gate reddens, so a broken generator can't ship
vacuously green. New CI job abi-golden (gcc + binutils, no boot, no pip) that
also gates release.
The frozen surface now also pins ring-crossing struct field offsets
(__builtin_offsetof for every field of the six udp/cap_derive/qpu_*
twins, cross-checked user↔kernel) — so a size-preserving field reorder, which
the size check alone would miss, reddens the gate. The capability resource-type
namespace (cap_resource_type_t), the arg-page field offsets, and the device-ID
namespace (net/qpu/com2/console/disk — the resource_id a
CAP_RESOURCE_DEVICE capability names) are frozen too (237 golden entries). With
this, ADR-0020 is Accepted for the guest syscall ABI (contract a) — frozen and
CI-enforced. The MCP-schema lane (contract b), the COM2/attestation lane
(contract c, now unblocked by ADR-0019), and the durable audit-format freeze
remain scoped as deferred follow-ups.
ADR-0021 dynamic-PMM — hardening groundwork (no behavior change)
Groundwork before dynamic PMM sizing lands, all no-ops at the current hardcoded
128 MB (kernel/src/memory.c, kernel/include/kernel/memory.h):
- Added
PMM_PHYS_CEIL(0x40000000) /PMM_MAX_FRAMES(262144) — the 1 GB
identity-map ceilingboot.Salready provides — and clamp the frame count to
it before the bitmap is sized, so no frame the allocator can ever hand out is
unmappable (a>= 1 GBframe would be a #158-class escalation). - A static-layout floor: if a machine reports less RAM than the kernel heap's
backing frames need,pmm_initpanics loudly instead of lettingkheap_init
build the heap on unbacked physical memory. - Guarded the kernel-image reservation loop against a short frame count, asserted
the frame bitmap can't overrun the kernel region, and widened the frame-address
multiply to 64-bit (the clamp was the only thing preventing a 32-bit wrap).
Verified no-op at 128 M: ci-smoke boots to QuantumOS ready with the PMMROVER,
PMMHEAP, and PMMCLAMP self-tests green. The last is a synthetic teeth-check — the
clamp is factored into pmm_clamp_frames() and pmm_clamp_selftest feeds it a
1 GB frame count and asserts it clamps to exactly 262144, so the 1 GB ceiling is
proven load-bearing on the-m 128Mleg without any > 1 GB RAM (revert-confirmed:
removing the clamp panics the boot and reddens the gate). The mmap-driven dynamic
sizing + high-memory feature legs follow in subsequent increments.
ADR-0021 dynamic-PMM — mmap-driven sizing (PR-3)
The 128 * 1024 * 1024 hardcode (and its TODO: Get actual memory size from multiboot) is gone: memory_init now takes the RAM total as a parameter
(memory_init(void) -> memory_init(uint64_t total_memory)), sized from a real
parse of the Multiboot1 memory map. multiboot_parse_memory()
(kernel/src/main.c) treats the e820 map as untrusted bootloader input — the
whole mmap buffer must sit inside the 1 GB identity map with no overflow; each
entry's size must be >= 20 and advance the cursor without walking past the
window (iteration-capped at 1024); only type-1 available regions count; each
region end is overflow-checked and the RAM total is the MAX region end (never
a sum), clamped to PMM_PHYS_CEIL. It falls back to mem_upper, then panics
if neither is present rather than sizing a wild bitmap, and runs only after
boot_validate_multiboot confirms the MB1 magic (parsing a v2 block at v1 offsets
would wild-read like the closed framebuffer bug). Byte-wise mb_rd32/mb_rd64
avoid unaligned access on the untrusted buffer.
The parsed total flows kernel_main -> boot_config.total_memory ->
memory_init -> pmm_init. The frame bitmap is now sized for the 1 GB maximum
(fixed 32 KB) rather than detected RAM, so it always covers the fixed
kernel-image/heap reservations no matter what the mmap reported — structurally
ruling out a small-RAM overrun; frames above the detected total stay
free-but-unreachable (the alloc scan is bounded by total_frames).
A one-line greppable PMMSIZE=<hex> boot marker reports the derived frame count.
New ci-smoke gate asserts it lands in the 128 MB-class window [0x7F00, 0x8000]
— observed 0x7FE0 at -m 128M (QEMU reserves ~128 KB at the top of RAM, which
is exactly why the count is not the naive 0x8000 a hardcode would give).
Revert-confirmed: forcing a 2 GB total clamps to 0x40000 and still boots to
QuantumOS ready (the clamp survives), but trips the gate with frame count 262144 outside window — the gate has teeth. The -m 256M/512M scaling legs (the
true hardcode-vs-live differential) follow in PR-4.
ADR-0021 dynamic-PMM — high-memory proof + differential CI (PR-4), Accepted
Completes the feature and flips ADR-0021 to Accepted. Two new boot self-tests
(kernel/src/memory.c, wired after PMMCLAMP):
pmm_highmem_selftest(PMMHIGH) — the load-bearing proof that dynamic sizing
is safe above 128 MB. On RAM past 128 MB it allocates a top-of-pool frame,
asserts its physical address is genuinely high (>= 128 MB,< 1 GB), and —
the real proof — writes two distinct sentinels through the returned identity VA
and reads them back, so aboot.Sthat mapped only 128 MB (or a wrong/stale
map) faults or mismatches here instead of silently handing out an unreachable
frame. On the-m 128Mleg there is no high frame, so it reports the skip
branch (proving the guard ran — not a vacuous always-pass).pmm_residency_storm_selftest(PMMSTORM) — extends the single-shot heap
reservation proof to a bulk drain: 1024 frames pulled with the rover aimed
straight at the reserved kernel heap, asserting none land in
[__heap_start, __heap_end). Catches a multi-allocation rover regression that
the single-shotPMMHEAPcheck could miss.
New differential CI legs make ci-smoke-mem256 / ci-smoke-mem512 (CI jobs
pmm-differential-256/512, both gating release) boot at 256/512 MB and assert
the frame count lands in that size's window (~0xFFE0 / ~0x1FFE0, i.e. it
~doubles / ~quadruples the 128 MB leg's 0x7FE0) and that the PMMHIGH
writeback took its live branch. This is the true hardcode-vs-live differential
that the -m 128M leg alone cannot show: a 128 MB hardcode prints 0x8000 at
every -m and never the live PMMHIGH. Revert-confirmed two ways — re-hardcoding
memory_init to 128 MB reddens the mem256 sizing window (0x8000 outside
[65280,65536]); broadening the high-memory guard so 256 MB wrongly skips reddens
the mem256 PMMHIGH-live check while sizing still passes (the two gates are
independent). Every "un-fakeable gate" named in the ADR-0021 design is now live.
Build clean under -Werror; format/cppcheck/api-consistency green.
ADR-0022 prereq-2 — scheduler perf/stability baseline (report-only + calibration)
ADR-0022 deferred the COM2-latency fix behind two prerequisites; prereq 1 (the
latency gate) shipped. This is prereq 2: a scheduler perf/stability baseline so a
future scheduler-constant change is measured, not blind (ADR-0016). Measurement-
only — it changes no timer HZ / SCHED_QUANTUM_TICKS / service model.
An adversarial design panel refuted the naive metric: aggregate context switches are
dominated by voluntary yield()s (every long-lived citizen busy-yields with a non-
blocking recv, resetting the quantum counter thousands of times per 10 ms tick), so
switch_count/tick is both quantum-insensitive (a gate on it is vacuous) and host-
throughput dependent (CI-flaky). The fix is a dedicated preempt_count in the
scheduler, incremented only at the timer-quantum-expiry reschedule in scheduler_tick
(never on SYS_YIELD), which is 1/quantum-paced by construction. Exposed as a new
SYSINFO_SCHED sub-op (no new syscall — no ADR-0020 golden churn) on one atomic line
SCHED: switches=.. preempt=.. ticks=.. maxgap=.. spread=.. runnable=.., driven by a
qsh sched command and QosVM.sched(). The dead now = 0 /* TODO */ timing stub in
process_switch_to is made live (timer_get_ticks()), and a per-PCB sched_picks
counter added, so the line also carries the fairness/tail snapshot (max reschedule gap,
run-count spread) over the runnable roster.
scripts/test_qos_sched.py (gate make ci-smoke-sched, CI job scheduler-baseline)
is report-only: it records the tick-normalized baseline and asserts only a non-
vacuous liveness floor (≥100 switches and ≥12 runnable over the window), so it
reddens on a dead/wedged scheduler but arms no perf band yet. Empirical calibration
(WSL, 3 runs each) selected the gated scalar for the follow-up — preemptions per
1000 guest ticks tracks 1/quantum almost exactly and host-invariantly:
SCHED_QUANTUM_TICKS |
preempt/1000t (idle, load) | switch/1000t |
|---|---|---|
| 5 (default) | 199.5–200.0 | ~520 |
| 20 | 49.9, 49.8 | ~131 |
| 80 | harness times out (system too sluggish to fill the tick window) |
So a 4× quantum change moves the metric 4× with <0.5 % variance — the follow-up arms a
floor of ~100 (= q5-median × 0.5), revert-confirmed by a SCHED_QUANTUM_TICKS bump to
20 (q=80 grinds the harness itself, so 20 is the practical lever). Build clean under
-Werror; format/cppcheck/api-consistency green.
ADR-0022 prereq-2 — arm the scheduler gate, ADR Accepted
scripts/test_qos_sched.py flips from report-only to a real gate: it asserts
preemptions-per-1000-guest-ticks in [100, 600] (the PR-1-calibrated scalar; ~200 at
the default quantum) on top of the liveness floor. Everything asserted is a delta over
guest ticks, so the band is host-CPU-invariant — a slow CI runner reads the same rate.
The floor catches a coarsened round-robin cadence (the scheduler getting more sluggish —
ADR-0022's regression direction); the loose ceiling catches pathological thrash without
reddening on the intended future fix (a smaller quantum raises the rate). Revert-
confirmed on WSL: at the default SCHED_QUANTUM_TICKS=5 the gate is green (rate 199.9);
bumping it to 20 drops the rate to 49.9 and reddens the floor; restoring returns it to
green. This completes ADR-0022's second prerequisite, so the ADR flips to Accepted —
the deferral decision and both prerequisites (latency gate + scheduler baseline) are done;
the latency fix itself stays a deliberately deferred, now-unblocked future epic.
Fixed — post-ADR-0021/0022 sweep
pmm_highmem_selftestrover clamped to the first high frame (#219, ADR-0021 follow-up).- The echo/client IPC demos NUL-terminate their
recv_msgbuffers — a reply could
disclose stale stack bytes past the message (#220, self-disclosure class). - The browser boot-gate interactivity probe keys on
uptimeinstead of a
ghost-EPERM-prone command, de-flaking the qemu-wasm CI leg (#221).
ADR-0023 — IPC peer re-wiring on watchdog rebirth + dead-target unlink (#222–#224), Accepted
Two defects, one decision (adversarial design review pre-implementation: 15 findings,
3 blockers — all folded into the ADR before a line of code):
- No capability outlives the process it names. The epic #175 spawn-channel unlink
generalizes to EVERYCAP_RESOURCE_IPCcap naming a dead pid (cap_revoke_ipc_targets)
— closing the recycled-pid authority leak forSYS_SEND_TOand theSYS_CAP_DERIVE
targeted-peer check. The capability self-test's survivor assert inverts to gate it. - Wiring comes back by declaration, not pid-reuse luck.
service_definition_tgrows
a per-directionipc_peers[]table;start_slotre-mints declared pairs on every
start inside the grant cli window — two passes (my running peers; running services
declaring me), every mint guarded by liveness + pid generation (a stale RUNNING
slot would reintroduce the exact leak Part 1 closes), pair mints transactional.
paradoxd/fieldsyncd/swarm-svc/qsh ↔ ghostd and the ADR-0019 key cap converted; the
citizens.c hand mints are gone, along with every "known restart limitation" caveat. - swarm_svc re-forwards its cached session key when fieldsyncd's pid changes — the
re-mint restores the delivery path; the re-forward restores the key (a reborn
fieldsyncd would otherwise emit zero-tag frames its keyed peers silently reject). - Gates: the ci-smoke session delivers a delayed
ghostto the REBORN shell, asserted
on the post-QSH: rebornlog slice (whole-log grep passes vacuously — the session
printsghost R=twice pre-rebirth);ghost exit(exit-is-a-feature for ghostd)
restarts the TARGET under the living shell and a post-GHOSTD: FIELD REBORNslice
gate proves Part 1's necessity in the live routing path. Revert-confirmed in both
directions; the forward run happened to rebirth ghostd onto a recycled pid — the
exact scenario the unlink protects.
ADR-0022 — the I/O-priority boost: the deferred latency epic, shipped (#225)
The option table's principled fix, hardened by a 4-lens adversarial panel (31 findings:
7 blockers, 15 majors). A process whose awaited I/O just arrived — an IPC delivery, or
a COM2 RX byte for the registered holder — is flagged io_boost and picked out of turn
at the next reschedule. PING 0.45 s → 0.052 s (~9×), STATUS 0.90 s → 0.048 s (~19×);
every agent-facing MCP tool call rides this wire. The panel's teeth are in the design:
a sticky 0xFF absent-COM2 latch (an unbacked port reads all-ones — the boost would have
fired every tick of every COM2-less CI boot), SCHED_BOOST_MAX_CONSEC=2 (a boost
ping-pong would starve the roster AND freeze quantum expiry itself, invisibly to every
liveness floor), consume-at-dispatch-commit (pick_next stays pure — it is a bare
predicate in the CPUKILL pre-check), a (pid, generation)+live-cap-validated COM2-holder
registration, and boost_count counting HONORED picks only. Gates re-armed in the same
increment: ci-smoke-latency asserts PING median < 0.30 s AND a positive honored-boost
delta (binding effect to mechanism — a bare median is gameable by a quantum change the
sched gate deliberately tolerates), STATUS < 0.6 s; ci-smoke-sched arms a max_gap ≤ 600
tick starvation ceiling under a boost-invariant paced load. Four-way revert-confirmed:
SCHED_IO_BOOST 0 reddens both latency assertions (the boost-off median lands exactly
on the phase-locked 0.45 s rotation floor) while the preempt band stays green in both
directions.
ADR-0020 — MCP + wire freeze lanes: the v1 contract freeze is complete (#226)
The two deferred lanes land, mirroring the ABI golden discipline (2-lens panel first:
21 findings, 4 blockers):
contracts/wire/v1.golden— the COM2 swarm-bridge framing, opcodes, Lamport
attestation parameters, CRC8 parameters, reply-auth geometry, FSYN/FSYP coupling-frame
sizes+offsets, and attestation-string KATs, compiler-measured from a probe
(user/wire_probe.c) whose macros are the SAME ones the emitters now use — twinned
against a host ring extracted from the livescripts/qos_bridge.pyparser constants.
The twin rule + a MUST_TWIN set turn guest↔host drift (previously guarded only by a
code comment) into a red diff;host:kat:ping_framefreezes byte order + CRC span in
one measured number.contracts/mcp/v1-tools.json— all 22 MCP tools, name + inputSchema (descriptions
and result-dict shapes deliberately excluded and recorded as such); pydantic's volatile
titlekeys normalized away;mcp/pydantic/pydantic-corepinned in
requirements-mcp-gate.txtAND the golden_metawith a generator-skew self-check, so
a wrong-environment regen cannot be committed silently. Teeth live in the extractors
(changed-value AND added-name mutations both proven caught; exit 1 is reserved for
diffs so an inverted teeth-check cannot pass on a crash). New pinned-pip CI lane for
the MCP gate; the wire gate rides the stdlib-only abi-golden job.
With all three lanes frozen — syscall ABI (#206–#212), wire, and MCP — the v1
agent-surface contract of ADR-0020 is complete.