DragonFlyBSD Kernel Audit
← triage · dashboard
DF-2015

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_ON at 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 memcpy copies datasize-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->len pointer 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).

Validate every field read from the firmware blob before use:

  • minimum blob size,
  • num bounded by the val[] array dimension,
  • len bounded so the sigval write stays inside the BO,
  • datasize bounded so the payload memcpy stays 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

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2015 · 5 files
FileTypeDescriptionSize
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
VERDICT.md verdict Source verification narrative
↓ download 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_testable
baseline reproduced→ patch + rebuild →patched clean

not_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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Confirmed kernel references

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.