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)
PoC verification
Evidence pack
findings/poc/DF-2984 Β· 23 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
- Sink:
sys/kern/kern_syscalls.c:72-78(syscall_deregister) - Reachable via:
sys/kern/kern_module.c:110-116(MOD_LOAD-failure rollback callsmodule_unload()βMOD_UNLOAD) βsys/kern/kern_syscalls.c:99-106 - Dispatch of the corrupted slot:
sys/platform/pc64/x86_64/trap.c:1235,1285(narg = callp->sy_nargβ¦(*callp->sy_call)(...), no NULL check)
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:
- 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 invokingsyscall(N)performs a kernel-mode call through a NULL function pointer β deterministic, reboot-only-fixable kernel panic. - EINVAL (fixed offset outside
[0, SYS_MAXSYSCALL)): rollback executessysent[N] = zerosatsysent + N*24for author-chosen N β (-2Β³ΒΉ, 2Β³ΒΉ) β semi-arbitrary kernel-memory zero write (24 bytes). - ENFILE (all ten dynamic slots 210-219 busy):
*offsetstaysNO_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 commandsbuild.log/run.logβ baseline (buggy kernel #0) runpanic.txtβ serial-console capture of the unprivileged NULL-call panicrun.fix.logβ patched-kernel (#1) validationfix.diffβ the fix (bounds re-validation +registeredgate)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):
kldload ./modA/dfpoca.koβ rc=0; unprivilegedcall210β4242kldload ./modB/dfpocb.koβ rc=0 and dmesgmodule_register_init: MOD_LOAD (dfpocb, β¦) error 17- 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.
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):
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).kldload ./modB/dfpocb.koβ rc=0 (kldload "succeeds") while dmesg showsmodule_register_init: MOD_LOAD (dfpocb, ffffffff806826c0, 0xffffffff82601080) error 17β EEXIST fromsyscall_register()(sys/kern/kern_syscalls.c:64-65, slot 210 no longer holdssys_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.- 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).
- EINVAL variant (
run.modc.log):kldload ./modC/dfpocc.ko(fixed offset 99999) βMOD_LOAD (dfpocc, β¦) error 22, rc=0, and the same sink executedsysent[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-influencedsysmsg/args) requires authoringstruct 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; unprivilegedcall210β4242(live).kldload modBβ rc=0 and dmesgMOD_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 theregistered == 0path: 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_MODULEmacro is bit-rotted in this tree (its{0, NULL}old_sysent initializer fails -Werror against the 4-fieldstruct sysent; zero in-tree users remain βrg syscall_registerover sys/ shows only kern_syscalls.c). The PoC hand-rolls exactly what the macro would emit, which is the documented way KLDs reachsyscall_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
fixedbounds 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)']
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.
No comments yet.