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

dal_vector_insert_at() performs no bounds check on position β€” kernel heap OOB write at a caller-chosen 32-bit element offset (latent library defect, primitive runtime-demonstrated)

Summary

insert_address = container + struct_size*position from the raw caller position, then memmove - position > count writes at any 16B-aligned offset within +-4GB (the 32-bit multiply also wraps). The header documents the missing check as caller responsibility. Distinct from DF-2068 (index vs size overflow). Sole in-tree path is dal_vector_append (passes count) - latent like DF-2068. RUNTIME-DEMONSTRATED on guest via /dev/kmem call of the real exported symbol: Fatal trap 12 at exactly container+16*0x0C000000, reproduced 3x; position>count reject fix authored.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5051 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 5.2 KB ↓ raw
kv.c β€” 15.1 KB view raw
mkpatch.c β€” 2.6 KB view raw
vectorpoc.c β€” 6.4 KB view raw
run-baseline.sh β€” 483 B view raw
gen_run.py β€” 2.3 KB view raw
Makefile β€” 58 B ↓ download
panic-baseline-oobwrite.log β€” 13.7 KB view raw
supplementary-run6-insertat-count-read.log β€” 13.1 KB view raw
fix.diff β€” 1.2 KB view raw

DF-5051 β€” dal_vector_insert_at() unchecked position β†’ kernel heap OOB write (runtime-demonstrated)

sys/dev/drm/amd/display/dc/basics/vector.c:204-234 (AMD DC "dal_vector" dynamic array, compiled into amdgpu.ko; vendored from Linux amdgpu).

Build / Run / Expected

This PoC does not build a kernel module (freshly-linked kmods cannot load on this guest snapshot β€” see VERDICT.md). It uses a root-owned /dev/kmem call facility (kv.c) that:

  1. writes a 49-byte hand-assembled trampoline into the dead body of sys_sbrk (cave at 0xffffffff809bda90, this kernel build),
  2. re-points sysent[534] (obsolete/unassigned vmm_guest_ctl slot, nosys) at it, with a magic guard (0x5151c0de5151c0de in argp[2]) rejecting stray callers,
  3. issues a direct syscall (rax=534) so libc's indirect SYS_syscall wrapper cannot truncate the 64-bit return value,
  4. lets userland call any kernel address with up to 6 register args; the trampoline stores the callee's rax into sysmsg->sm_result (offset 0),
  5. restores the original sysent entry and the cave bytes after each call.
# guest (root), one boot:
cc -O2 -o /root/kv kv.c
kldload amdgpu                     # module base from: kldstat | grep amdgpu
# dal_vector_create   = base + 0x11d070
# dal_vector_destruct = base + 0x11d120
# dal_vector_insert_at= base + 0x11d2b0
V=$(/root/kv call <base+11d070> 0 4 10)      # create(NULL, cap=4, size=16)
KV_BCOPY=<bcopy addr> /root/kv kread $V 32   # {container,16,count,cap=4,ctx}
/root/kv call <base+11d2b0> $V <kernel string addr> 0C000000   # THE BUG
# EXPECTED: kernel panic β€” "Fatal trap 12 ... supervisor write data, page not
# present ... Stopped at memmove+0x76: movq %rdx,(%rdi)" with the faulting
# address exactly container+0xC0000000 (= 16 * 0x0C000000).

run-baseline.sh is the exact script used; panic-baseline-oobwrite.log is the captured serial-console panic.

Result

REPRODUCED (panic). Faulting address 0xfffff8014db44340 = container (0xfffff8018db44340) + 16 * 0x0C000000; fault class supervisor WRITE; stopped instruction memmove+0x76 movq %rdx,(%rdi) β€” i.e. the write memmove of dal_vector_insert_at (vector.c:226-229) writing struct_size bytes at container + struct_size*position with a fully caller-chosen position.

Offset control: any 32-bit position β†’ any 16-byte-aligned offset up to 64 GB (modulo the 32-bit struct_size * position wrap, which itself redirects the write back near the buffer). See VERDICT.md for the primitive characterization.

VERDICT.md
↓ download raw

DF-5051 β€” VERDICT

Finding: dal_vector_insert_at() performs no bounds check on position (vector.c:204-234). insert_address = container + struct_size * position (vector.c:218) is computed from the raw caller value and written to unconditionally (vector.c:226-229, memmove(insert_address, what, struct_size)). The header (include/vector.h:69-71) documents the check as the caller's job ("do not check that index belongs to container since the function is private"), but the function is a plain exported symbol of the DC library.

Source analysis (pass 2): the only in-tree caller that passes a non-count index does not exist β€” dal_vector_append (vector.c:236-241) passes count; the sole consumer of the entire dal_vector API in the tree is dc/core/dc_link_ddc.c (construct/append/destruct only, EDID/DDC payloads bounded by write_size/read_size <= EDID_SEGMENT_SIZE, dc_link_ddc.c:573). So the OOB write is a latent library defect: any future/other caller that computes an index one element past count (the header itself suggests "get_count+1" as a legitimate caller pattern β€” which is ALREADY off-by-one: count+1 > count) gets a heap write at a chosen offset.

Runtime verification (QEMU guest, DragonFly 6.5-DEV #0 Jul 2 2026, kernel X86_64_GENERIC, no AMD GPU present β€” amdgpu.ko loaded as a module):

  • Built a root-owned arbitrary-kernel-call facility (kv.c, see README) because freshly-linked kmods GPF on this snapshot during module_register (strcmp on garbage moduledata.name β€” a toolchain/linker incompatibility of the snapshot, root-only impact, out of audit scope; documented in the pack as NOTES-module-loader.txt content in this file).
  • Called the REAL dal_vector_create from amdgpu.ko: returned a valid vector; bcopy-read its fields: container=0xfffff8008f9a4cc0, struct_size=16, count=0, capacity=4 β€” exactly the struct from include/vector.h:29-35.
  • Called the REAL dal_vector_insert_at(v, version, 0x0C000000):
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xfffff8014db44340
fault code            = supervisor write data, page not present
instruction pointer   = 0x8:0xffffffff80bca976
Stopped at memmove+0x76: movq %rdx,(%rdi)

0xfffff8014db44340 = container(0xfffff8018db44340) + 16*0x0C000000 β€” a supervisor WRITE at the exact predicted offset, in the exact predicted memmove. Three independent reproductions across boots (one with position=5, fault at container+0x50: 0xfffff801d6fa5880; one with 0x0C000000).

Primitive characterization: 16-byte write (attacker-of-the-caller chosen content, e.g. any kernel-readable 16 bytes) at container + (uint32)(struct_size * position) β€” any 16-byte-aligned offset within Β±4GB of the chunk (32-bit multiply can also wrap the offset). In the shipped DC code no unprivileged path controls position, so this is a library-primitive demonstration, not a direct privesc: exploitation requires a misusing caller, none exists in-tree today. Severity Low (latent), consistent with the pass-1 treatment of DF-2068 (same file, same latent class).

Why not uid=0: the primary-success criterion (unpriv β†’ uid=0) does not apply: reaching the sink requires kernel code to pass a bad index; the only in-tree index is count (append). The primitive is proven; the missing ingredient is a caller bug, which is what the fix eliminates.

Fix validation: fix.diff (bounds check + header doc fix) applies cleanly to sys/. A full fixed-module rebuild could not be exercised on this guest (freshly-linked kmods crash at load β€” snapshot toolchain issue, see above; hot-patching module text via /dev/kmem was attempted and the writes were accepted, but every attempt to execute the patched path was cut short by the guest's session instability after repeated kmem calls). Fix status for this finding: not_testable at runtime on this harness; the change is 4 lines with trivially auditable semantics (if (position > vector->count) return false), and the DF-5052 companion fix's semantics WERE runtime-validated (see that pack). fix.diff was authored after verification against the exact disassembly of the running kernel (insert_at+0x14 reads count at offset 0xC; the fix inserts the guard before the first dereference).

Artifacts: panic-baseline-oobwrite.log (decisive panic), supplementary-run6-insertat-count-read.log (same-boot evidence of executing insert_at+0x14 reading vector->count at 0xffffffff8f9a4d4c), kv.c (call facility), run-baseline.sh, gen_run.py, mkpatch.c, vectorpoc.c (the kmod PoC that could not be loaded on this snapshot β€” kept for guests with a working module loader), fix.diff.

Environment notes (guest uname): DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 QEMU/KVM, 4 GB, 6 vCPU, no SMAP/SMEP/KASLR. Guest left clean (with-src snapshot) after the run.

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

not_testable
baseline reproduced→ patch + rebuild →patched clean

baseline OOB-write panic reproduced 3x; patched-kernel run not possible on this harness (kmod loader broken for fresh builds); source fix is a 4-line guard with trivially auditable semantics; the companion DF-5052 fix's equivalent state-change WAS runtime-validated in the same session

fix.diff; VERDICT.md 'Fix validation' section
↓ fix.diffper-fix-DF-5051

Confirmed kernel references

Detail

Evidence (decisive lines)

panic-baseline-oobwrite.log (decisive: fault va 0xfffff8014db44340 = container 0xfffff8018db44340 + 0xC0000000, supervisor WRITE, memmove+0x76); supplementary-run6-insertat-count-read.log (insert_at+0x14 reading vector->count at V+0xC β€” real function executing); VERDICT.md narrative

PoC changes

Original sketch was a kld module (vectorpoc.c, kept); freshly-linked kmods GPF at module_register on this snapshot (guest toolchain/linker state, root-only impact), so the PoC was rebuilt as a /dev/kmem sysent[534]+trampoline call facility (kv.c) invoking the exported amdgpu symbols directly; had to bypass libc's indirect syscall wrapper (it truncates 64-bit return values) and fix an inline-asm clobber bug

Verified recommended fix

dal_vector_insert_at: reject position > vector->count (return false) before computing insert_address; update the include/vector.h contract comment

Verdict

REPRODUCED on the QEMU guest via a root-owned /dev/kmem call facility (kv.c): calling the real dal_vector_insert_at() from amdgpu.ko with position=0x0C000000 on a fresh vector (count=0, capacity=4, struct_size=16) causes a supervisor WRITE page fault at exactly container+16*0x0C000000 inside the function's write memmove (Stopped at memmove+0x76: movq %rdx,(%rdi)) β€” an arbitrary-offset (caller-chosen 32-bit element index) heap OOB write primitive in the vendored AMD DC vector library. Three independent panics captured across boots (offsets +0xC0000000 and +0x50). LATENT: no in-tree caller passes an index other than count (sole consumer dc_link_ddc.c uses construct/append/destruct with EDID-bounded counts), so this is a library-contract defect (documented as such in include/vector.h:69-71) β€” a misusing future caller gets the demonstrated primitive. uid0 does not apply: no unprivileged-reachable path feeds position.