# 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 `kfree`s 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:

1. `radeon_ring_lock()` → `radeon_ring_alloc()` (radeon_ring.c:104-127).
   The post-resume ring is fresh/empty (asic CP resume zeroes
   `ring->wptr`, e.g. cik.c:4104), so `ring_free_dw` maxes at
   `ring_size/4`.
2. `radeon_ring_backup()` can hand back up to `ring_size/4 - 1` dwords
   (rptr==wptr yields 0; a ring saturated under load yields exactly
   `ring_size/4 - 1` — `radeon_ring_alloc` always leaves ≥ 1 spare dw).
   With `align_mask = 0xf`, any size > `ring_size/4 - 16` **aligns up to
   exactly `ring_size/4`** (:114), which is unsatisfiable on the fresh
   ring (`ndw < free_dw` needs `free_dw > ring_size/4`, impossible), so
   the loop falls into `radeon_fence_wait_next()` (:120).
3. `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_reset` in its wait condition and converts it to
     `-EDEADLK` — and `needs_reset` is *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 way `radeon_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/`):

```diff
 	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.
