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

Stale pointer after kfree in drm_sysfs_connector_remove -- connector->kdev not NULLed, creating latent UAF via drm_sysfs_connector_add early-return guard

Summary

drm_sysfs_connector_remove() calls kfree(connector->kdev) at line 53 but never sets connector->kdev=NULL. drm_sysfs_connector_add() at line 35 has early-return guard if(connector->kdev) return 0. If add called after remove (register retried after failure) guard sees dangling non-NULL pointer returns 0 without allocating fresh device and all subsequent uses of connector->kdev (intel_sdvo.c:2198 sysfs_create_link intel_dp.c:5251-5253 aux.dev assignment radeon/amdgpu backlight_device_register) operate on freed memory = UAF. Error path in drm_connector_register (drm_connector.c:466-467) calls drm_sysfs_connector_remove on failure while leaving registration_state=INITIALIZING (state only set REGISTERED at line 461 after error-exit labels). If drm_connector_register subsequently re-invoked -- state machine permits INITIALIZING != REGISTERED -- drm_sysfs_connector_add hits stale non-NULL kdev returns 0 then late_register derefs freed connector->kdev. Several MST paths call drm_connector_register directly without checking return (intel_dp_mst.c:500 radeon_dp_mst.c:304 amdgpu_dm_mst_types.c:418) so failed first registration silently ignored connector remains INITIALIZING with dangling kdev. Subsequent topology change (DP hotplug hub re-enumeration) can re-trigger registration. UAF gives read/write primitive on freed kmalloc slab potentially exploitable for LPE via slab grooming. [VERIFIED pass2: reproduced deterministically 4 runs - dangling kdev after remove, immediate same-64-byte-class reclaim with forged content, exact intel_dp consumer expression returns DF2209-PWNED-BY-RECLAIM through the dangling pointer, re-add silent no-op success with zero allocations, second remove kfrees the RECLAIMED live object cross-object free; natural trigger hardware-gated MST re-registration, no GPU on guest so rig-verified per precedent; NULL-the-pointer fix-validated with fresh-allocation re-add and name-leak also fixed]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2209 Β· 16 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 7.6 KB ↓ raw
rig_head.c β€” 5.2 KB view raw
rig_tail.c β€” 14.4 KB view raw
df22_rig.c β€” 21.0 KB view raw
build.sh β€” 97 B view raw
run.sh β€” 121 B view raw
fix.diff β€” 2.2 KB view raw
build.log β€” 6.0 KB view raw
run.log β€” 3.5 KB view raw
run.2.log β€” 3.5 KB view raw
run_dmesg.log β€” 3.5 KB view raw
fix_module_build.log β€” 2.7 KB view raw
env.txt β€” 636 B view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 7.6 KB view raw

DF-2209 β€” Stale pointer after kfree in drm_sysfs_connector_remove (sys/dev/drm/drm_sysfs.c:48-54)

VERDICT: REPRODUCED β€” the full UAF chain ran deterministically through the UNMODIFIED production functions: dangling kdev after remove β†’ immediate same-zone reclaim with forged content β†’ the production consumer expression (connector->kdev->kobj.name, intel_dp.c:5251) returned ATTACKER-CONTROLLED data through the dangling pointer β†’ re-add returned success with ZERO allocations β†’ a second remove kfree()d the reclaimed live object (cross-object free).

Rig class

Verbatim-code kernel-compiled KLD harness (established drm rig pattern, DF-2214 precedent): UNMODIFIED production add/remove extracted from the guest /usr/src tree (sha256-pinned), compiled kernel-resident with production-faithful kzalloc/kasprintf/kfree shims (slab.h:35,38 + drm_dragonfly.c:65-94 shapes), driven with fabricated drm_connector state. Natural-path trigger requires real GPU hardware this guest does not have (/dev/dri absent).

Build (on the guest, as root)

cd /root/df22/DF-2209 && ./build.sh

Run

cd /root/df22/DF-2209 && ./run.sh

kldload β†’ matrix at load time (CASE D is the UAF cycle, CASE F the fixed cycle) β†’ kldunload β†’ dmesg capture into run.log.

Expected output (CASE D highlights)

[2] remove: kfree(0x…) done; connector->kdev still == 0x… (DANGLING)
[3] reclaim: fresh kmalloc(56, M_DF22, M_WAITOK|M_CACHEALIGN|M_ZERO)
    returned 0x… == freed A (IMMEDIATE SAME-ZONE REUSE)
[4] consumer read connector->kdev->kobj.name -> 'DF2209-PWNED-BY-RECLAIM'
    (UAF READ returns ATTACKER-CONTROLLED data)
[5] re-add: ret==0 (SUCCESS) with rig_kzalloc_calls UNCHANGED
    (silent no-op on the DANGLING pointer)
[6] second remove: production kfree()d the RECLAIMED live object
    (CROSS-OBJECT FREE)
VERDICT: UAF CONFIRMED through verbatim drm_sysfs.c code

Deterministic: identical in 4 runs across two guest generations.

Files

See VERDICT.md for the artifact map and the honest reachability + privesc-ceiling analysis (cross-object free + info-leak-shaped read, hardware-gated; no uid=0 chain built). Paired finding: DF-2208 (same file, same rig, same combined fix).

VERDICT.md
↓ download raw

DF-2209 β€” VERDICT

File: sys/dev/drm/drm_sysfs.c:48-54 (drm_sysfs_connector_remove) Claim: connector->kdev not NULLed after kfree β†’ dangling pointer; the if (connector->kdev) return 0; guard in add (line 35-36) turns re-registration into a silent no-op returning success while consumers operate on freed memory β†’ latent UAF.

Bottom line

REPRODUCED. The full UAF chain was executed deterministically through the UNMODIFIED production functions compiled kernel-resident: dangling pointer retained after remove β†’ freed slot reclaimed with forged content β†’ the exact production consumer expression read attacker-controlled data through the dangling pointer β†’ re-add returned success with ZERO allocations β†’ a second remove kfree()d the reclaimed live object (cross-object free). Four identical runs (two before, two after a fresh-guest reset).

Line-accurate confirmation

  • drm_sysfs.c:52-53 β€” if (connector->kdev) kfree(connector->kdev); and nothing else: the pointer is left dangling. Grep over the whole sys/dev/drm tree confirms no code anywhere else assigns connector->kdev = NULL β€” the only writer is drm_sysfs.c:39.
  • drm_sysfs.c:35-36 β€” if (connector->kdev) return 0; β€” re-add on a connector whose kdev dangles returns SUCCESS without allocating.
  • Consumers that deref connector->kdev (verified in-tree):
  • i915/intel_dp.c:5251 β€” connector->kdev->kobj.name (string read, DRM_DEBUG_KMS %s)
  • i915/intel_dp.c:5253 β€” intel_dp->aux.dev = connector->kdev
  • i915/intel_sdvo.c:2198 β€” sysfs_create_link(&connector->kdev->kobj,...) (no-op stub on DFly: include/linux/sysfs.h:50-56)
  • radeon/atombios_encoders.c:262, amd/amdgpu/atombios_encoders.c:245, radeon/radeon_legacy_encoders.c:397 β€” backlight_device_register(bl_name, drm_connector->kdev, ...)
  • Natural re-registration path (verified in-tree): drm_connector_register() error path (drm_connector.c:444-467) calls drm_sysfs_connector_remove() while registration_state stays DRM_CONNECTOR_INITIALIZING (set to REGISTERED only at :461 after all steps); a later drm_connector_register() passes the :441 state check and re-enters add β†’ guard β†’ dangling. MST topology code calls drm_connector_register() ignoring its return: i915/intel_dp_mst.c:500, radeon/radeon_dp_mst.c:304, so a failed first registration is silently ignored and a topology change can re-trigger registration.

Mechanical reproduction (verbatim-code KLD rig, DF-2214 pattern)

Tree lines 31-46 / 48-54 extracted on the guest (sha256-pinned, tree byte-identical to the audited tree) and compiled kernel-resident with production-faithful shims. Decisive output (run.log):

DF2209   [1] add: kdev=0xfffff80117608180 (kobj.name='card0-DP-3')
DF2209   [2] remove: kfree(0xfffff80117608180) done; connector->kdev still
         == 0xfffff80117608180 (DANGLING -- drm_sysfs.c:52-53 never NULLs it)
DF2209   [3] reclaim: fresh kmalloc(56, M_DF22, M_WAITOK|M_CACHEALIGN|M_ZERO)
         returned 0xfffff80117608180 == freed A (IMMEDIATE SAME-ZONE REUSE);
         block poisoned 0x41 and kobj.name forged
DF2209   [4] consumer read connector->kdev->kobj.name ->
         'DF2209-PWNED-BY-RECLAIM' (UAF READ returns ATTACKER-CONTROLLED data;
         was 'card0-DP-3' before the free)
DF2209   [5] re-add: ret==0 (SUCCESS) with rig_kzalloc_calls UNCHANGED (3) --
         guard at drm_sysfs.c:35-36 turned add into a silent no-op on the
         DANGLING pointer
DF2209   [6] second remove: production kfree()d the RECLAIMED live object
         0xfffff80117608180 owned by another allocator (CROSS-OBJECT FREE);
         owner still holds the pointer
DF2209   [6] owner-after-free read R->kobj.name -> 0xfffff8008f90cf80
         (== forged string, intact)
DF2209   VERDICT: UAF CONFIRMED through verbatim drm_sysfs.c code

Notes on fidelity: the reclaim uses the same size geometry as the production allocation (56 bytes + M_CACHEALIGN β†’ same power-of-2 kmalloc class), which is why the freed slot is reused immediately (LIFO zone free list) exactly as a same-zone spray would on a live system. The consumer expression in step [4] is byte-for-byte the production expression at intel_dp.c:5251.

Honest reachability

  • Natural trigger needs real GPU hardware: a DRM driver (i915/radeon/amdgpu) must register a connector, fail inside drm_debugfs_connector_add() or late_register() (drm_connector.c: 444-467), and then re-register (DP MST topology churn β€” the cited MST callers ignore the first failure). This QEMU guest has NO DRM device (/dev/dri absent, drm.ko unattached, runtime kldload of the stock drm module GPFs on this guest), so the natural path cannot run here β€” the same GPU-gated situation as the DF-2214 precedent, where the verbatim kernel-resident rig is the executed artifact.
  • The code-level defect and its mechanism are platform-independent and proven; on GPU-equipped DragonFly systems running i915/radeon/amdgpu the window is real.

Privesc assessment (standing directive) β€” memcorrupt bucket

  • The freed object is a 64-byte-class kmalloc(M_DRM) block (56-byte struct device + M_CACHEALIGN power-of-2 rounding).
  • Demonstrated primitives: (a) attacker-controlled READ through the dangling pointer (forged kobj.name β€” on DFly the DRM_DEBUG_KMS %s consumer would print a string from an attacker-chosen kernel address into dmesg: info-leak shape, needs drm debug enabled); (b) CROSS-OBJECT FREE β€” a second unregister kfree()s whatever now occupies the slot, the classic free-a-victim pivot: its owner then holds freed memory that an attacker can re-reclaim.
  • Not demonstrated / hard blockers on THIS guest: no GPU (no natural register-fail-retry window can be driven), no M_DRM spray surface without a drm device. sysfs_create_link is a no-op stub on DFly, which removes one consumer; the remaining consumers are the debug string read, aux.dev pointer propagation into drm_dp_aux_register, and backlight parent use.
  • uid=0 chain NOT built. Honest ceiling: memory-corruption class (cross-object free) with an info-leak read, gated on real GPU hardware and a driver-specific error-path window; realistic impact floor as directly demonstrable is a kernel panic/DoS when consumers touch the forged or freed block. Impact recorded as dos (enum floor) with the corruption/leak ceiling documented here.

Fix validation (A/B)

fix.diff (shared with DF-2208) NULLs connector->kdev after kfree and frees the name string first. In the same KLD, the FIXED cycle (run.log CASE F): remove β†’ kdev=0 (NULLed); re-add β†’ FRESH allocation (different pointer, kzalloc counter increments, name re-established card0-DP-5) β€” no dangling, no silent no-op, no cross-object free. The patched production drm module also rebuilds clean with -Werror (fix_module_build.log).

Artifacts

  • run.log, run.2.log β€” decisive + repeat matrix runs
  • run_dmesg.log β€” full dmesg of the decisive run
  • build.log β€” full untrimmed build+run transcript
  • fix_module_build.log β€” patched real drm module build (guest /usr/src)
  • df22_rig.c β€” assembled source as compiled (baseline + fixed)
  • env.txt β€” guest environment
  • fix.diff β€” the combined DF-2208+DF-2209 fix

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

fixed
baseline reproduced→ patch + rebuild →patched clean

A/B validated in the same verbatim KLD: the FIXED remove NULLs connector->kdev (breaking the dangling pointer) and frees the name string; the fixed cycle re-add allocates a FRESH block (kzalloc counter increments, pointer differs from the freed and the interposed block, name re-established card0-DP-5) -- no dangling, no silent no-op, no cross-object free (run.log CASE F). Additionally fix.diff applied to the guest /usr/src tree rebuilt the production drm KLD clean with -Werror (fix_module_build.log).

run.log CASE F lines: [1] fixed remove: kdev=0 (NULLed: fix breaks the dangling pointer); [2] fixed re-add: FRESH allocation 0xfffff80117608240 != freed 0xfffff80117608180 ... kzalloc_calls 6 -> 7 ... VERDICT: fixed cycle clean. fix_module_build.log: cc -Werror -c drm_sysfs.c + ld -r -o drm.ko, rc=0.
↓ fix.diffper-fix-DF-2209

Confirmed kernel references

Detail

Exploit chain

GPU-equipped system with i915/radeon/amdgpu + DP MST: first drm_connector_register fails inside drm_debugfs_connector_add or late_register (drm_connector.c:448-457) -> err_sysfs label calls drm_sysfs_connector_remove (drm_connector.c:466-467) freeing kdev and leaving it dangling while registration_state stays INITIALIZING -> MST topology change re-invokes drm_connector_register (return ignored at intel_dp_mst.c:500 / radeon_dp_mst.c:304) -> drm_sysfs_connector_add hits the guard at drm_sysfs.c:35-36 and returns 0 without allocating -> consumers deref the freed 64-byte-class M_DRM block: intel_dp.c:5251 reads forged kobj.name (attacker-chosen kernel address printed as %s -> info leak shape), :5253 stores the dangling device into aux.dev, a later unregister kfrees whatever now owns the slot (cross-object free -> victim UAF). Demonstrated end-to-end through the verbatim functions with the reclaim forged and the consumer expression returning attacker-controlled data; natural-window and spray stages are hardware-gated and were not driven on this guest, so no uid=0.

Evidence (decisive lines)

run.log CASE D: [2] connector->kdev still == freed pointer (DANGLING -- drm_sysfs.c:52-53 never NULLs it); [3] fresh kmalloc returned the freed address (IMMEDIATE SAME-ZONE REUSE) poisoned 0x41 with kobj.name forged; [4] consumer read -> DF2209-PWNED-BY-RECLAIM (UAF READ returns ATTACKER-CONTROLLED data); [5] re-add ret==0 with rig_kzalloc_calls UNCHANGED (silent no-op on the dangling pointer); [6] second remove kfree()d the RECLAIMED live object (CROSS-OBJECT FREE) and the owner read freed memory. CASE F (fixed): kdev NULLed after remove, fresh allocation on re-add, cycle clean. run.2.log identical (fresh-guest repeat). panic-side and matrix transcripts in build.log; patched real-module build in fix_module_build.log.

PoC changes

No seed pack existed; rig authored from scratch in verify mode following the DF-2214 verbatim-KLD pattern. Iterations: (1) does not define va_list -- switched to ; (2) first matrix run showed no reclaim aliasing: the 64-byte planted-string helper allocation itself stole the just-freed 64-class slot -- moved the planted string to the 128-byte class, after which the first candidate kmalloc returned the freed address deterministically (case F had already proven LIFO reuse of a just-freed same-class chunk); (3) owner-after-free read prints the pointer VALUE only (never %s an unknown forged pointer) so the matrix cannot fault outside the deliberate fire case.

Verified recommended fix

In drm_sysfs_connector_remove free the kasprintfd name, free kdev, then set connector->kdev = NULL -- see fix.diff hunk 2 (paired with the DF-2208 NULL checks in hunk 1).

Verdict

REPRODUCED: the full UAF chain executed deterministically through the UNMODIFIED production drm_sysfs_connector_add/_remove (tree lines 31-46/48-54, sha256-pinned, extracted on the guest from a tree byte-identical to the audited tree) compiled kernel-resident with production-faithful allocator shims. Chain: (1) add allocates kdev=0xfffff80117608180 with kobj.name card0-DP-3; (2) remove kfrees it and leaves connector->kdev DANGLING (drm_sysfs.c:52-53 NULLs nothing; grep over the whole drm tree shows no other writer ever resets kdev); (3) a fresh same-geometry allocation (56 bytes + M_CACHEALIGN, the production size class) immediately reclaimed the freed slot, which the rig poisoned and forged; (4) the exact production consumer expression connector->kdev->kobj.name (intel_dp.c:5251) read DF2209-PWNED-BY-RECLAIM through the dangling pointer -- attacker-controlled UAF READ; (5) re-add returned ret==0 with the kzalloc call counter UNCHANGED: the guard at drm_sysfs.c:35-36 turned re-registration into a silent no-op success on the dangling pointer, so registration_state advances while every consumer operates on freed memory; (6) a second remove production-kfree()d the RECLAIMED live object owned by another allocator (CROSS-OBJECT FREE, the free-a-victim pivot) and the owner read through freed memory afterwards. Identical across 4 runs on two guest generations. Honest reachability: the natural trigger needs real GPU hardware -- drm_connector_register error path (drm_connector.c:444-467) unregisters while staying INITIALIZING, and MST callers re-register ignoring the first failure (intel_dp_mst.c:500, radeon_dp_mst.c:304); this QEMU guest has no DRM device (natural path not runnable here, DF-2214 rig precedent). Privesc directive assessed and closed honestly: ceiling is the cross-object free plus an info-leak-shaped read (forged kobj.name printed via DRM_DEBUG_KMS %s needs drm debug), gated on a driver error-path window and an M_DRM 64-class spray surface that both require a real GPU; sysfs_create_link is a no-op stub on DFly removing one consumer; uid=0 chain NOT built -- impact recorded as the directly demonstrable dos floor with the corruption/leak ceiling documented in VERDICT.md. Fix A/B validated: the fixed remove NULLs kdev and the fixed cycle re-allocates fresh (run.log CASE F); patched production drm module rebuilds clean with -Werror.