Skip to content

Technical Risk Register ​

Status: Milestone 0 / C0 (master prompt §23.J and CosmoOS deliverable 9). Ranked by (likelihood × impact on safety). Reviewed at the end of every milestone.

IDRiskLIMitigationDetectionOwner milestone
R-1Borrow-checker unsoundness. NLL region solving, two-phase borrows, reborrows and partial moves are subtle. One hole silently breaks the safety guarantee (Goose's own lifetime checker had about half of recent commits fixing holes: research/goose.md §9.4)HighCriticalKeep v0 intraprocedural and small (E1–E4). Specify every rule in ownership.md before implementing it. Use a separate init check and borrow check. The checker runs once, before optimisation; each optimisation keeps to written preconditions (R-8)Compile-fail corpus (≥ 2 tests per rule; M6d adds a vector-invalidation suite, and closes two growable-buffer hazards in lowering: for over a vector holds a view loan, and an element write after a call re-checks its index; M6e fixed a liveness hole that made the checker over-strict, not unsound: Unpack targets were not definitions; M6x adds writes into aggregates and through references, signature flows and a drop check, each with compile-fail tests and a model-based property test), mutation-generated negatives with independently computed verdicts, ASan/UBSan on all run-pass output, the random program generator (M7d; its first runs found a false N0272: a Copy value read through a borrow mut parameter was treated as carrying the parameter's loans), the NIR reference interpreter (M7c) as a dynamic oracle for liveness, bounds and initialisation on every run program and sweep point; borrow tracking (Stacked-Borrows-style) is not done. M8a adds unsafe: raw pointers carry no loans (U3), and references made from them are unconstrained, so a wrong Safety: argument is unsound without any diagnostic. Containment: only the U1 operations need unsafe (N0280, compile-fail corpus), every unsafe function states its contract (N0283), safe code is checked exactly as before, and the interpreter faults on invalid accesses through pointers (UnsafeTests). An independent review of M13's closures and threads (2026-10-29) found two holes, both closed with compile-fail tests (check-fail/shared-path-mutation.nova): a borrow mut or SliceMut stored where a shared reference leads gave mutable access (in 0.1.1; a use after free under ASan, and with Thread.both a data race under TSan), and so did the field reborrow added the day before. A shared dereference now makes the rest of the path immutable (type-system §13.2)M2, M7, M8
R-2Unsafe/FFI contract errors. C libraries and kernel APIs do not follow Nova ownership. Unbounded regions from raw→ref conversions (U3). Wrong effect declarations in binding filesHighCriticalAll extern calls are unsafe. Safe wrappers carry /// Safety: contracts. Binding files are reviewed and default to the most conservative effects. Raw→ref conversion is only possible through APIs that bound the region by signatureLint for missing contracts. Wrapper unit tests under ASan. Kernel: lockdep and might_sleep stay on in debug. C3: the I/O runtime (nova_rt_io.c), the first binding with OS semantics, runs against CosmoOS's own libc in the guest (c3.py), including its non-POSIX sockaddr, spawn API and wait status; known gap: CosmoOS has no MSG_NOSIGNAL/setsockopt, so a socket write to a closed peer may raise SIGPIPE there. M8a: the Safety: lint is N0283, and the interpreter faults on invalid accesses through pointers (UnsafeTests). M8b: C prototypes use C's own types, @repr enum values from C are checked, and FfiTests runs C→Nova and Nova→C under ASan+UBSan. M8c: a freestanding object's undefined symbols are an allowlist, so nothing hosted leaks into a kernel build (FreestandingTests). C4/C5: Nova objects meet the module loader's rules (c4.py), and Nova code allocating from kmalloc passes an in-kernel selftest with an injected-failure sweep and no leaked blocks (c5.py); KernelHeap refuses alignments kmalloc does not give. C7c: lock bindings keep their data private to their file (type-system §12), so safe code reaches it only through a guard; a guard that failed to unlock is caught by the host testM8, C5, C7
R-3Undefined behaviour in generated C. Signed overflow, shifts, evaluation order, aliasing, side-effect-free loops, implicit promotions of narrow types. clang and gcc optimise UB differentlyMediumCriticalChecklist in c-backend.md §5, enforced by construction (NIR sequencing, helpers). No reliance on -fno-strict-aliasingDifferential testing (clang × gcc × O0/O2), UBSan, the random program generator (M7d: it found a C name collision with the runtime and four kinds of warnings on valid programs, all fixed; the M7e 24-hour campaign, 423,773 programs through clang and GCC at -O0/-O2, found no miscompile and one more warning kind, fixed), golden C review. M7b's structured form keeps an invariant (every block ends in an explicit transfer) and is checked against the flat form in its own run configuration on the whole run corpus. M8a's raw pointers emit no C pointer arithmetic and no pointer-to-pointer casts (all through uintptr_t, unsafe.md §4); the pointer corpus runs under ASan+UBSan. M8b: C prototypes use C's own types, because a call through an incompatible prototype is undefined (C11 6.5.2.2) even where the ABI agrees; @repr enum values from C are checkedM1→M8
R-4Host leakage into target layout/semantics. .NET sizeof/Marshal used for layout; char signedness; endianness assumptionsMediumHighTargetInfo is the only layout source. Banned-API analyzer. _Static_assert(sizeof/offsetof) in all generated C, except for types that hold C state, which have only the C compiler's layout (type-system §33.1): Nova computes no layout for them at all, and the interpreter faults instead of guessing one. Nova never uses C charStatic asserts fail the C build. Cross-compile tests for both CosmoOS triples in M1M1
R-5Kernel context-effect unsoundness. Atomic/sleepable is a runtime property in CosmoOS. Indirect calls, callbacks and wrong binding annotations can let sleeping code run under a spinlock or quiesce guardMediumCritical (deadlock, RCU violation)Conservative defaults (sleep+alloc for externs without @effects, for C function-pointer calls, and for calls through Nova function and closure types without an effect list; M11 G8b′, type-system §36.3). Effects inferred over the package, drops included, and checked in @context functions and wherever an @context(atomic) value (a spin guard) may be live (C7a, type-system §32). CosmoOS's own might_sleep() in every sleeping primitive stays the runtime backstop (Nova emits none: its debug half is not exported to modules). Never claim effects prove kernel concurrency propertiesKernel debug builds with lockdep. Negative effect tests (check-fail/effects.nova, 13 cases, C7a; c7/negative/, sleeping under CosmoOS's own spin guards, C7c). QEMU stress tests: C7e ran Nova locks, kernel objects and grace periods on 4 CPUs in CosmoOS's debug build, lockdep-clean, on both arches; lockdep still catches what Nova does not check (lock order)C7
R-6Kernel object lifetime and quiescence misuse. kobject_put with a blocking release under a spinlock (unchecked in CosmoOS today). Guard-scoped borrows escaping. Callbacks outliving module unload. No call_quiesce barrierMediumCriticalKRef drop has the sleep effect. Guard-scoped borrows (R4). Registrations are owned values torn down by generated shutdown(). deferDrop forbidden in modules until KQ-3Unload stress tests in QEMU (C8). Zombie-module detection. Lockdep C7d: KRef<T> drop is kobject_put with the sleep effect, so a final put under a spin guard does not compile; deferDrop is unsafe with the KQ-3 contract (unload does not wait for call_quiesce); release runs once, at the last put, against CosmoOS's own object.c (host test) C8: a module's state is one Nova value whose drop ends its timer (timer_cancel_sync), thread (stop + join) and objects, in declaration order; 1,000 load/unload cycles leave nothing (hook-patched copy); CosmoOS does not attribute objects made during init/shutdown to the module (finding, cosmoos/c8-report.md)C5–C8
R-7Drop elaboration and error-path bugs. Double drop or missed drop on ? paths, partial moves, early returns, loops. Allocation-failure paths are never exercisedMediumHighDrop flags are derived from the same init analysis as move checking. FailingAllocator fault injection in every heap testRun-pass tests with drop-counting types. Fault-injection sweeps (fail the k-th allocation for every k). ASan leak and double-free detection. Status (M3b): the sweep runs on every allocating run-pass program, with the runtime's live-allocation count as the leak check (LeakSanitizer is unavailable on macOS). M3a found and fixed two missed-drop bugs from M2: discarded call results, and statement temporaries on early exits. M6c: a lowering that returned "never completes" while its block was still open silently dropped a statement; the NIR builder now treats that as an internal errorM2, M3
R-8Optimisation changes semantics. Placement analysis, move elimination, BCE or closure-call specialisation (ADR 0019) changing destruction order or which code runs, allocator observability or bounds behaviourMediumHighPlacement rules in memory-model.md §5 (allocator must be placement-transparent; KernelHeap never is). Each optimisation has written preconditions. Check elimination and placement change no NIR, only what the backend emits for it; closure-call specialisation does change it, and is not re-checked: it fixes only a call's target, to the body the analysis proves the closure has (optimisations.md §5.1), so the checked program's statements, loans and effects are unchanged, and the interpreter runs the specialised NIROptimisation on/off equivalence tests, fuzz-differential on BCE (as Goose does), --explain-allocations snapshots. M5 (done): placement off/on gives equal outputs and identical drop traces on every run program, native and interpreter agree with placement, and a fuzz configuration builds with placement. ADR 0019: a clang -O2 no-closure-specialisation configuration of the run corpus and the fuzzer, ClosureSpecialisationTestsM5, M9
R-9Stack exhaustion. Recursive drop of deep Box trees and lists. 16 KiB kernel stacks shared with IRQs. Large by-value structsMediumHigh (kernel: corruption/panic; user: crash)Per-value size limits. Kernel frame (2 KiB) and chain checks. Recursion requires @unboundedStack in the kernel. Library drop for recursive types uses iterative destruction (explicit worklist). Status (M3c): compiler drop glue iterates over a box's tail (type-system.md §18.5), so lists of any length are destroyed in constant stack. deep-structures.nova (3 × 200,000 nodes) crashes with recursive glue at -O0 and passes with the loop. Trees still recurse along non-tail children: balanced trees are fine, left-degenerate ones are not (open for the kernel profile, C4)-fstack-usage reports. Deep-structure run-pass testsM3, C4
R-10Expressiveness gap drives unsafe use. v0 forbids references in structs (E3), so users write unsafe, which erodes the guaranteeHighMediumShip safe alternatives early: Span indices, SlotMap (M6e: handles can be stored in structs and collections), arenas. OD-3 is implemented (M6x, type-system.md §25): structs, enums and collections may hold references with region parameters, so zero-copy parsers and arena graphs need no unsafe. Track unsafe density per package. M8a: unsafe exists, and every use needs a Safety: contract (N0283), which keeps it visible in review. M11 closed the gaps that M9's suite and the README's tour found in safe code (milestone-11.md), among them a cursor walking a list without unsafe (G3), wrapping arithmetic for hashes (G4b), floats (G7) and closures that borrow what they capture (G8), so callbacks need no C function pointers. Closures cannot outlive their block (owning closures are OD-9b). M13 added owning closures (G3), dyn (G2) and splitAtMut (G5), which remove more reasons to reach for unsafeunsafe audit report in novacM6, M8, M11
R-11Moved or uninitialised C state. Kernel C state can point into itself once initialised (an empty list head; a mutex's wait queue), so a bitwise move corrupts it; C state used before its init runs misbehaves (a zeroed spinlock's owner is CPU 0)MediumCriticalC-laid-out values are made only in place, by a literal given to Box.create/createIn, never moved (every move is N0294), and @placed methods run the C initialisers at the final address before any code can see the value (C7b, type-system §33)check-fail/c-state-moves.nova; ffi/embedded.nova checks in C that self-pointing list heads survive in their boxesC7
R-12Fetched dependencies (M10 T6, ADR 0013). A git dependency is code the build compiles and trusts; a changed tag or branch, a rewritten repository, or a tampered cache could change what is built without the package changingMediumHighA dependency is pinned to a commit in nova.lock, and builds use the lock, never the manifest's tag or branch (tooling.md §4.3). Each checkout's manifest and sources are hashed into the lock and verified on every build (N0325). No build scripts: nothing runs but git and the C compiler. --offline builds never touch the network. Review of what a dependency contains stays the user'sPackageManagerTests (pinning, a moved branch, an edited checkout, offline)M10
R-13Data races through threads (M13 G4, ADR 0016). Safe code's race freedom rests on Send/Share inference and on lib/std's @threadSafe types keeping T1–T3 in their unsafe code; a wrong inference rule or a library bug would let two threads write the same memory. The interpreter runs threads one at a time, so it cannot find racesLowCriticalSend is structural and conservative (references, raw pointers, C types, user allocators and closures are not); @threadSafe is accepted only in library files (N0290); the library's unsafe states its contract (N0283); every thread is joined before main returns; Thread.both (ADR 0018) lets two threads borrow only for the call, which returns after the join (ownership T4), and extends Send/Share to references there only (TypeTable.IsForkSafe); atomic orderings (ADR 0021) are opt-in …With methods whose every access is still atomic, so a weak ordering is a logic error, not a race, and is found by neither the interpreter nor TSancheck-fail/threads.nova (each kind that is not Send); check-fail/thread-both.nova and thread-both-borrows.nova (each refused capture, each conflict); check-fail/shared-path-mutation.nova (two closures writing through one holder, found by the 2026-10-29 review, R-1); run-pass/threads.nova under every C configuration; ThreadSanitizer on the worker test (2026-10-28, clean); a standing TSan configuration is proposedM13
R-14The package index (M13 G6, ADR 0017). A registry entry chooses which commit a build compiles; a malicious or mistaken entry, a rewritten repository, or a compromised index could change what users buildMediumHighEntries name a commit and a source hash; novac fetch checks the checkout's hash, name and version against the entry, and nova.lock pins the result, re-verified on every build (N0325); entries are append-only, checked by novac index-check in the index's CI, and a new package name is reviewed by hand; --update is explicitPackageManagerTests (a wrong hash is N0328 in the index check; a lock no longer satisfied is N0324); the index CI on every pull requestM13

Project and integration risks ​

IDRiskMitigation
P-1The CosmoOS owner does not choose a Nova unit, so nothing integrates in-treeS0 path (cosmoos/build-integration.md) proves C1 with zero tree changes. Present the evidence and a minimal S1 proposal
P-2CosmoOS CI budget (AArch64 ~51.7 of 70 min)S1 adds one tiny port plus one marker line. Measure the added minutes in C1
P-3Module ABI churn (version 11; changes with every exported-struct change)Import kernel structs from the build's headers (ADR 0008, type-system §31; implemented in C6b): layouts are the C compiler's, and each field Nova uses is checked at compile time, so a change fails the build instead of miscompiling (negative controls in cosmoos/c6-report.md). Build per ABI version. Check the ABI version at build time
P-4.NET in the CosmoOS toolchainNot required for S0/S1 (checked-in C). S2 uses a pinned NativeAOT binary or a Dockerfile change, only with owner approval
P-5Scope creep: implementing many features before safety foundationsMilestone gates (roadmap.md). The master prompt's rule 15 is enforced at review
P-6Docs/code drift inside CosmoOS (≈18 disagreements found in the audits)Nova designs cite code, not docs. Re-audit the touched interfaces at the start of each C-milestone
P-7Checked Nova is too slow for kernel hot paths, so components stay in C or move to unsafeMeasured, not assumed: C6a's LZ4 port runs at 0.48–0.78 of C for compression and 0.02–1.12 for decompression (benchmarks/lz4/README.md), mostly in byte loops that C does with memcpy. C6b's fbvalid port costs the same as C with clang (0.98–1.03) but 1.76× with gcc 12, cause not established (benchmarks/fbvalid/README.md). C9a's GPT port, a parser of untrusted disk input, runs at 0.98–1.03 of C's time under clang and is faster under gcc (benchmarks/gpt/README.md). M9a's suite (milestone-9.md, 2026-10-25) confirms it across fifteen workloads: Nova is 0.86–1.63× C on 12 of the 15 with clang, 13–17× on block copies, 1.9–4.4× on vector and string growth, and further behind under gcc. M9b's checked bulk copy (copyFrom, 2026-10-25) brought the block copy to 1.06× C under clang (1.67× under gcc) and LZ4's literal decompression to 0.89–0.97 of C, keeping every check. M9c's bounds-check elimination removes the index checks that loop and guard tests prove (14 of LZ4's 21), keeping every check that could fail; it gains under gcc (LZ4 decompression 1.32×) and nothing under clang, which already removed them. M9d removes the overflow checks that cannot fail (35 of LZ4's 50) and found fbvalid's gcc gap (zero-initialised locals): LZ4 records decompression is now 0.77 of C under clang (3.44× faster), and fbvalid is 1.06× C under gcc. The suite is at 0.81–2.37× C under clang and 1.18–3.48× under gcc. M9e makes placement the default and extends it to boxes that die within a loop iteration (heap-alloc 0.92 of C under clang). M9e′ makes optional boxes one pointer and builds boxes in place: large-objects 0.84 of C under gcc (was 2.92), binary-trees' and arena's peak heaps equal C's. M9's acceptance (9f): 0.93–2.44× C under clang (median 1.17), 1.02–3.54× under gcc (median 1.40), 0.99–2.27× under MSVC (median 1.18), with every check that can fail kept. What remains is mostly vector and string growth and the allocation path; each optimisation is benchmarked against C before it is accepted
P-8An agent runs untrusted or runaway code through novac mcp's nova_runThe interpreter's sandbox (M12, tooling.md §6.3): files, sockets and processes refused; a step budget; a memory budget at which the heap reports out of memory; capped output. It runs in the server's process, so a compiler or interpreter bug is not contained: a host that runs code it does not trust should run novac mcp itself in an OS sandbox. McpTests checks each limit
P-9A Nova check fails in CosmoOS interrupt context (A5's cputime.c runs in the scheduler tick), which is a kernel panicCode that runs there may not fail on any input: every sum that could pass 2^64 saturates, and the remaining checked operations follow comparisons that rule failure out (cosmoos/a5-report.md). fuzz_cputime drives arbitrary sequences through it under the panic hook that aborts, and the guest test runs it under lockdep on both arches

Pre-1.0: the language and its tools are still changing.