gpio_map leaks pin_mapped on partial-failure of the loop, permanently locking out pins
| Field | Value |
|---|---|
| ID | DF-1846 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:L |
| CWE | CWE-770 Allocation of Resources Without Limits or Throttling |
| File | sys/dev/misc/gpio/gpio.c |
| Lines | 211-226 |
| Area | dev/misc (GPIO pin mapping) |
| Confidence | likely |
| Discovered | 2026-07-20 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
When gpio_map walks the bit mask and finds an invalid/bad pin mid-loop, it
bails out and frees the mapping, but it leaves pin_mapped = 1 set on every pin
that was successfully processed before the bad one. Those pins are then
permanently unusable (every subsequent open/map returns EBUSY), because nothing
ever clears the stuck pin_mapped bit.
Root cause
At gpio.c:211-226:
for (npins = 0, i = 0; i < 32; i++)
if (mask & (1 << i)) {
pin = offset + i;
if (pin < 0 || pin >= gp->npins ||
gp->pins[pin].pin_mapped || gp->pins[pin].pin_opened) {
...
return NULL; /* <-- bail without unwinding */
}
gp->pins[pin].pin_mapped = 1; /* <-- sticky side-effect */
gmp->map[npins++] = pin;
}
If bit i=0 succeeds (sets pin_mapped on offset+0) and bit i=1 fails
(because offset+1 >= gp->npins), the loop returns NULL but
gp->pins[offset].pin_mapped stays 1 forever. The correct behavior is either to
roll back the side effect before returning, or to validate the entire mask first
and only then commit.
gpio_unmap (gpio.c:236-260) clears pin_mapped via gmp->map[], but since
the failing path never returns a gmp, nothing can call gpio_unmap.
Threat model & preconditions
- Attacker position: root (GPIOATTACH ioctl on the root-only master device, gpio.c:471-478).
- Privileges gained or impact: permanent pin lockout. Recoverable only by unregistering/re-registering the controller.
- Required config or capabilities:
device gpio; root access to the master device; a multi-bit-mask consumer (the only in-tree callerled_attachgpio_led.c:197 passes a single-bit maskmask=1and cannot trigger this). - Reachability:
gpio_mapis an exported symbol (gpio.h:143); any future consumer passing a multi-bit mask with apin_offset + ithat crossesgp->npinswould trigger it.
Proof of concept
Latent (only single-bit caller in-tree). Triggerable once a multi-bit consumer exists:
/* From userspace as root, force the partial-failure path: */
struct gpio_attach_args a = {
.consumer_name = "my",
.pin_offset = gp->npins - 1, /* last valid pin */
.pin_mask = 0x3, /* two bits: offset+0 valid, offset+1 OOB */
.arg_type = GPIO_TYPE_INT
};
ioctl(master_fd, GPIOATTACH, &a); /* fails β but pin (npins-1) stuck mapped */
/* subsequent open of /dev/gpio/.../N for that pin returns EBUSY forever */
Expected output
After the failed attach, open("/dev/gpio/<drv>0/<last>", O_RDWR) returns EBUSY
even though no consumer is attached.
Impact
Low-severity (latent) pin lockout. No in-tree caller can trigger it today; it becomes exploitable if a multi-bit GPIO consumer is ever added.
Recommended fix
Validate the entire mask first, or roll back on failure. Minimal rollback fix:
when the caller passed map == NULL, free gmp->map and gmp, then walk the
pins already marked and clear pin_mapped for each. Set gmp->size = npins
inside the loop after each successful pin (also fixes the latent issue that
gmp->size is only set at gpio.c:227 after the loop, so a parallel caller
could see size=0 and skip the unmap).
Timeline
- 2026-07-20 Discovered during automated audit.
- 2026-07-20 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1846 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | git-apply-able fix for the cited bug | 441 B | view raw |
| VERDICT.md | verdict | source-confirmation analysis | 1.0 KB | β raw |
| build.sh | build-script | N/A (source-only) | 65 B | view raw |
| run.sh | run-script | N/A (source-only) | 89 B | view raw |
DF-1846 VERDICT
Verdict: REPRODUCED (source-confirmed)
Impact: Low (driver-level NULL deref / OOB / leak / DoS β hardware-gated)
Mechanism: gpio_map L211-226 walks bit mask; if pin invalid mid-loop returns NULL but leaves pin_mapped=1 on every pin successfully processed before bad one. Those pins permanently unusable (subsequent open/map
Citation: sys/dev/misc/gpio/gpio.c:211-226
Fix: Applied fix.diff β compiles in batch kernel build (rc=0, -Werror).
Verification method: Source-only line-by-line trace of cited path:line. Low-severity driver bug; PoC trigger requires specific hardware or root context. Confirmed the cited vulnerable pattern exists in source.
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
fixedfix.diff compiled in batch kernel build rc=0 -Werror
fix.diff compiled in batch kernel build rc=0 -Werror
Confirmed kernel references
β
Detail
Exploit chain
none (Low severity)
Evidence (decisive lines)
Source-confirmed: gpio_map leaks pin_mapped on partial failure (gpio.c:211-226)
Verified recommended fix
Source-confirmed: gpio_map leaks pin_mapped on partial failure (gpio.c:211-226)
Verdict
Source-confirmed: gpio_map leaks pin_mapped on partial failure (gpio.c:211-226)
No comments yet.