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.
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?"
- 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.
For each class:
- 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").
- Find the DuetOS equivalent surface. Cite file:line.
- 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.
- "Absent" is not "secure." If a roadmap item adds the surface later, this audit must be re-run for that 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.
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-audited — kCapCrypto gating is not a substitute for refusing
shared-page operands.
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.
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.
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.
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.
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.
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."
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.
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.
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:303BPB sanity-checks sector size (== 512), cluster size (!= 0), FAT count, image size before trusting any of them. LFN slot parsing bounds-checks offsets inkernel/fs/fat32_dir.cpp. - exFAT —
kernel/fs/exfat.cppminimal read-only, headers validated before field reads. - NTFS —
kernel/fs/ntfs.cppis a read-only stub. - ext4 —
kernel/fs/ext4.cppread-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."
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.
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:
- u32-wrap on
after_op + pkg_len(the original follow-up): resolved — every site now comparespkg_len > end - after_op(subtraction on the known-good slice) so the addition can never wrap small and pass the bound check. - Under-length
PkgLengthunderflow (found byfuzz_aml,tests/fuzz/): aPkgLengthencoding a value smaller than its own byte count (e.g. a 1-byte PkgLength of 0) madename_off = after_op + plen_consumedexceedpkg_end, so thepkg_end - name_offlength handed toReadNameStringunderflowed to ~4 GiB and OOB-read one byte past the mapped table. Fixed by rejectingpkg_len < plen_consumedat 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.
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.
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.
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.
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 (MakeHandle — gdi_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.
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.
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).
Reference: general class — audio drivers that trust codec verb-return counts (SubordinateNodeCount, ConnListLength) as loop bounds.
DuetOS surface: kernel/drivers/audio/hda.cpp —
WalkCodec (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.
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.
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.
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:89 — offset + 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.
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.
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.
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 = 32hard 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 withNumberOfSectionsbounds-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.
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.
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):
kernel/drivers/gpu/virtio_gpu.cpp:89—offset + length > bar.sizeis 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.kernel/acpi/aml.cpp:554/588/633— same `pkg_end = after_op- pkg_len` shape as class M; already tracked.
kernel/ipc/handle_table.cpp:49— pure literal bounds (i < kHandleTableCapacity), no arithmetic. Safe.kernel/fs/fat32.cpp:303— sector-size / cluster-size validated to constants before division. Safe.kernel/mm/address_space.cpp:375— page-unmap arithmetic uses bounded VA ranges and per-page invlpg loops, nocount * page_sizemultiplication 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 saturateEvery 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."
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.
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.
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, beforeCopyAndStripFatPathat 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) —kCapFsWritegate 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.
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.
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.
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.
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.
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.
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.
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.
The Linux and Win32 thunks must not bypass the kernel cap gate when handling any of the surfaces above. Spot-check:
- Linux I/O syscalls —
kernel/subsystems/linux/syscall_io.cppchecks the locked effective Process snapshot throughcore::ProcessHasCap(proc, kCapNet/kCapFsRead/kCapFsWrite)before dispatching to the underlying kernel routine. - Win32 file syscalls —
kernel/subsystems/win32/file_syscall.cppapplies the same cap-gate pattern. - pidfd / splice thunks —
kernel/subsystems/linux/pidfd_splice.cpproutes throughPipeSpliceFromPipe/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.
| 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-11 — SlabAllocZeroed() 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-11 — aml.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-11 — util::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-11 — KObjectAcquire 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-11 — mm::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-11 — security/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 |
Tracked in reference/Roadmap.md:
- Class-E follow-up — provide a
SlabAllocZeroed()helper, or per-cachezero_on_allocflag, 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. - Class-D pre-landing — when
fork()and COW are designed, write the dirty-bit /MADV_DONTNEEDatomicity invariant into the spec, not into a later patch. - Class-C pre-landing — when IPsec or any zero-copy
sendmsglands, define the "externally-backed fragment marker is mandatory, in-place transforms refuse marked fragments" invariant in the network-stack ABI. - 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.).
- Class-M follow-up (audit-pending) — in
kernel/acpi/aml.cpp, replacepkg_end = after_op + pkg_len; if (pkg_end > end) ...withif (pkg_len > end - after_op) ...so the addition cannot wrap. Low-risk (AML tables are bounded in practice) but structurally cleaner. - Class-N follow-up — landed.
util::MaskedIndex(idx, bound)ships inkernel/util/nospec.hand 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 writesif (idx >= kCap)against a user-supplied integer and then indexes an array with that integer, mask the index withutil::MaskedIndex(idx, kCap)between the check and the load. The check protects correctness; the mask bounds the speculative window. - Class-O pre-landing — saturating-increment refcount helper before any "shareable handle" surface lets unprivileged code drive a refcount.
- 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.
- Class-L pre-landing — when IPv6 lands, every length/offset
field involved in fragmentation arithmetic uses
len > end - off-style comparison (noend - len/off + lendirectly). - Class-K pre-landing — when ext4 write, NTFS directory parsing, or any FS write-remount path lands, re-walk this row.
- 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.
- 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").
- 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. - 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. - 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.
- Class-CC (stack-protector flag) — verify. Add
-fstack-protector-strongto the kernel CMake flags explicitly. The canary symbol is implemented but the flag isn't visible in the build files audited.
- Subsystem Isolation — the rule that Linux/Win32 thunks never bypass the kernel cap gate is the reason most of the verdicts above hold.
- Capabilities —
kCap*set, who grants, who checks. - Sandboxing, W^X / NX Enforcement, Attack Simulation — adjacent defensive surfaces.
- Roadmap — follow-up items live there.