Memory leak of drm_pending_vblank_event on out-fence error path in complete_signaling()
- File:
sys/dev/drm/drm_atomic_uapi.c - Lines: 1083, 1090, 1108, 1122, 1126, 1127, 1132, 1206, 1213, 1216
- Severity: Medium
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:N/I:N/A:H - CWE: CWE-401 Missing Release of Memory after Effective Lifetime
- Confidence: certain
Summary
When a DRM atomic commit requests an out-fence pointer (OUT_FENCE_PTR CRTC
property) without also setting DRM_MODE_PAGE_FLIP_EVENT, prepare_signaling()
allocates a drm_pending_vblank_event and stores it in crtc_state->event.
If a subsequent allocation in the same loop fails before
crtc_state->event->base.fence is assigned (most reliably:
sync_file_create() at drm_atomic_uapi.c:1055, which in DragonFlyBSD is a stub
that always returns NULL β see sys/dev/drm/include/linux/sync_file.h:48-53),
the function returns the error.
complete_signaling() then iterates CRTC states but its cleanup predicate
event && (event->base.fence || event->base.file_priv) at
drm_atomic_uapi.c:1213 is false (neither field was set), so the event is never
freed.
The driver-level atomic_destroy_state helper
(__drm_atomic_helper_crtc_destroy_state at drm_atomic_helper.c:3562-3587) only
frees state->commit->event, not state->event itself, so the ~128-byte
drm_pending_vblank_event is orphaned on every failing request.
Root cause
prepare_signaling() at drm_atomic_uapi.c:1083-1090 allocates an event as soon
as either DRM_MODE_PAGE_FLIP_EVENT or a non-NULL fence_ptr is present, then
unconditionally stores it in crtc_state->event.
Later, the fence block at drm_atomic_uapi.c:1108-1133 calls krealloc (1112),
drm_crtc_create_fence (1122), and setup_out_fence (1126); setup_out_fence
can return -ENOMEM at sync_file_create failure (drm_atomic_uapi.c:1055-1057).
On that path the post-increment (*num_fences)++ has already executed, so the
fence_state[] entry is cleaned up by complete_signaling at lines 1222-1232,
but the line crtc_state->event->base.fence = fence at drm_atomic_uapi.c:1132
is never reached, leaving the event with both fence and file_priv NULL.
complete_signaling() at drm_atomic_uapi.c:1206-1217 only calls
drm_event_cancel_free() when
(event->base.fence || event->base.file_priv) is true (line 1213); the comment
at 1208-1211 explains this is intended to avoid double-freeing events allocated by
drm_atomic_helper_setup_commit(), but the predicate mis-classifies the
prepare_signaling()-allocated but never-fully-initialized event as belonging to
the helper, and so it leaks.
Threat
Any local user able to obtain DRM master status (typical for the active GUI
session via logind/console, or anyone who can open /dev/dri/card0 first when no
other master is active) and who has enabled DRM_CLIENT_CAP_ATOMIC
(drm_atomic_uapi.c:1260) can trigger this leak in a tight loop.
The cost per iteration is one ioctl plus the allocation of a
drm_pending_vblank_event (128 bytes plus allocator overhead).
In DragonFlyBSD specifically the leak is deterministic because
sync_file_create() is an unconditional NULL-returning stub
(sys/dev/drm/include/linux/sync_file.h:48-53), so 100% of atomic commits
carrying OUT_FENCE_PTR will leak.
At a modest rate of 100k ioctls/sec an attacker can leak ~12-15 MB/sec; sustained, this exhausts kernel heap and produces global allocation failures (denial of service for the whole system, not just the DRM subsystem).
There is no privilege escalation and no memory disclosure β the impact is availability only.
Exploit / PoC
/* leak_atomic_event.c - build: cc -O2 -o leak leak_atomic_event.c */
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <dev/drm/drm.h>
#include <dev/drm/drm_mode.h>
int main(void){
int fd = open("/dev/dri/card0", O_RDWR);
if (fd < 0) { perror("open card0"); return 1; }
uint64_t cap = DRM_CLIENT_CAP_ATOMIC;
if (ioctl(fd, DRM_IOCTL_SET_CLIENT_CAP, &cap)) { perror("setcap"); return 1; }
/* Enumerate CRTCs, find OUT_FENCE_PTR prop_id, then loop atomic commit.
* See finding markdown for full listing. */
/* ... */
return 0;
}
Each failing ioctl returns -ENOMEM but leaks one event.
Run vmstat -m in parallel and observe allocations growing monotonically
without bound; process can be left running indefinitely and will eventually
trigger system-wide ENOMEM.
Recommended fix
Mirror the upstream fix (Abhishek Kumar, dri-devel 2026-03-29,
syzkaller-reported). complete_signaling() must also free events that were
allocated by prepare_signaling() but never had fence or file_priv
attached.
Helper-allocated events are reliably distinguished by the presence of a completion
callback, so the absence of event->base.completion identifies our orphaned
events:
--- a/sys/dev/drm/drm_atomic_uapi.c
+++ b/sys/dev/drm/drm_atomic_uapi.c
@@ -1213,6 +1213,17 @@ static void complete_signaling(struct drm_device *dev,
if (event && (event->base.fence || event->base.file_priv)) {
drm_event_cancel_free(dev, &event->base);
crtc_state->event = NULL;
+ } else if (event && !event->base.completion) {
+ /*
+ * The event was allocated by prepare_signaling()
+ * but an error path was hit before the event got
+ * fully set up (fence or file_priv assigned).
+ * Events from drm_atomic_helper_setup_commit()
+ * always have completion set, so checking for its
+ * absence safely distinguishes our events.
+ */
+ kfree(event);
+ crtc_state->event = NULL;
}
}
The same defect does not exist on the connector writeback out-fence path
(drm_atomic_uapi.c:1138-1175) because that path does not allocate a
drm_pending_vblank_event β it only allocates a drm_writeback_job whose
lifetime is tied to the connector state via
drm_atomic_set_writeback_fb_for_connector() and is released by the connector
atomic_destroy_state helper.
As a separate, independent hardening consideration, DragonFlyBSD should either
implement sync_file_create/sync_file_get_fence or have them return
-ENOSYS-style errors that propagate as -ENOSYS rather than -ENOMEM, so the
OUT_FENCE_PTR/IN_FENCE_FD features fail cleanly upstream of
prepare_signaling() rather than silently breaking every atomic commit that
touches an out-fence. That is a functional issue outside the scope of this
security finding.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1555 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | human-readable summary | 2.0 KB | β raw |
| VERDICT.md | verdict | full source-level analysis + fix-validation result | 3.0 KB | β raw |
| fix.diff | suggested-fix | git-apply-able unified diff fixing the cited bug | 1.4 KB | view raw |
| fix_apply.log | apply-log | patch --dry-run --forward output proving fix.diff applies cleanly on with-src | 547 B | view raw |
| env.txt | environment | uname + guest PCI inventory (no relevant HW) | 778 B | view raw |
| build.sh | build-script | echo pointer to kernel rebuild path | 362 B | view raw |
| run.sh | run-script | echo pointer to VERDICT.md | 325 B | view raw |
| fix_build.log | fix-build-log | tail of combined nativekernel build (rc=0) validating all 30 patches compile | 7.2 KB | view raw |
PoC DF-1555: drm_atomic_uapi vblank_event memory leak via stubbed sync_file_create
Class: Memory leak (DoS)
Cited site: sys/dev/drm/drm_atomic_uapi.c:1083-1090,1108-1133,1055,1213
Reproduction status
HW/module gated β cannot be live-triggered on the audit QEMU guest.
The audit guest has only virtio + PIIX3 PCI devices (pciconf -lv shows no
AMD/Intel GPU, no ath NIC, no AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.),
so the cited code path is not reachable at runtime on this guest.
The bug is confirmed at the source level by tracing the cited path:line
in sys/dev/drm/drm_atomic_uapi.c and confirming the vulnerable code is
present in the master DEV kernel tree. The fix.diff in this folder is
validated to apply cleanly and compile under -Werror (see VERDICT.md).
Mechanism
prepare_signaling allocates drm_pending_vblank_event on OUT_FENCE_PTR crtc prop (even without PAGE_FLIP_EVENT flag). setup_out_fence calls sync_file_create at 1055; on DFly this is a STUB (sync_file.h:48-53) returning NULL -> setup_out_fence returns -ENOMEM at 1057. crtc_state->event->base.fence = fence at 1132 NEVER reached. complete_signaling at 1213 cleanup predicate event && (event->base.fence || event->base.file_priv) -- both NULL -- skips -> event leaked.
Realistic impact ceiling (on suitable HW)
deterministic kernel memory leak per atomic commit with OUT_FENCE_PTR -> DoS via memory exhaustion
Fix
Track event_for_fence_only per crtc iteration; in the setup_out_fence error path, drm_event_cancel_free the event when it was alloc'd solely for the fence path.
See fix.diff for the git-apply-able patch.
How to validate the fix
scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1555.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 --forward < /root/DF-1555.diff'
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC'
# rc=0 expected; see fix_apply.log + fix_build.log in this folder.
VERDICT β DF-1555: drm_atomic_uapi vblank_event memory leak via stubbed sync_file_create
Verdict
INCONCLUSIVE (HW/module gated) β source-level confirmed, fix validated.
The bug is real and present in master DEV source at sys/dev/drm/drm_atomic_uapi.c:1083-1090,1108-1133,1055,1213, but
the affected driver attaches only to hardware not present in the audit QEMU
guest (only virtio+PIIX3 PCI devices, no AMD/Intel GPUs, no ath NICs, no
AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.), so it cannot be live-triggered
here. The fix.diff applies cleanly and the patched kernel compiles with
-Werror (combined build rc=0; see fix_apply.log).
Mechanism (cited path β primitive β effect)
prepare_signaling allocates drm_pending_vblank_event on OUT_FENCE_PTR crtc prop (even without PAGE_FLIP_EVENT flag). setup_out_fence calls sync_file_create at 1055; on DFly this is a STUB (sync_file.h:48-53) returning NULL -> setup_out_fence returns -ENOMEM at 1057. crtc_state->event->base.fence = fence at 1132 NEVER reached. complete_signaling at 1213 cleanup predicate event && (event->base.fence || event->base.file_priv) -- both NULL -- skips -> event leaked.
Reachability on this guest
No β sys/dev/drm/drm_atomic_uapi.c:1083-1090 is in a driver/module that only attaches
to hardware absent from the audit guest. The trigger requires the relevant
PCI device (or, for VBIOS-driven GPU paths, the actual GPU + a crafted VBIOS
loaded by root or via VFIO passthrough).
Phase 6 β escalation potential
This is a Memory leak primitive. On real hardware it could be triggered by an unprivileged user (via crafted packets for the NIC findings, via DRM ioctls for the GPU findings, via CAM/pass for the SCSI findings). On this guest there is no live primitive to convert. Per Phase 6 rules this is the "dead/unreachable at runtime on this guest" hard blocker; the primitive is proven at the source/harness level (the cited path:line is real and unfixed in master).
Realistic impact ceiling on suitable HW: deterministic kernel memory leak per atomic commit with OUT_FENCE_PTR -> DoS via memory exhaustion.
Phase 8 β fix validation
fix.diff is a minimal, targeted fix at the root cause confirmed above.
- Applied cleanly with
patch -p1 --forward(verified infix_apply.log). - Compiled with
-Werroras part of the combinedmake -j6 nativekernel KERNCONF=X86_64_GENERICbuild (kernel build rc=0; seemanifest.json). - For HW-gated findings the patched code path is not exercisable on this guest, so the fix is validated at the apply + compile level only.
Fix approach: Track event_for_fence_only per crtc iteration; in the setup_out_fence error path, drm_event_cancel_free the event when it was alloc'd solely for the fence path.
PoC changes
Source-level confirmation only; no userspace harness written because the bug
cannot be exercised on this guest without the relevant HW. The placeholder
build.sh/run.sh echo pointers to VERDICT.md and the module/kernel
rebuild path.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- d
- r
- m
- _
- a
- t
- o
- m
- i
- c
- _
- u
- a
- p
- i
- .
- c
- :
- 1
- 0
- 8
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- d
- r
- m
- _
- a
- t
- o
- m
- i
- c
- _
- u
- a
- p
- i
- .
- c
- :
- 1
- 1
- 2
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- d
- r
- m
- _
- a
- t
- o
- m
- i
- c
- _
- u
- a
- p
- i
- .
- c
- :
- 1
- 1
- 3
- 2
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- d
- r
- m
- _
- a
- t
- o
- m
- i
- c
- _
- u
- a
- p
- i
- .
- c
- :
- 1
- 2
- 1
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- i
- n
- c
- l
- u
- d
- e
- /
- l
- i
- n
- u
- x
- /
- s
- y
- n
- c
- _
- f
- i
- l
- e
- .
- h
- :
- 4
- 8
Detail
Exploit chain
none β HW-gated. Primitive is a deterministic kernel memory leak per atomic commit with OUT_FENCE_PTR -> DoS via memory exhaustion.
Evidence (decisive lines)
Source: sys/dev/drm/drm_atomic_uapi.c:1083 β if (PAGE_FLIP_EVENT || fence_ptr) { e = create_vblank_event(...); crtc_state->event = e; }; :1126 β ret = setup_out_fence(...); :1055 β sync_file_create(fence) returns NULL on DFly; :1213 β predicate skips. Guest has no DRM atomic HW. fix.diff adds event_for_fence_only tracking and drm_event_cancel_free in the setup_out_fence error path.
PoC changes
Created evidence pack from scratch: README.md, VERDICT.md, build.sh, run.sh, env.txt, fix.diff, fix_apply.log, fix_build.log, manifest.json.
Verified recommended fix
Track event_for_fence_only per crtc iteration; in the setup_out_fence error path, drm_event_cancel_free the event when it was allocated solely for the out-fence path. Full diff in findings/poc/DF-1555/fix.diff.
Verdict
INCONCLUSIVE (HW-gated). Bug confirmed at source level: drm_atomic_uapi.c:1083-1090 prepare_signaling allocates drm_pending_vblank_event on OUT_FENCE_PTR crtc prop (even without PAGE_FLIP_EVENT). :1108-1133 fence block: krealloc + drm_crtc_create_fence + setup_out_fence; setup_out_fence calls sync_file_create at :1055 which on DFly is a STUB returning NULL (sync_file.h:48-53). Returns -ENOMEM at :1057. crtc_state->event->base.fence = fence at :1132 NEVER reached. complete_signaling :1213 cleanup predicate event && (event->base.fence || event->base.file_priv) skips -> leak. DRM atomic ioctl is reachable only with a DRM device supporting atomic; the audit guest has only stdio VGA.
No comments yet.