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

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 caller led_attach gpio_led.c:197 passes a single-bit mask mask=1 and cannot trigger this).
  • Reachability: gpio_map is an exported symbol (gpio.h:143); any future consumer passing a multi-bit mask with a pin_offset + i that crosses gp->npins would 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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1846 Β· 4 files
FileTypeDescriptionSize
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
VERDICT.md verdict source-confirmation analysis
↓ download 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff compiled in batch kernel build rc=0 -Werror

fix.diff compiled in batch kernel build rc=0 -Werror
↓ fix.diffcombined build rc=0

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)