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

Atomic-ioctl out-fence path leaks a struct file per commit against the port's stubbed fd/sync_file shims: get_unused_fd_flags() really allocates while fd_install/fput/put_unused_fd are no-op stubs and sync_file_create() returns NULL β€” permanent nfiles exhaustion (system-wide ENFILE DoS until reboot)

Summary

SYSTEMIC compat-layer finding #6: on any DRIVER_ATOMIC device (i915/amdgpu), a DRM-master client issuing MODE_ATOMIC with an OUT_FENCE_PTR drives setup_out_fence, which calls get_unused_fd_flags - a REAL falloc in this port (struct file on the global list, nfiles++) - then publishes the fd number, hits the NULL-returning sync_file_create stub, and the error path's fput/put_unused_fd are no-op kprintf stubs. The struct file is never dropped: each attempt permanently leaks one file + 2 console lines. A trivial loop exhausts the global file table (~130k) after which every process system-wide fails open/socket/pipe with ENFILE until reboot. Master is auto-granted to the first card0 opener with NO privilege check. Same stubs also leak on the amdgpu_cs, drm_syncobj, and i915 execbuf out-fence paths. Refuse-out-fence fix until the shims are implemented. PoC builds on the guest; no DRM device.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4362 Β· 8 files
FileTypeDescriptionSize
trigger.c β€” 8.0 KB view raw
build.sh β€” 123 B view raw
run.sh β€” 147 B view raw
build.log β€” 284 B view raw
run.log β€” 134 B view raw
env.txt β€” 233 B view raw
VERDICT.md β€” 6.5 KB ↓ raw
fix.diff β€” 2.0 KB view raw
VERDICT.md
↓ download raw

DF-4362 β€” DRM atomic-ioctl out-fence path leaks struct files against unimplemented fd/sync_file shims

Verdict summary

  • Status: untested (runtime verification hardware-gated: the QEMU audit guest has no DRM device, and the only DRIVER_ATOMIC drivers in this tree β€” i915 and amdgpu β€” need real Intel/AMD graphics hardware that QEMU cannot emulate). Source-level certainty is certain: every link of the chain was verified in-tree, and the trigger builds cleanly on the guest.
  • Claimed impact (on i915/amdgpu machines): local DoS + permanent kernel memory leak, reachable by an unprivileged local user who opens the card before the display server does (auto-master), and by any running display server (DRM master).

Root cause (path:line)

sys/dev/drm/drm_atomic_uapi.c β€” prepare_signaling() CRTC out-fence block:

  1. drm_atomic_crtc_set_property() (sys/dev/drm/drm_atomic_uapi.c:460-469) accepts OUT_FENCE_PTR values (a DRM_MODE_PROP_ATOMIC range property 0..U64_MAX, created unconditionally in sys/dev/drm/drm_mode_config.c:288-292), writes -1 to the user pointer and stores it in the CRTC state.
  2. drm_mode_atomic_ioctl() (:1237) β€” any DRM-master client with file_priv->atomic (set via unprivileged DRM_IOCTL_SET_CLIENT_CAP, sys/dev/drm/drm_ioctl.c:327-337) β€” calls prepare_signaling() (:1360).
  3. prepare_signaling() (:1108-1133) sees the stored fence pointer, then setup_out_fence() (:1045-1060): * get_unused_fd_flags() β€” REAL allocation in this port (sys/dev/drm/include/linux/file.h:38-50): falloc() allocates a struct file (f_count=1, on the global file list, nfiles++) and reserves an fd number. The struct file * is discarded by the shim. * put_user(fd, out_fence_ptr) (:1052) β€” publishes the fd number to userspace. The fd is never installed: fd_install() is a no-op kprintf stub (file.h:52-56). * sync_file_create() (:1055) β€” stub that kprintf's and returns NULL (sys/dev/drm/include/linux/sync_file.h:48-53). * β†’ setup_out_fence returns -ENOMEM.
  4. Failure cleanup complete_signaling(install_fds=false) (:1222-1232) calls fput() and put_unused_fd() β€” both no-op kprintf stubs (file.h:58-68). The struct file allocated in step 3 is never dropped: nfiles is permanently incremented and ~sizeof(struct file) of kernel memory is leaked per call. The fd number handed to userspace refers to nothing.

Consequences

  • Permanent struct-file leak, system-wide: falloc() refuses to allocate once nfiles >= kern.maxfiles (sys/kern/kern_descrip.c, falloc: "kern.maxfiles limit exceeded"). Each out-fence attempt leaks one file; a trivial loop (while(1) ioctl(MODE_ATOMIC, {OUT_FENCE_PTR})) burns the global file table in seconds (default kern.maxfiles = 130112). Afterwards no process on the system can open() / socket() / pipe() (ENFILE) until reboot β€” logins, sshd, cron, everything. The loop does not even need to stop: get_unused_fd_flags kprintf's before calling falloc, so console/log spam continues forever.
  • Console/log spam from unprivileged context: two kprintf lines per call ("get_unused_fd_flags: is incomplete", "sync_file_create(): not implemented") β†’ unbounded dmesg/serial flooding.
  • Dangling fd published to userspace: the number written via put_user has no installed file (fdalloc only reserved the index).

Reachability / threat model

  • Requires a DRIVER_ATOMIC device: only i915 and amdgpu in this tree (sys/dev/drm/i915/i915_drv.c:3283, sys/dev/drm/amd/amdgpu/amdgpu_drv.c:1302). Standard desktop hardware; absent on servers without Intel/AMD graphics.
  • DRM_IOCTL_MODE_ATOMIC is gated DRM_MASTER|DRM_UNLOCKED (sys/dev/drm/drm_ioctl.c:681). DRM_IOCTL_SET_MASTER is DRM_ROOT_ONLY (drm_ioctl.c:579 area), but drm_master_open() (sys/dev/drm/drm_auth.c:261-274) makes the first opener of the primary node master automatically, with no privilege check. An unprivileged local user racing the display server to open("/dev/dri/card0") (before it starts, or after it crashes) becomes master. A running X/wayland server (or a compromised renderer inside it) is master as-is.
  • DRM_IOCTL_SET_CLIENT_CAP(ATOMIC) needs no privilege (drm_ioctl.c:312-337).
  • Not a remote bug; local-only, hardware-gated.

Verification run (QEMU audit guest β€” hardware-gated)

  • trigger.c built cleanly on the guest (cc -Wall -Wextra -O2) β€” see build.log.
  • Run result: open /dev/dri/card0: No such file or directory β†’ exit 1 (run.log). Guest has no /dev/dri; the tree's atomic drivers cannot bind to emulated QEMU hardware (no virtio-gpu/vkms in tree; radeon is not DRIVER_ATOMIC). This matches the repo's precedent for hardware-gated findings (e.g. DF-4160 xe_mii): source-level certainty, runtime untested.
  • On affected hardware the success criterion is: sysctl kern.openfiles climbs by 1 per iteration and never decreases (even after the process exits and fds are "closed"), console shows the two stub kprintf lines per iteration, ioctl returns ENOMEM.

Distinctness from knowns

  • DF-1555 = leaked drm_pending_vblank_event on a complete_signaling error path. This finding is the struct file lifecycle (falloc vs stubbed fd_install/fput/put_unused_fd) β€” a different object, different lines (:1048/:1052/:1055 vs the event handling), different fix.
  • The same stubs also make amdgpu_cs.c:1457, drm_syncobj.c:521 and i915_gem_execbuffer.c:2403 leak on their out-fence paths; same root cause, noted here, fix belongs in the shims / this file's gating.

Minimal security fix (see fix.diff): refuse non-NULL OUT_FENCE_PTR / WRITEBACK_OUT_FENCE_PTR values with -EOPNOTSUPP in drm_atomic_crtc_set_property() / drm_atomic_connector_set_property() until sync_file + the fd shims are actually implemented. The complete fix is to implement sync_file_create/sync_file_get_fence and fd_install/fput/put_unused_fd (requires threading the struct file * from falloc through get_unused_fd_flags, e.g. storing it in struct drm_out_fence_state).

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_testable
baseline no→ patch + rebuild →patched clean

Fix validation impossible on this guest: the trigger is hardware-gated (no DRM device), so neither the baseline bad behavior nor the patched behavior can be exercised. fix.diff is a source-level refusal patch (out-fence pointer values return -EOPNOTSUPP before any allocation), which makes the leak path unreachable by construction.

findings/poc/DF-4362/fix.diff
↓ fix.diffper-fix-DF-4362

Confirmed kernel references

Detail

Evidence (decisive lines)

trigger.c (self-contained PoC; builds on guest per build.log); run.log shows the honest hardware-gated outcome (open /dev/dri/card0: No such file or directory); env.txt (guest uname, /dev/dri absent, kern.maxfiles=130112); VERDICT.md contains the full path:line chain and impact analysis

PoC changes

Wrote the PoC from scratch (no seed existed): fully self-contained uapi structs/ioctl numbers verified against sys/dev/drm/include/uapi/drm/{drm,drm_mode}.h (SET_CLIENT_CAP=0x0d, OBJ_GETPROPERTIES=0xB9, MODE_ATOMIC=0xBC, drm_mode_atomic.reserved precedes user_data); prop discovery is dynamic via OBJ_GETPROPERTIES + per-prop atomic probe since the ioctl returns no prop names.

Verified recommended fix

Refuse non-NULL OUT_FENCE_PTR / WRITEBACK_OUT_FENCE_PTR values with -EOPNOTSUPP in drm_atomic_crtc_set_property()/drm_atomic_connector_set_property() until sync_file and the fd shims are implemented; the complete fix is implementing sync_file_create/sync_file_get_fence plus fd_install/fput/put_unused_fd (threading the falloc'd struct file * through get_unused_fd_flags).

Verdict

Hardware-gated: the QEMU audit guest has no DRM device (/dev/dri absent; the tree's only DRIVER_ATOMIC drivers, i915 and amdgpu, need real Intel/AMD graphics hardware QEMU cannot emulate), so the trigger could not run past open(). Source-level certainty is high: every link was verified in-tree. setup_out_fence() (drm_atomic_uapi.c:1045-1060) calls get_unused_fd_flags() which really allocates a struct file via falloc() (include/linux/file.h:38-50, nfiles++, f_count=1, on the global file list) and reserves an fd; put_user() publishes the never-installed fd number; sync_file_create() is a stub returning NULL (include/linux/sync_file.h:48-53); the failure path's fput()/put_unused_fd() are no-op stubs (include/linux/file.h:52-68), so the struct file is leaked permanently. One leaked file per DRM_IOCTL_MODE_ATOMIC commit carrying an OUT_FENCE_PTR value; a loop burns the global file table (default kern.maxfiles=130112) until every process system-wide gets ENFILE until reboot, plus two unbounded console kprintf lines per call from the stubs. Reachable by a local unprivileged user who is the first opener of /dev/dri/card0 (drm_master_open auto-master, drm_auth.c:261-274) satisfying the DRM_MASTER gate on the atomic ioctl (drm_ioctl.c:681), with the unprivileged SET_CLIENT_CAP(ATOMIC) (drm_ioctl.c:327).