radeon_ring_restore() leaks the kvmalloc'd ring-backup buffer on radeon_ring_lock() failure in the GPU-reset path (repeatable kernel heap exhaustion)
Summary
On lock failure restore returns without kvfree(data); the backup (up to ring_size bytes) was made during GPU reset and the only caller ignores the return, freeing only on the failed-reset branch. The failure is deterministic kernel state: restore runs with needs_reset still true so the fence wait returns -EDEADLK/-ENOENT whenever the aligned backup request equals ring_size/4 - exactly where a ring saturated at hang time lands. Each unpriv-triggered deadlock/reset cycle (fill ring, deadlock, 10s lockup timeout) leaks 64KiB-1MiB -> sustained heap exhaustion. VERIFIED at logic level with verbatim-function harness (1000/1000 cycles leak 65532B each in both error modes; fix: 0/1000 identical returns). Never fixed upstream (v4.15 and v6.1 identical). kvfree-before-return fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4820 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.7 KB | β raw | |
| VERDICT.md | β | 5.4 KB | β raw | |
| harness_env.h | β | 3.8 KB | view raw | |
| harness_main.c | β | 3.2 KB | view raw | |
| body_fallback.inc | β | 405 B | β download | |
| ring_restore_leak.c | β | 7.5 KB | view raw | |
| build.sh | β | 85 B | view raw | |
| run.sh | β | 89 B | view raw | |
| build.log | β | 1.3 KB | view raw | |
| run.log | β | 457.4 KB | β download | |
| run.2.log | β | 842 B | view raw | |
| env.txt | β | 612 B | view raw | |
| fix.diff | β | 361 B | view raw | |
| manifest.json | β | 1013 B | view raw | |
| verdict.json | β | 4.3 KB | view raw |
DF-4820 β radeon_ring_restore() leaks the kvmalloc'd ring-backup buffer on radeon_ring_lock() failure
File: sys/dev/drm/radeon/radeon_ring.c:341-361 (sink: :350-352)
Severity: Medium (kernel heap exhaustion DoS, kernleak bucket) Β· Confidence: likely
Upstream: bug byte-identical in Linux v4.15 and v6.1 (never fixed upstream).
Build
./build.sh # baseline: stock kernel body (verbatim extraction) ./build.sh fixed # + one-line fix from fix.diff
build.sh extracts lines 341-361 (the function under test) VERBATIM from
the kernel tree (falls back to the packed body_fallback.inc, extracted
from the audited tree) and compiles it inside harness_env.h's mock
environment (DragonFly cc, userspace).
Run
./run.sh
Expected output (decisive lines)
================ BASELINE (stock kernel body) ================ injected radeon_ring_lock error: -35 (-EDEADLK (needs_reset still true during restore)) LIVE (leaked) blocks: 1000 (65532000 bytes = 63996 KiB) RESULT: LEAK REPRODUCED β every failed restore leaks its 65532-byte kvmalloc backup buffer injected radeon_ring_lock error: -2 (-ENOENT (no pending fences)) LIVE (leaked) blocks: 1000 (65532000 bytes = 63996 KiB) RESULT: LEAK REPRODUCED ... ================ FIXED (kvfree on error path) ================ LIVE (leaked) blocks: 0 (0 bytes = 0 KiB) RESULT: FIXED build leaks nothing β fix VALIDATED (both modes)
Every simulated GPU-reset cycle in which the restore's ring allocation
fails leaks one ring_size-byte (here 64 KiB, typical GFX rings 1 MiB)
kvmalloc block. See VERDICT.md for why -EDEADLK/-ENOENT is the
deterministic kernel condition at that call site, not an invention.
DF-4820 VERDICT β reproduced (logic-level), fix validated
Bottom line
radeon_ring_restore() (sys/dev/drm/radeon/radeon_ring.c:341-361) returns
early at :350-352 when radeon_ring_lock() fails, without
kvfree(data). data is a kvmalloc_array() buffer (up to ring_size
bytes β 64 KiBβ¦1 MiB) allocated by radeon_ring_backup() (:317) during
GPU reset. The only caller, radeon_gpu_reset()
(sys/dev/drm/radeon/radeon_device.c:1879-1887), ignores the return value
(:1881-1882) and only kfrees the buffer on the failed-reset branch
(:1883-1886). Result: every reset whose restore-side allocation fails leaks
one ring-backup buffer. The bug is byte-identical in Linux v4.15 and v6.1.
Why the allocation failure is the kernel's real state, not a mock invention
Restore runs in radeon_gpu_reset() after radeon_resume(rdev)
(:1874) but before rdev->needs_reset = false is executed (:1921-1922).
At that point:
radeon_ring_lock()βradeon_ring_alloc()(radeon_ring.c:104-127). The post-resume ring is fresh/empty (asic CP resume zeroesring->wptr, e.g. cik.c:4104), soring_free_dwmaxes atring_size/4.radeon_ring_backup()can hand back up toring_size/4 - 1dwords (rptr==wptr yields 0; a ring saturated under load yields exactlyring_size/4 - 1βradeon_ring_allocalways leaves β₯ 1 spare dw). Withalign_mask = 0xf, any size >ring_size/4 - 16aligns up to exactlyring_size/4(:114), which is unsatisfiable on the fresh ring (ndw < free_dwneedsfree_dw > ring_size/4, impossible), so the loop falls intoradeon_fence_wait_next()(:120).radeon_fence_wait_next()(radeon_fence.c:644-659) has two exits: - -EDEADLK:radeon_fence_wait_seq_timeout()(radeon_fence.c:511-517) has|| rdev->needs_resetin its wait condition and converts it to-EDEADLKβ andneeds_resetis still true during the restore loop (cleared only at radeon_device.c:1921-1922, ~40 lines later). - -ENOENT: when no fences are pending (radeon_fence.c:649-654). Either wayradeon_ring_alloc()errors out β restore early-returns β leak.
So a GPU reset of a saturated ring deterministically leaks. Saturated =
the GPU hung while β₯ ring_size/4 - 15 dwords were in flight β exactly
the state a deadlocked GPU under continuous command-submission load
produces (each CS ioctl pushes kernel-counted packets to the ring; the
alloc wait-loop only stops submissions once the ring is ~full).
Threat model (unprivileged local)
An unprivileged local user with /dev/dri/cardN access (console user,
group) can, via the CS ioctl path:
- fill the ring with in-flight work (submit continuously), and
- deadlock/hang the GPU (e.g. user-requested cross-ring semaphores in CS
packets can deadlock two rings β the documented radeon hang vector that
the lockup detector then resets).
Each lockupβreset cycle with a saturated ring leaks one ring_size-byte
kernel heap block; cycles are bounded by radeon_lockup_timeout (default
10 s), i.e. a sustained ~ring_size/10s/ring leak β kernel heap exhaustion
β memory-pressure DoS. Not an info leak: the buffer never leaves the
kernel.
What was run (guest: DragonFly 6.5-DEVELOPMENT, cc 8.3, no GPU)
The QEMU guest has no radeon GPU, so the trigger chain (real CS flood +
GPU deadlock + lockup reset) cannot execute there. The verification
compiles the verbatim kernel body (extracted mechanically from
sys/dev/drm/radeon/radeon_ring.c:341-361 β the same bytes, see
body_fallback.inc / build.sh) inside a mock environment that implements
exactly the kernel conditions derived above (mock radeon_ring_lock
models radeon_ring_alloc's fresh-ring decision and injects the two
kernel-proven errors; kvmalloc_array/kvfree are instrumented).
Results (full untrimmed run.log, stability run.2.log):
- BASELINE: 1000/1000 restore calls leaked (63 996 KiB) with -EDEADLK;
1000/1000 with -ENOENT. Re-run at 500 iterations: same result.
- FIXED (fix.diff: kvfree(data) on the error path): 0 leaked, both
modes, error returns unchanged.
Classification honesty: this is a logic-level reproduction of the exact
shipped function body under kernel-proven conditions, not an end-to-end
hardware trigger. The code defect is certain; the unprivileged end-to-end
chain requires physical radeon hardware (documented above, not executable
on this guest). Impact ceiling: kernel heap exhaustion DoS (dos), no
privesc primitive.
Cross-reference (same family, other file)
radeon_device.c:1885 uses kfree() on the kvmalloc'd buffer (failed-reset
branch) instead of kvfree() β a latent invalid-free if kmalloc ever falls
back to vmalloc (only under severe memory pressure, sizes β€ 1 MiB normally
kmalloc). radeon_device.c is outside this audit's file scope (already
audited pass 1); noted here for the maintainers' fix.
Fix
fix.diff (never applied to sys/):
r = radeon_ring_lock(rdev, ring, size);
- if (r)
+ if (r) {
+ kvfree(data);
return r;
+ }
Fix validated: baseline leaks 1000/1000 per mode; patched body leaks 0/1000 with identical error returns.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff adds kvfree(data) before the error return in radeon_ring_restore. Same verbatim body, one-line change: baseline leaks 1000/1000 blocks per error mode (63996 KiB), patched leaks 0/1000 with identical error returns β leak eliminated, behavior otherwise unchanged. Kernel-rebuild validation not performed: the guest has no radeon GPU and no kernel tree on this snapshot; the harness compiles the verbatim kernel body itself, so the fix is exercised against the exact shipped code.
run.log FIXED sections: 'allocations freed: 1000 ... LIVE (leaked) blocks: 0 ... RESULT: FIXED build leaks nothing β fix VALIDATED' (both -EDEADLK and -ENOENT modes); fix.diff
Confirmed kernel references
Detail
Exploit chain
unpriv /dev/dri user: (1) sustain CS submissions to keep ring near-saturated with in-flight dwords; (2) deadlock GPU (user-requested cross-ring semaphores in CS) -> lockup detector (radeon_lockup_timeout, ~10s); (3) radeon_gpu_reset: backup saves ~ring_size/4-1 dwords (kvmalloc ring_size bytes); (4) hw reset + resume succeed (fresh empty ring); (5) restore's alloc aligns to ring_size/4 -> fence wait -> -EDEADLK (needs_reset still set) -> early return without kvfree; caller ignores rc. Repeat per cycle: leak ~ring_size bytes / ~10 s / ring -> kernel heap exhaustion DoS.
Evidence (decisive lines)
run.log (baseline: 'LIVE (leaked) blocks: 1000 (65532000 bytes = 63996 KiB)' x2 modes, then FIXED: 'LIVE ... 0 ... fix VALIDATED' x2); run.2.log (500-iter stability); build.log (verbatim body extraction + compile); VERDICT.md (kernel-condition derivation with path:line); fix.diff
PoC changes
n/a β finding and PoC authored in this run; harness build script extracts the function body verbatim from the kernel tree rather than transcribing it
Verified recommended fix
Free the backup buffer on the error path: brace the if (r) in radeon_ring_restore() and kvfree(data) before returning (see fix.diff).
Verdict
radeon_ring_restore() returns early at radeon_ring.c:350-352 when radeon_ring_lock() fails, leaking the kvmalloc'd ring-backup buffer; the only caller (radeon_gpu_reset, radeon_device.c:1881-1882) ignores the return value and never frees it on the reset-succeeded branch. During the restore loop needs_reset is still true (cleared later at radeon_device.c:1921-1922), so the fence wait inside radeon_ring_alloc returns -EDEADLK (radeon_fence.c:516-517) or -ENOENT (radeon_fence.c:649-654), making the failure deterministic whenever the ring was saturated (backup size within align_mask of ring_size/4 aligns up to exactly ring_size/4, unsatisfiable on the fresh post-resume ring). Verified on the DragonFly guest by compiling the VERBATIM kernel function body inside a mock environment reproducing these kernel-proven conditions: 1000/1000 simulated reset cycles leak their 64 KiB buffer in both error modes (full untrimmed run.log), 0/1000 with fix.diff applied. Full end-to-end trigger (CS flood + GPU deadlock + lockup reset) requires physical radeon hardware, absent from the QEMU guest; impact ceiling is repeatable kernel heap exhaustion (local DoS, kernleak bucket), not an info leak and not a privesc primitive.
No comments yet.