drm_crtc_helper_set_config() sizes save_connector_encoders with the racy num_connector while iterating the live list β concurrent DP-MST connector registration (spinlock-only) writes one pointer past the kcalloc'd array, and the fail path reads the OOB slot back into connector->encoder (heap OOB write + forged-pointer injection)
Summary
The array is sized from mode_config.num_connector under mode_config.mutex, but connector ADDITION takes only the connector_list_lock spinlock and is live via radeon DP-MST topology work. A connector added between kcalloc and the fill loop makes the iter walk visit N+1 connectors: save_connector_encoders[count++] writes one pointer past the array (clobbers the adjacent slab object's first qword); the fail path reads the OOB slot into connector->encoder - adjacent-heap-data pointer injection then deref'd. File is byte-identical to Linux v4.15 and still affected upstream. DRM-master + MST-hardware trigger. Bound-both-walks-by-snapshot fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4469 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| setcrtc_dup.c | β | 8.3 KB | view raw | |
| build.sh | β | 89 B | view raw | |
| run.sh | β | 159 B | view raw | |
| build.log | β | 100 B | view raw | |
| run.log | β | 220 B | view raw | |
| env.txt | β | 411 B | view raw | |
| VERDICT.md | β | 5.9 KB | β raw | |
| fix.diff | β | 1.7 KB | view raw |
DF-4469 VERDICT β racy array sizing vs live connector list in drm_crtc_helper_set_config
Status: INCONCLUSIVE (race proven at code level; guest has no DRM device)
The audit guest runs QEMU std VGA β vgapci0: chip=0x11111234 with no KMS
driver bound (no /dev/dri), and this DragonFly drm tree has no driver for
the QEMU-emulated VGA, so the ioctl layer of this finding cannot even be
entered on the guest. The race itself is proven by code trace below.
Root cause (path:line)
drm_crtc_helper_set_config runs under mode_config.mutex (taken by
drm_mode_setcrtc, drm_crtc.c:601) β but the connector list it snapshots is
NOT protected by that mutex:
sys/dev/drm/drm_crtc_helper.c:571-576save_connector_encoders = kcalloc(dev->mode_config.num_connector, ...)βnum_connectoris a plain counter.sys/dev/drm/drm_connector.c:256-260spin_lock_irq(&config->connector_list_lock); list_add_tail(...); config->num_connector++; spin_unlock_irq(...)β connector ADDITION only takes the spinlock, nevermode_config.mutex.- The addition path is live in-tree for radeon DP MST:
sys/dev/drm/drm_dp_mst_topology.c:1205(cbs->add_connector, topology work context) βsys/dev/radeon/radeon_dp_mst.c:267-286radeon_dp_add_mst_connectorβdrm_connector_initβ no mode_config.mutex anywhere on that path (destruction is likewise deferred todestroy_connector_workprecisely because the mutex may be held, drm_dp_mst_topology.c:950-960). sys/dev/drm/drm_crtc_helper.c:588-592drm_for_each_connector_iter(connector, &conn_iter) save_connector_encoders[count++] = connector->encoder;The iterator (drm_connector_list_iter_next, drm_connector.c:623-650) walks the LIVE list under the spinlock and WILL visit a connector added after step 1 β heap OOB write of one pointer (value = the fresh connector'sencoder, i.e. NULL for a new kzalloc'd MST connector) one slot past the kcalloc'd array. Withnum_connectorat a kmalloc bucket boundary (8, 16, 32, ...) the slot lands in the adjacent object.sys/dev/drm/drm_crtc_helper.c:766-770(fail path)drm_for_each_connector_iter(connector, &conn_iter) connector->encoder = save_connector_encoders[count++];reads the same out-of-bounds slot and stores adjacent heap data into the new connector's->encoderβ a forged-pointer injection. Later walks dereference it:drm_crtc_helper.c:663-664connector->encoder->crtc = NULL(write through the forged pointer),drm_crtc_helper.c:843(connector->encoder->crtcread),drm_crtc_helper.c:108-114(pointer compare, benign). Any subsequentdrm_mode_setcrtc/fail path then corrupts memory through that pointer.
Exploit chain (conceptual, on radeon + DP MST)
- Local unprivileged user becomes DRM master (first opener on a headless
system,
DRM_IOCTL_SET_MASTER). - Spray objects of the
num_connector+1pointers' kmalloc size class to place an attacker-interesting victim immediately after the future array. - Run
DRM_IOCTL_MODE_SETCRTCin a tight loop while a partner replugs the MST hub: each replug re-runs link-address enumeration β connector add. - On hit: NULL-write into the adjacent object (step 4 above) and, on a
subsequent failing setcrtc, OOB-read β forged
connector->encoderβ NULL-write at*(forged + offsetof(drm_encoder, crtc)). - Primitive: deterministic kernel NULL write at (almost) attacker-chosen address via heap-groomed adjacent data β escalation path standard for NULL/pointer writes (target refcount/flags fields), DoS trivially.
Why Medium, not High
Memory corruption class is real, but the trigger needs (a) radeon-class GPU with DP MST in legacy-helper mode, (b) physical-layer MST hotplug events during a microsecond-scale window, (c) DRM master. No software-only trigger exists in-tree (MST adds only run from topology work fed by HPD interrupts).
Fix (systemic, matches upstream-hardening direction)
Snapshot the connector list length consistently, e.g. size by iterating first
or take mode_config.mutex in drm_connector_init:
--- a/sys/dev/drm/drm_connector.c
+++ b/sys/dev/drm/drm_connector.c
@@ -253,9 +253,11 @@ int drm_connector_init(struct drm_device *dev,
/* We should add connectors at the end to avoid upsetting the connector
* index too much. */
+ drm_modeset_lock_all(dev);
spin_lock_irq(&config->connector_list_lock);
list_add_tail(&connector->head, &config->connector_list);
config->num_connector++;
spin_unlock_irq(&config->connector_list_lock);
+ drm_modeset_unlock_all(dev);
β¦or, minimally inside drm_crtc_helper_set_config, clamp the fill loop:
--- a/sys/dev/drm/drm_crtc_helper.c
+++ b/sys/dev/drm/drm_crtc_helper.c
@@ -588,7 +588,8 @@ int drm_crtc_helper_set_config(struct drm_mode_set *set,
count = 0;
drm_connector_list_iter_begin(dev, &conn_iter);
drm_for_each_connector_iter(connector, &conn_iter)
+ if (count < dev->mode_config.num_connector)
save_connector_encoders[count++] = connector->encoder;
drm_connector_list_iter_end(&conn_iter);
(the fail-path loop needs the symmetric guard). The lock fix is the correct one; the clamp is defense-in-depth.
Attempts
- Guest build of setcrtc_dup.c: build.log.
- Guest run: open("/dev/dri/card0") = ENOENT (no DRM device) β run.log.
- Upstream cross-check: Linux v4.15..master still sizes with
num_connectorand still adds connectors under the spinlock only β bug inherited, not fixed upstream (checked drm_connector.c @ master 2026-09).
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_testablenot validated on guest (no DRM device on guest; race needs radeon+MST hw); fix.diff bounds both list walks by the snapshot count taken at allocation time
findings/poc/DF-4469/fix.diff
Confirmed kernel references
Detail
Exploit chain
local DRM master (headless first opener) + radeon DP-MST + replug partner: SETCRTC loop vs topology-work connector add -> kcalloc(N) vs list N+1 -> OOB NULL write into adjacent slab object (groomable size class) and/or OOB read stored into connector->encoder -> NULL write at *(forged+offsetof(drm_encoder,crtc)) on next setcrtc -> deterministic kernel NULL-write primitive; DoS trivially, escalation heap-grooming dependent.
Evidence (decisive lines)
VERDICT.md (full path:line proof incl. upstream cross-check), setcrtc_dup.c (self-contained race harness), build.log/run.log (guest ENOENT), fix.diff (snapshot-bounded loops)
PoC changes
seed had disable-path SETCRTC loop only - that path early-returns before the kcalloc (drm_crtc_helper.c:543-556); rewrote to full modeset (dumb buffer + ADDFB + connector mode) so both variants actually reach the vulnerable code; added self-contained DRM ABI structs (guest has no drm userland headers); fixed gc.connection -> gc.connected
Verified recommended fix
Bound the save_connector_encoders fill and fail-restore loops by the num_connector snapshot taken at kcalloc time.
Verdict
drm_crtc_helper_set_config() sizes save_connector_encoders with dev->mode_config.num_connector (drm_crtc_helper.c:571) but fills it via drm_for_each_connector_iter over the live list (drm_crtc_helper.c:588-592); connector registration only takes the connector_list_lock spinlock (drm_connector.c:256-260), never mode_config.mutex, and the radeon DP-MST path registers connectors from topology work exactly that way (drm_dp_mst_topology.c:1205 -> radeon_dp_mst.c:267-286). A connector added between the kcalloc and the fill loop causes a one-pointer heap write past the array (value NULL for a fresh MST connector); the fail path (drm_crtc_helper.c:766-770) then reads the same OOB slot into the new connector->encoder, and later walks deref/write through it (drm_crtc_helper.c:663-664 connector->encoder->crtc = NULL) - heap OOB write plus forged-pointer injection. Upstream v4.15-identical (file is byte-identical except one include) and still present in current Linux. Not reproducible on the audit guest: it has no DRM device at all (QEMU std VGA, no KMS driver, /dev/dri absent) and the race needs radeon+DP-MST hardware with physical hotplug events. PoC builds and runs on the guest (ENOENT), race harness ready for hardware.
No comments yet.