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

PT_INTERP interpreter string never NUL-terminated: kernel heap OOB read disclosed to unprivileged users via uprintf("ELF interpreter %s not found")

Field Value
ID DF-2718
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
CWE CWE-125 / CWE-200
File sys/kern/imgact_elf.c
Lines 668 (exact-size kmalloc), 1817-1863 (bcopy, no NUL check), 844 (%s)
Area kern
Confidence certain
Discovered 2026-08-30
Pass 2 (GLM 5.3 second pass)
Bucket kernleak
Reported pending
Known CVE none
CVE match novel

Summary

exec_elfN(imgact) kmallocs exactly p_filesz bytes for the PT_INTERP string (no +1, no M_ZERO) and extract_interpreter bcopy's exactly pathsz bytes with no NUL-termination check. The buffer is then consumed as a C string by strcmp, nlookup/copystr via load_file, and uprintf("ELF interpreter %s not found\n", interp) β€” the %s walk runs past the allocation and prints adjacent kernel heap contents to the executing user's controlling terminal.

Threat model & preconditions

Any unprivileged local user execs a crafted ELF with a NUL-free PT_INTERP: the failing-interpreter message discloses M_TEMP size-class heap residue until the first zero byte; leak length unbounded and content groomable.

Proof of concept

VERIFIED (findings/poc/DF-2718/): seed1024 (interp 'B'Γ—1024, unterminated) then leak1000 (interp 'A'Γ—1000) under script(1) as uid 1001 β€” 18/20 iterations printed "ELF interpreter A1000 B24 not found" (residue of the previously freed allocation) and one iteration leaked real kernel strings "xx/.ssh/autho/home/maxx". Leak-only primitive; no uid=0 route. Fix (reject unterminated interpreter strings in extract_interpreter, mirroring FreeBSD) validated.

@@ extract_interpreter
    pathsz  = pinterpreter->p_filesz;
+   if (pathsz < 2)
+       return (result_failure);
    if (limited_to_first_page) {
            bcopy(imgp->image_header + pathloc, data, pathsz);
+           if (data[pathsz - 1] != '\0')
+               return (result_failure);
            return (result_success);
    }
@@  exec_unmap_page(lwb);
+   if (data[pathsz - 1] != '\0')
+       return (result_failure);
    return (result_success);

Timeline

  • 2026-08-30 Discovered during pass-2 audit of imgact_elf.c (GLM 5.3); unpriv leak reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2718 Β· 17 files
FileTypeDescriptionSize
make_poc.py β€” 2.8 KB view raw
make_poc2.py β€” 2.0 KB view raw
poc_unterm β€” 8.0 KB ↓ download
poc_term β€” 8.0 KB ↓ download
seed1024 β€” 8.0 KB ↓ download
leak1000 β€” 8.0 KB ↓ download
ctl1024 β€” 8.0 KB ↓ download
run.sh β€” 610 B view raw
run2.sh β€” 361 B view raw
run.log β€” 2.3 KB view raw
run2.log β€” 20.7 KB view raw
leak_sample.txt β€” 20.7 KB view raw
fix.diff β€” 692 B view raw
verdict.json β€” 3.2 KB view raw
VERDICT.md β€” 2.1 KB ↓ raw
README.md β€” 2.2 KB ↓ raw
env.txt β€” 268 B view raw

DF-2718 β€” PT_INTERP string never NUL-terminated β†’ kernel heap OOB read + disclosure via uprintf

  • File: sys/kern/imgact_elf.c
  • Severity: Medium (kernel info leak, unprivileged local)
  • Confidence: certain β€” reproduced on the guest
  • Class: kernleak / CWE-125 (OOB read) + CWE-200 (info disclosure)

Root cause

exec_elfN(imgact) allocates the interpreter-path buffer with the exact PT_INTERP p_filesz and never requires NUL-termination:

  • sys/kern/imgact_elf.c:663-673 β€” only checks p_filesz == 0 || > PAGE_SIZE || > MAXPATHLEN, then interp = kmalloc(phdr[i].p_filesz, M_TEMP, M_WAITOK) (no +1, no M_ZERO)
  • sys/kern/imgact_elf.c:1817-1863 (extract_interpreter) β€” bcopys exactly pathsz bytes and returns success; never checks data[pathsz-1] == '\0'

The unterminated buffer is then consumed as a C string:

  • sys/kern/imgact_elf.c:584 β€” strcmp(interp, bi->interp_path)
  • sys/kern/imgact_elf.c:440 (via __elfN(load_file) :840) β€” nlookup_init(nd, file, UIO_SYSSPACE, ...) β†’ copystr() reads past the allocation (bounded by MAXPATHLEN, contents discarded)
  • sys/kern/imgact_elf.c:844 β€” uprintf("ELF interpreter %s not found\n", interp) β€” %s formats the buffer until the first NUL past the allocation, writing adjacent kernel-heap contents to the executing user's controlling terminal.

Reproduce

python3 make_poc.py && python3 make_poc2.py
# push seed1024 leak1000 poc_unterm to the guest, chmod +x
sh run2.sh          # seed/leak pairs, as unprivileged user, under script(1)

Expected (observed 18/20 iterations):

ELF interpreter AAAA…(1000 A's)BBBBBBBBBBBBBBBBBBBBBBBB(24) not found

The 24 B bytes are residue of the previously freed 1024-class M_TEMP allocation seeded by the prior exec β€” i.e. kernel heap contents from a different, freed allocation, disclosed to an unprivileged user. One iteration additionally leaked genuine kernel path-string residue:

ELF interpreter AAAA…(1000)BBBB…(24)xx/.ssh/autho/home/maxx not found

poc_unterm (single run, 1024 A's) shows the raw OOB walk stopping at the first zero byte past the slab slot.

Fix

FreeBSD added the missing check in extract_interpreter (if (data[pathsz - 1] != '\0') return (result_failure);). See fix.diff.

VERDICT.md
↓ download raw

DF-2718 VERDICT

Status: reproduced β€” impact: leak β€” confidence: certain.

What was run

Guest: DragonFly 6.5-DEVELOPMENT #0 (INVARIANTS X86_64_GENERIC), uid 1001 (maxx).

  1. make_poc.py β†’ poc_unterm: ELF64, PT_INTERP p_filesz=1024, file bytes all 'A' with no NUL. Exec'd under script(1) so the process has a controlling terminal (uprintf output reaches us).
  2. make_poc2.py β†’ seed1024 (interp = 1024 'B', unterminated) and leak1000 (interp = 1000 'A', unterminated). The seed exec occupies a 1024-size-class kmalloc(M_TEMP) slot and frees it (imgact_elf.c:845); the leak exec's kmalloc(1000) reuses the same 1024-class slot, so bytes 1000..1023 are the freed allocation's residue.

Observed

  • run.log: ELF interpreter AΓ—1024 not found β€” uprintf walked one byte past the kmalloc'd buffer (stopped at a zero byte in a fresh slab page).
  • run2.log (20 seed/leak pairs): 18/20 iterations printed AΓ—1000 + BΓ—24 β€” the 24 B's are kernel-heap bytes from a different, freed M_TEMP allocation (imgact_elf.c:844 %s, imgact_elf.c:668 allocation).
  • iteration 15 additionally leaked real kernel path-string residue: …BBBBBBBxx/.ssh/autho/home/maxx not found β€” genuine kernel heap data (nlookup/namei path residue in the same size class), not attacker-supplied.

Impact

Unprivileged local kernel-heap information disclosure. Length of the disclosure is unbounded until the first NUL byte; contents are whatever follows in the M_TEMP size class (paths, and potentially pointers/binary data β€” iteration 15 shows binary non-ASCII bytes too). Also an unbounded %s walk in kernel address space (imgact_elf.c:844) β€” in principle able to walk into unmapped KVA; not observed across 40+ runs on this guest.

No uid=0 chain: leak-only primitive (contents are not attacker-steerable beyond grooming residue).

Fix validation

fix.diff adds the NUL-termination check in extract_interpreter (mirrors FreeBSD). Patched kernel build + rerun: leak gone (see fix_build.log / fix_run.log in this pack if present), baseline re-verified before patching.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel (fix.diff: NUL-termination check in extract_interpreter, both copy paths + pathsz<2 reject) built via make nativekernel and booted. Baseline leak (18/20) eliminated: 0 leak lines in 8/8 seed/leak pairs; /bin/ls and /usr/bin/true exec normally.

['findings/poc/DF-2718/fix_run.log', 'findings/poc/DF-2718/fix.diff', 'findings/poc/DF-2718/fix_build.log']
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Mon Aug 31 07:00:26 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

['findings/poc/DF-2718/run2.log β€” 18/20 iterations print A*1000+B*24 after the 1000-byte interp (cross-allocation residue)', "findings/poc/DF-2718/run2.log iteration 15 β€” 'xx/.ssh/autho/home/maxx' real kernel heap strings", 'findings/poc/DF-2718/run.log β€” baseline: A*1024 exactly (OOB walk into fresh slab, stops at zero)', 'findings/poc/DF-2718/leak_sample.txt β€” raw captured bytes', 'VERDICT.md β€” narrative']

PoC changes

Seed PoCs rewritten from scratch: python3 ELF64 generators (make_poc.py single 1024-A interp, make_poc2.py seed1024/leak1000 pair for the 1024-size-class residue groom); run under script(1) so uprintf reaches a controlling tty

Verified recommended fix

Reject PT_INTERP strings lacking a NUL terminator in extract_interpreter: if (data[pathsz-1] != '\0') return failure (both copy paths), plus reject pathsz < 2

Verdict

PT_INTERP interpreter string is copied with exactly p_filesz bytes and never NUL-terminated (imgact_elf.c:668 kmalloc, extract_interpreter:1817-1863); consumers treat it as a C string and uprintf("ELF interpreter %s not found") at imgact_elf.c:844 discloses kernel heap bytes past the allocation to the unprivileged user's controlling terminal. Reproduced 18/20 iterations leaking the 24-byte residue of a previously-freed M_TEMP allocation plus one iteration leaking genuine kernel path-string residue ('.ssh/autho', '/home/maxx'). Leak-only primitive; no uid=0 chain.