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

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2773 Β· 11 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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-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:

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched 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'
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 1 01:04:57 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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).