led_attach() kstrdup()s the GPIOATTACH consumer arg without type validation: GPIO_TYPE_INT delivers an integer-as-pointer (arbitrary strlen/kstrdup deref) and the string variant is not termination-bounded (kernel-stack over-read)
Summary
The framework casts a user integer to void* for GPIO_TYPE_INT and the consumer kstrdup()s it - lint=1 is a guaranteed panic, a mapped address discloses kmem as the world-visible /dev/led/<name>; the string variant walks past the unterminated 32B struct into the mapped_ioctl stack staging buffer. Root-only, non-GENERIC config, Super I/O hw. Trigger built+run on guest (ENOENT - no entry point on stock kernel), verdict inconclusive/static-certain. Snprintf-the-int + bounded-copy fix (two files).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5104 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.2 KB | β raw | |
| VERDICT.md | β | 5.0 KB | β raw | |
| gpioled_int_kstrdup.c | β | 2.8 KB | view raw | |
| build.sh | β | 92 B | view raw | |
| run.sh | β | 148 B | view raw | |
| build.log | β | 74 B | view raw | |
| run.log | β | 141 B | view raw | |
| env.txt | β | 277 B | view raw | |
| fix.diff | β | 2.0 KB | view raw | |
| verdict.json | β | 3.9 KB | view raw |
DF-5104 β gpio_led.c led_attach() kstrdup() of GPIOATTACH consumer arg
(integer-as-pointer dereference + unterminated 32-byte string over-read)
What
led_attach() treats the GPIOATTACH consumer_arg unconditionally as a
NUL-terminated C string:
sys/dev/misc/gpio/gpio_led.c:193βsc->name = kstrdup((char *)arg, M_LED);
But the gpio master ioctl delivers arg as a raw integer cast to a
pointer whenever arg_type == GPIO_TYPE_INT:
sys/dev/misc/gpio/gpio.c:473-477β(gpaa->arg_type == GPIO_TYPE_INT) ? ((void *)gpaa->consumer_arg.lint) : (gpaa->consumer_arg.string)
The only guard in led_attach is arg == NULL (gpio_led.c:186-187), i.e.
lint == 0. Any other integer reaches kstrdup() β strlen() on an
attacker-chosen kernel address.
Impact (on a system where the misc/gpio framework is live)
lint = 1(or any small non-zero value) βstrlen((char *)1)β guaranteed kernel page fault / panic. Trivial root-triggered DoS.lint = <mapped kernel address>βkstrdup()copies kernel memory as a string; that string is then published world-readably twice:- as the devfs node name
make_dev(..., "led/%s", sc->name)(gpio_led.c:206-207) β anyone canls /dev/led/; - via
kprintf("gpio_led: Attached led '%s' ...")(gpio_led.c:211-212) to the console / message buffer. β kernel-memory disclosure (as a NUL-terminated string) from a root ioctl. - String path:
consumer_arg.string[32](gpio.h:119) need not be NUL-terminated. DFlymapped_ioctl()stages the 64-byte ioctl argument in a 128-byte kernel stack buffer (sys/kern/sys_generic.c:572-574,674-679) with only 64 bytes initialized βkstrdup()walks past the struct into stale kernel stack and leaks it into the node name/console the same way (CWE-170 / CWE-125).
Preconditions (why Low, not High)
- ioctl on
/dev/gpio/<ctrl><unit>/masterβ mode 0600 root:wheel (gpio.c:574) β root only. - Kernel must be built with the misc/gpio framework:
device gpio(sys/conf/files:2119-2120) β not inX86_64_GENERIC(that kernel has only the unrelatedbus/gpiogpio_acpi/gpio_intelframework, sys/config/X86_64_GENERIC:332-333). - A registered controller must exist β in-tree that is only
nsclpcsio(4)(sys/conf/files:2599), PC87366 Super I/O GPIO hardware.
Reproduce
./build.sh # cc -Wall -o gpioled_int_kstrdup gpioled_int_kstrdup.c
./run.sh # as root on a gpio-equipped system
# -> expect immediate kernel panic (strlen @ addr 1)
run.sh takes an optional master-node path (default
/dev/gpio/pc873660/master, the node nsclpcsio unit 0 would create).
What happened on the audit guest (2026-09-27)
Built and ran the trigger as unprivileged maxx: no /dev/gpio and no
/dev/led exist β the guest kernel (uname: X86_64_GENERIC build) does
not compile the misc/gpio framework at all and QEMU does not emulate the
PC87366 GPIO logical device, so the kernel code path has no entry point on
this guest. See build.log, run.log, env.txt. Verdict:
inconclusive (trigger exercised, kernel path unreachable in this
environment; the defect itself is statically certain).
VERDICT β DF-5104
Status: inconclusive (runtime entry point absent on the audit guest; defect statically certain) Β· Severity: Low (root-only, non-GENERIC kernel config + specific Super I/O hardware) Β· Confidence: certain (code-path proof below is 6 lines long and fully type-checkable).
Reproduced?
Not on this guest β and provably not possible on this guest:
run.log:open("/dev/gpio/pc873660/master")β ENOENT;/dev/gpioand/dev/leddo not exist.env.txt: guest kernel is a stockX86_64_GENERICbuild;/usr/src/sys/config/X86_64_GENERICcontainsdevice gpio_acpi/device gpio_intel(the separatebus/gpionewbus framework) and nodevice gpio, nodevice nsclpcsio.sys/conf/files:2119-2120compilesdev/misc/gpio/{gpio,gpio_led}.conly whenoptional gpiois configured β gpio_led.c is not even in the guest kernel image.- The only in-tree controller registering a
struct gpioisnsclpcsio_isa.c:365-372(PC87366 Super I/O); QEMU does not emulate its GPIO logical device, so even a rebuilt kernel would expose no master node.
A custom kernel (device gpio + device nsclpcsio) on PC87366 hardware β
or any out-of-tree controller driver using this framework β makes the path
live: GPIOATTACH(arg_type=GPIO_TYPE_INT, lint=1) β panic in
strlen()/kstrdup().
Static proof (path:line)
sys/kern/sys_generic.c:674-686β ioctl data copyin'd into kernel buffer (ubuf.stkbuf[128]on the caller's kernel stack, or kmalloc'dM_IOCTLOPSif bigger; GPIOATTACH is 64 bytes β stack buffer).sys/dev/misc/gpio/gpio.c:471-478βcase GPIOATTACH:callsgpio_consumer_attach(gpaa->consumer_name, (gpaa->arg_type == GPIO_TYPE_INT) ? ((void *)gpaa->consumer_arg.lint) : (gpaa->consumer_arg.string), ...). The consumer API (gpio.h:103) passesvoid *argwith no arg_type discriminator β a consumer cannot tell integer from string.sys/dev/misc/gpio/gpio.c:112-117β the registered consumer is found (consumer_name == "led") andgcp->consumer_attach(gp, arg, pin, mask)is invoked.sys/dev/misc/gpio/gpio_led.c:186-187β only guard:if (arg == NULL) return 1;(rejectslint == 0only).sys/dev/misc/gpio/gpio_led.c:193βsc->name = kstrdup((char *)arg, M_LED);βstrlen()on the integer-as-pointer (panic for small values, kernel-memory-as-string copy for mapped addresses).- Publication of the copied string:
gpio_led.c:206-207(make_dev(..., "led/%s", sc->name)β world-listable devfs node) andgpio_led.c:211-212(kprintf("gpio_led: Attached led '%s' ...")). - Unterminated-string variant:
sys/dev/misc/gpio/gpio.h:119(char string[32]inside the 64-byte ioctl struct staged in the 128-byte stack buffer) β a 32-byte non-NULstringmakeskstrdup()read stale kernel stack past offset 64 (CWE-170 β CWE-125).
Not this finding (known, pass 1): DF-2112 (led_attach gpio_map-fail leak), DF-2113 (unimplemented led_detach UAF family).
Why the value/write path is NOT a finding (killed)
led_writebounds:uio_resid > sizeof(int)unsigned compare makes negative residuals EINVAL (gpio_led.c:115-116);uiomovecopies β€ 4 bytes into a 4-byteint(gpio_led.c:120) β no overflow, no OOB.- Derived value β [-128, 207] (
gpio_led.c:124-128) is passed raw togpio_pin_write()(gpio_led.c:131) unlikegpio_write()which validatesGPIO_PIN_LOW|HIGH(gpio.c:356-357) β but the only in-tree controller treats every value β {0,1} as a no-op (nsclpcsio_isa.c:608-611: if (value == 0) ... else if (value == 1)), so no memory-safety or register-corruption consequence exists in-tree (Info-level hardening only). - Map range:
gpio_map()validatesoffset + iagainstgp->npinsand rejects mapped/opened pins (gpio.c:211-226); LED maps are size 1 (mask=1, gpio_led.c:197) somap->map[0](gpio_led.c:131,152,237) is always in bounds. - No hints parsing exists anywhere for gpio consumers (attach is
GPIOATTACH-only), no callout/blink machinery,
led_ioctlis a no-op (gpio_led.c:162-169). - Lock order: nesting is one-way
gpiodev_lock β gpio_lock β led_lock; the theoreticalled_lock β gpio_map β gpio_lockinversion is defused becauseled_attachis only ever entered withgpio_lockalready held andgpio_map()'s lockstatus() probe (gpio.c:196-199) then skips re-acquisition. Latent fragility, not a reachable deadlock.
Fix
Two parts (framework + consumer). See fix.diff.
code_hash (final trigger source):
1ab846571d9969a91d211e2c36c3c216926df2885dcf3c29749e9a3eae5c983b
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
not_testablefix.diff authored post-verification against read-only sys/ (never applied); cannot be kernel-build validated on this guest because the gpio framework is not in its kernel config and the PC87366 controller hardware is not emulated
findings/poc/DF-5104/fix.diff
Confirmed kernel references
Detail
Exploit chain
(not executed) On a gpio-equipped system as root: open("/dev/gpio/pc873660/master") -> ioctl(GPIOATTACH, {consumer_name="led", arg_type=GPIO_TYPE_INT, lint=1}) -> gpio_consumer_attach -> led_attach -> kstrdup((char *)1) -> strlen at address 1 -> kernel panic. With lint=
Evidence (decisive lines)
VERDICT.md (static proof chain, path:line); run.log (ENOENT β no /dev/gpio on guest); env.txt (guest kernel config, uname); build.log (trigger compiles clean); gpioled_int_kstrdup.c (trigger); fix.diff (verified two-part fix)
PoC changes
wrote from scratch (no seed); userland mirror of struct gpio_attach_args + GPIOATTACH since
Verified recommended fix
gpio.c GPIOATTACH: convert GPIO_TYPE_INT lints to a decimal string before gpio_consumer_attach(); gpio_led.c led_attach: replace kstrdup with a bounded 32-byte copy + force NUL (see fix.diff)
Verdict
Static defect is certain: gpio_master_ioctl() (sys/dev/misc/gpio/gpio.c:471-478) hands consumers (void )(long)consumer_arg.lint when GPIO_TYPE_INT is set, and led_attach()'s only guard is arg==NULL (gpio_led.c:186-187) before kstrdup((char )arg) (gpio_led.c:193) β strlen() on an attacker-chosen value (lint=1 => guaranteed kernel panic; lint=device gpio/device nsclpcsio in X86_64_GENERIC; /dev/gpio and /dev/led absent β run.log/env.txt) and QEMU does not emulate the PC87366 GPIO logical device: the kernel path has no entry point on this guest. Root-only (master node 0600, gpio.c:574) + non-GENERIC config + Super I/O hardware => Low severity.
No comments yet.