amdgpu_fill_buffer uint32_t truncation skips VRAM clear for nodes>=4GiB (cross-user VRAM info leak)
Summary
amdgpu_fill_buffer at amdgpu_ttm.c:2183/2211: uint32_t byte_count=mm_node->size<<PAGE_SHIFT. mm_node->size is u64. For node size>=4GiB: byte_count truncates to 0 -> DIV_ROUND_UP(0,max_bytes)=0 loops -> NO clear performed. AMDGPU_GEM_CREATE_VRAM_CLEARED flag set by unprivileged render client (amdgpu_gem.c:226) is silently defeated. Freshly allocated VRAM contains stale contents of previously freed BOs. On shared GPU: cross-user disclosure of render frames/textures/crypto via SDMA copy to GTT+mmap readback. Fix: use uint64_t byte_count, min_t(uint64_t,...) in inner loop.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1332 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger-source | function-level harness: VRAM clear skipped via uint32 byte_count truncation | 1.5 KB | view raw |
| fix.diff | suggested-fix | git-apply-able diff that adds the guard verified at the function level | 1.4 KB | view raw |
| build.sh | build-script | exact build: cc -O2 -Wall -o trigger trigger.c | 125 B | view raw |
| run.sh | run-script | exact run: ./trigger | 111 B | view raw |
| run.log | run-log | decisive harness output BEFORE-FIX + AFTER-FIX | 314 B | view raw |
| fix_build.log | build-log | single batched patched-kernel build (rc=0); proves all 15 fixes compile | 5.6 MB | β download |
| env.txt | environment | uname, guest cc version, patch list | 500 B | view raw |
| VERDICT.md | verdict | narrative analysis: mechanism, why not live, fix | 2.1 KB | β raw |
| README.md | readme | human-facing reproduce instructions | 2.1 KB | β 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 |
DF-1332 β amdgpu_fill_buffer uint32 truncation skips VRAM clear
Summary
Make byte_count uint64_t in both while loops; ensure num_loops > 0 and cur_size_in_bytes is computed from the untruncated value.
How to reproduce
This bug lives in a device driver not reachable from the booted QEMU guest as
an unprivileged user (maxx) because the required hardware is absent (AMD GPU,
RAID HBA, sound PCI, AMD SCSI) or the trigger requires a malicious hypervisor
(virtio_net, virtio_scsi). The bug is reproduced at the function level by
porting the cited code path into a userspace harness that drives it with the
attacker-controlled inputs the original code fails to validate.
Build
cc -O2 -Wall -o trigger trigger.c
Run
./trigger
Expected
- BEFORE-FIX section shows the bug signature (SIGFPE for div-by-zero, OOB index report for overflows, wraparound count for underflows, over-read length for info leaks).
- AFTER-FIX section shows the guard from
fix.diffcleanly rejecting the attacker input.
The same harness was compiled and run on the patched single-fix kernel
(DragonFly 6.5-DEVELOPMENT #1) β output is identical because the harness
intentionally demonstrates both the unpatched and patched function logic side
by side, and the userspace behavior of those branches is independent of the
kernel. The patched kernel build (fix_build.log) confirms all 15 fix.diffs
compile cleanly in the real kernel / module context.
Impact classification
leak β gated by absent hardware / malicious-hypervisor precondition on
this guest; live trigger from maxx is not possible. See VERDICT.md for
the threat-model analysis.
Files
trigger.cβ function-level harness porting the cited code path.fix.diffβ git-apply-able unified diff againstsys/.build.sh/run.shβ exact repro commands.run.logβ decisive harness output (BEFORE-FIX + AFTER-FIX).fix_build.logβ patched kernel build log (proves all 15 fixes compile).VERDICT.mdβ full narrative analysis.manifest.jsonβ machine-readable catalog.
Host has no gcc; harnesses built in guest as maxx with cc (DragonFly gcc 8.3).
DF-1332 β VERDICT
REPRODUCED at the function level (impact: leak).
Mechanism
amdgpu_fill_buffer() at amdgpu_ttm.c:2183 declares 'uint32_t byte_count = mm_node->size << PAGE_SHIFT'. For an mm_node with size >= 2^20 (4 GiB of pages), the shift truncates to 0. DIV_ROUND_UP(0, max_bytes) = 0, so num_loops stays 0 and the SDMA fill never executes. The AMDGPU_GEM_CREATE_VRAM_CLEARED flag (set by unprivileged render clients via amdgpu_gem.c:226) is silently defeated: freshly allocated VRAM contains the stale contents of previously freed buffer objects. On a shared GPU this is cross-user disclosure of render frames, textures, or crypto material.
Why not live-reproduced on the QEMU guest
AMD GPU absent from QEMU guest. The amdgpu module loads only on matching HW. The bug fires at BO-allocation / VRAM-clear time when an unprivileged render client requests a large BO, which requires a real AMD GPU.
Recommended fix
Make byte_count uint64_t in both while-loops of amdgpu_fill_buffer. Compute num_loops and cur_size_in_bytes from the untruncated value so that the SDMA fill runs for all bytes (using chunked max_bytes writes inside the inner while).
Kernel references (confirmed during verification)
- sys/dev/drm/amd/amdgpu/amdgpu_ttm.c:2183 (uint32_t byte_count = mm_node->size << PAGE_SHIFT)
- sys/dev/drm/amd/amdgpu/amdgpu_ttm.c:2185 (DIV_ROUND_UP(byte_count, max_bytes) = 0)
- sys/dev/drm/amd/amdgpu/amdgpu_ttm.c:2211 (second occurrence; same truncation in the fill loop)
Build/run
- Build harness:
cc -O2 -Wall -o trigger trigger.c - Run harness:
./trigger - Apply fix:
cd /usr/src && patch -p1 < fix.diff - Build single-fix kernel:
make -j6 nativekernel KERNCONF=X86_64_GENERIC(validated β seefix_build.log; all 15 fixes compile cleanly in one batched build, rc=0).
Tested kernels
- baseline:
DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 - patched :
DragonFly 6.5-DEVELOPMENT #1: Mon Jul 20 21:51:01 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64
Fix verification
fixedVALIDATED via batched single-fix kernel build: amdgpu_ttm.c compiles cleanly with the fix (amdgpu module rc=0). Harness BEFORE-FIX shows num_loops=0 (truncation); AFTER-FIX shows num_loops=1024.
baseline #0 BEFORE-FIX: byte_count truncates to 0x00000000, num_loops=0 -> VRAM clear SKIPPED. patched #1 cc6aa06b AFTER-FIX: byte_count uint64_t, num_loops=1024; amdgpu module rc=0.
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
- _
- t
- t
- m
- .
- c
- :
- 2
- 1
- 8
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- m
- d
- g
- p
- u
- _
- t
- t
- m
- .
- c
- :
- 2
- 1
- 8
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- m
- d
- g
- p
- u
- _
- t
- t
- m
- .
- c
- :
- 2
- 2
- 1
- 1
Detail
Exploit chain
none β info leak via uncleared VRAM. No write primitive; the bug defeats a clear-on-alloc guarantee, exposing previous BO contents. Cross-user disclosure on shared GPU.
Evidence (decisive lines)
BEFORE-FIX (amdgpu_fill.c): byte_count = mm_node->size<<PAGE_SHIFT truncates to 0x00000000, num_loops=0 -> VRAM clear SKIPPED; freshly allocated VRAM contains previous BO contents -> cross-user leak. AFTER-FIX: byte_count is uint64_t, num_loops=1024 -> SDMA fill runs correctly. Patched-kernel build rc=0. See findings/poc/DF-1332/run.log and fix_build.log.
PoC changes
Wrote trigger.c (amdgpu_fill.c) harness demonstrating the truncation for mm_node->size = 0x100000 (4 GiB pages).
Verified recommended fix
fix.diff changes byte_count to uint64_t in both while-loops and computes num_loops/cur_size_in_bytes from the untruncated value. Matches finding proposal. Full diff in findings/poc/DF-1332/fix.diff.
Verdict
REPRODUCED at function level. amdgpu_fill_buffer() at amdgpu_ttm.c:2183 declares 'uint32_t byte_count = mm_node->size << PAGE_SHIFT'. For mm_node->size >= 2^20 (4 GiB of pages), the shift truncates to 0, so DIV_ROUND_UP(0, max_bytes)=0 and num_loops stays 0 -> SDMA fill never executes -> AMDGPU_GEM_CREATE_VRAM_CLEARED flag silently defeated -> freshly allocated VRAM contains stale previous-BO contents (cross-user render frame/texture/crypto disclosure). Harness amdgpu_fill.c demonstrates the truncation to 0 and the post-fix uint64_t path computing num_loops=1024. AMD GPU absent from guest.
No comments yet.