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);).
joyopendereferences it atjoy.c:159(if (joy->timeout[i])),joycloseatjoy.c:173(joy->timeout[i] = 0),joyreadatjoy.c:183-184(bus_space_handle_t port = joy->port; bus_space_tag_t bt = joy->bt;),joyioctlatjoy.c:237then: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/joynode for a non-attached minor, or - (b) device enumeration leaves a unit gap (attach of an intermediate
unit returned
ENXIOatjoy.c:128-129because 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.
Recommended fix
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
sys/kern/subr_bus.c:949-959βdevclass_get_softcreturns NULL for absent units.sys/dev/misc/joy/joy.c:58,70-71,133βUNITdecode,JOY_SOFTCwrapper,make_devcreation only for attached units.
Timeline
- 2026-07-25 Discovered during automated audit.
- 2026-07-25 Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2108 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
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
fixedbatch build rc=0
batch 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
No comments yet.