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