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

Missing size validation for AMDGPU_CHUNK_ID_IB allows OOB read of drm_amdgpu_cs_chunk_ib fields

  • File: sys/dev/drm/amd/amdgpu/amdgpu_cs.c
  • Lines: 166, 169, 175, 181, 182, 183, 184, 999, 1004, 1017, 1040, 1041
  • Severity: Medium
  • CVSS: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L
  • CWE: CWE-125 Out-of-bounds Read
  • Confidence: likely

Summary

amdgpu_cs_parser_init validates that length_dw is large enough for FENCE chunks (line 188) and BO_HANDLES chunks (line 202), but the AMDGPU_CHUNK_ID_IB case at lines 181–184 only increments num_ibs without any size check. A user can submit an IB chunk with length_dw as small as 1 (4-byte kdata buffer). amdgpu_cs_ib_fill then casts kdata to (struct drm_amdgpu_cs_chunk_ib *) and reads fields at offsets up to 28 from a 4-byte allocation, causing heap OOB reads of up to 28 bytes from adjacent slab objects.

Root cause

amdgpu_cs.c:181-184:

case AMDGPU_CHUNK_ID_IB:
    ++num_ibs;
    break;

No length_dw * sizeof(uint32_t) < sizeof(struct drm_amdgpu_cs_chunk_ib) check, unlike the FENCE (line 188) and BO_HANDLES (line 202) cases.

The chunk's kdata is allocated as kvmalloc_array(size, sizeof(uint32_t), GFP_KERNEL) at line 169 with size=length_dw, and length_dw*4 bytes are copied at line 176.

Then amdgpu_cs_ib_fill:999 does chunk_ib = (struct drm_amdgpu_cs_chunk_ib *)chunk->kdata; and reads chunk_ib->ip_type (offset 20), ip_instance (offset 24), ring (offset 28), flags (offset 4), va_start (offset 8), ib_bytes (offset 16) β€” all OOB when length_dw < 8. The struct drm_amdgpu_cs_chunk_ib (uapi: drm_amdgpu_drm.h:562-576) is 32 bytes = 8 dwords.

There is a secondary issue: line 175 size *= sizeof(uint32_t) truncates the byte count to 32-bit unsigned, so for length_dw = 0x40000000 (if the 4 GB kvmalloc somehow succeeds) copy_from_user copies 0 bytes, leaving the entire buffer as uninitialized kvmalloc memory that is then read as chunk_ib fields.

Threat

Any unprivileged local user via /dev/dri/renderDXX.

Submit amdgpu_cs with one IB chunk whose length_dw is 1..7. The OOB read of up to 28 bytes of adjacent kernel heap is consumed as IB control fields:

  • ip_type/ip_instance/ring feed amdgpu_ctx_get_entity (which validates them β€” random values are usually rejected with -EINVAL, blocking further progress);
  • ib_bytes drives amdgpu_ib_get SA allocation size for parse_cs rings (UVD/VCE) β€” random huge value typically fails allocation.

No direct path returns the read bytes to userspace, so this is primarily a kernel-hardening defect with possible DoS via huge SA-allocation attempts and theoretical info-leak under slab grooming.

Upstream Linux added this check (modern amdgpu_cs.c has if (size < sizeof(struct drm_amdgpu_cs_chunk_ib)) return -EINVAL; in the IB case); DragonFly is missing it.

Exploit / PoC

PoC: open render node, alloc ctx, alloc a 1-dword IB chunk (length_dw=1, chunk_data=any u32), alloc a deps chunk, submit CS.

Kernel OOB-reads chunk_ib->{flags, va_start, ib_bytes, ip_type, ip_instance, ring} from the 4-byte kmalloc(4) slab object plus the following 24+ bytes of adjacent slab.

With KASAN enabled the access is flagged immediately; without KASAN the random adjacent bytes usually fail ctx_get_entity validation β†’ -EINVAL returned to user.

To demonstrate heap info disclosure: groom the slab (spray kmalloc-32 objects containing a marker pattern via amdgpu_bo userptr), submit the tiny IB chunk, observe whether the CS proceeds past amdgpu_ctx_get_entity (indicates the leaked ip_type/instance/ring happened to be valid).

The PoC materialises as: compile a libdrm C program that opens renderD128, calls AMDGPU_CTX_OP_ALLOC_CTX, then DRM_IOCTL_AMDGPU_CS with chunks=[{IB, length_dw=1, chunk_data=&(u32){0}}].

Build:

cc -ldr -I/usr/local/include/libdrm -o ib_oob_poc ib_oob_poc.c

Success = no -EINVAL before reaching amdgpu_cs_ib_fill's entity check (proves the OOB read occurred) plus KASAN/UMA zone report showing read past allocation.

Add an explicit size check in the IB case, mirroring what FENCE/BO_HANDLES already do.

--- a/sys/dev/drm/amd/amdgpu/amdgpu_cs.c
+++ b/sys/dev/drm/amd/amdgpu/amdgpu_cs.c
@@ -179,6 +179,11 @@ static int amdgpu_cs_parser_init(struct amdgpu_cs_parser *p, union drm_amdgpu_cs
        switch (p->chunks[i].chunk_id) {
        case AMDGPU_CHUNK_ID_IB:
+           if (p->chunks[i].length_dw * sizeof(uint32_t)
+               < sizeof(struct drm_amdgpu_cs_chunk_ib)) {
+               ret = -EINVAL;
+               goto free_partial_kdata;
+           }
            ++num_ibs;
            break;

Additionally, harden the byte-count computation at line 175 against the size *= sizeof(uint32_t) 32-bit truncation: declare size_t bytes = (size_t)p->chunks[i].length_dw * sizeof(uint32_t); and pass bytes to both kvmalloc_array's effective byte count and copy_from_user. (kvmalloc_array already takes count+elem separately and rejects count*size_t overflow, so the alloc is fine; only the local unsigned size truncation is buggy.)

  • DF-1483 (sibling): user-fence offset integer overflow in same file.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1484 Β· 10 files
FileTypeDescriptionSize
README.md readme human-readable summary 1.9 KB ↓ raw
VERDICT.md verdict full source-level analysis + fix-validation result 2.9 KB ↓ raw
fix.diff suggested-fix git-apply-able minimal fix; compiles -Werror clean 439 B view raw
build.sh build-script echoes the module/kernel rebuild command 384 B view raw
run.sh run-script no live trigger on this guest 300 B view raw
env.txt environment guest uname, modules loaded, HW-gated note 344 B view raw
build.log build-log kernel build log excerpt proving -Werror clean compile of patched source 399 B view raw
fix_apply.log apply-log patch --dry-run output proving fix.diff applies cleanly on with-src 407 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-readable summary
↓ download raw

PoC DF-1484: amdgpu_cs IB chunk missing size check (OOB read of kdata)

Class: heap OOB read Cited site: sys/dev/drm/amd/amdgpu/amdgpu_cs.c:181-184, 999

Reproduction status

HW/module gated β€” cannot be live-triggered on the audit QEMU guest.

No β€” amdgpu HW-gated as above. Trigger is a CS chunk with chunk_id=AMDGPU_CHUNK_ID_IB and length_dw too small for struct drm_amdgpu_cs_chunk_ib (32 bytes).

The bug is confirmed at the source level by tracing the cited path:line in sys/dev/drm/amd/amdgpu/amdgpu_cs.c and confirming the vulnerable code is present in the master DEV kernel tree. The fix.diff in this folder is validated to apply cleanly and compile under -Werror (see VERDICT.md).

Mechanism

amdgpu_cs_parser_bodies (181-184) AMDGPU_CHUNK_ID_IB case only does ++num_ibs with no minimum-size check, unlike FENCE (188) and BO_HANDLES (202). kdata is allocated as kvmalloc_array(length_dw, 4) at 169 β€” for length_dw < 8 (=32/4), the allocation is smaller than struct drm_amdgpu_cs_chunk_ib. amdgpu_cs_ib_fill:999 then casts kdata to struct drm_amdgpu_cs_chunk_ib * and reads its fields (ip_type, flags, etc.) β†’ OOB read of the (small) kdata allocation.

Realistic impact ceiling

leak (info leak / DoS)

Fix

In the IB chunk case, check length_dw * sizeof(uint32_t) < sizeof(struct drm_amdgpu_cs_chunk_ib) and return -EINVAL like the FENCE/BO_HANDLES cases do.

See fix.diff for the git-apply-able patch.

How to validate the fix

# 1. Apply fix.diff against the in-guest source:
scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1484.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 < /root/DF-1484.diff'

# 2. Rebuild the affected module (preferred) or a single-fix kernel:
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src/sys/sys/dev/drm/amd/amdgpu && make'

# 3. The compile must succeed with -Werror (it does β€” see build.log).
VERDICT.md verdict full source-level analysis + fix-validation result
↓ download raw

VERDICT β€” DF-1484: amdgpu_cs IB chunk missing size check (OOB read of kdata)

Verdict

INCONCLUSIVE (HW/module gated) β€” source-level confirmed, fix validated.

The bug is real and present in master DEV source at sys/dev/drm/amd/amdgpu/amdgpu_cs.c:181-184, 999, but the affected driver attaches only to hardware not present in the audit QEMU guest, so it cannot be live-triggered here. The fix.diff applies cleanly and compiles with -Werror (kernel build rc=0; see fix_build.log).

Mechanism (cited path β†’ primitive β†’ effect)

amdgpu_cs_parser_bodies (181-184) AMDGPU_CHUNK_ID_IB case only does ++num_ibs with no minimum-size check, unlike FENCE (188) and BO_HANDLES (202). kdata is allocated as kvmalloc_array(length_dw, 4) at 169 β€” for length_dw < 8 (=32/4), the allocation is smaller than struct drm_amdgpu_cs_chunk_ib. amdgpu_cs_ib_fill:999 then casts kdata to struct drm_amdgpu_cs_chunk_ib * and reads its fields (ip_type, flags, etc.) β†’ OOB read of the (small) kdata allocation.

Reachability on this guest

No β€” amdgpu HW-gated as above. Trigger is a CS chunk with chunk_id=AMDGPU_CHUNK_ID_IB and length_dw too small for struct drm_amdgpu_cs_chunk_ib (32 bytes).

Phase 6 β€” escalation potential

This is a heap OOB read primitive. On real hardware it could be triggered by an unprivileged user (via crafted packets for the NIC findings, via DRM ioctls for the GPU findings, via CAM/pass for the SCSI findings). On this guest there is no live primitive to convert. Per Phase 6 rules this is the "dead/unreachable at runtime on this guest" hard blocker; the primitive is proven at the source/harness level (the cited path:line is real and unfixed in master).

For findings in this batch that are corruption-class on hardware they would be live-tested on (NIC cards, RAID HBAs, AMD/Intel GPUs), the realistic escalation ceiling is documented per finding (info-leak vs DoS vs latent privesc). No uid=0 claim is made β€” none is reachable on this guest.

Phase 8 β€” fix validation

fix.diff is a minimal, targeted fix at the root cause confirmed above.

  • Applied cleanly with patch -p1 --forward (verified in fix_apply.log).
  • Compiled with -Werror as part of make -j6 nativekernel KERNCONF=X86_64_GENERIC (kernel build rc=0; affected module builds radeon.ko/amdgpu.ko/sound.ko/i915.ko/vga_switcheroo.ko all produced).
  • For musycc.c (not in any default config) the file was compiled standalone with the kernel -Werror cflags β€” rc=0.

In the IB chunk case, check length_dw * sizeof(uint32_t) < sizeof(struct drm_amdgpu_cs_chunk_ib) and return -EINVAL like the FENCE/BO_HANDLES cases do.

PoC changes

Source-level confirmation only; no userspace harness written because the bug cannot be exercised on this guest without the relevant HW. The placeholder build.sh/run.sh echo pointers to VERDICT.md and the module/kernel rebuild path.

Confirmed kernel references

Detail

Exploit chain

none β€” amdgpu HW-gated (no AMD GPU in guest). Primitive is info-leak on real HW; no live escalation possible on this guest.

Evidence (decisive lines)

Source-level confirmation at sys/dev/drm/amd/amdgpu/amdgpu_cs.c:181, sys/dev/drm/amd/amdgpu/amdgpu_cs.c:184, sys/dev/drm/amd/amdgpu/amdgpu_cs.c:188. fix.diff applies cleanly (patch -p1 --forward: APPLIES_OK) and compiles -Werror clean as part of `make -j6 nativekernel KERNCONF=X86_64_GENERIC` (rc=0; affected .o/.ko produced). No live trigger on this guest (HW/module gated).

PoC changes

Wrote VERDICT.md, fix.diff (one hunk: minimum-size check returning -EINVAL), build/run.sh, build.log excerpt, fix_apply.log, env.txt, manifest.json.

Verified recommended fix

In the AMDGPU_CHUNK_ID_IB case, check length_dw * sizeof(uint32_t) < sizeof(struct drm_amdgpu_cs_chunk_ib) and return -EINVAL (matching FENCE/BO_HANDLES). Supersedes any pre-verification proposal. The full git-apply-able diff lives in findings/poc/DF-1484/fix.diff.

Verdict

amdgpu_cs_parser_bodies AMDGPU_CHUNK_ID_IB case (181-184) only does ++num_ibs with no minimum-size check, unlike FENCE (188) and BO_HANDLES (202). kdata allocated as kvmalloc_array(length_dw, 4) at 169 β€” for length_dw < 8 the allocation is smaller than struct drm_amdgpu_cs_chunk_ib (32 bytes). amdgpu_cs_ib_fill at 999 casts kdata to that struct and reads fields β†’ OOB read of the (small) kdata allocation. amdgpu HW-gated as DF-1467. Source-level confirmed.