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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4580 Β· 13 files
FileTypeDescriptionSize
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-chunk kvmalloc_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 is physmem/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, next kcalloc trips the M_DRM type limit β†’ deterministic kernel panic;
  • num_chunks = 0xFFFFFFFF β†’ first allocation asks for 32 GB β†’ permanent uninterruptible hang of the calling thread (1 s vm_wait poll 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.

VERDICT.md
↓ download raw

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)

  1. Unprivileged-but-authenticated client (ioctl is DRM_AUTH|DRM_RENDER_ALLOW, sys/dev/drm/radeon/radeon_kms.c:912) issues DRM_IOCTL_RADEON_CS with num_chunks = N (u32, uncapped) and chunks pointing at NΒ·8 bytes of user memory.
  2. radeon_cs_parser_init radeon_cs.c:287: kcalloc(N, 8, GFP_KERNEL) β†’ DF shim sys/dev/drm/include/linux/slab.h:44 kzalloc(N*8) β†’ kmalloc(N*8, M_DRM, M_WAITOK|M_CACHEALIGN|M_ZERO).
  3. 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_loosememuse over the type limit; radeon_cs.c:298's kcalloc(N, 24, GFP_KERNEL) then enters _kmalloc and hits sys/kern/kern_slaballoc.c:877 β†’ panic("M_DRM: malloc limit exceeded") β€” whole-system crash.
  4. Alternative, N = 0xFFFFFFFF: first kcalloc requests 32 GB (64-bit multiply, no overflow in the DF shim); _kmalloc oversized path (kern_slaballoc.c:944-950) β†’ kmem_slab_alloc(32 GB, M_WAITOK) β†’ VA fits (kernel_map β‰ˆ 8 TB) β†’ page loop kern_slaballoc.c:1780-1806 vm_wait(0) = 1 s bounded sleep, retried forever since 32 GB > physmem; vm_wait sleeps with flags 0 (no PCATCH) β†’ unkillable thread; repeatable per ioctl.
  5. Latent cleanup crash: had the radeon_cs.c:298 kcalloc returned NULL (it does on upstream Linux where KMALLOC_MAX_SIZE forces a clean NULL for N > ~175 K), radeon_cs_parser_fini radeon_cs.c:447-448 derefs parser->chunks[i] with parser->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 adds M_NULLOK semantics β€” fix both together.

Mechanism verification actually performed (this guest)

  • Built kld df4580.ko (build.log) replicating GFP_KERNEL allocation semantics with its own malloc type (same physmem/10 limit 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/346 rides, 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-src snapshot 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 output
  • run.log, panic.txt β€” serial console capture of the run + panic backtrace
  • env.txt β€” guest uname/physmem/compiler
  • fix.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_testable
baseline no→ patch + rebuild →patched clean

fix.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.
↓ fix.diffper-fix-DF-4580

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.