# DF-2773 — VERDICT

**REPRODUCED (panic / NULL-deref DoS, root-gated).**

## Root cause

In `link_elf_obj_load_file` the section-header scan increments the tracking
counters *before* the corresponding arrays are allocated:

```c
517:	for (i = 0; i < hdr->e_shnum; i++) {
...
530:		case SHT_REL:
531:			ef->nreltab++;          /* counted ... */
533:		case SHT_RELA:
534:			ef->nrelatab++;         /* ... but reltab/relatab allocs   */
				                 /* only happen later at :561-566 */
```

Several rejections sit **between** counting and allocation and bail out via
`goto out` → `linker_file_unload(lf)` (:832-834):

- `:540-544` `nprogtab == 0` ("file has no contents")
- `:545-550` `nsym != 1` (zero or multiple symbol tables)
- `:551-556` invalid `symstrindex`
- `:567-571` allocation failure (not attacker-triggerable with M_WAITOK)

A freshly made linker file has `refs == 0` and no registered modules, so
`linker_file_unload` falls straight through to `file->ops->unload(file)`
(kern_linker.c:549). `link_elf_obj_unload_file` then runs the entry-free
loops **unguarded**:

```c
887:	for (i = 0; i < ef->nreltab; i++)
888:		if (ef->reltab[i].rel)          /* reltab == NULL, nreltab > 0 */
...
890:	for (i = 0; i < ef->nrelatab; i++)
891:		if (ef->relatab[i].rela)        /* relatab == NULL, nrelatab > 0 */
```

→ kernel read at address 0x0 → fatal page fault. (The progtab loop just
above is guarded by `if (ef->progtab)` at :849 — the same guard was simply
never added for the relocation tables. The preloaded branch is unaffected
because it returns early at :867-885 and only frees the array pointers.)

## Reproduction (baseline, stock kernel)

Guest `#0: Thu Jul  2 06:02:54 UTC 2026`, securelevel -1.
`gen_module.py df2773.ko nrel-nullderef`: 5 sections — NULL, .symtab
(1 null sym, link=2), .strtab, .shstrtab, one .rela (24 bytes, sh_info=1),
and **no PROGBITS/NOBITS section at all**. Scan yields nsym=1, nprogtab=0,
nrelatab=1 → "file has no contents" → goto out → unload → NULL deref.
Serial console (panic.txt):

```
kldload: /tmp/df2773.ko: file has no contents
Fatal trap 12: page fault while in kernel mode
fault virtual address    = 0x0
fault code               = supervisor read data, page not present
Stopped at link_elf_obj_unload_file+0x6e:  movq (%rax),%rdi
```

The "file has no contents" message immediately preceding the fault is the
decisive proof that the panic comes from the *error path of the loader
itself*, exactly as traced. (Also visible in the log:
"Fatal user address access from kernel mode from kldload".)

## Impact ceiling

Deterministic panic from a crafted module; NULL read (no write, no info
disclosure). Root-gated (SYSCAP_NOKLD). Low severity, matching the
DF-0040/0041/0042 calibration. Additional trigger shapes for the same sink:
`nsym != 1` (two symtabs + one rela), invalid symstrindex + one rela —
all confirmed by code trace; the nprogtab==0 shape was run live.

Interaction note: this defect is also why naive hardening of the scan loop
(e.g. DF-2772's fix) must be paired with the unload guard — any new
scan-time rejection with counted-but-unallocated relocation tables would
otherwise re-open the same NULL-deref.

## Fix validation

`fix.diff` (combined with DF-2771/DF-2772): wraps both entry loops in
`if (ef->reltab)` / `if (ef->relatab)` guards in
`link_elf_obj_unload_file`. Rebuilt kernel #1 (Sep 1 2026, build.log,
-Werror clean). Same PoC: rejected cleanly — with the DF-2772 scan-time
sh_info validation it now fails at the scan ("Unsupported file type" in
dmesg, RC=1), and even on paths that still reach "file has no contents" the
unload is guarded. Guest survives (`ALL-SURVIVED`, 0 panics). Baseline
panic gone → fix_status=fixed.
