Skip to content

Commit 336b395

Browse files
riccajasonclaude
andcommitted
audit: canonical event taxonomy in cambios-abi (class + generated doc)
What - New cambios-abi/src/audit.rs: the audit_taxonomy! macro emits, from one table, AuditEventKind (the 22 wire discriminants), its name()/class()/args_doc()/from_u8() methods, and the TAXONOMY const slice. Match arms and slice are generated from the same input, so they cannot drift. - Canonical domain.action naming vocabulary (cap.granted, enforce.syscall_denied, meta.audit_dropped, ...) and a coarse AuditClass (security / dataflow / lifecycle / context / anomaly / meta) carried in flags-byte bits 1..=3. - RawAuditEvent::build stamps the class centrally from kind.class(); the emit call site never chooses it. src/audit/mod.rs re-exports AuditEventKind + FLAG_SAMPLED from the ABI crate and drops its local copies. Builders unchanged. - tools/gen-audit-taxonomy renders docs/generated/audit-taxonomy.md from the TAXONOMY slice. make audit-taxonomy regenerates; make check-audit-taxonomy verifies in-sync (generate-and-verify, same shape as check-adrs -> INDEX.md). Why - The ADR-007 "what gets logged" table had frozen at 15 events while the enum grew to 22 - the recurring "stale ADR" shape: enumerable data duplicated into a durable doc. Source of truth now lives once in code; the doc is generated. - Class set at the definition site (not the call site) makes the 2026-05-10 backpressure-mislabel class - an operational drop emitted as cap.denied - structurally hard: a meta.* event cannot be confused with a security event. - domain.action gives legible logs + a stable grep surface across arches; the domain prefix answers "which layer denied" (cap.denied vs enforce.syscall_denied; enforce.policy_query is a consultation, not a denial). Wire stability - Discriminants are append-only / never reused. AUDIT_TAXONOMY_VERSION = 1 bumps on any add or rename. flags byte: bit 0 sampled, bits 1-3 class, 4-7 reserved (no growth; still 64 bytes / one cache line). Out of scope (follow-ups) - user/audit-tail adopting the shared vocabulary + rendering domain.action. - Surfacing AUDIT_TAXONOMY_VERSION via SYS_AUDIT_INFO. - ADR-007 amendment: replace the frozen event table with categories + rationale + a pointer to the generated doc, prior table -> Divergence appendix. Needs sign-off on the diff first (ADR-edit gate). - check-audit-taxonomy is a local make-target lint, not ubuntu-CI-gated (the host tool targets aarch64-apple-darwin like sign-elf); matches the precedent that generated-doc staleness is not CI-gated (INDEX.md is not either). Verification - make check-all clean (x86_64 + aarch64 + riscv64). - 907 host lib tests pass; 8 new abi taxonomy tests pass (12 total in cambios-abi). - make check-audit-taxonomy: in sync. - check-assumptions / check-deferrals / check-unsafe-coverage: 0 new. - check-index-isolation: pass (STATUS.md +1 line). Staged files: - CLAUDE.md - Cargo.toml - Makefile - STATUS.md - cambios-abi/src/audit.rs - cambios-abi/src/lib.rs - docs/generated/audit-taxonomy.md - src/audit/mod.rs - tools/gen-audit-taxonomy/.cargo/config.toml - tools/gen-audit-taxonomy/Cargo.lock - tools/gen-audit-taxonomy/Cargo.toml - tools/gen-audit-taxonomy/src/main.rs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e9d5651 commit 336b395

12 files changed

Lines changed: 661 additions & 96 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ make stats
239239

240240
# Lints (all don't-grow-the-baseline gates; baselines in tools/check-*-baseline.txt)
241241
make check-adrs # ADR cross-refs + regenerate docs/adr/INDEX.md
242+
make audit-taxonomy # regenerate docs/generated/audit-taxonomy.md from cambios-abi (audit_taxonomy! macro)
243+
make check-audit-taxonomy # verify the generated taxonomy doc is in sync (local make-target lint, like check-adrs; not ubuntu-CI-gated)
242244
make check-assumptions # Convention 8 numeric-const tags
243245
make check-deferrals # Convention 9 Revisit-when triggers
244246
make check-boot-panics # ADR-021 Phase C — no panic!/expect/etc in boot init; exempt via `// BOOT_PANIC_OK: <reason>`

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[workspace]
55
# User-space crates and tools are built separately (different targets, linker scripts).
66
# Exclude them from the kernel workspace to prevent accidental cross-compilation.
7-
exclude = ["cambios-abi", "cambios-fde-proto", "cambios-ccid-proto", "user/fs-service", "user/key-store-service", "user/virtio-net", "user/virtio-blk", "user/virtio-input", "user/usb-host", "user/ccid", "user/i219-net", "user/udp-stack", "user/shell", "user/policy-service", "user/libsys", "user/libterm", "user/libflag", "user/libfs", "user/libfs-proto", "user/libscanout", "user/libgui-proto", "user/libgui", "user/libinput-proto", "user/fb-demo", "user/hello-window", "user/compositor", "user/scanout-limine", "user/scanout-virtio-gpu", "user/tree", "user/worm", "user/ping", "user/sprouty", "user/terminal-window", "user/audit-tail", "user/fde-mount", "user/cambios-style", "tools/sign-elf", "tools/mkinitrd", "tools/gen-dev-piv-keys", "tools/format-volume", "verification/buddy-proofs", "verification/elf-proofs", "verification/frame-proofs", "verification/capability-proofs", "verification/userslice-proofs", "verification/dtb-proofs"]
7+
exclude = ["cambios-abi", "cambios-fde-proto", "cambios-ccid-proto", "user/fs-service", "user/key-store-service", "user/virtio-net", "user/virtio-blk", "user/virtio-input", "user/usb-host", "user/ccid", "user/i219-net", "user/udp-stack", "user/shell", "user/policy-service", "user/libsys", "user/libterm", "user/libflag", "user/libfs", "user/libfs-proto", "user/libscanout", "user/libgui-proto", "user/libgui", "user/libinput-proto", "user/fb-demo", "user/hello-window", "user/compositor", "user/scanout-limine", "user/scanout-virtio-gpu", "user/tree", "user/worm", "user/ping", "user/sprouty", "user/terminal-window", "user/audit-tail", "user/fde-mount", "user/cambios-style", "tools/sign-elf", "tools/mkinitrd", "tools/gen-dev-piv-keys", "tools/format-volume", "tools/gen-audit-taxonomy", "verification/buddy-proofs", "verification/elf-proofs", "verification/frame-proofs", "verification/capability-proofs", "verification/userslice-proofs", "verification/dtb-proofs"]
88

99
[lints.rust]
1010
# `cfg(fuzzing)` is set by cargo-fuzz when building under libfuzzer-sys; the

Makefile

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ else
199199
SIGN_FLAGS :=
200200
endif
201201

202-
.PHONY: all kernel iso run run-gui run-uefi test clean symbols img-x86 run-img-x86 img-usb run-img-usb usb verify-usb disk-img kernel-aarch64 img-aarch64 run-aarch64 run-aarch64-gui kernel-riscv64 img-riscv64 run-riscv64 check-all check-stable check-x86 check-aarch64 check-riscv64 check-clippy check-clippy-x86 check-clippy-aarch64 check-clippy-riscv64 check-adrs new-adr check-doc-refs update-doc-refs-baseline check-index-isolation check-deferrals update-deferrals-baseline claude-preflight sync-site sync-site-check user-elf fs-service key-store-service virtio-net virtio-blk virtio-input usb-host ccid i219-net udp-stack shell policy-service fb-demo compositor scanout-limine scanout-virtio-gpu hello-window tree worm ping sprouty terminal-window audit-tail fde-mount user-elf-aarch64 fs-service-aarch64 key-store-service-aarch64 virtio-net-aarch64 virtio-blk-aarch64 usb-host-aarch64 ccid-aarch64 i219-net-aarch64 udp-stack-aarch64 shell-aarch64 policy-service-aarch64 fb-demo-aarch64 compositor-aarch64 scanout-limine-aarch64 scanout-virtio-gpu-aarch64 virtio-input-aarch64 hello-window-aarch64 tree-aarch64 worm-aarch64 ping-aarch64 sprouty-aarch64 terminal-window-aarch64 audit-tail-aarch64 fde-mount-aarch64 fs-service-riscv64 key-store-service-riscv64 virtio-blk-riscv64 usb-host-riscv64 ccid-riscv64 virtio-net-riscv64 udp-stack-riscv64 shell-riscv64 policy-service-riscv64 scanout-virtio-gpu-riscv64 virtio-input-riscv64 compositor-riscv64 hello-window-riscv64 tree-riscv64 worm-riscv64 ping-riscv64 sprouty-riscv64 terminal-window-riscv64 audit-tail-riscv64 fde-mount-riscv64 sign-tool mkinitrd gen-dev-piv-keys format-volume bake-font export-pubkey kernel-dev-piv key-store-service-dev-piv iso-dev-piv run-quiet-dev-piv
202+
.PHONY: all kernel iso run run-gui run-uefi test clean symbols img-x86 run-img-x86 img-usb run-img-usb usb verify-usb disk-img kernel-aarch64 img-aarch64 run-aarch64 run-aarch64-gui kernel-riscv64 img-riscv64 run-riscv64 check-all check-stable check-x86 check-aarch64 check-riscv64 check-clippy check-clippy-x86 check-clippy-aarch64 check-clippy-riscv64 check-adrs new-adr check-doc-refs update-doc-refs-baseline audit-taxonomy check-audit-taxonomy check-index-isolation check-deferrals update-deferrals-baseline claude-preflight sync-site sync-site-check user-elf fs-service key-store-service virtio-net virtio-blk virtio-input usb-host ccid i219-net udp-stack shell policy-service fb-demo compositor scanout-limine scanout-virtio-gpu hello-window tree worm ping sprouty terminal-window audit-tail fde-mount user-elf-aarch64 fs-service-aarch64 key-store-service-aarch64 virtio-net-aarch64 virtio-blk-aarch64 usb-host-aarch64 ccid-aarch64 i219-net-aarch64 udp-stack-aarch64 shell-aarch64 policy-service-aarch64 fb-demo-aarch64 compositor-aarch64 scanout-limine-aarch64 scanout-virtio-gpu-aarch64 virtio-input-aarch64 hello-window-aarch64 tree-aarch64 worm-aarch64 ping-aarch64 sprouty-aarch64 terminal-window-aarch64 audit-tail-aarch64 fde-mount-aarch64 fs-service-riscv64 key-store-service-riscv64 virtio-blk-riscv64 usb-host-riscv64 ccid-riscv64 virtio-net-riscv64 udp-stack-riscv64 shell-riscv64 policy-service-riscv64 scanout-virtio-gpu-riscv64 virtio-input-riscv64 compositor-riscv64 hello-window-riscv64 tree-riscv64 worm-riscv64 ping-riscv64 sprouty-riscv64 terminal-window-riscv64 audit-tail-riscv64 fde-mount-riscv64 sign-tool mkinitrd gen-dev-piv-keys format-volume bake-font export-pubkey kernel-dev-piv key-store-service-dev-piv iso-dev-piv run-quiet-dev-piv
203203

204204
all: iso
205205

@@ -1273,6 +1273,29 @@ check-doc-refs:
12731273
update-doc-refs-baseline:
12741274
python3 tools/check-doc-refs.py --update-baseline
12751275

1276+
# Regenerate docs/generated/audit-taxonomy.md from the canonical audit event
1277+
# taxonomy in cambios-abi (the `audit_taxonomy!` macro). The doc is rendered
1278+
# from the `TAXONOMY` const slice — never hand-edited. ADR-007 carries the
1279+
# categories and rationale; this generated file carries the enumeration.
1280+
audit-taxonomy:
1281+
@mkdir -p docs/generated
1282+
( cd tools/gen-audit-taxonomy && cargo run --release -q ) > $(CURDIR)/docs/generated/audit-taxonomy.md
1283+
@echo "=== regenerated docs/generated/audit-taxonomy.md ==="
1284+
1285+
# Verify docs/generated/audit-taxonomy.md is in sync with cambios-abi.
1286+
# Regenerates to a temp file and diffs against the committed copy; fails if
1287+
# stale. Same generate-and-verify shape as check-adrs -> INDEX.md. Run after
1288+
# any audit-taxonomy change (and in the commit gate).
1289+
check-audit-taxonomy:
1290+
@tmp=$$(mktemp); \
1291+
( cd tools/gen-audit-taxonomy && cargo run --release -q ) > $$tmp 2>/dev/null; \
1292+
if diff -u docs/generated/audit-taxonomy.md $$tmp >/dev/null; then \
1293+
rm -f $$tmp; echo "=== audit-taxonomy.md in sync ==="; \
1294+
else \
1295+
echo "ERROR: docs/generated/audit-taxonomy.md is stale — run 'make audit-taxonomy'"; \
1296+
diff -u docs/generated/audit-taxonomy.md $$tmp || true; rm -f $$tmp; exit 1; \
1297+
fi
1298+
12761299
# Mirror kernel ADRs to the cambios-site repo. Reads tools/sync-to-site.toml
12771300
# for the slug map (kernel ADR stem -> site URL slug) and cross-doc reference
12781301
# maps. Idempotent: re-running on a synced state writes nothing. Override

STATUS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ belongs in the linked ADR, not here.
3030

3131
Chronological, newest first. ~3 week window — older items rotate out; git log has the full history.
3232

33+
- **2026-06-15** — Audit event taxonomy moves to a single source of truth in `cambios-abi`. New `cambios-abi/src/audit.rs` defines the `audit_taxonomy!` macro, which emits `AuditEventKind` (the 22 wire discriminants), the `name()`/`class()`/`args_doc()`/`from_u8()` methods, and the `TAXONOMY` const slice from one table — so the match arms and the slice cannot drift (the anti-drift property the restructure exists to buy). Adds a canonical `domain.action` naming vocabulary (`cap.granted`, `enforce.syscall_denied`, `meta.audit_dropped`, …) and a coarse `AuditClass` (security / dataflow / lifecycle / context / anomaly / meta) carried in flags-byte bits 1..=3 — stamped centrally in `RawAuditEvent::build` from `kind.class()`, so the emit call site can never mislabel an event (closes the 2026-05-10 backpressure-mislabel class structurally). The kernel's `src/audit/mod.rs` now re-exports the enum + `FLAG_SAMPLED` from the ABI crate and deletes its local copies; the event builders are unchanged. New host tool `tools/gen-audit-taxonomy` renders `docs/generated/audit-taxonomy.md` from the `TAXONOMY` slice (renders from the const data, no parser = no second representation); `make audit-taxonomy` regenerates, `make check-audit-taxonomy` verifies in-sync (local make-target lint, same generate-and-verify shape as check-adrs → INDEX.md; not ubuntu-CI-gated because the host tool targets aarch64-apple-darwin like sign-elf). Wire-stable: discriminants are append-only / never-reused, `AUDIT_TAXONOMY_VERSION = 1` bumps on any add or rename. Verified: tri-arch `make check-all` clean, 907 host tests + 8 new abi taxonomy tests pass, check-assumptions / check-deferrals / check-unsafe-coverage 0 new. **Out of scope (follow-ups)**: `user/audit-tail` adopting the shared vocabulary + rendering `domain.action`; surfacing `AUDIT_TAXONOMY_VERSION` via `SYS_AUDIT_INFO`; the ADR-007 amendment (replace the frozen event table with categories + rationale + a pointer to the generated doc, prior table → Divergence appendix) which needs sign-off on the diff first. No new syscall, no new capability, no new lock, no new unsafe.
3334
- **2026-06-08** — Correctness fix: `IrqSpinlock` now actually masks interrupts on aarch64 + riscv64 (was x86-only). `src/arch/spinlock.rs`'s `save_and_disable_interrupts` / `restore_interrupts` implemented only the x86_64 path (pushfq/cli); the `not(target_arch = "x86_64")` branch was a no-op, so the two `IrqSpinlock` consumers (`PER_CPU_SCHEDULER`, `PER_CPU_TIMER`) did NOT disable interrupts while held on aarch64/riscv64 — their documented same-CPU-deadlock protection was absent (caught while fixing the riscv64 H5 race; the prior entry flagged it as a separate finding). Fix: factor `arch::irq_save_disable()` / `irq_restore()` into each arch's `mod.rs` next to `interrupts_enabled()` (x86_64 RFLAGS/`cli`, aarch64 DAIF/`daifset`, riscv64 sstatus/`csrci`), each a real `#[cfg(target_os = "none")]` impl plus a host-test no-op stub; `spinlock.rs` now calls those and carries zero inline asm (portable). The token is the native flags register, so `irq_restore` re-enables only if interrupts were enabled at save time. Also fixes a latent wart: the old x86 branch ran `cli` via `target_arch` with no `target_os` gate and only survived host tests because those locks are never taken on darwin; the arch helpers gate properly. Not biting before the fix (the dispatcher's explicit per-arch mask is the real protection for the block paths, and `try_lock()` in ISRs covers SCHEDULER reentrancy), but it was a latent trap for any future non-x86 ISR-mutual-exclusion consumer. Verified: tri-arch build + clippy + unsafe-coverage clean, 907 host tests (confirms the host no-op — no `cli` fault), and boots to shell on all three arches with masking active (x86_64 10.0s, riscv64 4.6s, aarch64 18.3s — no deadlock/hang from SCHEDULER/TIMER now masking). Matches the `IrqSpinlock` contract in [ADR-001](docs/adr/001-smp-scheduling-and-lock-hierarchy.md) (no ADR change — the decision was always "disable interrupts"; only the non-x86 impl was missing). Baselines line-shift-resynced for the new arch helpers.
3435
- **2026-06-07** — Correctness fix (riscv64 H5 race): the timer-ISR lost-context window before `block_task` is now closed on riscv64. Four blocking syscall paths in `src/syscalls/dispatcher.rs` (`handle_wait_irq`, `handle_recv_msg`, the child-wait, the channel-quiesce wait) mask local interrupts right before `block_task` so the timer ISR can't observe `Blocked` state before `yield_save_and_switch` saves the task's context. The x86_64 (`cli`) and aarch64 (`msr daifset, #2`) arms were present, but the `#[cfg(target_arch = "riscv64")]` arm was missing — so on riscv64 the window was open: if the timer fired and the slice expired between `block_task` and the yield, `isr_tick_and_schedule` skips the RSP save (it only saves `Running` tasks, `scheduler/mod.rs` line 530), leaving a stale/zero `saved_rsp` the task later resumes from (the "ZERO RSP" diagnostic or a fault). Adds `csrci sstatus, 2` (clear SIE) at the four disable sites and `csrsi sstatus, 2` (set SIE) at the child-wait no-block return path, mechanically matching the existing arms; the yield path's synthetic `sstatus` (SPIE=1) re-enables on `sret`, so the three pure-block sites need no explicit re-enable (same as x86/aarch64). Narrow window (timer tick must land in a ~3-instruction gap and coincide with slice expiry), which is why riscv64-to-shell never tripped it. [SCHEDULER.md](src/scheduler/SCHEDULER.md)'s blocking-pattern example + its "full implementations" line gain the riscv64 arm / `sret`. Verified: tri-arch build + clippy clean (riscv64 arms lint on their own target), unsafe-coverage clean, 907 host tests, QEMU riscv64 `-smp 2` boot to shell in 4.6s with no "ZERO RSP" / panic (every service's `recv_msg` block→yield→wake→resume runs through the now-masked path); x86_64/aarch64 bytes unchanged (riscv64-only cfg arms). Separate latent finding noted, not addressed here: `IrqSpinlock` is a no-op on all non-x86 targets, so its same-CPU-deadlock masking is absent on aarch64/riscv64 (`try_lock()` in ISRs covers the actual deadlock risk).
3536
- **2026-06-07** — Build fix: `fde-mount` boot module wired into the aarch64 + riscv64 images. Since 94bd574 (Stream A A-v.a, 2026-05-19) `limine-aarch64.conf` referenced `boot():/boot/fde-mount.elf` but `img-aarch64` never built/signed/copied it — `make run-aarch64` panicked at Limine ("Failed to open module") before the kernel ran, silently falsifying the "all three boot to shell" claim above for aarch64; the riscv64 initrd shipped no `fde-mount` at all. This adds `fde-mount-{aarch64,riscv64}` to the `img-aarch64` / `img-riscv64` dependency lists plus the copy/sign/mcopy (aarch64 FAT image) and copy/sign/`--module` (riscv64 initrd) recipe lines, placed right after `virtio-blk` to match `limine.conf`'s positional boot-gate order. `fde-mount` calls `sys::module_ready()` before its unlock flow, so it releases the next module immediately and can't stall the chain. Scope: this only makes the module load + idle cleanly (default builds log "NoPiv" + exit on every arch); end-to-end encrypted-volume unlock still needs the dev-piv path + `format-volume`, which stay x86_64-only (this corrects the A-v.a entry below, which claimed "iso recipe inclusion" but only wired the x86_64 `iso`). Makefile-only — no kernel/userspace source changed, x86_64 `iso` untouched. Verified: riscv64 boots headless to shell in 4.6s with fde-mount signed + spawned as task 5 → process 7 at module 4; aarch64 (with `-device virtio-gpu-pci`) boots to shell, no Limine panic, fde-mount signed + spawned at module 4.

0 commit comments

Comments
 (0)