32-bit wraparound family in evergreen CS surface-size validation ((slice+1)*64 wrap β nby=0/layer_size=0; layer_size*mslice u32 product wrap; <<8 base-offset truncation vs 40-bit GPU addresses; htile nbx*nby*4 wrap; streamout <<8 and *4 wrapped before the u64 check) β CS validator bypass β GPU OOB read/write past BO, cross-process GPU memory corruption
Summary
CB/DB SLICE registers are stored raw and the validators compute nby = ((slice+1)*64)/nbx in u32: slice=0xffffffff wraps (slice+1) to 0 -> nby=0 -> layer_size=0 -> every offset>bo_size check passes for any BO, while the GPU honours the masked SLICE_TILE_MAX=0x3fff field and renders/samples 16384 tiles past the relocated BO. The same u32 truncation defeats layer_size*mslice products, the x<<8 base-offset shifts computed in u32 against 40-bit GPU addressing, htile roundup(nbx*nby*4), and the streamout stores which wrap in u32 before the correct-looking u64 check. Unprivileged DRM-authenticated local user on any pre-SI radeon asic (non-VM CS): renders, texture fetches and streamout read/write GPU memory past the attacker's BOs into other processes' GTT/VRAM allocations (incl. kernel-managed ring/IB pool) - cross-process corruption and disclosure. Identical class to DF-1340 (r600, High) and DF-3765/3766 (r100). 38-hunk 64-bit widening fix mirroring the r600 upstream hardening family.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3838 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | file | 2.1 KB | β raw | |
| VERDICT.md | file | 4.0 KB | β raw | |
| build.log | file | 82 B | view raw | |
| build.sh | file | 82 B | view raw | |
| env.txt | file | 286 B | view raw | |
| fix.diff | file | 9.2 KB | view raw | |
| manifest.json | file | 921 B | view raw | |
| run.log | file | 959 B | view raw | |
| run.sh | file | 105 B | view raw | |
| trigger.c | file | 4.7 KB | view raw | |
| verdict.json | file | 3.5 KB | view raw |
DF-3838 β 32-bit wraparound family in the evergreen CS surface-size validator (evergreen_cs_track_validate_{cb,stencil,depth,texture} + htile + streamout tracking) -> validator bypass -> GPU out-of-bounds read/write past the BO (cross-process GPU memory corruption). High.
WHAT TO LOOK AT trigger.c programs CB_COLOR0_SLICE = 0xffffffff with a valid colour format and a draw β on a vulnerable kernel the validator computes nby = ((0xffffffff+1)*64)/nbx == 0 in u32, derives layer_size 0, and accepts ANY (even 1-page) BO for the target, while the GPU honours SLICE_TILE_MAX = 0x3fff and renders 16384 tiles past it. fix.diff u64 math + HW-field masking for every wrap site. verdict.json status inconclusive (no GPU on guest), source-level certain.
SITES (evergreen_cs.c)
(slice+1) wrap, raw register stored at :1208/:1416/:1425:
:404 cb nby = ((slice + 1) * 64) / surf.nbx
:571 stencil (db_depth_slice raw)
:668 depth (db_depth_slice raw)
-> nby=0 -> layer_size=0 -> offset check at :442/:616/:635/:714/:730
trivially passes.
u32 product wraps layer_size * mslice / * depth:
:442, :616, :635, :714, :730, :846-848, :906-908
x << 8 computed in u32 (GPU address is 40-bit):
:435 (cb_color_bo_offset), :610/:629 (stencil), :708/:724 (depth),
:787-788 (texture toffset/moffset)
htile roundup(nbx * nby * 4, ...) u32 product:
:547
streamout offsets wrapped BEFORE the u64 check at :948-950:
:1278 vgt_strmout_bo_offset[tmp] = value << 8 (u32 shift)
:1289 vgt_strmout_size[tmp] = value * 4 (u32 mult)
BUILD cc -O2 -o trigger trigger.c RUN ./trigger /dev/dri/card0 EXPECT On real pre-SI radeon HW (evergreen/barts/turks/caicos/cayman), with the CB_COLOR0_BASE reloc pointing at a 1-page BO, the CS is ACCEPTED by the validator (no "cb[%d] bo too small" in dmesg) and the draw writes far past the BO. On the audit QEMU guest: ENOENT.
FAMILY DF-1340 (r600_cs.c, same class, High), DF-3765/DF-3766 (r100.c). Distinct from DF-1245 (pitch div-by-zero in the same expression).
DF-3838 VERDICT β status: inconclusive (hardware-gated), source-level: CERTAIN
Attack chain (all evergreen_cs.c unless noted):
1. Unprivileged DRM-authenticated user submits a non-VM RADEON_CS on an
evergreen-family asic -> evergreen_cs_parse() -> evergreen_packet3_check()
-> evergreen_cs_handle_reg() records CB/DB/texture state into
struct evergreen_cs_track. The SLICE registers are stored RAW:
:1208 track->db_depth_slice = radeon_get_ib_value(p, idx);
:1416 track->cb_color_slice[tmp] = radeon_get_ib_value(p, idx);
(PITCH registers likewise raw, but their div-by-zero sibling is the
already-filed DF-1245.)
2. On the next draw (evergreen_cs_track_check at every
DRAW_INDEX/DISPATCH/INDIRECT packet), validate_cb/stencil/depth compute
surf.nby = ((slice + 1) * 64) / surf.nbx; (:404/:571/:668)
with slice a full attacker-chosen u32. slice = 0xffffffff wraps
(slice+1) to 0 -> nby = 0 -> layer_size 0 in every mode (linear:
:192/:207/:230; 2D tiled: mtile_ps = (mtile_pr * 0)/halign = :271).
3. All BO-size checks then degrade to bo_offset<<8 + 0 > bo_size:
:442-443 (cb read/write), :616/:635 (stencil r/w), :714/:730 (depth
r/w), :850 (texture), :910 (mip). They pass for any BO >= offset.
4. The GPU, however, masks CB_COLORn_SLICE/DB_DEPTH_SLICE to its hardware
field SLICE_TILE_MAX (bits [13:0]) β programming 0xffffffff gives the
maximum 0x3fff (16384-tile) surface height, so the color/depth engine
and texture sampler address far past the relocated BO β into other
processes' GTT/VRAM allocations (no GPUVM isolation on pre-SI non-VM
CS). That is cross-process GPU memory corruption: read (texture
fetch -> render to readable target) and write (render target /
streamout).
5. The same u32-truncation family defeats the remaining bounds:
- offset += surf.layer_size * mslice (:442 etc.) β u32u32 wraps;
e.g. stencil with db slice 0x01ffffff (no wrap there) gives
layer_size 0x80000000; mslice=2 wraps the product to 0.
- x << 8 (:435/:610/:629/:708/:724/:787) β computed in u32 while the
GPU address register is bits [39:8] of a 40-bit address; user offset
0x01000000 aliases to validator offset 0 but GPU offset 4 GB.
- htile roundup(nbxnby4) (:547) β u32 product wraps to a tiny size.
- streamout (:1278/:1289) β value << 8 and value * 4 wrap in u32
at store time; the u64 promotion at :948-950 is too late. GPU
streamout then writes up to (raw size)4 bytes at (raw base)<<8.
Why not reproduced: audit guest is virtio-only (no /dev/dri, no drm β run.log). The PoC compiles clean (build.log) and documents the exact register programming; full relocation plumbing (GEM bo + reloc chunk) needs libdrm on real hardware. The arithmetic, however, is fully determined at compile time of the validator: ((0xffffffff + 1) * 64) is 0 in u32 β there is no runtime condition that could rescue it.
Severity: High β matches DF-1340 (identical r600 class, filed High/certain, bucket memcorrupt): GPU-side OOB read/write across process boundaries from an unprivileged DRM client. Not CPU-kernel memory corruption on its own, but on pre-SI asics the GTT also hosts kernel-managed buffers (ring/IB pool), so a write primitive can corrupt command streams of other clients.
Fix: fix.diff β mask SLICE registers to the HW field, compute every surface size/offset product in 64-bit, keep streamout raw values and promote at check time. Structurally the same hardening Linux applied to r600 (commit 571043f6ccb6 family). Fix validation: impossible on this guest β fix_status not_applicable.
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
not_testableFix validation impossible on this guest: the vulnerable device (AMD radeon CS / Intel 2200BG) does not exist, so neither baseline nor patched kernel can exercise the path. fix.diff authored against the read-only sys/ tree, never applied.
fix.diff in this pack; structurally mirrors the bounds checks the same driver family already enforces elsewhere (radeon_cs_packet_parse fit check; iwn ring-index masking).
Confirmed kernel references
- sys/dev/drm/radeon/evergreen_cs.c:404
- sys/dev/drm/radeon/evergreen_cs.c:571
- sys/dev/drm/radeon/evergreen_cs.c:668
- sys/dev/drm/radeon/evergreen_cs.c:1416
- sys/dev/drm/radeon/evergreen_cs.c:1208
- sys/dev/drm/radeon/evergreen_cs.c:442
- sys/dev/drm/radeon/evergreen_cs.c:435
- sys/dev/drm/radeon/evergreen_cs.c:787
- sys/dev/drm/radeon/evergreen_cs.c:547
- sys/dev/drm/radeon/evergreen_cs.c:1278
- sys/dev/drm/radeon/evergreen_cs.c:1289
- sys/dev/drm/radeon/evergreen_cs.c:948
Detail
Evidence (decisive lines)
README.md (what to look at), VERDICT.md (line-by-line trace), trigger.c/mkbadfw.c, build.log (clean guest compile, gcc 8.3), run.log (honest ENOENT / generator RUN_OK), env.txt (guest has no DRI, no iwi NIC), fix.diff
PoC changes
PoC authored fresh for this finding (cross-file family seed): raw DRM ioctl ABI (cmd low byte 0x66 dispatches RADEON_CS on DF drm_ioctl.c:809), chunk structs and packet encodings verified against sys/dev/drm/include/uapi/drm/radeon_drm.h; iwi mkbadfw.c emits the v3 combined-image header per if_iwireg.h:161-166.
Verified recommended fix
Mask SLICE/PITCH registers to their HW fields, compute all surface sizes/offset products in 64-bit, store raw streamout values and widen at check time.
Verdict
Source-level certain, runtime inconclusive (no radeon GPU on the virtio-only guest; run.log shows ENOENT on /dev/dri/card0 and clean compile in build.log). The CB/DB SLICE registers are stored raw (:1208/:1416/:1425) and the validators compute nby = ((slice+1)64)/nbx in u32 (:404/:571/:668): slice=0xffffffff wraps to nby=0, layer_size=0 and every offset>bo_size check (:442/:616/:635/:714/:730/:850/:910) then passes for any BO while the GPU honours SLICE_TILE_MAX=0x3fff and renders/samples far past the relocated BO. Same u32-truncation defeats layer_sizemslice products, the <<8 base-offset shifts (GPU addresses are 40-bit), the htile roundup(nbxnby4) product, and the streamout <<8/*4 stores made before the u64 check at :948-950. Identical class to DF-1340 (r600, High/memcorrupt) and DF-3765/3766 (r100); distinct from DF-1245 (pitch div0).
No comments yet.