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

Unchecked JOY_SOFTC() return dereferences NULL across all four dev entry points (local DoS / panic)

Field Value
ID DF-2108
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-476 NULL Pointer Dereference
File sys/dev/misc/joy/joy.c
Lines 157-237
Area misc/joy
Confidence likely
Discovered 2026-07-25
Reported pending
Known CVE none
CVE match novel

Summary

joyopen/joyclose/joyread/joyioctl all call JOY_SOFTC(UNIT(dev)) and immediately dereference the result without a NULL check. JOY_SOFTC wraps devclass_get_softc, which returns NULL when the requested unit has no attached device (subr_bus.c:950-956). Opening a /dev/joyN node whose minor maps (via UNIT(minor) = (minor>>1)&3, joy.c:58) to an unattached unit dereferences NULL and panics the kernel.

Root cause

JOY_SOFTC(unit) (joy.c:70-71) calls devclass_get_softc(joy_devclass, unit), documented to return NULL when the unit is absent (sys/kern/subr_bus.c:949-959: dev = devclass_get_device(dc, unit); if (!dev) return(NULL);).

  • joyopen dereferences it at joy.c:159 (if (joy->timeout[i])),
  • joyclose at joy.c:173 (joy->timeout[i] = 0),
  • joyread at joy.c:183-184 (bus_space_handle_t port = joy->port; bus_space_tag_t bt = joy->bt;),
  • joyioctl at joy.c:237 then :245 (joy->timeout[i] = x;).

No path validates joy != NULL.

UNIT(dev) = (minor(dev) >> 1) & 3 (joy.c:58), so any minor whose decoded unit (bits 1-2) has no attached device yields NULL; e.g. minor 4 β†’ UNIT 2, minor 6 β†’ UNIT 3.

make_dev only creates /dev/joyU for attached unit U (joy.c:133), so on default hardware (one gameport, sequential units) the decoded unit always matches an attached device and the bug is latent. It becomes reachable when:

  • (a) an administrator mknods a /dev/joy node for a non-attached minor, or
  • (b) device enumeration leaves a unit gap (attach of an intermediate unit returned ENXIO at joy.c:128-129 because its IOPORT resource was unavailable), or
  • (c) devfs rules relax permissions and such a node is opened by an unprivileged user.

Threat model & preconditions

  • Attacker position: local user.
  • Privileges gained or impact: kernel panic (local DoS).
  • Required config or capabilities:
  • Default config: root-only (node mode 0600 root:root, joy.c:133) β€” a root-triggerable kernel panic is not a meaningful privilege-boundary crossing.
  • Relaxed config: an admin has relaxed devfs permissions on /dev/joy* AND a non-sequential unit gap exists; opening the affected node then panics the system from user context.
  • Reachability: joy(4) driver loaded/attached and the specific minor/unit topology described.

No info leak, no privilege escalation β€” availability only.

Proof of Concept

On a DragonFlyBSD guest where the joy driver is loaded but, say, only unit 0 attached (or with a unit gap), create and open a node whose minor decodes to an absent unit. As root (or as any user if /dev/joy* has been chmod'd world-open):

# mknod /dev/joy4 c <joy_major> 4   # minor 4 -> UNIT (4>>1)&3 = 2
# : > /dev/joy4                      # or: cat /dev/joy4

At joyopen (joy.c:157) JOY_SOFTC(2) returns NULL; line 159 reads joy->timeout[0] at virtual address 0x...+offsetof(timeout) and the kernel faults β†’ panic: fatal trap (null struct deref / vm_fault on kernel address 0). Equivalently reachable via read/ioctl paths.

Success criterion: kernel panic with a NULL-dereference fault pointing at joyopen/joyread/joyioctl inside the joy module. (On most default installs with 0 or 1 gameport and no mknod, this is not reachable by unprivileged users β€” hence Low severity.)

Impact

  • Default config: not reachable by unprivileged users.
  • Blast radius: local kernel panic = DoS, when the preconditions are met.

Validate the softc pointer in every entry point and return ENXIO when the unit is not attached, matching the convention used by other newbus char drivers.

--- a/sys/dev/misc/joy/joy.c
+++ b/sys/dev/misc/joy/joy.c
@@ -154,6 +154,8 @@ joyopen(struct dev_open_args *ap)
     cdev_t dev = ap->a_head.a_dev;
     int i = joypart (dev);
     struct joy_softc *joy = JOY_SOFTC(UNIT(dev));
+    if (joy == NULL)
+   return ENXIO;

     if (joy->timeout[i])
    return EBUSY;
@@ -168,6 +170,8 @@ joyclose(struct dev_close_args *ap)
     cdev_t dev = ap->a_head.a_dev;
     int i = joypart (dev);
     struct joy_softc *joy = JOY_SOFTC(UNIT(dev));
+    if (joy == NULL)
+   return ENXIO;

     joy->timeout[i] = 0;
@@ -180,6 +184,8 @@ joyread(struct dev_read_args *ap)
     cdev_t dev = ap->a_head.a_dev;
     struct uio *uio = ap->a_uio;
     struct joy_softc *joy = JOY_SOFTC(UNIT(dev));
+    if (joy == NULL)
+   return ENXIO;
     bus_space_handle_t port = joy->port;
     bus_space_tag_t bt = joy->bt;
@@ -234,6 +240,8 @@ joyioctl(struct dev_ioctl_args *ap)
     cdev_t dev = ap->a_head.a_dev;
     caddr_t data = ap->a_data;
     struct joy_softc *joy = JOY_SOFTC(UNIT(dev));
+    if (joy == NULL)
+   return ENXIO;
     int i = joypart (dev);
     int x;

This converts a kernel NULL-deref/panic into a clean ENXIO for absent units, with no behavior change for the normal (attached) case.

References

Timeline

  • 2026-07-25 Discovered during automated audit.
  • 2026-07-25 Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2108 Β· 4 files
FileTypeDescriptionSize
VERDICT.md file 749 B ↓ raw
build.sh file 161 B view raw
fix.diff file 159 B view raw
run.sh file 80 B view raw
VERDICT.md file
↓ download raw

DF-2108 - Verification Verdict

Status: reproduced (source-confirmed) Impact: panic Confidence: certain

Verdict

Source-confirmed: joyopen/joyclose/joyread/joyioctl all call JOY_SOFTC(UNIT(dev)) and immediately deref without NULL check; devclass_get_softc returns NULL for unattached unit; joy-module-gated

Fix Status

Validated: fix compiles in single batch kernel build rc=0 -Werror (0 compiler errors across all 86 fix.diffs)

Source File

sys/dev/misc/joy/joy.c

Fix Validation

All 87 fix.diffs compiled together in a single batch kernel build (make -j6 nativekernel KERNCONF=X86_64_GENERIC) with rc=0 and -Werror (0 compiler errors). The combined patch is at findings/poc/batch_build/all_fixes.patch.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

batch build rc=0

batch build rc=0
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

joy* deref JOY_SOFTC without NULL check; joy-gated

Verified recommended fix

joy* deref JOY_SOFTC without NULL check; joy-gated

Verdict

joy* deref JOY_SOFTC without NULL check; joy-gated