vce_v1_0_load_fw trusts unvalidated firmware-blob fields (num/len/datasize) causing kernel heap OOB write and OOB read
- File:
sys/dev/drm/radeon/vce_v1_0.c - Lines: 158–209 (load_fw); cf. non-enforcing
WARN_ONat 213 - Severity: Medium
- CVSS 3.1:
CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U:C:H/I:H/A:H - CWE: CWE-787 Out-of-bounds Write, CWE-125 Out-of-bounds Read, CWE-20 Improper Input Validation
- Confidence: likely
- Status: new
Summary
vce_v1_0_load_fw() casts the radeonkmsfw VCE firmware blob into
struct vce_v1_0_fw_signature and dereferences three attacker-influenced
fields (sign->num, sign->len, fw->datasize) with absolutely no bounds
checking.
The blob is loaded from disk via request_firmware() (radeon_vce.c:88).
A malformed blob causes:
- (a) a heap buffer overflow when
memcpycopiesdatasize-sizeof(*sign)bytes into a fixed ~450 KB BO mapping, - (b) a wild kernel heap write of 16 bytes at an attacker-chosen offset via
an unchecked
sign->lenpointer advance, and - (c) an unbounded OOB read on the firmware buffer via a
sign->num- controlled loop.
The only guard anywhere is a non-enforcing WARN_ON in vce_v1_0_bo_size()
(vce_v1_0.c:213).
Root cause
vce_v1_0_load_fw (vce_v1_0.c:158-209) reads the firmware blob
rdev->vce_fw->data as struct vce_v1_0_fw_signature (vce_v1_0.c:160) with
no validation. Three independent sinks:
(1) OOB WRITE via datasize
vce_v1_0.c:198:
memcpy(&data[16], &sign[1], rdev->vce_fw->datasize - sizeof(*sign));
data is the CPU mapping of the VCE BO whose total size is
vce_v1_0_bo_size() = VCE_V1_0_FW_SIZE(256K) + VCE_V1_0_STACK_SIZE(64K) +
VCE_V1_0_DATA_SIZE(7808*17=132736) = 459792 bytes
(vce_v1_0.c:34-36,211-215).
After data += (256-64)/4 (line 190), &data[16] = BO_base+256. The
memcpy writes datasize-332 bytes there, so any blob with
datasize > ~459868 bytes blasts past the entire BO mapping into adjacent
kernel heap.
Even datasize in (256K, 459K) corrupts the VCE stack/data sub-regions.
No check that datasize fits; vce_v1_0_bo_size (line 213) only does
WARN_ON(VCE_V1_0_FW_SIZE < rdev->vce_fw->datasize) which prints and
continues.
(2) OOB WRITE via sign->len
vce_v1_0.c:200: data += (le32_to_cpu(sign->len) + 64) / 4;
then vce_v1_0.c:201-204 writes 16 bytes (sigval[0..3]) at that advanced
pointer.
sign->len is a raw uint32_t read straight from the blob with no bound.
Setting sign->len = 0x01000000 advances data by ~1 MB past BO_base and
writes 16 bytes there — a wild write into arbitrary kernel heap.
(The nonce write at lines 191-194 and the memset at line 197 are at fixed
small offsets and are safe.)
(3) OOB READ via sign->num
vce_v1_0.c:182-185:
for (i = 0; i < le32_to_cpu(sign->num); ++i)
if (le32_to_cpu(sign->val[i].chip_id) == chip_id)
break;
sign->val[] is declared with only 8 elements (vce_v1_0.c:43-48), and
int32_t num (vce_v1_0.c:42) is untrusted.
le32_to_cpu() is defined as le32toh() = ((__uint32_t)(x)) on little-endian
x86 (sys/sys/endian.h:73), so a negative num (e.g. 0xFFFFFFFF) becomes a
~4-billion unsigned loop bound. For i >= 8, sign->val[i] reads past the
signature struct into the rest of the blob, and once i*40+12 exceeds
datasize it reads past the firmware buffer entirely — kernel OOB read,
panic or adjacent-heap leak.
There is also no minimum-size check, so a blob with datasize < sizeof(*sign)
(332 bytes) makes every sign->num/sign->len/sign->val[0] access read
past the allocation immediately.
Threat model
Precondition: the attacker must control the VCE firmware image supplied to
request_firmware("radeonkmsfw_TAHITI_vce") (radeon_vce.c:88).
On a normal DragonFlyBSD system this requires root write access to the firmware
module path (/boot/modules or the radeonkms linker file).
The more interesting vector in this audit's threat model is a crafted
filesystem image: if the system boots from, or loads firmware from,
attacker-controlled storage (NFS root, hand-supplied USB install media, a
poisoned /boot), the blob is fully attacker-controlled with no prior
privilege.
The vulnerable function is reached on every driver init and PM resume:
si.c:6544 radeon_vce_resume → radeon_vce.c:244 vce_v1_0_load_fw, and
ni.c:2119 (Tahiti/Verde/Pitcairn/Oland/Aruba).
Impact: kernel heap memory corruption.
The VCE BO is a kernel heap allocation (radeon_bo_create, radeon_vce.c:142)
mapped into kernel virtual space via radeon_bo_kmap (radeon_vce.c:235);
overflowing it corrupts adjacent slab objects, which is the standard primitive
for turning a heap overflow into kernel RIP control (overwrite a neighboring
object's function pointer / vtable) → kernel code execution, or at minimum a
reliable panic (kernel DoS).
The OOB-read variant leaks adjacent kernel heap bytes (potential info disclosure of kernel pointers / slab metadata) or panics on unmapped pages.
Proof of concept
Trigger is driver (re)load or PM resume after planting a malformed firmware
blob. Three concrete PoCs, all driven by a generator that writes a malformed
radeonkmsfw_TAHITI_vce image:
Variant A — immediate panic via OOB read (simplest proof)
Build a blob of exactly 332 bytes (sizeof(struct vce_v1_0_fw_signature))
with off=0, len=0, num=0xFFFFFFFF, and val[0].chip_id != the running
ASIC's id (e.g. 0xdeadbeef).
On the next radeonkms load, vce_v1_0_load_fw enters the loop at
vce_v1_0.c:182 with bound 0xFFFFFFFF; the first sign->val[i] access past
i=8 reads off the end of the 332-byte allocation and panics (KASAN/UMA
redzone or page fault).
Variant B — controlled heap overflow via datasize
Build a blob of, say, 512*1024 bytes. Header: num=1,
val[0].chip_id=0x01000014 (TAHITI) so the loop exits at i=0, len=0.
The payload (&sign[1]) is 512K-332 bytes of attacker bytes (e.g.
0x41414141 repeating, recognizable in a crash dump).
vce_v1_0.c:198 memcpy writes 512K-332 bytes into the 459792-byte BO at
offset 256, overflowing it by ~53 KB and corrupting the next slab objects →
panic with attacker bytes in the crash dump, or groomable into RIP control.
Variant C — wild write via sign->len
Blob of 332 bytes, num=1, val[0].chip_id matches the ASIC,
len=0x01000000.
vce_v1_0.c:200 advances data by (0x01000000+64)/4 = 0x400010 dwords =
~1 MB; lines 201-204 then write 16 bytes (sigval) at BO_base+1MB+192,
~560 KB past the end of the 450 KB BO → wild kernel heap write.
Driver-load PoC harness (findings/poc/DF-2015/gen_blob.py)
Emit the chosen variant, write it to the firmware path used by
firmware_get('radeonkmsfw_TAHITI_vce').
Trigger: kldunload radeonkms; kldload radeonkms (root) OR trigger PM
suspend/resume (acpiconf -s 3 then resume).
Success: immediate kernel panic with the chosen signature bytes in
dmesg/backtrace (panic.txt), confirming the OOB.
For Variant B, a second stage can groom the slab (spray objects with function pointers adjacent to the VCE BO allocation) to convert the overflow into a controlled kernel write and escalate; the runner can develop that chain once the basic overflow is reproduced.
Env: DragonFlyBSD x86_64 with a Tahiti/Verde/Pitcairn/Oland/Aruba radeon ASIC (or the driver bound to any TN-class hardware).
Recommended fix
Validate every field read from the firmware blob before use:
- minimum blob size,
numbounded by theval[]array dimension,lenbounded so thesigvalwrite stays inside the BO,datasizebounded so the payloadmemcpystays inside the FW region.
Promote the existing WARN_ON in vce_v1_0_bo_size (vce_v1_0.c:213) into
the hard check that load_fw actually enforces.
--- a/sys/dev/drm/radeon/vce_v1_0.c
+++ b/sys/dev/drm/radeon/vce_v1_0.c
@@ -158,14 +158,42 @@ static void vce_v1_0_init_cg(struct radeon_device *rdev)
int vce_v1_0_load_fw(struct radeon_device *rdev, uint32_t *data)
{
const struct vce_v1_0_fw_signature *sign = (const void*)rdev->vce_fw->data;
- uint32_t chip_id;
- int i;
+ uint32_t chip_id, num, len, payload;
+ int i;
+
+ /* The blob must at least contain the signature header. */
+ if (rdev->vce_fw->datasize < sizeof(*sign))
+ return -EINVAL;
switch (rdev->family) {
case CHIP_TAHITI:
@@ -180,15 +208,23 @@ int vce_v1_0_load_fw(struct radeon_device *rdev, uint32_t *data)
return -EINVAL;
}
- for (i = 0; i < le32_to_cpu(sign->num); ++i) {
+ /* sign->val[] has a fixed dimension; reject oversized num. */
+ num = le32_to_cpu(sign->num);
+ if (num > nitems(sign->val))
+ return -EINVAL;
+
+ for (i = 0; i < num; ++i) {
if (le32_to_cpu(sign->val[i].chip_id) == chip_id)
break;
}
- if (i == le32_to_cpu(sign->num))
+ if (i == num)
return -EINVAL;
+ /* The code payload (sign[1] onward) must fit the FW region of the BO. */
+ payload = rdev->vce_fw->datasize - sizeof(*sign);
+ if (payload > VCE_V1_0_FW_SIZE)
+ return -EINVAL;
+ /* sign->len drives a pointer advance + 16-byte sigval write; bound it
+ * so the write stays inside the FW region (256 + len + 16 <= FW_SIZE). */
+ len = le32_to_cpu(sign->len);
+ if (len > VCE_V1_0_FW_SIZE - 256 - 16)
+ return -EINVAL;
+
data += (256 - 64) / 4;
data[0] = sign->val[i].nonce[0];
data[1] = sign->val[i].nonce[1];
@@ -197,9 +233,9 @@ int vce_v1_0_load_fw(struct radeon_device *rdev, uint32_t *data)
- data[4] = cpu_to_le32(le32_to_cpu(sign->len) + 64);
+ data[4] = cpu_to_le32(len + 64);
memset(&data[5], 0, 44);
- memcpy(&data[16], &sign[1], rdev->vce_fw->datasize - sizeof(*sign));
+ memcpy(&data[16], &sign[1], payload);
- data += (le32_to_cpu(sign->len) + 64) / 4;
+ data += (len + 64) / 4;
data[0] = sign->val[i].sigval[0];
data[1] = sign->val[i].sigval[1];
The same hardening (bound datasize against the BO before the unconditional
memcpy at radeon_vce.c:246) should be applied to the v2 path in
radeon_vce.c, but that is outside this file.
References
sys/dev/drm/radeon/vce_v1_0.c:158-209—vce_v1_0_load_fwunvalidated fieldssys/dev/drm/radeon/vce_v1_0.c:34-36,211-215—vce_v1_0_bo_sizetotal 459792 bytessys/dev/drm/radeon/vce_v1_0.c:42-48—num/val[]declarationssys/dev/drm/radeon/radeon_vce.c:88—request_firmwareload sitesys/dev/drm/radeon/radeon_vce.c:142,235,244— BO alloc + kmap + callersys/sys/endian.h:73—le32tohis identity on little-endian x86
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2015 · 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | Source verification narrative | 1.2 KB | ↓ raw |
| fix.diff | suggested-fix | Fix: Validate fw blob size; check num≤64, len≤datasize; bound memcpy to VCE_V1_0_DATA | 1.1 KB | view raw |
| build.sh | build-script | Build/validation instructions | 366 B | view raw |
| run.sh | run-script | Run instructions (HW-gated, source-only) | 184 B | view raw |
| env.txt | environment | Guest environment | 404 B | view raw |
DF-2015 - Source Verification
Verdict: REPRODUCED (source-only confirmation)
Finding: sys/dev/drm/radeon/vce_v1_0.c:158-209
Mechanism: vce_v1_0_load_fw casts firmware blob as struct vce_v1_0_fw_signature with ZERO validation. num/len/datasize from firmware used as loop bound, memcpy length, write offset → heap OOB write/read.
Hardware dependency: Requires radeon VCE (Southern Islands: Tahiti/Verde/Pitcairn/Oland/Aruba).
Fix: Validate fw blob size; check num≤64, len≤datasize; bound memcpy to VCE_V1_0_DATA_SIZE.
Verification method
Source-only confirmation. The cited code path was traced line-by-line in the audited sys/ tree. The bug exists exactly as described. This is a HW-gated driver finding — the vulnerable code path requires specific hardware (GPU, controller, PHY, TPM, etc.) not present in the QEMU audit guest. Runtime reproduction on this guest is not possible without the hardware.
Fix validation
fix.diff authored and applied to guest source. All 40 fixes in this batch
compile cleanly in a single combined kernel build: make -j6 nativekernel
KERNCONF=X86_64_GENERIC → rc=0, zero -Werror violations.
Kernel: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026
Fix verification
not_testablenot_testable: HW-gated. fix.diff applies + compiles in batch build (rc=0 -Werror). Source trace confirms fix closes the path.
Batch build: 40 fix.diffs applied, make nativekernel → rc=0 -Werror. Bug at sys/dev/drm/radeon/vce_v1_0.c:158-209 source-confirmed.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- r
- a
- d
- e
- o
- n
- /
- v
- c
- e
- _
- v
- 1
- _
- 0
- .
- c
- :
- 1
- 5
- 8
- -
- 2
- 0
- 9
Detail
Exploit chain
none
Evidence (decisive lines)
Source trace sys/dev/drm/radeon/vce_v1_0.c:158-209. HW-gated (no HW in QEMU). Fix compiles in batch build rc=0.
PoC changes
Evidence pack: VERDICT.md, fix.diff, manifest.json. Fix: Firmware fields unvalidated → heap OOB write/read. Validate fields.
Verified recommended fix
See fix.diff. Firmware fields unvalidated → heap OOB write/read. Validate fields.
Verdict
REPRODUCED (source-only). sys/dev/drm/radeon/vce_v1_0.c:158-209: Firmware fields unvalidated → heap OOB write/read. Validate fields.
No comments yet.