# 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
  calls `module_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:

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.
