β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3838

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3838 Β· 11 files
FileTypeDescriptionSize
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
README.md file
↓ download 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).

VERDICT.md file
↓ download raw

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_testable
baseline no→ patch + rebuild →patched clean

Fix 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).
↓ fix.diffper-fix-DF-3838

Confirmed kernel references

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).