ADR 0025 — M14 K3: the kernel library
- Status: Accepted (project owner, 2026-10-29), with option B, the library in Nova's repository, instead of the recommended A.
trustedpackages (§3.2) and the v1 API (§3.3) were accepted as recommended. §6 records what B changes. The wording is the agent's. - Scope: M14 K3, OD-30. The types CosmoOS's roadmap 2.0 needs for SA2 (allocation and object lifetimes) and SA3 (locks, waits and quiescence): where they live, who may declare them thread-safe, and their API.
1. Context
What exists. These were written as demonstrations, each with its own C glue and tests:
Type Where Status KernelHeapC5, integrations/cosmoos/c5/kernel.novaan Allocatoroverkmalloc/kfreeKRef<T>C7, c7/kobject.novaa counted kobject whose drop is the release SpinLock<T>,IrqSpinLock<T>,Mutex<T>and their guardsC7, c7/sync.novathe data only through the guard; a spin guard is an atomic context QuiesceC7, c7/quiesce.novaquiescence guards ObjectCache<T>— not built PerCpu<T>,PreemptGuard— not built (ADR 0024 §3.3) Each lock is made at run time, in a
KernelHeapbox (@placed init). None can be a static yet.Statics (ADR 0024) now let a lock live in a static, built by a
@constantconstructor and a C constant macro. CosmoOS hasSPINLOCK_INIT(name). It has no static initialiser forstruct mutex, onlymutex_init.@threadSafe, which makes a typeSendandShareby declaration, is accepted only in Nova's own library files (N0290). A static's type must beShare, and a kernel lock isShareonly by that declaration.How CosmoOS builds Nova today: each
.novafile is compiled alone (scripts/nova-gen.py), so files share no Nova types.
2. Options
| A. A Nova package in CosmoOS's tree (proposed) | B. A library in Nova's repository (lib/kernel) | C. Keep demonstrations | |
|---|---|---|---|
| Owner | CosmoOS: it binds CosmoOS's C API | Nova, but it binds one kernel's API | — |
| Changes with the kernel | in the same PR as the C it binds | needs a Nova release for each change | — |
@threadSafe | needs a way to trust a package | already allowed | — |
3. Proposed decision: A
3.1 Home and build
kernel/nova/core/in CosmoOS is a Nova package (nova.toml:name = "kcore",kind = "library",profile = "freestanding",float = false).- CosmoOS's kernel code written in Nova becomes packages that depend on it by path.
nova-gen.pybuilds each package withnovac build --emit c --split --program <package>(ADR 0022), so the kernel's Nova packages share Nova types throughkcore. Packages replace today's one-file ports as each is touched; the four existing ports keep working as they are.
3.2 Trusted packages
- A package may declare in its manifest that it holds
unsafesynchronisation code:trusted = true. In such a package@threadSafeis accepted, as in Nova's libraries. Elsewhere it stays N0290. A dependency that is trusted is listed innovac build's output, so a reviewer sees which packages carry the argument. trustedpermits nothing else:unsafealready works in any package, with its/// Safety:comments (U2). What it adds is that a type's thread-safety becomes the package's claim.
3.3 The API (v1)
KernelHeap, from C5, unchanged.ObjectCache<T>, a typed slab cache overkmem_cache. A static or boxed cache made byObjectCache<T>.create(name)(kmem_cache_createwithT's size and alignment).cache.alloc(value)returnsResult<CacheBox<T>, AllocError>. ACacheBox<T>is one pointer whose drop drops the value and returns the memory to the cache. BecauseBox<T, A>needs a stateless allocator (C5),CacheBox<T>is its own type, scoped to the cache it came from.SpinLock<T>,IrqSpinLock<T>,Mutex<T>and their guards, from C7, plus@constantconstructors:SpinLock.constant(name, data)withSPINLOCK_INIT, andMutex.constant(name, data)with a newMUTEX_INIT(name)macro, which CosmoOS adds tomutex.hin the same change.PreemptGuard(preemption off until dropped; an atomic context) andIrqGuard(interrupts off; also atomic), andPerCpu<T>:CONFIG_MAX_CPUSvalues,PerCpu.constant(v), andp.local(borrow guard), which lends this CPU's value while the guard is borrowed. Itsunsafecore is argued once: a guard pins the CPU, and anIrqGuardalso keeps this CPU's interrupt handlers out.KRef<T>andQuiesce, from C7.
3.4 Tests
The C5 and C7 self-tests and stress tests move into CosmoOS as kernel self-tests of kcore. They run in every debug boot, on both architectures, under lockdep. Nova's c5.py and c7.py stay as they are, against their own copies, until kcore replaces them.
3.5 What Nova changes
trusted = truein the manifest, and@threadSafein trusted packages (tooling §4, type-system §38.1).- Nothing else. The API is Nova code in CosmoOS.
4. Consequences
- Order:
- Nova:
trusted, then the 0.2.0 release with statics and--split. - CosmoOS: the pin moves to 0.2.0,
kcorelands with its self-tests, andMUTEX_INITis added. - M14 K1's acceptance, a kernel object table in a static
SpinLock, is then CosmoOS's SA2 proving conversion.
- Nova:
kcore'sunsafecode is a new place unsoundness can hide (R-2, R-13). Its/// Safety:arguments go in the safety-audit pack (safety-audit.md§3) for the outside audit (K6).- Not decided here: a stateful
Boxallocator, which would letObjectCachebe anAllocator, and wait queues and completions (SA3's own design, on top of these types).
5. Questions for the owner
- The library as a Nova package in CosmoOS's tree,
kernel/nova/core/("kcore")? (Recommended.) trusted = trueinnova.tomlto allow@threadSafethere? (Recommended; the alternative is to keep the kernel library's thread-safe types in Nova's repository.)- The v1 API in §3.3, including the new
MUTEX_INITmacro in CosmoOS? (Recommended.)
6. The decision: B, and what it changes
The owner chose B: the kernel library is part of Nova's repository and ships inside novac, like lib/std.
lib/kernel, a library for a kernel with CosmoOS's C API. It is not compiled into every freestanding build, as lib/core is, because it names CosmoOS's headers. A build selects it withkernel = "cosmoos"innova.toml's[package], ornovac --kernel cosmoosfor a build of files. That is valid only with the freestanding profile (N0320 otherwise). Its names are global in the build, as lib/std's are in a hosted one (§38.5).@threadSafeis accepted in lib/kernel as in Nova's other libraries.trusted = true(§3.2) is still accepted, for packages outside Nova that hold synchronisation code of their own, such as a CosmoOS driver's.- A change to the library is a Nova change, released and then picked up by CosmoOS's pin. §3.1's "in the same PR as the C it binds" becomes two changes, Nova's first.
MUTEX_INITwas to be added to CosmoOS'smutex.h(not done; §7). - Testing.
- Nova's suite type-checks programs that use lib/kernel (
--kernel cosmoos, freestanding) and the misuses it refuses. It does not compile their C, because CI has no CosmoOS headers. integrations/cosmoos/k3.pybuilds and runs the library's tests natively against a CosmoOS checkout, asc7.pydoes (its host harness and shims).- Once the pin moves, CosmoOS's own self-tests, in debug boots on both architectures, are the library's acceptance.
- Nova's suite type-checks programs that use lib/kernel (
7. Implementation (2026-10-29)
lib/kernel(heap.nova,sync.nova,kobject.nova,quiesce.nova), embedded in novac. It is selected by--kernel cosmoos,kernel = "cosmoos"innova.toml, or// option: kernel cosmoosin a corpus file.- From C5 and C7:
KernelHeap,SpinLock/IrqSpinLock/Mutexand their guards, now@threadSafewithT: Send;KRef<T>;Quiesce,Deferred<T>,deferDrop. - New: the
@constantconstructors (SpinLock<T>.constantandIrqSpinLock<T>.constant, withSPINLOCK_INIT);PreemptGuard;IrqGuard; andPerCpu<T: Copy + Send>, whoselocal(IrqGuard)returns aPerCpuRefthat owns the guard and lends the value, a second loan on one CPU panicking.
- From C5 and C7:
- What this needed in the compiler (type-system §39.4, "C state at compile time"):
- C state allowed in a static's type, a C constant macro's result and a
@constantconstructor's literal; @constantfunctions whose result holds C state compile-time only;- each static C string emitted as a
chararray of its own, because CosmoOS builds with-Wwrite-strings; - the emitter now counts a box's construction (allocator,
@placedmethods) and a taken function address as reaching library code, which lib/kernel's boxed locks andKRefneed; - lib/kernel's own composite types are emitted only when used, as lib/std's are.
- C state allowed in a static's type, a C constant macro's result and a
- Trusted packages:
trusted = trueinnova.toml(PackageInfo.Trusted) admits@threadSafe. - Found in the implementation:
- CosmoOS exports to modules neither
arch_irq_save/restore(they inline toarch_irq_*_hw) norpreempt_enable. SoIrqGuard,PreemptGuardandPerCpuare for code built into the kernel. Locks, kernel objects and the heap work in modules too. ObjectCache<T>is not built. Moving a value that is notCopyinto raw memory has no primitive (p.writeneedsCopy, §27.3), so a cache box cannot be filled. It needs a language addition (a move into raw memory, or a statefulBoxallocator) and is left open. Resolved by ADR 0027, with a staticMutexbelow.- No static
Mutex. A CosmoOS mutex holds list heads that point to themselves. CosmoOS's static initialisers take the variable's own name for that (WAITQUEUE_INIT(name),LIST_HEAD_INIT(name)), and a C constant macro is not given it. So §3.3'sMUTEX_INIT(name)could not initialise one correctly, and it was not added to CosmoOS. A static mutex needs initialisers that know their own address (a §39 extension), or a lock made at start-up. It is left open. Static spinlocks are not affected:SPINLOCK_INIThas no self-pointers.
- CosmoOS exports to modules neither
- Tests:
- Nova's suite:
check-pass/kernel-library.nova,check-fail/kernel-library.nova(sleeping under a static spin guard, with interrupts off),check-fail/kernel-library-constant.nova(N0338), andKernelLibraryTests(trusted packages, the manifest keys, the library absent unless selected). integrations/cosmoos/k3.py(2026-10-29, both architectures, one file and split): the module subset (k3/module.nova) meets C4's module checks against CosmoOS's headers and exports. The whole library compiles as kernel code; its only kernel-internal references arearch_irq_save_hw/arch_irq_restore_hw.- In a booted kernel (2026-10-29, CosmoOS #489): the self-test
nova-kcore(CosmoOSkernel/nova/kcore_test.nova, built with novac 0.2.0) uses every type butObjectCacheas code built into the kernel: a table under a staticSpinLock, a staticIrqSpinLock, aPerCpucounter, boxed locks, aKRef's clone and release, a grace period and a deferred free. It passes in debug boots (lockdep on) on AArch64 and x86-64. It needed CosmoOS's lockdep class table raised from 512 to 640: main was at about 500, and the test's classes overflowed it later in an x86-64 boot. - Static analysis. CosmoOS's analyzer gate (
make analyze, clang--analyze) reports "Potential memory leak" for eachBox.createin aKernelHeap. They are false: each box frees its block on drop. CosmoOS #489 baselines the four the test makes. The cause, reduced in plain C (2026-10-29, Apple clang 21):KernelHeap.allocatereturnsOk(p), which the emitter writes as a compound literal with a union member ((R){ .tag = 0, .u.v_Ok = { .f__0 = p } }), and the analyzer loses the pointer read back from that union. The same value built by member stores (r.u.v_Ok.f__0 = p; return r;) is tracked, and so is a plain struct. - Fixed in the emitter (2026-10-29, after 0.2.0; c-backend §2): a variant whose payload may hold a pointer is built by a
static inlineconstructor of member stores (nvMk_<E>_v_<variant>); scalar payloads keep the compound literal. CosmoOS'skcore_test.novaanalyzes clean on both architectures with it (4 reports with 0.2.0). A first version used constructors for every payload: gcc then ranstring-building1.8× slower, with a constructor only on its cold growth path (inferred: inlining). With constructors for pointer payloads only, the suite on the Ryzen box (Nova, both compilers, interleaved, two rounds) moves within its noise (±6% where the C did not change at all), except memory-copy under clang, 1.10×, whose only change runs once outside the loop and which is 1.03× with-falign-loops=32(code placement, inferred). CosmoOS's baseline entries can go once its pin moves past 0.2.0.
- Nova's suite: