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

Use-after-free: unimplemented led_detach leaves dangling gpio pointer in surviving LED cdevs

Field Value
ID DF-2113
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H
CWE CWE-416 Use After Free
File sys/dev/misc/gpio/gpio_led.c
Lines 206-222
Area misc/gpio
Confidence likely
Discovered 2026-07-25
Reported pending
Known CVE none
CVE match novel

Summary

led_detach is a stub (gpio_led.c:217-222) that returns success without destroying the /dev/led/<name> cdev, without unmapping the gpio pin, and without detaching sc from the gpio struct. gpio_unregister (gpio.c:587-617) does not iterate or notify consumers either, so when a GPIO controller driver detaches and frees its struct gpio, every surviving LED softc retains sc->gp and sc->gp_map pointing at freed memory. The next open/read/write on /dev/led/<name> dereferences sc->gp (gpio_led.c:131,152,237 via gpio_pin_write/read in gpio.c:274-284) β†’ use-after-free, typically a kernel panic or exploitable corruption depending on what recycles the freed gpio struct.

Root cause

  • gpio_led.c:217-222 c static int led_detach(struct gpio *gp, void *arg, int pin) { /* XXX: implement */ return 0; } returns 0 (success).
  • gpio_consumer_detach (gpio.c:158-159) interprets 0 as "consumer cleaned up" and reports success (gpio.c:167-168).
  • Separately, gpio_unregister (gpio.c:587-617) destroys only gp->master_dev and gp->pins[i].dev (gpio.c:596-602) and never calls any consumer detach.

So nothing in the GPIO stack ever tears down the LED cdev created at gpio_led.c:206-208 (make_dev(..., "led/%s", sc->name); sc->dev->si_drv1 = sc). After the controller driver's detach handler frees gp (the struct gpio owned by the controller driver, e.g. nsclpcsio_isa's sc->sc_gpio_gc), sc->gp (gpio_led.c:195) and sc->gp_map->gp (gpio.c:202) dangle.

  • led_open reads sc = dev->si_drv1 (gpio_led.c:73) then led_read/led_write call gpio_pin_read/write(sc->gp, sc->gp_map, 0, ...) (gpio_led.c:131,152), which dereferences gp->arg and gp->pin_read/gp->pin_write (gpio.c:277,283) β†’ UAF.

Threat model & preconditions

  • Attacker position: root. Attaching the LED requires GPIOATTACH on the 0600 master device, and reading/writing /dev/led/X requires the 0600 LED device (gpio_led.c:206-207). The dereference itself also requires root.
  • Privileges gained or impact: kernel memory corruption / panic; with heap grooming by root, potentially arbitrary kernel code execution (though root already has kldload, so no privilege boundary is gained).
  • Required config or capabilities: to reach the UAF the attacker: (1) attaches a LED via ioctl(master, GPIOATTACH, ...); (2) causes the GPIO controller driver to detach and free its struct gpio (e.g. kldunload of a loadable controller, or physical/device removal on hotplug-capable buses); then (3) opens and reads /dev/led/<name>.
  • Reachability: the only in-tree gpio_register caller is sys/dev/misc/nsclpcsio/nsclpcsio_isa.c:372 (ISA, typically not runtime-detachable), which is why confidence is "likely" rather than "certain" β€” the code defect is unambiguous, but the realistic trigger requires a detachable controller.

Proof of Concept

Conceptual chain (concrete source the runner can adapt once a loadable GPIO controller is available in the test guest):

// Step 1 (root): attach a LED to the controller
int m = open("/dev/gpio/<ctl>/master", O_RDWR);
struct gpio_attach_args a = { .arg_type=0 };
strlcpy(a.consumer_name, "led", sizeof(a.consumer_name));
strlcpy(a.consumer_arg.string, "victim", sizeof(a.consumer_arg.string));
a.pin_offset = 0; a.pin_mask = 1;
ioctl(m, GPIOATTACH, &a);

// Step 2 (root): unload the GPIO controller driver so its struct gpio is freed
//   kldunload <controller_module>   OR unplug the device on hotplug buses
//   -> gpio_unregister(gp) destroys /dev/gpio/<ctl>/* but NOT /dev/led/victim
//   -> controller driver frees struct gpio after gpio_unregister returns

// Step 3 (root): touch the stale LED cdev -> dereferences freed gp
int l = open("/dev/led/victim", O_RDWR);
int zero = 0;
write(l, &zero, sizeof(zero));   // gpio_led.c:131 -> gpio_pin_write
                                 // -> gp->pin_write(gp->arg,...) UAF

Expected output

Kernel panic on the write (e.g. fatal trap in gp->pin_write indirect
call, or NULL deref if the slab was zeroed), confirming the dangling
gp deref.

For a corruption PoC, groom the freed sizeof(struct gpio) slab with a controlled allocation between step 2 and step 3 to steer gp->pin_write.

Impact

  • Default config: not triggered unless a runtime-detachable GPIO controller is present.
  • Blast radius: kernel UAF β†’ panic, or kernel code execution with heap grooming by root.

Implement led_detach to destroy the cdev, unmap the pin, and free the softc for every LED attached to the detaching gpio/pin. Must hold led_lock across the scan; gpio_unmap internally takes gpio_lock only if not already held, so no inversion is introduced (order stays gpio_lock β†’ led_lock from the caller gpio_consumer_detach which already holds gpio_lock).

--- a/sys/dev/misc/gpio/gpio_led.c
+++ b/sys/dev/misc/gpio/gpio_led.c
@@ -217,8 +217,28 @@ led_detach(struct gpio *gp, void *arg, int pin)
+   struct ledsc *sc, *tmp;
+
+   lockmgr(&led_lock, LK_EXCLUSIVE);
+   LIST_FOREACH_MUTABLE(sc, &led_list, list, tmp) {
+       if (sc->gp != gp || sc->pin != pin)
+           continue;
+       LIST_REMOVE(sc, list);
+       /* destroy_dev drains active opens, so it is safe even if opened */
+       if (sc->dev != NULL)
+           destroy_dev(sc->dev);
+       if (sc->gp_map != NULL)
+           gpio_unmap(sc->gp_map);
+       if (sc->name != NULL)
+           kfree(sc->name, M_LED);
+       kfree(sc, M_LED);
+   }
+   lockmgr(&led_lock, LK_RELEASE);
+   return 0;
 }

Additionally, gpio_unregister (gpio.c:587) should iterate registered consumers and call consumer_detach for every pin of the controller before returning, so that drivers which free struct gpio immediately after gpio_unregister cannot leave consumers dangling. That second change belongs in the gpio.c audit pass; the gpio_led.c-side fix above closes the LED consumer's half of the race.

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-2113 Β· 4 files
FileTypeDescriptionSize
VERDICT.md file 707 B ↓ raw
build.sh file 161 B view raw
fix.diff file 165 B view raw
run.sh file 80 B view raw
VERDICT.md file
↓ download raw

DF-2113 - Verification Verdict

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

Verdict

Source-confirmed: led_detach (:217-222) is a stub returning 0 without destroying cdev/unmapping gpio/detaching sc; incomplete teardown; GPIO-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/gpio/gpio_led.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)

led_detach stub; GPIO-gated

Verified recommended fix

led_detach stub; GPIO-gated

Verdict

led_detach stub; GPIO-gated