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 | 704 B | β raw |
| build.sh | build-script | N/A (source-only) | 61 B | view raw |
| run.sh | run-script | N/A (source-only) | 87 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 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.