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)
PoC verification
Evidence pack
findings/poc/DF-2209 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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 wholesys/dev/drmtree confirms no code anywhere else assignsconnector->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->kdevi915/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) callsdrm_sysfs_connector_remove()whileregistration_statestaysDRM_CONNECTOR_INITIALIZING(set to REGISTERED only at :461 after all steps); a laterdrm_connector_register()passes the :441 state check and re-enters add β guard β dangling. MST topology code callsdrm_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()orlate_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/driabsent, 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-bytestruct device+M_CACHEALIGNpower-of-2 rounding). - Demonstrated primitives: (a) attacker-controlled READ through the
dangling pointer (forged
kobj.nameβ on DFly the DRM_DEBUG_KMS%sconsumer 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_linkis 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 runsrun_dmesg.logβ full dmesg of the decisive runbuild.logβ full untrimmed build+run transcriptfix_module_build.logβ patched real drm module build (guest /usr/src)df22_rig.cβ assembled source as compiled (baseline + fixed)env.txtβ guest environmentfix.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
fixedA/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.
Confirmed kernel references
- sys/dev/drm/drm_sysfs.c:35-36
- sys/dev/drm/drm_sysfs.c:52-53
- sys/dev/drm/drm_connector.c:444-467
- sys/dev/drm/i915/intel_dp.c:5251
- sys/dev/drm/i915/intel_dp.c:5253
- sys/dev/drm/i915/intel_sdvo.c:2198
- sys/dev/drm/i915/intel_dp_mst.c:500
- sys/dev/drm/radeon/radeon_dp_mst.c:304
- sys/dev/drm/radeon/atombios_encoders.c:262
- sys/dev/drm/amd/amdgpu/atombios_encoders.c:245
- sys/dev/drm/include/linux/sysfs.h:50-56
- sys/dev/drm/include/linux/slab.h:35
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)
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.
No comments yet.