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

Off-by-one in amdgpu_gfx_kiq_acquire loop invokes test_bit / bit_to_queue with bit = -1 (undefined shift)

  • File: sys/dev/drm/amd/amdgpu/amdgpu_gfx.c
  • Lines: 211–215
  • Severity: Low
  • CVSS 3.1: CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U:C:N/I:N/A:L
  • CWE: CWE-197 Undefined Shift, CWE-835 Off-by-one loop termination
  • Confidence: likely
  • Status: new

Summary

amdgpu_gfx_kiq_acquire scans queue bits downward with while (queue_bit-- >= 0).

Because of the post-decrement, when the loop variable reaches 0 the condition 0 >= 0 is still true, queue_bit becomes -1, and the body executes test_bit(-1, queue_bitmap) and amdgpu_gfx_bit_to_queue(adev, -1, ...).

The correct bound is > 0. The -1 iteration is undefined behavior and, on a non-x86 or aggressive-optimization build, could test a wrong bitmap bit; on x86-64 the shift masks to 63 and the queue!=0 filter then rejects the garbage, so the function falls through to -EINVAL with no memory-safety violation.

Root cause

amdgpu_gfx.c:211: while (queue_bit-- >= 0).

Post-decrement means the body sees queue_bit one less than the tested value; when the tested value is 0 (true under >= 0), the body runs with queue_bit = -1.

  • Line 212: test_bit(queue_bit, ...) and line 215: amdgpu_gfx_bit_to_queue(adev, queue_bit, ...) then run with -1.

test_bit expands (sys/dev/drm/include/linux/bitops.h:311-313) to atomic_load_acq_long(&((volatile long*)(a))[(i)/NBLONG]) & (1LU << ((i) % NBLONG)).

With i = -1 (int):

  • (-1)/64 = 0 β†’ the word index stays in-bounds
  • (-1) % 64 = -1 (C99: sign of dividend)
  • 1LU << -1 is undefined behavior (shift count negative / >= width after unsigned conversion)

bit_to_queue (49-58) then computes queue = -1 % num_queue_per_pipe = -1, and the filter (mec==1 && pipe>1) || queue != 0 at line 222 rejects queue=-1, so the loop continues to the next check -1 >= 0 which is false and the function returns -EINVAL at line 233.

The intent was clearly to scan bits [product-1 .. 0]; > 0 achieves that (body sees 0 when tested value is 1) and eliminates the -1 iteration.

Threat model

amdgpu_gfx_kiq_acquire is reachable only from amdgpu_gfx_kiq_init_ring (line 254), which runs once during GPU IP-block init (gfx_v{7,8,9}_0.c sw_init).

It is NOT reachable from any sysfs attribute, ioctl, or unprivileged render path. The queue_bitmap is populated earlier in the same init by amdgpu_gfx_compute_queue_acquire, which sets only mec==0 bits, so mec==1/pipe{0,1}/queue0 candidates are free and the scan returns 0 long before reaching bit 0; the -1 iteration is reached only if every candidate bit is either set or filtered out, a state not producible by an unprivileged user.

Worst realistic impact: local DoS (GPU/KIQ init failure) under a state an attacker cannot create.

No OOB read (word index stays 0) and no corruption on x86-64.

Proof of concept

No meaningful exploit. To even observe the UB you must be inside the kernel during driver probe and force every KIQ-candidate queue bit to be set/filtered before amdgpu_gfx_kiq_init_ring runs β€” not controllable from userspace.

A reproducer would require patching amdgpu_gfx_compute_queue_acquire to set all 64 bits, reloading amdgpu, and watching dmesg for "Failed to find a queue for KIQ" (the -EINVAL path) while a UBSAN / UBSAN_SHIFT-instrumented kernel logs "shift exponent -1" at amdgpu_gfx.c:212.

Success criterion: UBSAN report / KIQ init failure; there is no memory-corruption or privilege-escalation outcome to demonstrate.

Change the loop bound from >= 0 to > 0 so the body never executes with queue_bit = -1. This still tests bit 0 (when the tested value is 1) and stops one step earlier, eliminating the undefined 1LU << -1 shift and the spurious bit_to_queue(-1) call.

--- a/sys/dev/drm/amd/amdgpu/amdgpu_gfx.c
+++ b/sys/dev/drm/amd/amdgpu/amdgpu_gfx.c
@@ -208,7 +208,7 @@ static int amdgpu_gfx_kiq_acquire(struct amdgpu_device *adev,
            * adev->gfx.mec.num_pipe_per_mec
            * adev->gfx.mec.num_queue_per_pipe;

-   while (queue_bit-- >= 0) {
+   while (queue_bit-- > 0) {
        if (test_bit(queue_bit, adev->gfx.mec.queue_bitmap))
            continue;

Additionally, as defense-in-depth, bound queue_bit against AMDGPU_MAX_COMPUTE_QUEUES before the loop (queue_bit = min(queue_bit, AMDGPU_MAX_COMPUTE_QUEUES)) so a future ASIC whose num_mec * num_pipe_per_mec * num_queue_per_pipe product exceeds 128 cannot drive test_bit out of the 128-bit queue_bitmap; today the product is <=64 for all gfx_v7/8/9 asics.

References

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1996 Β· 5 files
FileTypeDescriptionSize
VERDICT.md verdict Source verification narrative 1.1 KB ↓ raw
fix.diff suggested-fix Fix: Change >= to > in loop condition. 405 B 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-1996 - Source Verification

Verdict: REPRODUCED (source-only confirmation)

Finding: sys/dev/drm/amd/amdgpu/amdgpu_gfx.c:211

Mechanism: amdgpu_gfx_kiq_acquire while(queue_bit-- >= 0): when queue_bit==0, condition true, body executes with queue_bit=-1. test_bit(-1,...) β†’ 1UL<<(-1) undefined behavior (negative shift).

Hardware dependency: Requires AMD GPU with KIQ (GFX compute).

Fix: Change >= to > in loop condition.

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/amd/amdgpu/amdgpu_gfx.c:211 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/amd/amdgpu/amdgpu_gfx.c:211. HW-gated (no HW in QEMU). Fix compiles in batch build rc=0.

PoC changes

Evidence pack: VERDICT.md, fix.diff, manifest.json. Fix: while(queue_bit-->=0): -1 shift UB. Change >= to >.

Verified recommended fix

See fix.diff. while(queue_bit-->=0): -1 shift UB. Change >= to >.

Verdict

REPRODUCED (source-only). sys/dev/drm/amd/amdgpu/amdgpu_gfx.c:211: while(queue_bit-->=0): -1 shift UB. Change >= to >.