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)
PoC verification
Evidence pack
findings/poc/DF-5051 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
- writes a 49-byte hand-assembled trampoline into the dead body of
sys_sbrk(cave at0xffffffff809bda90, this kernel build), - re-points
sysent[534](obsolete/unassignedvmm_guest_ctlslot, nosys) at it, with a magic guard (0x5151c0de5151c0dein argp[2]) rejecting stray callers, - issues a direct
syscall(rax=534) so libc's indirectSYS_syscallwrapper cannot truncate the 64-bit return value, - 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), - 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.
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 duringmodule_register(strcmpon garbagemoduledata.nameβ a toolchain/linker incompatibility of the snapshot, root-only impact, out of audit scope; documented in the pack asNOTES-module-loader.txtcontent in this file). - Called the REAL
dal_vector_createfrom 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_testablebaseline 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
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.
No comments yet.