β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2984

syscall_deregister() writes sysent[*offset] through an unvalidated index with an unvalidated value: MOD_LOAD-failure rollback destroys a live syscall slot (EEXIST) β†’ any unprivileged user's syscall(N) is a kernel NULL function call β€” deterministic persistent panic; EINVAL variant = arbitrary-index 24-byte kernel zero-write; ENFILE variant = sysent[-1] OOB write

Field Value
ID DF-2984
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:N/I:L/A:H
CWE CWE-787 / CWE-20
File sys/kern/kern_syscalls.c
Lines 72-78, 99-105 (sink: trap.c:1285)
Area kern
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

syscall_deregister executes sysent[offset] = old_sysent guarded only by 'if (offset)' β€” no bounds check (NO_SYSCALL == βˆ’1 passes the truthiness test) and no proof a successful syscall_register established the pair. kern_module.c dispatches MOD_UNLOAD as a MOD_LOAD-failure rollback (the DF-2936 class), so the sink is reached with (offset, old_sysent) that register never initialized: (a) EEXIST β€” two legitimate fixed-slot KLDs conflicting zero the FIRST module's live sysent entry while kldload(2) of the second still returns 0; every subsequent unprivileged syscall(210) then calls NULL at trap.c:1285; (b) EINVAL β€” fixed offset outside [0,SYS_MAXSYSCALL) writes 24 zero bytes at sysent+N24 for author-chosen N (sink driven to execution in-guest: MOD_LOAD error 22, write landed silently in already-zero .bss); (c) ENFILE β€” offset stays NO_SYSCALL=βˆ’1 β†’ sysent[βˆ’1] = zeros. VERIFIED on the guest: kldload modA (slot 210 live, returns 4242) β†’ kldload modB (same slot, EEXIST rollback, rc=0, slot zeroed) β†’ uid 1001 ./call210 β†’ 'Fatal user address access from kernel mode … RIP=0', guest down β€” deterministic and persistent until reboot. modC (offset 99999) demonstrates the arbitrary-index zero-write path. Unprivileged ceiling is persistent panic (written value is the module's static zero initializer; controlling value or index requires authoring a KLD = root). Any machine that ever loads third-party syscall KLDs (the only consumers of this API) is exposed. Fix validated in-guest (bounds re-validation + 'registered' gate + chainevh-failure rollback): identical sequence leaves slot live, no panic, guest healthy.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of kern_syscalls.c (GLM 5.3); unpriv persistent panic reproduced + fix validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2984 Β· 23 files
FileTypeDescriptionSize
README.md β€” 4.3 KB ↓ raw
VERDICT.md β€” 4.8 KB ↓ raw
modA/dfpoc_a.c β€” 1.4 KB view raw
modA/Makefile β€” 138 B ↓ download
modB/dfpoc_b.c β€” 1.6 KB view raw
modB/Makefile β€” 138 B ↓ download
modC/dfpoc_c.c β€” 1.3 KB view raw
modC/Makefile β€” 138 B ↓ download
call210.c β€” 1.0 KB view raw
build.sh β€” 402 B view raw
run.sh β€” 900 B view raw
build.log β€” 2.1 KB view raw
run.log β€” 1008 B view raw
run.modc.log β€” 382 B view raw
panic.txt β€” 2.1 KB view raw
run.fix.log β€” 1.0 KB view raw
fix_run.log β€” 1.0 KB view raw
fix_build.log β€” 1.3 KB view raw
fix_build_kld.log β€” 2.1 KB view raw
env.txt β€” 268 B view raw
fix.diff β€” 2.9 KB view raw
verdict.json β€” 6.7 KB view raw
manifest.json β€” 1.5 KB view raw

DF-2984 β€” syscall_deregister() writes sysent[*offset] through an unvalidated index with an unvalidated value (live-slot destruction β†’ unprivileged persistent kernel panic; arbitrary-index 24-byte zero write)

Where

What (one paragraph)

syscall_deregister() does if (*offset) sysent[*offset] = *old_sysent; β€” no bounds check on *offset (note NO_SYSCALL == -1, which passes the truthiness test) and no check that *offset/*old_sysent were ever established by a successful syscall_register(). When a KLD's MOD_LOAD fails, module_register_init() (kern_module.c:112) dispatches MOD_UNLOAD as a rollback β€” which lands in syscall_module_handler() and calls syscall_deregister() even though registration never succeeded. Three concrete variants:

  1. EEXIST (two legitimate fixed-offset modules both claiming reserved slot N, e.g. 210): the failed loader's rollback executes sysent[N] = { sy_narg=0, sy_rsize=0, sy_call=NULL, sy_abort=NULL }, destroying the live syscall of the first module. kldload(2) of the second module still reports success (SYSINIT void return β€” the DF-2936 class). From then on, any unprivileged user invoking syscall(N) performs a kernel-mode call through a NULL function pointer β†’ deterministic, reboot-only-fixable kernel panic.
  2. EINVAL (fixed offset outside [0, SYS_MAXSYSCALL)): rollback executes sysent[N] = zeros at sysent + N*24 for author-chosen N ∈ (-2Β³ΒΉ, 2Β³ΒΉ) β€” semi-arbitrary kernel-memory zero write (24 bytes).
  3. ENFILE (all ten dynamic slots 210-219 busy): *offset stays NO_SYSCALL == -1 β†’ sysent[-1] = zeros, a 24-byte OOB write below the array in kernel .data.

Contents

  • modA/ β€” legitimate KLD claiming fixed slot 210 (syscall returns 4242)
  • modB/ β€” legitimate KLD also claiming fixed slot 210 (EEXIST trigger)
  • modC/ β€” KLD with fixed offset 99999 (EINVAL / OOB-index trigger)
  • call210.c β€” unprivileged trigger (syscall(210))
  • build.sh / run.sh β€” exact commands
  • build.log / run.log β€” baseline (buggy kernel #0) run
  • panic.txt β€” serial-console capture of the unprivileged NULL-call panic
  • run.fix.log β€” patched-kernel (#1) validation
  • fix.diff β€” the fix (bounds re-validation + registered gate)
  • verdict.json / manifest.json

Build

As root in the guest (proven KLD pattern, KERNBUILDDIR /usr/obj/usr/src/sys/X86_64_GENERIC):

sh build.sh

Run (baseline β€” expect panic)

sh run.sh                        # root: kldload A; unpriv live check; kldload B
/tmp/df2984/call210              # as ANY user: kernel panic, RIP=0

Success criterion (bug present):

  1. kldload ./modA/dfpoca.ko β†’ rc=0; unprivileged call210 β†’ 4242
  2. kldload ./modB/dfpocb.ko β†’ rc=0 and dmesg module_register_init: MOD_LOAD (dfpocb, …) error 17
  3. unprivileged call210 β†’ Fatal trap 12: page fault while in kernel mode, fault virtual address = 0x0, instruction pointer = 0x8:0x0, process = call210 (uid 1001)

Run (patched β€” fix.diff applied, kernel #1)

Same steps: kldload B still logs error 17 and still returns rc=0 (the success-reporting bug is DF-2936's, unchanged), but the live slot is NOT touched: unprivileged call210 keeps returning 4242, and loading modC (out-of-range offset) writes nothing (guest healthy).

Impact ceiling (why panic, not uid0, for the unprivileged user)

The written value (old_sysent) is the module's static initializer — {0,0,NULL,NULL} for any stock-macro/hand-rolled module — so the state an unprivileged attacker can reach is a NULL sy_call (panic) or an ENOSYS-ish stub, never a controlled non-zero pointer. Presetting old_sysent.sy_call to an arbitrary kernel address requires authoring the module — which requires root (kldload → caps_priv_check_self (SYSCAP_NOKLD), kern_linker.c:794), i.e. no additional power. Variants 2/3 (corruption writes) are root-timed. Ceiling for uid≠0: persistent DoS.

VERDICT.md
↓ download raw

DF-2984 VERDICT β€” reproduced (panic, unprivileged persistent) / fix validated (fixed)

Baseline (stock INVARIANTS kernel #0, Thu Jul 2 06:02:54 UTC 2026)

Full chain executed and captured (run.log, panic.txt, run.modc.log):

  1. kldload ./modA/dfpoca.ko β†’ rc=0. Module A is a legitimate, stock-pattern KLD: syscall_module_data + DECLARE_MODULE, fixed offset 210. Slot 210 goes live (syscall(210) == 4242).
  2. kldload ./modB/dfpocb.ko β†’ rc=0 (kldload "succeeds") while dmesg shows module_register_init: MOD_LOAD (dfpocb, ffffffff806826c0, 0xffffffff82601080) error 17 β€” EEXIST from syscall_register() (sys/kern/kern_syscalls.c:64-65, slot 210 no longer holds sys_lkmnosys). The rollback then fires: kern_module.c:112 module_unload(mod) β†’ MOD_EVENT(mod, MOD_UNLOAD) β†’ syscall_module_handler (kern_syscalls.c:99-106, chainevh NULL) β†’ syscall_deregister(&210, &{0,0,NULL,NULL}) β†’ sysent[210] = {0,0,NULL,NULL} (kern_syscalls.c:75-76). The LIVE slot belonging to module A is destroyed.
  3. Unprivileged trigger (uid 1001, call210): ssh session hung (timeout), vm.sh status β‡’ down. Serial console (panic.txt):

Fatal user address access from kernel mode from call210 at 0000000000000000 Fatal trap 12: page fault while in kernel mode fault virtual address = 0x0 instruction pointer = 0x8:0x0 current process = 978 (call210)

RIP=0 β€” the NULL sy_call written by syscall_deregister was invoked by trap.c:1285. Deterministic (every subsequent syscall(210) panics until reboot).

  1. EINVAL variant (run.modc.log): kldload ./modC/dfpocc.ko (fixed offset 99999) β†’ MOD_LOAD (dfpocc, …) error 22, rc=0, and the same sink executed sysent[99999] = zeros β€” silent this boot (the write lands ~2.3 MB past the table in already-zero kernel .bss), proving the arbitrary-index write reaches execution. (The identical sink produced the visible panic in step 2-3.)

Exploitability ceiling (adversarial assessment)

  • For an unprivileged user the reachable corrupted state is exactly {sy_narg=0, sy_rsize=0, sy_call=NULL, sy_abort=NULL}: the value comes from the module's static initializer (the SYSCALL_MODULE macro's {0,0,NULL,NULL}), not from attacker input β†’ NULL-call β†’ panic. No controlled non-zero pointer, no data write, no info leak via this state.
  • Controlling the written value (old_sysent.sy_call = arbitrary kernel address β†’ the classic fake-syscall-call primitive with user-influenced sysmsg/args) requires authoring struct syscall_module_data β€” i.e. kldload, which requires root (caps_priv_check_self(SYSCAP_NOKLD), kern_linker.c:794; securelevel gated). Root already owns the machine.
  • The index (variant 2/3 OOB writes) is likewise root-chosen.
  • Conclusion: unprivileged impact = persistent local kernel panic (DoS); corruption primitives are real kernel-memory writes but root-gated. Severity Medium, bucket memcorrupt (unvalidated write into the global dispatch table), CVSS 3.1 AV:L/AC:L/PR:H/UI:N/S:C/C:N/I:L/A:H (β‰ˆ5.5).

Fix validation (kernel #1, fix.diff applied in-guest, make nativekernel)

run.fix.log, kernel #1: Fri Sep 4 17:32:24 UTC 2026:

  • kldload modA β†’ rc=0; unprivileged call210 β†’ 4242 (live).
  • kldload modB β†’ rc=0 and dmesg MOD_LOAD (dfpocb, …) error 17 (unchanged β€” failure still occurs, and kldload still mis-reports success, which is DF-2936's separate defect) β€” but the rollback now takes the registered == 0 path: no deregister, no write.
  • Unprivileged call210 β†’ survived: syscall(210) returned 4242 β€” the exact input that panicked kernel #0 is harmless on kernel #1.
  • kldload modC (offset 99999) β†’ MOD_LOAD (dfpocc, …) error 22, guest healthy (no OOB write; bounds check would return EINVAL anyway).
  • Final call210 β†’ 4242, vm.sh status β‡’ up.

fix_status = fixed (baseline behavior gone; no regression visible).

Honest notes

  • The stock SYSCALL_MODULE macro is bit-rotted in this tree (its {0, NULL} old_sysent initializer fails -Werror against the 4-field struct sysent; zero in-tree users remain β€” rg syscall_register over sys/ shows only kern_syscalls.c). The PoC hand-rolls exactly what the macro would emit, which is the documented way KLDs reach syscall_module_handler. The kernel-side sink is unchanged by the macro's rot.
  • The trigger modules are not buggy and contain nothing malicious β€” they are the standard registration pattern with two products claiming the same reserved number (210). No malformed structures, no crafted values: the corruption is purely kernel-side contract violation.
  • Guest reset to clean-source snapshot after validation (vm.sh reset with-src), guest_dirty = 0.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

bounds re-validation in syscall_deregister plus a registered flag in syscall_module_data gating MOD_UNLOAD deregistration (and rolling back the registration when the chainevh init fails) removes the unvalidated write entirely; the exact baseline panic input (unprivileged call210 after kldload A+B) is inert on the patched kernel (call210 still returns the live marker 4242, also after loading the out-of-range modC), guest stays up; kldload success-misreporting is DF-2936 and intentionally unchanged

['fix_run.log (patched-kernel sequence: error 17 + error 22 rollbacks leave slot live)', 'VERDICT.md fix-validation section (kernel #1 uname, KBUILD_RC=0, IK_RC=0)']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Fri Sep 4 17:32:24 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

root one-time setup: kldload legitimate module A (fixed slot 210) -> kldload legitimate module B (same fixed slot): syscall_register EEXIST -> MOD_LOAD fails -> kern_module.c:112 module_unload dispatches MOD_UNLOAD -> syscall_module_handler:105 syscall_deregister(&210, &{0,0,NULL,NULL}) -> sysent[210].sy_call = NULL (kern_syscalls.c:76) while kldload(B) returns 0. Then ANY unprivileged user: syscall(210) -> trap.c:1224 callp=&sysent[210], :1235 narg=0, :1285 (*NULL)(&sysmsg, argp) -> Fatal trap 12, RIP=0, reboot required. No uid0 route for unprivileged users: written value is fixed zeros from stock module initializers; controlling value/index requires module authorship = root.

Evidence (decisive lines)

["panic.txt: 'Fatal user address access from kernel mode from call210 at 0000000000000000' / 'instruction pointer = 0x8:0x0' after unprivileged syscall(210) as uid 1001", "run.log: KLDLOAD_A_RC=0 ... KLDLOAD_B_RC=0 with dmesg 'module_register_init: MOD_LOAD (dfpocb, ...) error 17' (EEXIST rollback fired, kldload still reports success)", "run.modc.log: 'MOD_LOAD (dfpocc, ...) error 22' (EINVAL variant reached the same sink; kldload rc=0)", "run.fix.log: on fix.diff kernel #1, same kldload B produces 'error 17' but unprivileged call210 'survived: syscall(210) returned 4242' (twice, plus after modC load), guest up", 'VERDICT.md: full narrative + exploitability-ceiling analysis']

PoC changes

Seed lead assumed only 'root-loaded modules with fixed offsets' could mis-index. Deepened: the reachable path is the MOD_LOAD-failure rollback (kern_module.c:112), so NO module bug is needed - two legitimate fixed-offset KLDs conflicting (EEXIST) corrupts a live slot; ENFILE(*offset=-1)/EINVAL(arbitrary index) variants added. Stock SYSCALL_MODULE macro is bit-rotted ({0,NULL} vs 4-field sysent, -Werror), so the PoC hand-rolls the exact macro expansion to reach syscall_module_handler. call210 uses plain syscall(210); run.sh switched sudo->run_user (no sudo in guest).

Verified recommended fix

Re-validate the index in syscall_deregister (reject <=0 or >=SYS_MAXSYSCALL) and gate deregistration on a 'registered' flag set only after a successful syscall_register (also roll back the registration when the module's chainevh init fails).

Verdict

syscall_deregister() (kern_syscalls.c:75-76) writes sysent[offset] = old_sysent with no bounds check on the index (NO_SYSCALL == -1 passes the old 'if (*offset)' test) and no proof that a successful syscall_register() established the pair. The MOD_LOAD-failure rollback in module_register_init() (kern_module.c:112 module_unload -> MOD_UNLOAD -> syscall_module_handler kern_syscalls.c:99-106) reaches this sink with a never-initialized (offset, old_sysent). Verified on the guest: two legitimate fixed-slot-210 KLDs; loading the second fails syscall_register with EEXIST yet kldload(2) returns 0, and the rollback executes sysent[210] = {0,0,NULL,NULL}, destroying the first module's LIVE syscall; every subsequent unprivileged syscall(210) (verified as uid 1001) performs a kernel NULL function call - deterministic 'Fatal trap 12 ... instruction pointer = 0x8:0x0' panic persistent until reboot. The EINVAL variant (fixed offset 99999) was likewise driven to the sink (MOD_LOAD error 22, kldload rc=0), executing sysent[99999]=zeros ~2.3MB past the table (silent on that boot: target was already-zero .bss); ENFILE gives sysent[-1] identically. Unprivileged ceiling is persistent panic: the written value is the module's static zero initializer, and controlling it (or the OOB index) requires authoring a KLD, i.e. root via SYSCAP_NOKLD. fix.diff (bounds re-validation + registered gate + proper rollback on chainevh failure) applied in-guest, kernel rebuilt: same steps leave slot 210 live (call210 still returns 4242, no panic), modC writes nothing, guest healthy.