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 << -1is undefined behavior (shift count negative />= widthafter 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.
Recommended fix
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
sys/dev/drm/amd/amdgpu/amdgpu_gfx.c:211-215β the off-by-one loopsys/dev/drm/include/linux/bitops.h:311-313βtest_bitexpansionsys/dev/drm/amd/amdgpu/amdgpu_gfx.c:222βqueue != 0filter that masks the bug on x86-64
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1996 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_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/amd/amdgpu/amdgpu_gfx.c:211 source-confirmed.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- m
- d
- g
- p
- u
- _
- g
- f
- x
- .
- c
- :
- 2
- 1
- 1
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 >.
No comments yet.