A kernel-internal observation-only record of "this gap was hit at this site." When the running kernel reaches a // STUB: / // GAP: site, an unmapped Win32 thunk, an unknown syscall number, a soft-fault recovery, or a PE/ELF loader reject, the journal captures the event into a structured FixRecord. Records persist (in priority order) into:
- Tier 1 — in-RAM ring (
kernel/diag/fix_journal.cpp, 1024 records × 128 B = 128 KiB) - Tier 2 — FAT32 file
/KERNEL.FIXwith cross-boot rotation (kernel/diag/fix_journal_persist.cpp, depth = 4) - Tier 3 — NVMe panic-reserved LBA region (
kernel/drivers/storage/nvme.cpp, second half of the existing 4 MiB crash-dump reservation). Fires for both soft panics (core::Panic/PanicWithValue) and hard crashes (#PF / #GP / #UD / etc. viaEmitMinidumpFromTrapFrame) — both routes throughPersistToDiskinkernel/diag/minidump.cpp. The Tier-3 snapshot is lock-free (FixJournalSnapshotPanicSafe) so a hard crash that interrupts a recorder mid-update does not deadlock on the journal's spinlock; readers (e.g.gen-fix-report.py) validate per-record magic and skip torn rows.
A reviewer (typically a Claude session attached to a live boot) reads the journal — via the dfix shell command, the /proc/fixjournal ramfs view, or the offline tools/build/gen-fix-report.py summarizer — and converts records into real source fixes in the tree. The kernel never auto-applies a fix.
Design-Decision #016 explicitly forbids silent self-healing: "sophisticated rootkits actively exploit self-healing code … silent self-heal is the anti-pattern, security-relevant corruption must be visible." Every fix-journal record IS the audit event #016 demands. There is no path in the kernel where reaching a journal record causes .text, dispatch tables, function pointers, or any other runtime state to mutate.
The "self-healing" framing in the feature name describes the workflow, not the mechanism: the kernel observes its own gaps, the reviewer applies the fixes, the kernel evolves. The kernel itself stays passive.
The runtime detector taxonomy is defined by the FixDetector enum
in kernel/diag/fix_journal.h.
Numeric values are stable IDs (appended-to, never renumbered) so
on-disk records from older boots remain readable.
Source: kernel/diag/fix_journal.h — the on-disk record format. These values are stable IDs (appended-to, never renumbered).
| Value | Name | Description |
|---|---|---|
| 0 | None |
sentinel; never appears in a real record |
| 1 | StubMarker |
// STUB: site reached; behaviour known incomplete |
| 2 | GapMarker |
// GAP: site reached; happy path ok, edge missing |
| 3 | UnknownSyscall |
syscall.cpp default arm fired with no GapFill match |
| 4 | UnmappedThunk |
Win32ThunksLookupCatchAll hit (kOffMissLogger) |
| 5 | SoftFaultRecov |
RetryWithBackoff success after >=1 retry, page-fault |
| 6 | LoaderReject |
PE/ELF loader rejected an image |
| 7 | CapDenial |
SyscallGate cap-set check denied a syscall. |
| 8 | TrapCapture |
Hard kernel-mode CPU exception about to |
| 9 | UserFault |
Ring-3 CPU exception (the user task is being |
| 10 | KassertFail |
core::Panic / KASSERT site reached. The |
| 11 | AutonomicProposal |
Emitted by the env autonomic neural |
| 12 | InferredGap |
A RECOGNIZED syscall whose handler returned the |
Source pin shape per detector (handwritten — the on-disk format doesn't carry it; this is the convention each producer uses):
| Detector | Source pin format | Where it fires |
|---|---|---|
StubMarker |
path:Function |
FIX_NOTE_STUB(...) macro (1 line below a // STUB: comment) |
GapMarker |
path:Function |
FIX_NOTE_GAP(...) macro (1 line below a // GAP: comment) |
UnknownSyscall |
syscall#<hex> |
kernel/syscall/syscall.cpp default arm, after NativeGapFill declines |
UnmappedThunk |
<dll>!<func> |
kernel/loader/pe_loader.cpp Win32 catch-all branch |
SoftFaultRecov |
caller-supplied label | RetryWithBackoff, fault-react RetryNow/RestartDomain/KillProcess, kheap OOM, frame-allocator OOM, sandbox cap denial |
LoaderReject |
loader/pe:<status> |
PE rejected for BadMachine, RelocsNonEmpty, TlsCallbacksUnsupported, etc. |
CapDenial |
cap.<MissingCap> |
kernel/security/cap_audit.cpp RingPushDenial — mirrors the in-RAM 256-slot deny ring into the persistent journal. ctx_a = syscall number, ctx_b = proc id. Dedup keys on the missing cap kind so a wave of denies for one cap across many syscalls collapses to one row keyed by "this cap is chronically missing." |
TrapCapture |
auto-pin func+0xOFF |
kernel/arch/x86_64/traps.cpp panic-bound CPU-exception arm. Recorded from trap context via FixJournalRecordFromTrap2 BEFORE the panic so the FAT32 / NVMe panic-write tier persists the bytes even when the box halts. caller_rip = faulting RIP, ctx_a = (vector << 32) | error_code, ctx_b = CR2 (for #PF; 0 otherwise). The offline patch generator resolves the RIP via addr2line, reads ±8 lines of source context, optionally disassembles the faulting instruction, classifies common fault patterns (null-deref, SMAP-bypass, stack-overflow, divide-by-zero, undefined-opcode, GP), and emits a per-trap brief with a proposed defensive shape. The proposed shape is REVIEW-framed in markdown, never auto-applied (Decision #016). |
UserFault |
user.fault |
kernel/arch/x86_64/traps.cpp ring-3 trap arm. Captures CPU exceptions in ring 3 (the user task is killed; the kernel keeps running). Same ctx_a / ctx_b shape as TrapCapture but caller_rip is a USER RIP — addr2line against the kernel ELF won't resolve it. The brief surfaces three triage paths (chronically-broken task vs vtable-smashing task vs broken shared DLL) and decodes the page-fault flag bits / sentinel CR2 values. |
KassertFail |
subsystem name (first arg to Panic) |
kernel/core/panic.cpp Panic + PanicWithValue entry points (which is where every KASSERT macro lands). Recorded after the recursive-panic short-circuit so a panic-during-panic doesn't re-enter. hint = assertion message; caller_rip = the Panic call site (addr2line resolves to the KASSERT statement); ctx_b = the value passed to PanicWithValue (0 for plain Panic). For recurring asserts (repeat >= --kassert-demote-threshold) the brief proposes converting the assertion to if (!(cond)) { KLOG_ONCE_WARN(...); return Err{InvalidState}; }; with --enable-kassert-demote the generator additionally emits a real kassert-demote-<subsys>.patch containing that demotion gated behind #if 0 so the reviewer affirmatively flips the switch (a mechanical KASSERT demotion that shipped silently would convert an audible bug into a silent one — the #if 0 is the safety brake). The synth refuses to generate the patch when the enclosing function isn't Result<…>-returning, since the demoted shape needs a typed Err{} and there's no safe equivalent for a void return. |
| AutonomicProposal | config:<symbol> or env policy site | kernel/env/ — the autonomic learner surfaces a REVIEWABLE proposal as DATA (Decision #016, never a code mutation). A config: pin proposes a bounded constant change (ctx_a = current, ctx_b = proposed); other pins carry learned policy/gate signals. |
| InferredGap | syscall:0x<num> | kernel/syscall/inferred_gap.cpp, recorded from the SyscallTrailGuard destructor (the one point that runs on every SyscallDispatch return) when a guest receives kStatusNotImplemented for a RECOGNIZED syscall. ctx_a = syscall number. Distinct from UnknownSyscall (an unknown number); this is a known number whose behaviour is unimplemented, discovered at runtime with NO source marker. Per-boot distinct-pin cap (kInferredGapPinCap); over-cap drops are the Phase B learner's evidence to raise it. |
Dedup is keyed on (detector, source_pin). A workload that hits the same gap 1000 times produces one record with repeat_count=1000, not 1000 records.
The journal's richest input used to be hand-placed // GAP: / // STUB:
markers. Three discovery layers (spec:
docs/superpowers/specs/2026-06-11-dynamic-fix-discovery-design.md)
find fix-worthy sites without a human first annotating them, all feeding this
same pipeline. Decision #016 is upheld throughout: data in, reviewable patches
out, a human flips the gate.
- A — runtime inference (kernel). The
InferredGapdetector above. Guest hits a recognized-but-unimplemented syscall → one record, zero annotation. - C — static discovery (build-time).
tools/build/gap-scan.pyscans source for un-annotated gap-shaped sites (kStatusNotImplemented,-ENOSYS,TODO/FIXME, not-impldefault:arms), excluding any already carrying a// GAP:/// STUB:/FIX_NOTE_annotation, intogap-candidates.json.gen-fix-patches.py --gap-candidatesjoins it with the runtime records: a candidate whose file also has a hit this boot is confirmed live (high priority); one never hit is a cold candidate. - B — learner config proposals (kernel, data-only). In Live mode the
autonomic learner (
kernel/env/config_proposal.cpp) emits a bounded, evidence-backedAutonomicProposalconfig:<symbol>record when runtime pressure crosses threshold (e.g. inferred-gap discovery dropping pins because the cap is too low). Proposals are limited to an allow-list of tunable symbols, never raise more than 2× or past a hard ceiling, and write only a journal record — the generator renders the diff, a human applies it.
Every tier uses the same layout:
[u32 magic 'FIXJ' = 0x4A584946]
[u32 version = 1]
[u32 record_count]
[u32 reserved (must be 0)]
[FixRecord × record_count] // each record is exactly 128 B
FixRecord field order is part of the on-disk ABI — see kernel/diag/fix_journal.h. Bumping version is the only sanctioned way to change the record stride or field set; readers (gen-fix-report.py) check the version and refuse to interpret older / newer files.
While a boot is running:
$ dfix list # tail the last 20 un-audited records
$ dfix list --detector=cap_denial # narrow to one detector kind
$ dfix show 42 # one record by seq, with caller_rip symbolized
$ dfix stats # counters + per-detector tally
$ dfix mark-done 42 # filter seq=42 from default `list`
$ dfix flush # force a write to KERNEL.FIX
$ cat /proc/fixjournal # tab-separated dump (no shell needed)Audited records are excluded from dfix list unless --all is passed, so the working set stays focused on un-triaged gaps.
After capture (or a panic), a host-side script summarizes the journal:
$ python3 tools/build/gen-fix-report.py KERNEL.FIX KERNEL.F0 KERNEL.F1The output is a markdown report grouping records by detector and source pin, sorted by repeat count, with a triage workflow at the end.
A second host-side script consumes KERNEL.FIX and emits candidate source patches for the gaps the journal recorded. Same #016 contract as the rest of the subsystem — the kernel never auto-applies; the script just removes the mechanical busywork of writing the diff:
$ python3 tools/build/gen-fix-markers.py --output markers.json
$ python3 tools/build/gen-fix-patches.py KERNEL.FIX --markers markers.json --out=fix-patches/For each unique record:
| Detector | Auto-patch? | Action shape |
|---|---|---|
unmapped_thunk (not in thunks_table.inc) |
YES | Inserts a row pointing at kOffMissLogger (safe catch-all) so the next boot logs each call site instead of silently returning 0 |
unmapped_thunk (in table at kOffReturnZero/One/CritSecNop/GetProcessHeap) |
YES | Emits a named-equivalent bytecode patch at a fresh kOff* offset and rewrites the row, suppressing repeat journal noise for accepted placeholders while preserving a reviewable diff |
unknown_syscall |
Brief + YES (additive) | Emits the implementation brief AND a syscall-stub-0xNN.patch that inserts a case 0xNNu: arm before the catch-all default: in kernel/syscall/syscall.cpp. The arm calls FIX_NOTE_STUB("syscall:0xNN", ...) and returns the same -ENOSYS the catch-all already returned (no semantic delta). The next boot then records StubMarker:syscall:0xNN (acknowledged) instead of UnknownSyscall:syscall#NN (catch-all) — the reviewer flips the body to real semantics later. Suppress with --no-syscall-stub. |
stub / gap (repeat >= --marker-log-threshold, default 10) |
Brief + YES (additive) | Emits the marker-hit brief AND a marker-log-<pin>.patch that inserts a KLOG_ONCE_WARN("<subsys>", "fix-journal hot: <hint>") line right after the existing FIX_NOTE_* call. The next boot then surfaces the gap at serial level (one fire per call site per boot, gated by the existing klog level system) without operators needing to dfix-poll. Suppress with --no-marker-log or by raising the threshold. |
stub / gap (below threshold) / loader_reject |
No | Detector-specific implementation brief pointing at the source pin and captured runtime context |
cap_denial |
No | Three-option policy brief — deny is correct (sandbox working as intended), grant is missing (spawn / RBAC fix), or cap too coarse (split the gate). The choice between them is policy, not mechanical, so the brief lays out the three shapes and leaves the reviewer to pick. |
trap_capture |
No (brief with proposed shape) | Fault-site brief that pulls together every layer of offline evidence: addr2line-resolved function (file:line), ±8 lines of source context with the faulting line marked, decoded error-code bits, optional objdump-disassembled instructions, and — when the (vector, error_code, CR2) tuple matches a recognised pattern — a proposed defensive shape (null guard, divide-zero guard, CopyFromUser wrap, etc.) embedded as REVIEW-framed text. Decision #016 forbids auto-applying these guesses; the reviewer reads the proposal, decides if the pattern match is right, and writes the real fix manually. |
user_fault |
No | Ring-3 crash brief — three triage paths (broken task / vtable smash / broken shared DLL) plus error-code decode and sentinel-CR2 recognition. Userland fixes live in the offending PE/ELF binary, not in the kernel, so no auto-patch is generated. Cross-reference with unmapped_thunk records when the triage suggests "broken shared DLL." |
kassert_fail |
Brief + YES (opt-in, gated) | For recurring asserts (repeat >= --kassert-demote-threshold, default 3), the brief proposes converting the assertion to a defensive return + KLOG_ONCE_WARN. With --enable-kassert-demote the generator additionally emits a kassert-demote-<subsys>.patch containing that conversion, wrapped in #if 0 ... #endif so applying the patch does NOT change kernel behaviour until the reviewer also flips the #if 0 to #if 1. Off by default because KASSERT demotion is a semantic change; the #if 0 brake gates the actual semantic shift behind a second affirmative action by the reviewer. Synth safely refuses to fire when the enclosing function isn't Result<…>-returning (no safe Err{} to return). |
trap_capture (null deref) |
YES (opt-in, gated) | With --enable-trap-guards, a trap-null-guard-<pin>.patch is emitted alongside the brief: a real diff that inserts if (ptr == nullptr) { KLOG_ONCE_WARN(...); return <type-appropriate graceful>; } immediately BEFORE the faulting source line, wrapped in #if 0. The synth recognises the dereferenced pointer name from the source line via _DEREF_PTR_RE, picks the graceful-return shape from the enclosing function's return type (Result/void/pointer/integer), and refuses to fire when neither parse succeeds. |
trap_capture (#DE divide-zero) |
YES (opt-in, gated) | With --enable-trap-guards, a trap-divzero-guard-<pin>.patch inserts if (divisor == 0) { ... } before the faulting division, same #if 0 brake + return-type detection as the null-deref guard. Refuses when the rhs of / or % isn't a single identifier (complex expressions need a different guard shape). |
soft_fault_recov (mm/kheap / mm/frame-alloc) |
YES (opt-in, gated) | With --enable-oom-nullcheck, an oom-nullcheck-<pin>.patch inserts if (p == nullptr) { return Err{OutOfMemory}; } immediately AFTER the auto* p = KMalloc(...); site, wrapped in #if 0. Requires the new FixJournalRecordAtCaller API (added on the kheap / frame-allocator OOM paths) which captures the UPSTREAM caller_rip — addr2line resolves to the allocation statement the synth keys off, not to the address inside the primitive. Refuses to fire when the enclosing function isn't Result-returning. |
soft_fault_recov (fault-react producer) |
YES (additive only) | Always emits the advisory brief, AND once per run emits a fault-react-recover-probe.patch that hardens the recovery dispatch (see below) |
soft_fault_recov (trap.recov / other) |
No | Advisory brief only — the right fix is domain-specific and human-judged |
marker manifest rows without fix-journal instrumentation (via --markers) |
YES, when safe | Adds diag/fix_journal.h and a FIX_NOTE_STUB / FIX_NOTE_GAP macro for in-function kernel .c/.cc/.cpp markers; unsafe header/userland/namespace-scope rows become review notes |
Patch files are unified diffs ready for git apply. Re-running after applying is safe — the script doesn't re-emit a patch for a row that already exists. Optional --apply runs git apply on each patch with a y/n prompt; --yes skips the prompts (use only in CI). Marker-manifest generation is deliberately conservative: it only auto-inserts macros into indented, in-function kernel source markers where the macro is a valid statement. Header comments, userland DLL code, and namespace-scope declarations are surfaced as notes instead of risking an invalid patch.
Instrumentation recognition. A marker counts as already-wired (has_macro in the manifest) when the lookahead window holds either a FIX_NOTE_STUB / FIX_NOTE_GAP macro or a direct FixJournalRecord*(... FixDetector::StubMarker / GapMarker ...) call. The direct-call form is what a site uses when it needs to attach detector-specific ctx_a / ctx_b (e.g. kernel/drivers/virtio/virtio.cpp records the unprobed cls_idx / device_id) — the parameterless macro can't carry that. Recognising both forms stops the generator emitting a redundant double-instrumentation patch over a site that is already observable.
Runtime-fault → modified-code patch (the soft_fault_recov fault-react path). When the journal shows the fault-react dispatcher took a recovery decision (RetryNow / RestartDomain / KillProcess) for a detected runtime fault — an exception, a driver fault, a memory-corruption / page-fault that decayed to a Class-C kill — the generator emits a candidate patch with modified code, not just a brief. The patch is additive observability only: it adds ProbeId::kFaultReactRecover to probes.h, the matching kProbeTable row to probes.cpp (keeping the size static_assert balanced), and a KBP_PROBE_V(...) next to each of the three FixJournalRecordSev recovery records in fault_react.cpp, plus the debug/probes.h include. After it is applied, the next fault-react recovery of any kind is GDB-breakable (b duetos::debug::ProbeFire) and leaves a [probe] line — exactly the discipline CLAUDE.md "Diagnostic Logging — Keep It, Gate It, Probe It" prescribes for a recurring fault path.
This is the only mechanically-sound shape of "emit a fix patch in response to a detected runtime fault" under Design-Decision #016: the patch never changes control flow, never swallows a fault, never guesses semantics, and is applied by a human or CI (--apply / --yes) — never by the running kernel. A semantic fix for an arbitrary memory bug cannot be mechanically synthesised (a wrong candidate a reviewer might git apply is worse than a brief), so the synthesiser is deliberately scoped to this additive fault-capture hardening and is idempotent (re-running after apply is a no-op; missing anchors fall back to the brief).
Synthetic self-test records are filtered. FixJournalSelfTest() (and FaultReactSelfTest()) inject validation records each boot — one per detector plus an auto-pinned probe — with pins that point at no real source (selftest/stub.cpp:1, selftest!ThunkSelftest, selftest/syscall#999, selftest.fault-react, …FixJournalSelfTest()+0xNN). Both gen-fix-report.py and gen-fix-patches.py drop these before planning any action. This is not cosmetic: before the filter, selftest!ThunkSelftest made the patch generator synthesise a thunks_table.inc row for a fake selftest.dll, which --apply --yes in CI would have committed straight into the Win32 ABI table. The predicate (is_selftest_record) is kept in sync between the two scripts; genuine subsystem faults the self-tests simulate with realistic sources (kernel/mm/kheap, drivers/usb/xhci) are deliberately NOT filtered — they are indistinguishable from real faults by design and their briefs/patches are sound regardless.
Documented comment-only markers carry a precise rationale instead of the generic "likely namespace scope" guess. gen-fix-patches.py's _MARKER_SKIP_REASONS pins the why for each marker that is intentionally never auto-instrumented (DMA hot-path cache maintenance, the virtio_pci transport-only DRIVER_OK design boundary, the fault_inject namespace-scope assumption note, and the NtQueryDefaultLocale / NtQueryDefaultUILanguage GAPs that annotate their absent NtSet* counterparts rather than the complete Query function they sit in). A reviewer reading the plan sees the real reason, not a heuristic stand-in.
Current in-tree marker coverage is intentionally source-aware rather than blanket. The journal is fed from the actual runtime branches of:
- GPU bring-up —
nvidia_gpuGSP channel, Intel GSC manufacturing partitions. - DuetFS emulator probe gating.
- iwlwifi legacy RBD encoding + TFD DMA upload.
- virtio transport — unprobed scsi/input/socket device classes (carries
cls_idx/device_idas ctx), virtio-blk single-in-flight request assumption, virtio-balloon inflate/deflate dispatch. - Security — RBAC role/membership tables that re-seed every boot (persistence pending a writable system FS).
- Linux subsystem —
DoOpenread-only / non-mount-aware FAT32 path. - NT translation —
NtSetInformationThread(TLS-slot / exit-status classes ignored) andNtTestAlert(no alertable-wait drain). - DRSH desktop — client-side resize-ack negotiation.
Comment-only-on-purpose (a journal call here would be noise or mis-attributed, not a real gap signal):
- Architecture-deferred DMA cache maintenance (
mm/dma.cpp) — hot synchronization path; a record on every DMA sync would be the wrong fix. virtio_pcinegotiate DRIVER_OK — correct for the transport-only path; per-device drivers install queues, so this is not a behavioural gap.NtQueryDefaultLocale/NtQueryDefaultUILanguage— the// GAP:there annotates the missing Set counterpart; the Query function itself is complete, so wiring it would mis-attribute the gap.
When a thunk row already exists at kOffReturnZero/One/CritSecNop/GetProcessHeap, the script can remove repeat journal noise by emitting a named-equivalent byte sequence at a fresh offset and rewriting the row. That patch is mechanical, but it is still a candidate patch: the reviewer must decide whether the generic behavior is genuinely the correct contract or whether the row deserves real ABI work instead. Two review outcomes are expected:
-
Real implementation: write x86-64 bytecode in
kernel/subsystems/win32/thunks_bytecode.inc, declare akOff<Name>constant inthunks.cpp, and update the row. See the eight thunks landed alongside this script (__chkstk,_cexit,_crt_atexit,_register_onexit_function,_initialize_onexit_table,_set_app_type,_configure_narrow_argv,FreeEnvironmentStringsW) for worked examples covering page probing, atexit-list walking, and proc-env-backed value storage. -
Named-equivalent noop: when the noop IS the correct contract (e.g.
FreeEnvironmentStringsWover a static env block), accept the generated distinctkOffFooBarconstant whose bytecode is equivalent to the generic helper. The noop classifier inWin32ThunksLookupHashedchecks the offset value, not the bytes, so moving equivalent bytes to a fresh offset stops surfacing the row as a journal record on every boot.
Both patterns keep the audit trail intact — the journal continues to record any TRULY unimplemented gap, while suppressing entries the reviewer has already decided about.
The full "run the OS → patches arrive → review and apply" cycle:
# 1. Run the OS. Any breakage class the journal can intercept
# (KASSERT, ring-3 fault, hard kernel trap, OOM, cap denial,
# unmapped thunk, unknown syscall, hot STUB/GAP marker, soft
# fault recovery) gets dedup-recorded into the in-RAM ring,
# flushed to FAT32 KERNEL.FIX on the heartbeat tick, and
# persisted to the NVMe panic-reserved LBA region on a hard
# crash.
DUETOS_SMOKE_PROFILE=pe-winapi tools/qemu/run.sh
# 2. Extract KERNEL.FIX from the NVMe image and run the offline
# patch generator with EVERY auto-patch class enabled. Each
# semantic-change patch is wrapped in `#if 0 ... #endif` so
# applying does NOT change kernel behaviour until step 4.
tools/qemu/run-fix-cycle.sh # writes fix-patches/*.patch
python3 tools/build/gen-fix-patches.py \
build/x86_64-debug/KERNEL.FIX \
--kernel-elf build/x86_64-debug/kernel/duetos-kernel.elf \
--enable-all-patches \
--out fix-patches/
# 3. Review interactively: each patch is shown with y/N/e=edit/s/q.
# `e` opens $EDITOR — the modify step where the reviewer can
# rewrite the synthesised guard before applying. Accepted patches
# land as one commit each on a fresh branch.
tools/qemu/dfix-apply-interactive.sh
# 4. For each gated patch the reviewer wants to actually activate,
# flip the `#if 0` to `#if 1` in a SEPARATE commit so a revert
# is one `git revert <sha>`. Push when the branch is ready.
$EDITOR <files-with-gated-changes>
git commit -am "<feature>: activate fix-journal demote for X"
git push -u origin <branch>| What broke at runtime | What the auto-patch landed (gated #if 0) |
What the reviewer does |
|---|---|---|
| Hard #PF on a null pointer | if (ptr == nullptr) { KLOG_ONCE_WARN; return Err{...}; } inserted before the deref |
Decide if nullptr is a legitimate case, flip the #if 0 |
| Hard #DE divide-by-zero | if (divisor == 0) { ... return; } before the division |
Decide if zero is a real case or a math bug, flip the #if 0 |
KMalloc(...) -> null at a known site |
if (p == nullptr) { return Err{OutOfMemory}; } after the allocation |
Confirm the propagation up the call stack, flip the #if 0 |
Recurring KASSERT in a Result<>-returning fn |
if (!(cond)) { return Err{InvalidState}; } replacing the assert |
Confirm the invariant can be relaxed, flip the #if 0 |
Unknown syscall 0xNN |
case 0xNNu: arm with FixJournalRecord(StubMarker, ...) + -ENOSYS |
Implement the real syscall body (the patch lands the arm; the body is yours) |
| Hot stub/gap marker | KLOG_ONCE_WARN("subsys", "fix-journal hot: <hint>"); next to the FIX_NOTE_* |
Already active; no second-step flip needed (additive observability only) |
| Unmapped Win32 thunk | thunks_table.inc row pointing at kOffMissLogger |
Either accept the noop or write real x86-64 bytecode |
- Pick a gap.
dfix list(or the markdown report) gives the un-audited rows. The highest-repeat row in each detector is the best ROI. - Open the source pin.
path:Function→ open the file.dll!fn→ checkwiki/reference/Win32-Surface-Status.mdfor the DLL's REAL/STUB/GAP/MISSING table. - Decide the fix. Implement the missing path; route through an existing primitive; or accept the gap.
- Land the source change as a normal commit on a feature branch. The journal is observational — it does not commit anything to the tree.
- Mark the record audited so future
dfix listcalls don't re-surface it:dfix mark-done <seq>. The audited bit also persists into the next FAT32 flush.
- No userland flusher. The original plan included a userland service that would mirror
/proc/fixjournalto durable storage. The Tier-2 FAT32 sink already does that from kernel space, so the userland service would be a duplicate subsystem. Per the anti-bloat guidelines, dropped. - No "fix templates" library or DSL. The
hintfield is one 40-byte string. Anything more elaborate belongs in the source tree, not the runtime record. - No new fault-domain. The journal is a passive observer of the existing
FaultReactDispatchchokepoint; it does not register itself as a domain that could itself fail and get restarted. - No
auto-applyeven behind a feature flag. Adding it is a separate design conversation that has to engage with #016 head-on; nothing in this subsystem is one knob away from auto-application.
| File | Role | LOC |
|---|---|---|
kernel/diag/fix_journal.{h,cpp} |
Public API + ring + dedup + selftest | 165 + 430 |
kernel/diag/fix_journal_persist.{h,cpp} |
Tier-2 FAT32 sink + Tier-3 NVMe panic write | 90 + 320 |
kernel/shell/shell_diag.cpp |
dfix command (5 sub-operations) |
280 |
kernel/fs/ramfs.cpp (additions) |
/proc/fixjournal snapshot view |
~80 |
tools/build/gen-fix-report.py |
Offline markdown summarizer | 250 |
tools/build/gen-fix-patches.py |
Offline source-patch generator (--apply) | 380 |
Insertions into existing files (single-digit lines each): kernel/syscall/syscall.cpp, kernel/loader/pe_loader.cpp, kernel/diag/recovery.h, kernel/core/main.cpp, kernel/core/panic.cpp, kernel/diag/heartbeat.cpp, plus six representative // STUB: / // GAP: sites that gained FIX_NOTE_* macros.
- Boot self-test (
FixJournalSelfTest()) injects one record per detector kind, assertsrecords_uniquerose by exactly the number injected, asserts a known dedup hit collapses, asserts mark-done sets the audited flag, asserts mark-done on a missing seq returnsNotFound. Panics on mismatch viakBootSelftestFail. Prints[smoke] fix_journal=ok records=<n>on pass. - Persistence self-test (
FixJournalPersistSelfTest()) flushes, reads back the FAT32 header, validates magic + version + size = header + count × 128. Prints[smoke] fix_journal_persist=ok records=<n>on pass; SKIP if FAT32 isn't mounted. - Probe: a brand-new unique record fires
kFixJournaledwith(seq << 32) | detectorpacked into the value field. ArmedLog by default → a clean run logs the count of unique gaps; an attached GDB canb duetos::debug::ProbeFireand break on each one.