radeon_cs_parser_init: fully attacker-controlled u32 num_chunks/length_dw drive unbounded wired GFP_KERNEL allocations β on DragonFly GFP_KERNEL is M_WAITOK without M_NULLOK, so failure is panic('malloc limit exceeded')/panic('kernel_map ran out of space') or an eternal unkillable vm_wait() loop (local kernel DoS); latent NULL-return path would additionally crash radeon_cs_parser_fini via parser->chunks[i]
Summary
Three kernel allocations are sized directly from unvalidated ioctl fields (kcalloc(num_chunks,8), kcalloc(num_chunks,24), kvmalloc_array(length_dw,4)). Upstream turns oversized requests into clean -ENOMEM; DF's GFP_KERNEL is M_WAITOK without M_NULLOK, so failure cannot be graceful: type-limit exhaustion panics, VA exhaustion panics, page shortage loops forever in vm_wait (no PCATCH) leaving an unkillable thread per ioctl. num_chunks ~40-50M: first alloc succeeds and the second panics; 0xFFFFFFFF: 32GB request hangs permanently. Auth'd DRM client, one ioctl. Mechanism reproduced live on the guest via a KLD with the identical allocation pattern (panic at alloc #13). Bound-num_chunks-at-64 + chunk-size cap + fini NULL-guard fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4580 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.1 KB | β raw | |
| VERDICT.md | β | 4.4 KB | β raw | |
| df4580_limitpanic.c | β | 2.8 KB | view raw | |
| Makefile | β | 67 B | β download | |
| build.sh | β | 116 B | view raw | |
| run.sh | β | 100 B | view raw | |
| build.log | β | 5.7 KB | view raw | |
| run.log | β | 1.2 KB | view raw | |
| panic.txt | β | 931 B | view raw | |
| env.txt | β | 232 B | view raw | |
| fix.diff | β | 1.3 KB | view raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 3.9 KB | view raw |
DF-4580 β radeon_cs: uncapped num_chunks wired allocations; DF GFP_KERNEL failure = panic / eternal hang (local DoS)
What the finding is
radeon_cs_ioctl β radeon_cs_parser_init (sys/dev/drm/radeon/radeon_cs.c):
two kernel allocations sized directly from the fully attacker-controlled
__u32 drm_radeon_cs.num_chunks, with no upper bound:
radeon_cs.c:287βkcalloc(cs->num_chunks, sizeof(uint64_t), GFP_KERNEL)(chunk pointer array, 8Β·n bytes)radeon_cs.c:298βkcalloc(p->nchunks, sizeof(struct radeon_cs_chunk), GFP_KERNEL)(24Β·n bytes)radeon_cs.c:346β per-chunkkvmalloc_array(length_dw, 4, GFP_KERNEL)(4Β·length_dw bytes, length_dw also an uncapped u32)
On Linux, failure of these is a clean -ENOMEM. On DragonFly, the DRM
port maps GFP_KERNEL to M_WAITOK | M_CACHEALIGN without M_NULLOK
(sys/dev/drm/include/linux/gfp.h:40), and DF _kmalloc() therefore can
never return NULL:
- type-limit exceeded β
panic("%s: malloc limit exceeded")(sys/kern/kern_slaballoc.c:877; every malloc type's limit isphysmem/10,kern_slaballoc.c:372) - kernel_map full β
panic("kmem_slab_alloc(): kernel_map ran out of space!")(kern_slaballoc.c:1724) - pages unavailable β
vm_wait()retry loop forever, unkillable thread (kern_slaballoc.c:1780-1806,sys/vm/vm_page.c:2927)
So an authenticated local DRM client on radeon hardware can, with a single CS ioctl:
num_chunksβ 40β50 M β chunk-pointer array (β330β400 MB) succeeds, nextkcalloctrips theM_DRMtype limit β deterministic kernel panic;num_chunks=0xFFFFFFFFβ first allocation asks for 32 GB β permanent uninterruptible hang of the calling thread (1 svm_waitpoll loop).
Latent aggravator (upstream-visible even where kcalloc can return NULL):
radeon_cs_parser_fini then does for (i = 0; i < parser->nchunks; i++)
kvfree(parser->chunks[i].kdata); (radeon_cs.c:447-448) β a NULL
parser->chunks (after the radeon_cs.c:298-300 failure return) is
dereferenced β second, independent crash path.
What this PoC does / does not do
The audit QEMU guest has no AMD/ATI GPU (pciconfig -l shows no
1002:xxxx device, /dev/dri does not exist), so radeon_cs_ioctl cannot
be entered on it. This pack therefore verifies the sink mechanism the
ioctl rides: df4580_limitpanic.c is a kld that performs the identical
class of allocation (kmalloc(..., M_WAITOK|M_ZERO) β no M_NULLOK, own
malloc type with the standard physmem/10 limit, exactly like M_DRM)
until the limit trips.
Build / run (inside the guest as root)
cd /root/df4580 && make kldload ./df4580.ko
Expected result
Serial console (see run.log, panic.txt):
df4580: malloc-type limit ~389 MB; looping kmalloc(32 MB, M_WAITOK|M_ZERO) [no M_NULLOK], GFP_KERNEL-equivalent df4580: alloc #0 ... alloc #13 (type usage ~416 MB) panic: df4580: malloc limit exceeded _kmalloc() at _kmalloc+0xb09 df4580_modevent() at df4580_modevent+0xd8
The radeon trigger itself (single CS ioctl with crafted num_chunks) is
code-traced in VERDICT.md; it needs radeon hardware.
DF-4580 VERDICT
Status: not_reproduced (full chain) β sink mechanism reproduced on guest (panic). Impact claimed: local kernel DoS (panic or unkillable hang).
Why the full chain did not run here
The finding's entry point is radeon_cs_ioctl
(sys/dev/drm/radeon/radeon_cs.c:655), gated by rdev->accel_working
(radeon_cs.c:662) β i.e. a bound, initialized radeon GPU. The audit guest
(QEMU/KVM, DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC) has no AMD/ATI
PCI device and no /dev/dri (checked via pciconfig -l, ls /dev/dri);
every radeon ioctl returns before the parser runs. That is a hard
environment blocker for the ioctl itself, not a refutation: the code path
is fully traceable and its allocation sink was exercised directly.
Code trace of the trigger (radeon hardware present)
- Unprivileged-but-authenticated client (ioctl is
DRM_AUTH|DRM_RENDER_ALLOW,sys/dev/drm/radeon/radeon_kms.c:912) issuesDRM_IOCTL_RADEON_CSwithnum_chunks = N(u32, uncapped) andchunkspointing at NΒ·8 bytes of user memory. radeon_cs_parser_initradeon_cs.c:287:kcalloc(N, 8, GFP_KERNEL)β DF shimsys/dev/drm/include/linux/slab.h:44kzalloc(N*8)βkmalloc(N*8, M_DRM, M_WAITOK|M_CACHEALIGN|M_ZERO).- With N β 40β50 M on a 4 GB box (M_DRM limit = physmem/10 β 389β405 MB):
the 320β400 MB chunk-pointer array is allocated (wired, M_ZERO β feasible
with >1 GB free RAM), pushing
M_DRM.ks_loosememuseover the type limit;radeon_cs.c:298'skcalloc(N, 24, GFP_KERNEL)then enters_kmallocand hitssys/kern/kern_slaballoc.c:877βpanic("M_DRM: malloc limit exceeded")β whole-system crash. - Alternative, N =
0xFFFFFFFF: first kcalloc requests 32 GB (64-bit multiply, no overflow in the DF shim);_kmallocoversized path (kern_slaballoc.c:944-950) βkmem_slab_alloc(32 GB, M_WAITOK)β VA fits (kernel_map β 8 TB) β page loopkern_slaballoc.c:1780-1806vm_wait(0)= 1 s bounded sleep, retried forever since 32 GB > physmem;vm_waitsleeps with flags 0 (no PCATCH) β unkillable thread; repeatable per ioctl. - Latent cleanup crash: had the
radeon_cs.c:298kcalloc returned NULL (it does on upstream Linux whereKMALLOC_MAX_SIZEforces a clean NULL for N > ~175 K),radeon_cs_parser_finiradeon_cs.c:447-448derefsparser->chunks[i]withparser->chunks == NULL,nchunks > 0β NULL-page read panic. On DF-as-compiled this is masked by the allocation panic itself (GFP_KERNEL cannot yield NULL); it becomes live the moment the port addsM_NULLOKsemantics β fix both together.
Mechanism verification actually performed (this guest)
- Built kld
df4580.ko(build.log) replicatingGFP_KERNELallocation semantics with its own malloc type (samephysmem/10limit rule). kldloadβ allocations #0..#13 (32 MB each, 416 MB cumulative β₯ 389 MB limit) βpanic: df4580: malloc limit exceeded, backtrace_kmalloc() β df4580_modevent()(run.log,panic.txt).- This proves both (a) the sink behavior
radeon_cs.c:287/298/346rides, and (b) that the Linux-shaped NULL return (and hence the parser_fini NULL-deref) is unreachable on DF until allocator flags change. - Guest reset to
with-srcsnapshot afterwards; module gone.
Exploit chain
DoS only: one ioctl β panic (or one unkillable thread per ioctl; threads are not bounded). No memory-corruption primitive identified in this path.
Fix
Cap num_chunks and per-chunk length_dw at sane maxima before allocating
in radeon_cs_parser_init, and make radeon_cs_parser_fini NULL-safe (see
fix.diff). Suggested caps mirror upstream practice (chunks are at most a
handful; 64 is generous, length_dw β€ 1 M dwords).
Artifacts
df4580_limitpanic.c/Makefileβ sink-mechanism kld (source of record)build.logβ untrimmed guest build outputrun.log,panic.txtβ serial console capture of the run + panic backtraceenv.txtβ guest uname/physmem/compilerfix.diffβ proposed fix (authored post-verification; never applied to sys/)
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.diff authored and git-apply-check clean, but baseline/patched behavior cannot be exercised on this guest (no radeon hardware); the mechanism kld is unaffected by the radeon-side fix.
findings/poc/DF-4580/fix.diff (3 hunks: num_chunks cap, length_dw cap, fini NULL-guard); git apply --check passes.
Confirmed kernel references
Detail
Evidence (decisive lines)
panic.txt + run.log: 'df4580: alloc #13 (type usage ~416 MB)' then 'panic: df4580: malloc limit exceeded' with _kmalloc backtrace = the exact sink radeon_cs.c:287/298 rides; VERDICT.md section 'Code trace of the trigger' for the ioctl-side chain; build.log for the kld build; fix.diff applies cleanly (git apply --check).
PoC changes
No PoC seed existed; authored from scratch. kld module reproduces the DF GFP_KERNEL allocation-failure sink (M_WAITOK|M_ZERO, no M_NULLOK, own malloc type with the standard physmem/10 limit) rather than the ioctl path, because the guest lacks AMD GPU hardware. One build fix: DECLARE_MODULE argument (moduledata_t, not handler).
Verified recommended fix
Cap num_chunks (<=64) and per-chunk length_dw (<=1M dwords) in radeon_cs_parser_init before allocating, and NULL-guard the parser->chunks loop in radeon_cs_parser_fini.
Verdict
Full trigger (DRM_IOCTL_RADEON_CS with crafted num_chunks) cannot run on the audit guest: it has no AMD/ATI GPU and no /dev/dri, so radeon_cs_ioctl is unreachable there. The code path is certain by inspection: radeon_cs_parser_init sizes wired GFP_KERNEL allocations from the uncapped u32 num_chunks (8n bytes at radeon_cs.c:287, 24n at :298, 4*length_dw at :346), and DragonFly maps GFP_KERNEL to M_WAITOK WITHOUT M_NULLOK (gfp.h:40), so allocation failure is panic('M_DRM: malloc limit exceeded') at kern_slaballoc.c:877 (num_chunks ~40-50M pushes the first 320-400MB alloc over the physmem/10 type limit, next kcalloc panics) or an eternal unkillable vm_wait() loop (kern_slaballoc.c:1780-1806) for num_chunks=0xFFFFFFFF (32GB request > physmem). The sink mechanism was independently REPRODUCED on the guest with a kld performing the identical M_WAITOK|M_ZERO no-M_NULLOK allocation pattern: panic 'df4580: malloc limit exceeded' at alloc #13 (416MB cumulative vs 389MB limit), backtrace _kmalloc -> df4580_modevent (panic.txt, run.log). Additionally the Linux-shaped NULL-return path would NULL-deref parser->chunks[i] in radeon_cs_parser_fini (radeon_cs.c:447-448) with chunks==NULL/nchunks>0; on DF as compiled this is masked by the allocation panic (kmem_slab_alloc never returns NULL without M_NULLOK) but becomes live if allocator flags are fixed without also fixing fini.
No comments yet.