Skip to content

Latest commit

 

History

History
1022 lines (847 loc) · 50.8 KB

File metadata and controls

1022 lines (847 loc) · 50.8 KB

Kernel CVE Audit — Applicability to DuetOS

Audience: Kernel hackers, security folks

Execution context: N/A — this is an audit log, not a runtime surface

Maturity: v1 — opened 2026-05-10 against the "Copy Fail" + "Dirty Frag" disclosure wave; expanded 2026-05-10 to cover Windows kernel + driver CVE families; 2026-05-11 fixes landed for classes M, E, N, O, CC, FF, GG, II. Intended to be re-run after every major kernel CVE with a public exploit.

Overview

DuetOS shares zero kernel source with Linux or Windows. Every CVE numbered against a Linux file (mm/gup.c, fs/splice.c, crypto/algif_aead.c, net/ipv4/esp4.c, …) or a Windows driver (tcpip.sys, win32k.sys, ntoskrnl.exe) cannot apply by name. But the vulnerability classes behind those CVEs are universal kernel hazards — COW races, uninitialised struct flags, in-place writes over externally-backed memory, heap cross-cache pivots, missing fault fixup on user copies, integer underflow in fragment reassembly, device-supplied loop bounds, etc. — and this page tracks whether the underlying class has a native landing spot in DuetOS, where the relevant surface lives, and what (if anything) needs to change.

The intended workflow: when a high-impact CVE drops, walk the classes below, decide whether DuetOS has a native equivalent, and if so file a roadmap entry. Don't extrapolate from CVE names — the question is always "could a DuetOS process do the equivalent thing through our actual syscall / driver / loader surface?"

When to Use / When to Read

  • A new Linux kernel privesc/CVE has a public PoC and you want a same-day answer on whether DuetOS is in scope.
  • You're about to add a new syscall, IPC primitive, or in-kernel protocol stack — check the matching class entry before designing the ownership invariants.
  • You're auditing an existing surface and want to know which classes have already been ruled in or out for it.

Audit Methodology

For each class:

  1. Identify the Linux root cause. Not the CVE number — the shape of the bug (e.g. "kernel function writes in place to memory referenced through an externally-backed page table").
  2. Find the DuetOS equivalent surface. Cite file:line.
  3. Decide the verdict:
    • Absent — the surface doesn't exist (e.g. no AF_ALG, no RxRPC). Class is structurally inapplicable.
    • Present, hardened — surface exists and the relevant invariant is enforced. Cite the enforcement code.
    • Present, audit pending — surface exists, invariant is not obviously enforced or has not been walked. File a roadmap item.
  4. "Absent" is not "secure." If a roadmap item adds the surface later, this audit must be re-run for that class.

CVE Classes — Verdicts

A. Zero-copy pipe page sharing — Dirty Pipe class

Linux reference: CVE-2022-0847. splice() plants a page-cache page into a pipe; a later write() to the pipe coalesces into that page via the PIPE_BUF_FLAG_CAN_MERGE flag, which was not zeroed at pipe-buffer alloc. Net effect: unprivileged write into the page cache of any readable file.

DuetOS surface: kernel/subsystems/linux/syscall_pipe.cpp:309 (PipeSpliceFromPipe), kernel/subsystems/linux/pidfd_splice.cpp:285 (DoSplice / DoTee / DoVmsplice).

Verdict: Absent. DuetOS pipes are kernel-internal byte rings (dst.buf[dst.head] = src.buf[src.tail]syscall_pipe.cpp:347). There is no pipe_buffer struct, no page reference inside the pipe, no SPLICE_F_GIFT path, no merge-flag. splice and tee walk the ring byte-by-byte; vmsplice falls through PipeWrite which goes through mm::CopyFromUser. File↔pipe splice is a documented GAP (returns -EINVAL). The page-sharing primitive that Dirty Pipe needs does not exist.

B. AF_ALG-style kernel-crypto socket — Copy Fail class

Linux reference: CVE-2026-31431. algif_aead recvmsg sets up an in-place AEAD operation (input scatterlist == output scatterlist), and authencesn skips byte-copying the auth tag — memcpy_sglist performs a 4-byte controlled write into whichever page the user-side scatterlist points at, including page-cache pages reachable via splice() from a readable file. Net effect: 732-byte exploit to root.

DuetOS surface: kernel/crypto/ (AES, HMAC, PBKDF2, SHA). All in-kernel functions called by name from kernel-internal callers. There is no AF_ALG, no algif_*, no socket-exposed crypto API, and no user-supplied scatterlist surface.

Verdict: Absent. The Linux exploit primitive requires (a) a kernel-crypto API reachable from user mode without privilege (b) an in-place mode where src and dst alias the same scatterlist (c) a tag-skip optimisation that turns into an OOB write when src and dst differ in length. None of these exist in DuetOS today. If the roadmap ever adds a user-facing crypto API, this class must be re-auditedkCapCrypto gating is not a substitute for refusing shared-page operands.

C. SKB fragment ownership / MSG_SPLICE_PAGESDirty Frag class

Linux reference: CVE-2026-43284 (xfrm/ESP), CVE-2026-43500 (RxRPC). The IPv4/IPv6 datagram append paths did not set SKBFL_SHARED_FRAG when splicing user pages into a socket buffer, so ESP input took its no-CoW fast path and decrypted in place over memory the skb did not own. Net effect: controlled write into page-cache pages.

DuetOS surface: kernel/net/stack.h (bare IPv4 + Ethernet headers). No skb-equivalent with fragment lists; no MSG_SPLICE_PAGES-equivalent zero-copy send path; no IPsec / XFRM / ESP stack; no RxRPC / AFS.

Verdict: Absent. The network stack is v0. When IPv6, IPsec, or zero-copy sendmsg lands, this class must be re-audited — the defensive invariant is "any externally-backed buffer fragment is marked, and any in-place transform refuses to operate on a marked fragment." Write that invariant into the protocol stack from day one; do not retrofit it.

D. Copy-on-write race — Dirty COW class

Linux reference: CVE-2016-5195. get_user_pages followed by a madvise(MADV_DONTNEED) raced against the fault handler's write-permission re-check, letting an unprivileged process write into a private-mapped read-only file (e.g. /etc/passwd) via the page cache.

DuetOS surface: kernel/mm/paging.cpp, kernel/arch/x86_64/traps.cpp.

Verdict: Absent (today), audit-pending (when COW lands). DuetOS does not currently implement COW, fork-time shared-and-write-protected mappings, or madvise(DONTNEED). When fork() and COW arrive (see roadmap entry below) the invariant is: the dirty-bit clear-and-fault sequence must be atomic with respect to MADV_DONTNEED-style region mutations. Linux's fix was to require FOLL_WRITE to retake the faulted-and-marked-write path even after a discard; mirror that.

E. Uninitialised struct flags — Dirty Pipe root cause

Linux reference: the underlying bug behind Dirty Pipe — pipe buffers were allocated without zeroing .flags, so stale CAN_MERGE survived across a free/realloc.

DuetOS surface: kernel/mm/slab.cpp:290 (SlabAlloc), kernel/mm/kheap.cpp (kheap).

Verdict: Present, partially hardened. Slab cache descriptors are zero-initialised (*c = SlabCache{}slab.cpp:230). Slab object payloads are NOT zeroed on alloc — they are poisoned with kSlabFreedObjectPoison = 0xCC on free and the band is verified on next alloc (slab.cpp:401), which catches UAF but does not give the caller zeroed memory. The caller is responsible for initialising every field before any kernel code reads it. That is the same contract Linux's slab has, and it is the contract that Dirty Pipe violated. The kheap poison fill of 0xDE (kheap.cpp:98) is for free, not alloc.

Mitigating factor today: no DuetOS surface today consumes a flag-style field from a slab-allocated object without an explicit field write. There is no equivalent of pipe_buffer.flags. But this is a footgun for future slab consumers — see the roadmap entry below for either a SlabAllocZeroed() helper or a per-cache "zero on alloc" flag for caches that hold privilege-relevant fields.

F. Slab cross-cache / heap pivot — DirtyCred class

Linux reference: CVE-2022-2588 family. Free a cred (or any small privilege-bearing object), reallocate the same slot as an attacker-controlled type via cross-cache slab merging, and the privilege check in the kernel now reads attacker bytes.

DuetOS surface: kernel/proc/process.h (inline durable caps, leases, deadlines, generations, and monotonic ceiling).

Verdict: Absent. The complete capability authority state lives inline inside the Process struct and is serialized by Process::cap_lock — it is not a separately heap-allocated credential object, not in a dedicated slab cache, and not allocatable by a user-mode flow that an attacker can trigger. There is no slab-cache merging in DuetOS (every SlabCache is its own slab pool — slab.cpp:225), so cross-cache reallocation is structurally impossible. If a future refactor moves credentials out of Process into a separately allocated object, this class must be re-audited.

G. copy_to_user / copy_from_user fault handling

Linux reference: general class — any kernel path that dereferences a user pointer without a fixup handler can be turned into either a kernel oops (DoS) or, when paired with a page-vanish primitive, an info leak.

DuetOS surface: kernel/mm/user_copy.S, kernel/arch/x86_64/traps.cpp (fault fixup dispatcher), kernel/mm/paging.cpp:432 (pre-check).

Verdict: Present, hardened. SMAP is gated on entry/exit (stac/clac), the user-copy code is tagged with __copy_user_{from,to}_{start,end} ranges so the trap dispatcher can redirect a faulting RIP to __copy_user_fault_fixup, and the pre-check at paging.cpp:432 refuses writes to a read-only user page before SMAP is opened. Callsite spot-check: every Linux thunk in kernel/subsystems/linux/syscall_io.cpp and every Win32 thunk in kernel/subsystems/win32/file_syscall.cpp routes through mm::CopyFromUser / mm::CopyToUser; no raw dereference of a user-supplied pointer was observed in the audit.

H. io_uring-style submission-queue races

Linux reference: "Déjà Vu in io_uring" research, multiple CVEs. A user-shared submission/completion queue exposes kernel state machines to attacker-controlled re-entry and TOCTOU.

DuetOS surface: none. kernel/ipc/ and kernel/syscall/ do not expose user-shared ring buffers; the existing ring buffers in the tree are kernel-internal diagnostic / log structures.

Verdict: Absent. When (if) a high-throughput async syscall surface lands, this class must be re-audited from scratch — the design rule is "no kernel state machine advances on user-writable memory; submission entries are copied in, completion entries are copied out, and the kernel side reads neither again."

I. Bluetooth L2CAP / RFCOMM — BlueBorne / BleedingTooth class

Linux reference: BlueBorne (CVE-2017-1000251) stack buffer overflow in L2CAP config response parsing; BleedingTooth (CVE-2020-12351) type confusion in L2CAP; CVE-2025-21969 UAF in L2CAP. All ring-0 zero-click via Bluetooth proximity.

DuetOS surface: kernel/net/bluetooth/hci.{h,cpp} — HCI transport layer only. The upper-stack TUs (L2CAP, RFCOMM, SDP) are named as architectural placeholders, not implemented.

Verdict: Absent. No parser surface, no fragment reassembly, no connection state machine reachable from over-the-air bytes. When L2CAP lands, the protocol-parser invariants from class C (externally-backed fragments, length-field validation before indexing) must be designed in, not patched in.

J. USB descriptor parsing — BadUSB / CVE-2016-2384 class

Linux reference: CVE-2016-2384 USB MIDI double-free turned into arbitrary kernel code execution; generic class — any USB descriptor parser that trusts device-supplied lengths.

DuetOS surface: kernel/drivers/usb/xhci_descparse.cpp, kernel/drivers/usb/hid_descriptor.cpp, kernel/drivers/usb/usb_class_desc.cpp.

Verdict: Present, hardened. Descriptor parsing validates the length byte against the remaining buffer before trusting array counts; HID boot descriptors parse with offset guards; class drivers (CDC-ECM, MSC-SCSI, RNDIS) bounds-check payload sizes before dispatch. The "trust device length without remaining-buffer check" pattern was not found. Re-audit any new class driver (audio-class, video-class, mass-storage write path) before merge.

K. Filesystem parser bugs — ext4 UAF, NTFS OOB

Linux reference: CVE-2024-0775 ext4 remount UAF; broad class of FS-parser bugs trusting on-disk lengths.

DuetOS surface:

  • FAT32 — kernel/fs/fat32.cpp:303 BPB sanity-checks sector size (== 512), cluster size (!= 0), FAT count, image size before trusting any of them. LFN slot parsing bounds-checks offsets in kernel/fs/fat32_dir.cpp.
  • exFAT — kernel/fs/exfat.cpp minimal read-only, headers validated before field reads.
  • NTFS — kernel/fs/ntfs.cpp is a read-only stub.
  • ext4 — kernel/fs/ext4.cpp read-only mount, no remount path (the surface CVE-2024-0775 exploits does not exist).

Verdict: FAT32 hardened, ext4/NTFS structurally absent for write paths. When ext4 write or NTFS directory parsing lands, re-audit this row — the invariant is "every on-disk length field is bounded to the in-memory slice before being used as an array index or loop bound."

L. IPv6 fragmentation / reassembly — CVE-2024-38063 class

Linux/Windows reference: CVE-2024-38063 (Windows tcpip.sys) integer underflow in IPv6 fragment reassembly → heap OOB write → zero-click RCE. CVE-2021-24086 and the EvilESP family are siblings.

DuetOS surface: kernel/net/stack.cpp is IPv4-only — comments at lines 9–17 enumerate "Ethernet → ARP → IPv4 → ICMP/UDP/TCP." There is no IPv6 stack, no fragment reassembly, no Destination Options parsing.

Verdict: Absent. When IPv6 lands, design the reassembly path with the invariant "fragment-offset and length fields are unsigned and bounded before arithmetic that could underflow." Most of CVE-2024-38063 was a missed length < header_size check in a path that did length - header_size directly.

M. ACPI / AML parser — CVE-2024-56662 class

Linux reference: CVE-2024-56662 vmalloc-OOB read in acpi_nfit_ctl; broad class — any AML interpreter that trusts table-supplied lengths.

DuetOS surface: the recursive AML TermList walker now lives in the memory-safe no_std duetos_aml Rust crate (kernel/acpi/aml_rust/); kernel/acpi/aml.cpp is a thin FFI caller plus the small offset slicers (AmlMethodBody / AmlNameValue / AmlReadS5) the evaluator drives. Every package-style read site (handle_container, handle_method, index_field_list in Rust; AmlMethodBody in C++) bounds pkg_len against the remaining slice and against its own encoding length, and the Rust walker's slice indexing is bounds-checked by construction (a logic slip aborts rather than reads OOB).

Verdict: Present, hardened + continuously fuzzed. Two distinct length-trust bugs were closed:

  1. u32-wrap on after_op + pkg_len (the original follow-up): resolved — every site now compares pkg_len > end - after_op (subtraction on the known-good slice) so the addition can never wrap small and pass the bound check.
  2. Under-length PkgLength underflow (found by fuzz_aml, tests/fuzz/): a PkgLength encoding a value smaller than its own byte count (e.g. a 1-byte PkgLength of 0) made name_off = after_op + plen_consumed exceed pkg_end, so the pkg_end - name_off length handed to ReadNameString underflowed to ~4 GiB and OOB-read one byte past the mapped table. Fixed by rejecting pkg_len < plen_consumed at all four sites (a PkgLength counts its own encoding bytes per ACPI 6.x §20.2.4, so this is a true invariant).

The whole DSDT/SSDT AML walker — plus the AmlMethodBody / AmlNameValue / AmlReadS5 consumers — is now driven by the fuzz_aml libFuzzer harness on every fuzz-all run, and the firmware ACPI tables (RSDP / header / MADT / FADT / MCFG / HPET / SRAT) by fuzz_acpi, so a regression in this class surfaces as a recorded crash rather than an audit-pending note.

N. CPU speculative execution — Spectre / Retbleed / Downfall

Linux reference: Spectre v1/v2 (CVE-2017-5753 / 5715), Meltdown (CVE-2017-5754), Retbleed (CVE-2022-29900), Downfall, MDS, SRSO. Microarchitectural side-channels that leak kernel memory across the user/kernel boundary.

DuetOS surface: kernel/arch/x86_64/cpu_mitigations.cpp, kernel/arch/x86_64/retpoline_thunks.S.

Verdict: Present, partially hardened. ARCH_CAPABILITIES MSR is probed and needs_* flags decide IBRS / STIBP / SSBD / MDS_CLEAR / TAA / RFDS gating. Retpoline thunks exist for indirect branches. SMAP / SMEP are honoured (user_copy.S). What's missing: KPTI (page-table isolation against Meltdown) is deferred per wiki/reference/Roadmap.md ("KPTI enable — settled — DEFERRED"). On a CPU that needs Meltdown mitigation (pre-Coffee Lake without silicon fix), DuetOS today is exposed. This is a known and explicitly-tracked decision, not an audit finding. Spectre v1 (bounds-check bypass) is mitigated by util::MaskedIndex (see kernel/util/nospec.h) applied at every audited user-controlled array-index dispatch site across the Win32 and Linux ABIs — the Win32 NT handle table (ipc/handle_table.cpp), the Win32 thread / process / GDI handle tables (syscall/syscall.cpp, subsystems/win32/gdi_objects.cpp), the Win32 file handle resolver (fs/file_route.cpp::HandleToSlot), and the full Linux fd dispatch surface (linux/syscall_io.cpp, syscall_file.cpp, syscall_path.cpp, syscall_xattr.cpp, pidfd_splice.cpp, syscall_fs_mut.cpp, syscall_fd.cpp, syscall_socket.cpp, syscall_mm.cpp, syscall_async_io.cpp, inotify.cpp, fanotify.cpp, syscall_misc.cpp, extra_syscalls.cpp, syscall_stub.cpp). Discipline for new dispatch sites: after the runtime if (idx >= bound) check, mask with util::MaskedIndex(idx, bound) before the array load.

O. Reference-count overflow — CVE-2016-0728 keyring class

Linux reference: CVE-2016-0728 (Linux keyring refcount overflow → UAF → root). General class — any 32-bit refcount that an unprivileged path can drive without saturation.

DuetOS surface: kernel/ipc/kobject.cpp:94 (KObjectAcquire), kernel/ipc/kobject.cpp:115 (KObjectRelease).

Verdict: Present, hardened. KObjectAcquire checks refcount == 0 before incrementing (catches resurrection / UAF); KObjectRelease checks for underflow before decrementing. The global g_kobject_lock serialises all mutations, removing the race window the CVE-2016-0728 exploit relied on. The refcount is not saturating, so a pathological path that drives 2³² increments without a free would still wrap; gating that by capability (handles to KObjects are issued through cap-gated syscalls) makes this unreachable from unprivileged code today, but a future "shareable handle" surface should reconsider. Follow-up: saturating increment helper, used by any future high-throughput refcount.

P. TOCTOU between syscall arg validation and use

Linux reference: CVE-2024-43882 (exec TOCTOU), CVE-2025-40331 (SCTP diag), general class.

DuetOS surface: spot-checked across kernel/subsystems/linux/syscall_*.cpp, kernel/subsystems/win32/file_syscall.cpp, kernel/syscall/.

Verdict: Absent. Every audited syscall path copies user input to a kernel-local struct via mm::CopyFromUser once and operates on the kernel copy thereafter. No "validate-then-re-read-from-user" pattern was observed. File-descriptor numbers are validated against the per-process FD table once and the cached slot is used. SMAP + stac/clac further ensures the kernel can't accidentally re-read user memory mid-handler.

Q. Win32k / GDI memory corruption — CVE-2025-49667 / 62215 class

Windows reference: CVE-2025-49667 (ICOMP double-free → NT AUTHORITY\SYSTEM), CVE-2025-62215 (race + double-free in kernel shared state), generic Win32k surface — GDI handles, brushes, pens, DCs.

DuetOS surface: kernel/subsystems/win32/gdi_objects.{h,cpp}, userland/libs/gdi32/.

Verdict: Present, hardened. GDI objects (bitmaps, brushes, pens, mem-DCs) live in fixed-size kernel arrays (g_bitmaps[kMaxBitmaps] etc.). Handles are 64-bit tag | index values (MakeHandlegdi_objects.cpp:65); every lookup checks the tag (line 74), bounds-checks the index against the array size, and validates the alive flag before use. No heap allocation, no separately allocated kernel object, no refcount race window. The cross-cache primitive Win32k exploits rely on does not exist. Re-audit if GDI ever moves to heap-allocated objects or per-thread caches.

R. PE/COFF loader OOB parsing

Windows reference: general class — PE loaders that trust NumberOfRvaAndSizes, NumberOfSections, import / export / relocation table lengths without bounds-checking against the in-memory image slice.

DuetOS surface: kernel/loader/pe_loader.cpp, kernel/loader/pe_exports.cpp.

Verdict: Present, hardened. ReadDataDir (pe_loader.cpp:279) reads NumberOfRvaAndSizes and refuses idx >= num_dirs. Optional header bounds are validated at line 318 (min_opt = NumberOfRvaAndSizes + 4). Section count and section table bounds are checked before iteration. Import / export table RVAs are converted to file offsets via a bounds-checked RVA-to-file mapping. Relocation parsing validates RVA before applying. The patterns CVE-class PE bugs rely on (out-of-image read, integer wrap on num * entry_size, signed/unsigned confusion on relocation offsets) were spot-checked and not found.

S. IOCTL size validation — driver-shaped CVE class

Linux/Windows reference: general class — IOCTL handlers that trust user-supplied buffer sizes and copy variable-length data based on them. Many driver CVEs land here.

DuetOS surface: kernel/subsystems/linux/syscall_io.cpp:448 (Linux ioctl() thunk).

Verdict: Present, hardened. Every supported ioctl() case hard-codes the kernel-side struct size (sizeof(Termios) etc.) and uses it explicitly in CopyToUser / CopyFromUser. There is no variable-size dispatch table where a user-supplied length controls the copy. Native DuetOS does not expose Linux-style ioctl on its own syscall surface (kernel objects are addressed via typed handle ops, not opaque ioctl codes).

T. Audio HDA codec parsing — driver-shaped CVE class

Reference: general class — audio drivers that trust codec verb-return counts (SubordinateNodeCount, ConnListLength) as loop bounds.

DuetOS surface: kernel/drivers/audio/hda.cppWalkCodec (line 346) walks function groups and widgets reported by the codec.

Verdict: Present, hardened. Device-supplied counts are bounded before loop iteration: fg_walk_limit = (fg_count > 4) ? 4 : fg_count (line 370), widget_walk_limit = (widget_count > 64) ? 64 : widget_count (line 392). ConnListLength is masked with & 0x7Fu (line 451) per spec. No accumulator allows a single codec response to drive unbounded work. The HDA-specific pattern Linux's old snd_hda_codec bugs relied on (trust the codec's claimed widget count) is structurally prevented here.

U. Netfilter / nf_tables UAF — CVE-2024-1086 "Flipping Pages"

Linux reference: double-free in nft_verdict_init; a positive "drop error" value collides with NF_ACCEPT and nf_hook_slow double-frees the SKB. Universal LPE 5.14–6.6; CISA-listed for active ransomware exploitation in 2025.

DuetOS surface: kernel/net/firewall.{h,cpp}.

Verdict: Hardened. Firewall rules are a fixed-size array of slots (max 32) with first-match-wins evaluation; no dynamic attach / detach of hooks, no nft_expr-style per-rule heap allocation, no verdict struct that can collide with itself, no heap-allocated SKB. Connection tracking is a fixed-capacity LRU table (firewall.h:204). The structural primitives Linux's exploit requires (heap-allocated verdict, hook-list mutation during a hot-path walk, unprivileged user namespaces) do not exist in DuetOS. Re-audit if rule tables ever move to heap-backed storage or if a netlink-style configuration channel is added.

V. eBPF verifier bypass — CVE-2020-8835 / NCC 2024 audit class

Linux reference: the BPF verifier's scalar-tracking failed under arithmetic that confuses safe-bounds, allowing the JIT to emit code that reads/writes arbitrary kernel memory.

DuetOS surface: bpf() syscall slot in linux_syscall_table_generated.h:321 is wired to a stub dispatcher; there is no BPF VM, no verifier, no JIT, and no bytecode upload path. The only related reference is the W^X enforcement at mmap (syscall_mm.cpp strips X from RW pages, which would refuse any user-side JIT region).

Verdict: Absent. If a programmable kernel-side filter is ever added (eBPF-equivalent, even just for sockets or tracing), the verifier becomes the load-bearing piece and the entire BPF CVE family becomes in-scope. Treat that as a design decision, not an incremental feature.

W. GPU driver vulnerabilities — CVE-2024-0126 (NVIDIA),

CVE-2024-36342 (AMD heap OOB), CVE-2025-23280 (NVIDIA UAF)

Reference: GPU drivers are a perennial source of kernel elevation-of-privilege bugs — large IOCTL surface, complex command-buffer parsing, DMA into shared rings.

DuetOS surface: kernel/drivers/gpu/{intel_gpu, amd_gpu, nvidia_gpu, virtio_gpu}.{cpp,h}. NVIDIA / AMD / Intel native drivers are minimal stubs today (KB-scale). virtio-gpu (~31 KB) is the only path that touches a real device model.

Verdict: Present, partially exposed (virtio-gpu only). No proprietary-vendor command parser is implemented yet. virtio-gpu uses fixed-layout ring buffers and bounds-checks BAR offsets (virtio_gpu.cpp:89offset + length > bar.size). There is no user-mode IOCTL surface that submits commands to the GPU today — graphics access is entirely kernel-internal. Re-audit trigger: the moment any GPU driver accepts a user-supplied command buffer (Vulkan ICD, D3D11/12 translation layer, native DRM-style submit syscall), the entire NVIDIA/AMD/Intel CVE family becomes in-scope. Design the user→kernel command-submit boundary with explicit per-field bounds checks and a separate "verified submission" structure that the GPU consumes, not the user's buffer directly.

X. Hypervisor escape / vsock — CVE-2025-21756 "Attack of the

Vsock" class

Linux reference: vsock transport-reassignment refcount race → UAF → guest-to-host kernel RCE. Broader class: any guest driver that shares a buffer with the host without ownership invariants.

DuetOS surface: kernel/arch/x86_64/hypervisor.{h,cpp} detects hypervisor presence for CPU-feature masking only. There is no vsock driver, no vhost-user, no virtio-fs guest channel.

Verdict: Absent. If DuetOS ever adds a guest-shared transport (vsock, virtio-9p, virtio-fs), the host is in the attacker's trust boundary, and every refcount on a shared object becomes load-bearing. Add a saturating refcount helper (class-O follow-up) before that surface lands.

Y. User namespace / cgroup escape — CVE-2022-0185 /

CVE-2022-0492

Linux reference: legacy_parse_param integer underflow in filesystem-context API, exploitable from an unprivileged user namespace; cgroup-v1 release_agent abuse for container escape.

DuetOS surface: none. CLAUDE.md states "No containers" as a project pillar. There is no unshare(), no namespaces (user, mount, pid, net, ipc), no cgroups, no seccomp filter chain. Per-process isolation is purely address-space + capability-based.

Verdict: Absent by design. This is the strongest "absent" verdict in the audit — namespaces are a stated non-goal, not a deferred feature. If sandboxing ever needs container-like boundaries, prefer extending the existing capability set over adding namespaces; the entire container-CVE family is structural, not patchable.

Z. Kernel stack overflow via recursion — Project Zero class

Linux reference: Project Zero "Exploiting recursion in the Linux kernel" (2016); generic class — any kernel parser with unbounded recursion exhausts the small (16 KiB) kernel stack and overflows into adjacent data.

DuetOS surfaces audited:

  • AML interpreter (kernel/acpi/aml.cpp:351): kMaxAmlRecursion = 32 hard cap, with a depth guard (aml.cpp:369) that bails out cleanly. Hardened.
  • PE loader (kernel/loader/pe_loader.cpp): section iteration is loop-based with NumberOfSections bounds-checked at line 419; DLL imports are iterated, not recursively resolved. Hardened.
  • VFS path resolution (kernel/fs/vfs.h): no symlinks implemented, FAT32 directory walk is iterative (cluster-chain follow). Hardened.
  • Network stack (kernel/net/stack.cpp): IPv4-only, no nested encapsulation parser. Hardened.
  • FAT32 (kernel/fs/fat32.cpp): iterative cluster traversal, not recursive. Hardened.

Verdict: Hardened. Every parser-style surface in the audited tree is iterative or has an explicit recursion cap. Re-audit trigger: any new parser that calls itself transitively (symlinks landing, IPv6 nested headers, AML method call/return across tables) needs an explicit depth bound at the recursion site, not at the call site.

AA. Format string / printk-recursion class

Linux reference: historical recursive-printk lockups; broad class — any kernel logger that accepts a user-controlled format string or a re-entrant code path.

DuetOS surface: kernel/log/klog.h:29 documents the design choice: "No variadic printf" — every log macro (KLOG_INFO, KLOG_INFO_S, KLOG_INFO_V, KLOG_INFO_2V, etc.) takes a fixed shape with typed arguments. There is no snprintf-style user-format surface.

Verdict: Hardened. The bug class is structurally eliminated by the macro shape. Implementation (klog.cpp) writes directly to the serial / ring sink without re-entering klog from a format-conversion helper. Re-audit trigger: any future KLOG_* helper that takes a const char* fmt, ... undoes this guarantee — refuse the variadic form when reviewing.

BB. Integer overflow in size/length arithmetic

Reference: general class — count * sizeof(T), offset + len, end - len, cap * 2 all can wrap. Many CVEs fall into one of these shapes.

DuetOS spot-check (5 sites):

  1. kernel/drivers/gpu/virtio_gpu.cpp:89offset + length > bar.size is the comparison, but the addition can wrap if both inputs are attacker-influenced. virtio-gpu BAR values come from PCI config and are trusted, so this is safe today.
  2. kernel/acpi/aml.cpp:554/588/633 — same `pkg_end = after_op
    • pkg_len` shape as class M; already tracked.
  3. kernel/ipc/handle_table.cpp:49 — pure literal bounds (i < kHandleTableCapacity), no arithmetic. Safe.
  4. kernel/fs/fat32.cpp:303 — sector-size / cluster-size validated to constants before division. Safe.
  5. kernel/mm/address_space.cpp:375 — page-unmap arithmetic uses bounded VA ranges and per-page invlpg loops, no count * page_size multiplication on a u32. Safe.

Verdict: Hardened. Pattern is consistent: "compare differences, not sums" or "validate operands to small constants before arithmetic." Class-M pkg_end fixed 2026-05-11.

Defensive infrastructure landed 2026-05-11 (kernel/util/saturating.{h,cpp}):

util::SatAdd<u64>(a, b)        // clamps to max on overflow
util::SatSub<u64>(a, b)        // clamps to 0 on underflow (unsigned)
util::SatMul<u32>(a, b)        // clamps to max on overflow
util::SatU32 count{0};         // wrapper type: ++/--/+= all saturate

Every clamp event emits one klog WARN with attempted, clamped_to, and the caller RIP resolved to [name+0xOFF (file:line)] via util::symbols.h. Rate-limited at 32 emissions (the counter g_clamp_events keeps growing so post-mortem analysis sees the true volume). Boot self-test (util::SaturatingSelfTest) confirms u32 + u64 add / sub / mul edges and the wrapper-type ++/-- saturation.

Use the helper or wrapper type at any new counter or arithmetic on attacker-influenced operands; it is opt-in (C++ can't intercept raw primitive arithmetic), so the audit's recurring follow-on is "new arithmetic on user-influenced operands must use SatAdd / SatU32 / SatU64." Complements UBSAN (which panics on signed overflow); use UBSAN for "must never happen" paths, Saturating for "could happen, recover and warn."

CC. Stack canaries and stack-guard pages

Linux reference: general class — kernel stack overflow without canary detection lets a corrupted return address land in adjacent data and survive function return.

DuetOS surface: kernel/security/stack_canary.cpp:54 (__stack_chk_guard) + :68 (__stack_chk_fail). The toolchain-emitted canary symbol is implemented; failure path panics cleanly. Per-thread guard pages are mapped between kernel stacks per kernel/mm/kstack.h:50.

Verdict: Hardened. Canary symbol + guard page are both present. Follow-up: verify -fstack-protector-strong is set in the kernel build flags (CMakeLists.txt did not show an explicit flag in the spot-check; the canary self-test compiles and runs, suggesting the compiler default is enabling it, but this should be explicit). Tracked in roadmap.

DuetOS-specific findings (DD–JJ)

This section is the result of code-reading the tree against common-but-unnamed kernel hazards rather than mapping public CVEs. Findings are honest verdicts; items called out as "FINDING" are real issues to act on.

DD. Capability-gate ordering in syscall handlers

Question: is the cap-gate check always before any user-pointer dereference or kernel state mutation?

Spot-check (3 handlers):

  • kernel/subsystems/linux/syscall_fs_mut.cpp:130 (DoTruncate) — RequireFsWrite(p) at line 130, before CopyAndStripFatPath at 132.
  • kernel/subsystems/linux/syscall_fs_mut.cpp:262 (DoRename) — RequireFsWrite(p) at line 262, before the path copies at 268–271.
  • kernel/subsystems/linux/syscall_io.cpp:122 (DoWrite) — kCapFsWrite gate at line 122, after read-only FD validation, before the FAT32 mutation at line 146.

Verdict: Hardened. No ordering bug found in the audited handlers. The discipline "cap-gate first, work second" is followed.

EE. HandleTable validation

Surface: kernel/ipc/handle_table.{h,cpp} — the table that backs DuetOS kernel-object handles (mutex, event, semaphore, mailbox, file).

Verdict: Hardened. HandleInRange (handle_table.cpp:32) checks h != kHandleInvalid && h < kHandleTableCapacity. Every slot access is preceded by HandleInRange (:65) and a type-tag check (:75). No raw g_handles[idx] pattern was found.

FF. TLB shootdown — FINDING (deferred-by-design, but worth tracking)

Surface: kernel/mm/address_space.cpp:430 — comment in the unmap path: "no cross-CPU shootdown in v0 — SMP activation is pending."

Verdict: Gap, currently safe. On a single CPU this is fine — the local invlpg flushes the only TLB that holds the mapping. The instant DuetOS goes multi-CPU, every page-protection downgrade or unmap must IPI the remote CPUs and wait for ack before the caller treats the page as no-longer-mapped; otherwise a remote CPU can write through a stale RW TLB entry to a page that's been re-allocated to a different process.

Action: SMP-AP bringup MUST land with a TLB-shootdown helper in the same slice. This is the single most exploit-relevant "gap" surfaced by this audit. Roadmap entry filed.

GG. Scheduler lock invariants

Surface: kernel/sched/ — spinlocks, wait queues, runqueues.

Verdict: OK in v0; gap deferred with SMP. WaitQueueBlock (sched.h:550) requires interrupts off (arch::Cli), which is incompatible with sleeping. No documented lock-ordering rule yet. Action: before per-CPU runqueues land, write the lock hierarchy (process-table > runqueue > wait-queue > kobject) into the sched header and enforce it in debug builds with a debug-only "lock rank" assertion. Roadmap entry filed.

HH. Per-CPU race during AP bringup

Surface: kernel/cpu/percpu.cpp, kernel/arch/x86_64/ (AP trampoline path).

Verdict: Safe today (uniprocessor); review required when AP code lands. Per-CPU struct is BSP-initialised statically (percpu.cpp:71). The AP-entry comment in cpu/topology.h:27 notes the invariant ("Each AP, in ApEntryFromTrampoline before signaling online_flag") — initialise PerCpu before setting the online flag, and the BSP must spin on the online flag before issuing IPIs to that AP. Track as a checklist for the SMP slice.

II. KASLR — FINDING

Surface: kernel/util/build_config.h:188:

#ifdef DUETOS_KASLR
inline constexpr bool kKaslrEnabled = (DUETOS_KASLR != 0);
#else
inline constexpr bool kKaslrEnabled = false;
#endif

Kernel linker script (boot/linker.ld:30): KERNEL_VIRTUAL_BASE = 0xFFFFFFFF80000000; — fixed.

Verdict: KASLR DISABLED in default builds. A kernel info leak (through any vector) gives the attacker the full kernel image location for free. User-mode ASLR for PE/ELF is implemented (syscall_mm.cpp aslr_delta), but the kernel image is not randomised. Mitigation status: explicitly tracked as "DEFERRED" in wiki/reference/Roadmap.md ("KASLR enable — settled — DEFERRED") — so this is a known decision, not a surprise. Recommend lifting from DEFERRED to "blocker for any multi-tenant deployment" — the moment DuetOS hosts code from more than one trust domain, fixed-base kernel is an exploit cliff.

JJ. W^X kernel image — verified

Surface: kernel linker script (boot/linker.ld:78–123) groups .text R+X, .rodata RO, .data/.bss RW with 4 KiB alignment boundaries; syscall_mm.cpp downgrades user-mode PAGE_EXECUTE_READWRITE / EXECUTE_WRITECOPY to RW+NX at mmap.

Verdict: Hardened. No writable-and-executable page found in the kernel image layout or in the audited mmap paths.

Runtime regression observed during audit (2026-05-11)

A live QEMU boot of the post-fix tree (build/x86_64-debug/duetos.iso) reached every audited surface (KASLR init + self-test logs cleanly, TLB shootdown infra builds, klog/random/auth all pass).

UEFI-firmware GRUB+Multiboot2 profile (historical run): boots to login cleanly, runs the full ring3 ELF smoke (4 ELF spawns to ring 3), auth / pentest probes run, no panic. This was the hybrid ISO's GRUB path, not the experimental direct BOOTX64.EFI loader.

BIOS-firmware GRUB+Multiboot2 profile (DUETOS_LEGACY=1): the profile double-faults during the first AddressSpaceMapUserPage call out of LoadSegment. The crash signature:

#DF Double fault
  #0  RwLockAcquireExclusive+0x25   (sync/rwlock.cpp:68)
  #1  (inlined in AddressSpaceMapUserPage)
  #2  AddressSpaceMapUserPage+0x14c (mm/address_space.cpp:295)
  #3  LoadSegment+0x266             (loader/elf_loader.cpp:283)
  ...
  #8  SpawnElfFile+0xff             (proc/ring3_smoke.cpp:2046)
followed by recursive #GP (vec=0xd) during panic dump.

Pre-existing on origin/main — verified by rebuilding clean 96e9026 and booting via the BIOS path. Not caused by this audit's fixes. Tracked separately in reference/Roadmap.md "Kernel / runtime"; does not gate any audit verdict because the crash path is downstream of every surface hardened by that audit. The original additional rationale treated the UEFI-firmware profile as the sole production path; that policy is superseded by the 2026-07-31 boot contract. Current release support is GRUB + Multiboot2 on BIOS or UEFI firmware. The direct UEFI loader remains experimental until segment loading, ExitBootServices, versioned BootInfo, kernel handoff, and required CI are complete.

Subsystem-isolation spot-check

The Linux and Win32 thunks must not bypass the kernel cap gate when handling any of the surfaces above. Spot-check:

  • Linux I/O syscallskernel/subsystems/linux/syscall_io.cpp checks the locked effective Process snapshot through core::ProcessHasCap(proc, kCapNet/kCapFsRead/kCapFsWrite) before dispatching to the underlying kernel routine.
  • Win32 file syscallskernel/subsystems/win32/file_syscall.cpp applies the same cap-gate pattern.
  • pidfd / splice thunkskernel/subsystems/linux/pidfd_splice.cpp routes through PipeSpliceFromPipe / PipeTeeFromPipe / PipeWrite (kernel-internal); no direct mutation of pipe internals from subsystem code.

Cap-gate violation would itself be the bug, regardless of which CVE class triggered the investigation. None was found in the surfaces walked above.

Summary Table

Class CVE(s) DuetOS verdict Re-audit trigger
A. Zero-copy pipe page sharing CVE-2022-0847 (Dirty Pipe) Absent file↔pipe splice w/ page lending
B. Kernel-crypto socket CVE-2026-31431 (Copy Fail) Absent any user-facing crypto API
C. SKB fragment ownership CVE-2026-43284, CVE-2026-43500 (Dirty Frag) Absent skb fragments, IPsec, zero-copy sendmsg
D. COW race CVE-2016-5195 (Dirty COW) Absent fork() / COW / madvise() landing
E. Stale struct flags (Dirty Pipe root cause) Fixed 2026-05-11SlabAllocZeroed() helper added (slab.{h,cpp}) new flag-bearing slab consumer should prefer the zeroed variant
F. Heap cross-cache CVE-2022-2588 (DirtyCred) Absent credentials moved out of Process inline
G. User-copy faults general class Hardened new kernel↔user surface
H. Async submission queue io_uring family Absent any user-shared SQ/CQ ring
I. Bluetooth L2CAP / RFCOMM CVE-2017-1000251, CVE-2020-12351, CVE-2025-21969 Absent L2CAP / RFCOMM / SDP land
J. USB descriptor parsing CVE-2016-2384 (USB MIDI) Hardened new class driver (UAC, UVC, write-MSC)
K. Filesystem parser bugs CVE-2024-0775 (ext4 remount UAF) FAT32 hardened; ext4/NTFS RO ext4 write, NTFS directory parsing
L. IPv6 reassembly CVE-2024-38063 (Windows tcpip.sys) Absent IPv6 stack lands
M. ACPI / AML parser CVE-2024-56662 (acpi_nfit_ctl OOB) Fixed 2026-05-11aml.cpp rewrites all three sites to pkg_len > end - after_op n/a
N. CPU speculative execution Spectre, Meltdown, Retbleed, Downfall Helper + every audited dispatch site wired 2026-05-11util::MaskedIndex in util/nospec.h applied at Win32 NT handle table (ipc/handle_table.cpp Lookup / LookupRef / Remove), Win32 thread / process / GDI handle dispatch (syscall/syscall.cpp, subsystems/win32/gdi_objects.cpp), Win32 file handle resolver (fs/file_route.cpp::HandleToSlot), and the full Linux fd dispatch surface (linux/syscall_io.cpp, syscall_file.cpp, syscall_path.cpp, syscall_xattr.cpp, pidfd_splice.cpp, syscall_fs_mut.cpp, syscall_fd.cpp, syscall_socket.cpp, syscall_mm.cpp, syscall_async_io.cpp, inotify.cpp, fanotify.cpp, syscall_misc.cpp, extra_syscalls.cpp, syscall_stub.cpp). KPTI still deferred. n/a — new dispatch sites apply the mask by discipline
O. Refcount overflow CVE-2016-0728 (keyring) Hardened 2026-05-11KObjectAcquire now uses util::RefcountIncSaturating high-throughput refcount surface
P. TOCTOU in syscall args CVE-2024-43882, CVE-2025-40331 Absent new syscall with multi-step user-read
Q. Win32k / GDI corruption CVE-2025-49667, CVE-2025-62215 Hardened GDI moves to heap-allocated objects
R. PE / COFF loader OOB general class Hardened new directory-table parse path
S. IOCTL size validation general driver class Hardened variable-size IOCTL dispatch
T. HDA codec parsing (driver class) Hardened new codec verb requiring unbounded loops
U. Netfilter nf_tables UAF CVE-2024-1086 (Flipping Pages) Hardened rules move to heap-backed storage
V. eBPF verifier bypass CVE-2020-8835, NCC 2024 Absent any programmable kernel-side filter
W. GPU driver vulns CVE-2024-0126, CVE-2024-36342, CVE-2025-23280 Stubs only; virtio-gpu fenced user-mode GPU command submission lands
X. Hypervisor escape (vsock) CVE-2025-21756 Absent vsock / virtio-fs / virtio-9p host channel
Y. Container / namespace escape CVE-2022-0185, CVE-2022-0492 Absent by design namespaces are a non-goal
Z. Kernel-stack recursion Project Zero recursion class Hardened (AML cap=32) new parser with transitive recursion
AA. Format string / printk (general class) Hardened any new variadic KLOG helper
BB. Integer overflow on size (general class) Hardened 2026-05-11 — class-M pkg_end fixed, plus util::Saturating<T> opt-in type and SatAdd/Sub/Mul helpers landed in util/saturating.{h,cpp}. Every clamp event logs a klog WARN with the caller RIP + resolved symbol + file:line. Boot self-test confirms add / sub / mul / ++ / -- clamping. apply SatU32 / SatU64 to any new counter that an attacker-influenced path could drive
CC. Stack canaries + guard pages (general class) Hardened 2026-05-11-fstack-protector-strong now explicit in kernel/CMakeLists.txt n/a
DD. Cap-gate ordering (DuetOS-specific) Hardened (3-handler spot-check) new syscall with multi-step state mutation
EE. HandleTable bounds (DuetOS-specific) Hardened direct slot access bypassing HandleInRange
FF. TLB shootdown (DuetOS-specific) Fixed 2026-05-11mm::TlbShootdownAddr/Range + arch::SmpTlbShootdown* IPI infrastructure landed; unmap / protect / unmap-borrowed paths now broadcast n/a (wired)
GG. Scheduler lock invariants (DuetOS-specific) Hardened 2026-05-11 — full lock hierarchy documented in kernel/sync/lockdep.h with absolute rules (no-sleep-with-spinlock, no-lock-across-CR3, no-lock-across-shootdown) n/a
HH. AP bringup per-CPU init race (DuetOS-specific) Safe today (uniprocessor) AP entry path implementation
II. KASLR (DuetOS-specific) Scaffolding landed 2026-05-11security/kaslr.{h,cpp} computes a 2-MiB-aligned candidate slide from core::RandomU64, wired into boot; slide-application stub depends on PIE-build follow-on slice apply slide via PIE relocations
JJ. W^X kernel image (DuetOS-specific) Hardened n/a

Follow-up Items

Tracked in reference/Roadmap.md:

  1. Class-E follow-up — provide a SlabAllocZeroed() helper, or per-cache zero_on_alloc flag, so callers that need a clean object don't have to remember to memset. Required before any slab consumer holds a flag-style field with attacker-observable semantics.
  2. Class-D pre-landing — when fork() and COW are designed, write the dirty-bit / MADV_DONTNEED atomicity invariant into the spec, not into a later patch.
  3. Class-C pre-landing — when IPsec or any zero-copy sendmsg lands, define the "externally-backed fragment marker is mandatory, in-place transforms refuse marked fragments" invariant in the network-stack ABI.
  4. Class-B pre-landing — if a user-facing crypto API ever ships, it must refuse src/dst aliasing on user-supplied scatterlists for any operation that doesn't byte-copy the full output (auth tags, short writes, etc.).
  5. Class-M follow-up (audit-pending) — in kernel/acpi/aml.cpp, replace pkg_end = after_op + pkg_len; if (pkg_end > end) ... with if (pkg_len > end - after_op) ... so the addition cannot wrap. Low-risk (AML tables are bounded in practice) but structurally cleaner.
  6. Class-N follow-up — landed. util::MaskedIndex(idx, bound) ships in kernel/util/nospec.h and is applied at every audited user-controlled array-index dispatch site (see the row above for the inventory). KPTI remains independently tracked under Roadmap "KPTI enable — DEFERRED". Discipline for new code: any time a syscall handler writes if (idx >= kCap) against a user-supplied integer and then indexes an array with that integer, mask the index with util::MaskedIndex(idx, kCap) between the check and the load. The check protects correctness; the mask bounds the speculative window.
  7. Class-O pre-landing — saturating-increment refcount helper before any "shareable handle" surface lets unprivileged code drive a refcount.
  8. Class-I pre-landing — when L2CAP / RFCOMM / SDP land, the protocol-parser invariants from class C apply: every length field is bounded against the buffer slice before being used as an index or loop bound, and connection-state mutations are serialised against incoming-fragment handling.
  9. Class-L pre-landing — when IPv6 lands, every length/offset field involved in fragmentation arithmetic uses len > end - off-style comparison (no end - len / off + len directly).
  10. Class-K pre-landing — when ext4 write, NTFS directory parsing, or any FS write-remount path lands, re-walk this row.
  11. Class-V (eBPF) — design-decision flag. If any programmable kernel-side filter is ever added (BPF, eBPF, XDP-equivalent), it ships with a formally-verified bytecode interpreter or a privileged-only configuration channel. The "unprivileged JIT" path is the load-bearing assumption behind a decade of LPE CVEs; do not adopt it.
  12. Class-W (GPU command submission) — pre-landing invariant. No user-mode IOCTL surface accepts a GPU command buffer directly. The user submits a request; the kernel translates it into a verified GPU-side command buffer that the user cannot edit after submission. Echo of class-H invariant ("kernel state machine never advances on user-writable memory").
  13. Class-FF (TLB shootdown) — SMP-blocker. The SMP AP-bringup slice MUST land with a tlb_shootdown(addr_space, range) helper that IPIs every CPU executing in the target address space and waits for ack before treating the page as no-longer-mapped. Failing to do this turns every page-unmap into a UAF on remote CPUs.
  14. Class-GG (lock hierarchy) — SMP pre-landing. Document the kernel lock hierarchy (e.g. process_table > runqueue > wait_queue > kobject) in the sched header and enforce it with a debug-only "lock rank" assertion before per-CPU runqueues land.
  15. Class-II (KASLR) — lift from DEFERRED. Fixed-base kernel is acceptable for a single-tenant developer kernel today; it becomes an exploit cliff the moment DuetOS hosts code from more than one trust domain (multi-user, network-facing, or PE sandbox). Move out of "deferred" before that milestone, not after.
  16. Class-CC (stack-protector flag) — verify. Add -fstack-protector-strong to the kernel CMake flags explicitly. The canary symbol is implemented but the flag isn't visible in the build files audited.

Related Pages