link_elf_obj_unload_file NULL-derefs reltab/relatab when load aborts between the scan and the table allocation (e.g. 'file has no contents')
| Field | Value |
|---|---|
| ID | DF-2773 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-476 NULL Pointer Dereference |
| File | sys/kern/link_elf_obj.c |
| Lines | 531/534 (counters) vs 561-566 (alloc), aborts 540/545/551; deref 887-892 |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:kern |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
The scan loop increments ef->nreltab/nrelatab before the tracking arrays are allocated. Rejections in between (nprogtab==0, nsym!=1, bad symstrindex) goto out to linker_file_unload, which for a fresh refs==0 file tears straight through to ops->unload; link_elf_obj_unload_file then runs the unguarded relocation-table loops with relatab==NULL β kernel read at 0x0 β panic. The progtab loop above is guarded β the same guard was never added for the relocation tables. Also a latent trap for any future scan-time hardening of the REL/RELA counting (e.g. DF-2772's fix) which without this guard would re-open the same deref. Root-supplied-input class; NULL read only.
Proof of contest
VERIFIED on the stock guest (findings/poc/DF-2773/): crafted .ko
(symtab+strtab+one .rela, zero PROGBITS) β kldload: file has no
contents immediately followed by Fatal trap 12 ... fault virtual
address = 0x0 ... Stopped at link_elf_obj_unload_file+0x6e β the
loader's own error path is the killer. Patched kernel: clean ENOEXEC.
Recommended fix
Mirror the existing if (ef->progtab) guard for reltab/relatab β
one-line fix validated in the pack.
Timeline
- 2026-08-30 Discovered during pass-2 audit of link_elf_obj.c (GLM 5.3); panic reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2773 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.6 KB | β raw | |
| VERDICT.md | β | 3.7 KB | β raw | |
| gen_module.py | β | 3.7 KB | view raw | |
| build.sh | β | 57 B | view raw | |
| run.sh | β | 314 B | view raw | |
| run.log | β | 1.2 KB | view raw | |
| panic.txt | β | 658 B | view raw | |
| build.log | β | 5.7 MB | β download | |
| env.txt | β | 191 B | view raw | |
| fix.diff | β | 5.6 KB | view raw | |
| verdict.json | β | 4.4 KB | view raw |
DF-2773 β link_elf_obj_unload_file NULL-deref when load aborts between scan and allocation
Build (host)
python3 gen_module.py df2773.ko nrel-nullderef
Run (guest, root β kldload is SYSCAP_NOKLD-gated)
scp -F dfbsd-qemu/config df2773.ko dfbsd:/tmp/ dfbsd-qemu/vm.sh run_root 'kldload /tmp/df2773.ko'
Expected β stock kernel (baseline, reproduced)
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 scan loop (link_elf_obj.c:517-539) increments ef->nreltab/nrelatab
before the tracking arrays are allocated (:557-572). A module with zero
PROGBITS/NOBITS sections (or nsym != 1, or bad symstrindex) takes
goto out (:540/:545/:551) β linker_file_unload tears the fresh file down
through ops->unload (kern_linker.c:549) β link_elf_obj_unload_file
executes for (i = 0; i < ef->nrelatab; i++) if (ef->relatab[i].rela)
(:890-892) with relatab == NULL β read at address 0x0 β panic.
Expected β patched kernel (fix.diff applied, kernel #1 Sep 1 2026)
kldload: an error occurred while loading module /tmp/df2773.ko ... RC=1 dmesg: linker_load_file: Unsupported file type
(The module now dies even earlier β scan-time sh_info validation β and the
unload guard if (ef->reltab)/if (ef->relatab) closes the NULL-deref for
every other abort path.)
Threat model
Root-supplied-input class (SYSCAP_NOKLD), like DF-0040/0041/0042.
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:
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-544nprogtab == 0("file has no contents"):545-550nsym != 1(zero or multiple symbol tables):551-556invalidsymstrindex:567-571allocation 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:
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.
Fix verification
fixedPatched kernel rejects the identical module with ENOEXEC (RC=1, no panic, guest up); baseline kernel #0 NULL-deref panics on the same input. fix.diff authored against read-only sys/ and applied only inside the guest's /usr/src copy.
run.log (baseline vs patched), build.log (kernel #1 build), dmesg 'linker_load_file: Unsupported file type'
Confirmed kernel references
Detail
Exploit chain
root crafts module (gen_module.py mode=nrel-nullderef: no PROGBITS + one .rela) -> kldload -> scan counts nrelatab=1, arrays unallocated -> 'file has no contents' ENOEXEC path -> linker_file_unload -> link_elf_obj_unload_file reads ef->relatab[0].rela via NULL -> fatal page fault at 0x0 -> kernel panic. NULL-deref read only; no unprivileged reach (SYSCAP_NOKLD); ceiling is deterministic panic/DoS.
Evidence (decisive lines)
["panic.txt β baseline serial console: 'file has no contents' then Fatal trap 12 VA=0x0 stopped at link_elf_obj_unload_file+0x6e movq (%rax),%rdi", 'run.log β baseline (panic) vs patched (clean RC=1 rejection) transcripts', 'gen_module.py β trigger generator (mode nrel-nullderef): 5 sections, zero PROGBITS/NOBITS, one SHT_RELA', 'build.log β full untrimmed nativekernel build of the fix (-Werror clean)', 'VERDICT.md β path:line trace incl. all four abort paths between scan and allocation and the kern_linker.c:549 teardown', 'fix.diff β unload-loop NULL guards (plus the companion DF-2771/2772 changes), baseline-vs-patched validated']
PoC changes
n/a β trigger authored fresh (hand-crafted ET_REL); no prior seed for this finding
Verified recommended fix
Guard the entry-free loops in link_elf_obj_unload_file with if (ef->reltab) / if (ef->relatab), mirroring the existing if (ef->progtab) guard.
Verdict
link_elf_obj_load_file's scan loop (sys/kern/link_elf_obj.c:517-539) increments ef->nreltab/nrelatab before the tracking arrays are allocated (:557-572); several rejections in between (nprogtab==0 at :540, nsym!=1 at :545, bad symstrindex at :551) goto out to linker_file_unload, which for a fresh refs==0 file with no modules tears straight through to ops->unload (kern_linker.c:549), where link_elf_obj_unload_file dereferences the NULL arrays in unguarded loops (:887-892). A 434-byte crafted ET_REL module with a symtab+strtab+one .rela and zero PROGBITS sections panics the stock guest: console shows 'kldload: /tmp/df2773.ko: file has no contents' immediately followed by 'Fatal trap 12: page fault, fault virtual address = 0x0, supervisor read data' stopped at link_elf_obj_unload_file+0x6e (movq (%rax),%rdi) β the error path of the loader itself is the killer. NULL read only: no write, no disclosure, root-gated (SYSCAP_NOKLD) β Low, consistent with DF-0040/0041/0042. fix.diff guards both loops with if (ef->reltab)/if (ef->relatab); on rebuilt kernel #1 the same module is rejected cleanly (RC=1, guest up, 0 panics).
No comments yet.