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

4-byte OOB read past PT_NOTE segment/page end in bsd_trans_osrel (note_overflow never bounds the descriptor)

Summary

note_overflow bounds only n_namesz while bsd_trans_osrel unconditionally reads *(int32*)(note+24): a note placed at first-page end with p_filesz=22 reads image_header[4096..4099] - fully past the lwbuf-mapped page into the adjacent physical page via DMAP (kmalloc-adjacent variant in the beyond-first-page path). KLD-harness differential: control p_osrel=0x44332211 (in-file) vs bug 0x200a2a2f (adjacent page bytes, present nowhere in the crafted file) 3/3. p_osrel has no userspace reader today so impact is none now, but it is a ready-made 4-byte kernel-memory oracle the moment procstat/sysctl exposes it. Bound-the-whole-note fix validated on a rebuilt kernel.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5630 Β· 9 files
FileTypeDescriptionSize
make_note.c β€” 4.8 KB view raw
df_osrel.c β€” 1.4 KB view raw
kld/Makefile β€” 56 B ↓ download
run_guest.sh β€” 1.5 KB view raw
run.log β€” 1.4 KB view raw
verdict.json β€” 4.2 KB view raw
VERDICT.md β€” 3.4 KB ↓ raw
fix.diff β€” 1.6 KB view raw
env.txt β€” 287 B view raw
VERDICT.md
↓ download raw

DF-5630 VERDICT

Status: reproduced (OOB read proven by value differential) β€” fix validated

Guest: DragonFly 6.5-DEVELOPMENT x86_64 (X86_64_GENERIC), cc 8.3. Trigger runs as unprivileged maxx (uid 1001); observability via a root-loaded KLD sysctl harness (df_osrel.ko, in this pack) that exposes struct proc::p_osrel β€” the exact field the OOB read fills.

Chain

execve(crafted ELF, uid 1001) β†’ exec_elf64_imgact (imgact_elf.c:602) β†’ get_brandinfo loop 1 (imgact_elf.c:550-561) β€” DragonFly brand has BI_BRAND_NOTE (elf_machdep.c:66) with brandnote {10,4,1,"DragonFly"} (imgact_elf.c:129-136) β†’ check_note β†’ check_PT_NOTE (imgact_elf.c:1721) β†’ note_overflow (imgact_elf.c:1700) bounds ONLY n_namesz β†’ 22 bytes proven; note fields {10,4,1} + vendor "DragonFly\0" all within those 22 bytes β†’ match (imgact_elf.c:1786-1790) β†’ BN_TRANSLATE_OSREL set (imgact_elf.c:134) β†’ bsd_trans_osrel called (imgact_elf.c:1792-1794) β†’ *osrel = *(const int32_t *)(note + 1 + roundup2(10,4)) = (int32_t)(note+24) (imgact_elf.c:1870-1872) β€” reads 4 bytes at offsets 24..27 of a 22-byte-proven region β†’ with the note at file offsets [4072, 4094): read lands at image_header[4096..4099] β€” past the first mapped page into the adjacent physical page via the DMAP (kern_exec.c:838-839 β†’ cpu/x86_64/misc/lwbuf.c:95 PHYS_TO_DMAP) β†’ imgp->proc->p_osrel = (imgact_elf.c:867).

Proof (vulnerable kernel #0)

  • control binary (full 28-byte note, descriptor 0x44332211 inside the segment): execs; KLD harness reads p_osrel = 1144201745 (0x44332211) β€” the harness and note plumbing work exactly.
  • bug binary (22-byte note, declared descriptor beyond segment/page): execs; p_osrel = 537537071 (0x200a2a2f) = bytes "/*\n " β€” present nowhere in the 4095-byte crafted file. 3/3 runs. Only possible source: memory past the mapped first page (DMAP neighbor physical page), i.e. the OOB read.
  • nonote binary: ENOEXEC (the note was the only branding path).

Impact ceiling on this guest: the 4 bytes land in p_osrel which has no userspace-visible reader (checked: only writes at kern_exec.c:298, imgact_elf.c:867, init_main.c:385) and the read hits DMAP/kmalloc-adjacent memory that stays mapped β€” no panic, no disclosure today. It is a genuine CWE-125 whose impact is one future p_osrel consumer away (e.g. procstat/sysctl exposure would turn it into a repeatable 4-byte kernel memory oracle), and it can read past a kmalloc chunk in the heap path (imgact_elf.c:1757-1769).

Fix validation (kernel #1, patched, built 23:30 UTC)

  • bug binary β†’ "Exec format error" (ENOEXEC): the truncated note is now rejected by the strengthened note_overflow (whole declared note must fit), so no brand, no trans_osrel call.
  • control binary β†’ still execs, p_osrel = 0x44332211 β€” legitimate ABI notes (every real DragonFly binary) are unaffected.
  • nonote binary β†’ ENOEXEC as before.

Root cause lines: sys/kern/imgact_elf.c:1700-1707 (note_overflow) + 1866-1875 (bsd_trans_osrel), enabled by elf_machdep.c:66-67 brandnote wiring.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel (kernel #1 with fix.diff): bug-note binary rejected with ENOEXEC (was: execs + p_osrel=0x200a2a2f from OOB); control note still execs with p_osrel=0x44332211 (no regression for every real DragonFly binary's ABI note); nonote still ENOEXEC.

run.log 'DF-5630 fix validation' section; VERDICT.md; fix.diff in pack.
↓ fix.diffper-fix-DF-5630

Confirmed kernel references

Detail

Exploit chain

execve(crafted ELF EI_OSABI=255, uid 1001) -> get_brandinfo loop 1 (BI_BRAND_NOTE) -> check_PT_NOTE accepts {10,4,1}+'DragonFly\0' within 22 proven bytes -> BN_TRANSLATE_OSREL callback bsd_trans_osrel reads (int32_t)(note+24) past segment/page end (DMAP neighbor physical page) -> imgp->proc->p_osrel = OOB bytes; observable only via the root-loaded df_osrel.ko sysctl harness (evidence gathering, not part of the attack).

Evidence (decisive lines)

run.log: case2 control p_osrel=0x44332211 vs case3 bug p_osrel=0x200a2a2f (3/3 runs), case1 nonote ENOEXEC; fix validation: bug binary ENOEXEC on patched kernel while control still yields 0x44332211. df_osrel.c KLD harness in pack.

PoC changes

Authored from scratch. Three harness iterations were needed: (1) note offsets must be 4-aligned or check_PT_NOTE breaks at aligned(note, Elf32_Addr); (2) e_entry must lie within [p_vaddr, p_vaddr+p_memsz) or exec_elf64_imgact leaves entry=0 (SIGSEGV at address 0) - used p_memsz=258 to span the stub at 0x400100; (3) CTLTYPE_STRING sysctl harness replaced with RW-int pid + RD-int value pair.

Verified recommended fix

note_overflow(): require the whole declared note (header + rounded name + descriptor) to fit within the remaining segment bytes (see fix.diff).

Verdict

Unprivileged-triggerable 4-byte OOB read: note_overflow() (imgact_elf.c:1700-1707) bounds only n_namesz, but bsd_trans_osrel() (imgact_elf.c:1866-1875) reads the 4-byte descriptor at note+24 of any note whose header matches the DragonFly brandnote {10,4,1,'DragonFly'} bound into the exec path at elf_machdep.c:66-67. A note truncated right after the vendor name (22 bytes) at the end of the first page makes the kernel read image_header[4096..4099] - the adjacent physical page via the DMAP. Proven by value differential with a KLD sysctl harness: control note -> p_osrel=0x44332211 (exact in-file descriptor), bug note -> p_osrel=0x200a2a2f ('/*\n ') which appears nowhere in the crafted file. Silent on this guest (p_osrel has no userspace reader; DMAP read never faults) - Low severity now, a 4-byte kernel-memory oracle the day p_osrel gains exposure. Fix validated: truncated note rejected with ENOEXEC, full notes unaffected.