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-222c static int led_detach(struct gpio *gp, void *arg, int pin) { /* XXX: implement */ return 0; }returns0(success).gpio_consumer_detach(gpio.c:158-159) interprets0as "consumer cleaned up" and reports success (gpio.c:167-168).- Separately,
gpio_unregister(gpio.c:587-617) destroys onlygp->master_devandgp->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_openreadssc = dev->si_drv1(gpio_led.c:73) thenled_read/led_writecallgpio_pin_read/write(sc->gp, sc->gp_map, 0, ...)(gpio_led.c:131,152), which dereferencesgp->argandgp->pin_read/gp->pin_write(gpio.c:277,283) β UAF.
Threat model & preconditions
- Attacker position: root. Attaching the LED requires
GPIOATTACHon the0600master device, and reading/writing/dev/led/Xrequires the0600LED 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 itsstruct gpio(e.g.kldunloadof 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_registercaller issys/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.
Recommended fix
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
sys/dev/misc/gpio/gpio.c:158-159,587-617β consumer detach andgpio_unregisterneither tear down LED cdevs.sys/dev/misc/nsclpcsio/nsclpcsio_isa.c:372β the only in-treegpio_registercaller.
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-2113 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
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)
led_detach stub; GPIO-gated
Verified recommended fix
led_detach stub; GPIO-gated
Verdict
led_detach stub; GPIO-gated
No comments yet.