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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5104 Β· 10 files
FileTypeDescriptionSize
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:

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 can ls /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. DFly mapped_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 in X86_64_GENERIC (that kernel has only the unrelated bus/gpio gpio_acpi/gpio_intel framework, 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.md
↓ download raw

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/gpio and /dev/led do not exist.
  • env.txt: guest kernel is a stock X86_64_GENERIC build; /usr/src/sys/config/X86_64_GENERIC contains device gpio_acpi / device gpio_intel (the separate bus/gpio newbus framework) and no device gpio, no device nsclpcsio.
  • sys/conf/files:2119-2120 compiles dev/misc/gpio/{gpio,gpio_led}.c only when optional gpio is configured β†’ gpio_led.c is not even in the guest kernel image.
  • The only in-tree controller registering a struct gpio is nsclpcsio_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)

  1. 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'd M_IOCTLOPS if bigger; GPIOATTACH is 64 bytes β‡’ stack buffer).
  2. sys/dev/misc/gpio/gpio.c:471-478 β€” case GPIOATTACH: calls gpio_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) passes void *arg with no arg_type discriminator β€” a consumer cannot tell integer from string.
  3. sys/dev/misc/gpio/gpio.c:112-117 β€” the registered consumer is found (consumer_name == "led") and gcp->consumer_attach(gp, arg, pin, mask) is invoked.
  4. sys/dev/misc/gpio/gpio_led.c:186-187 β€” only guard: if (arg == NULL) return 1; (rejects lint == 0 only).
  5. 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).
  6. Publication of the copied string: gpio_led.c:206-207 (make_dev(..., "led/%s", sc->name) β€” world-listable devfs node) and gpio_led.c:211-212 (kprintf("gpio_led: Attached led '%s' ...")).
  7. 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-NUL string makes kstrdup() 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_write bounds: uio_resid > sizeof(int) unsigned compare makes negative residuals EINVAL (gpio_led.c:115-116); uiomove copies ≀ 4 bytes into a 4-byte int (gpio_led.c:120) β€” no overflow, no OOB.
  • Derived value ∈ [-128, 207] (gpio_led.c:124-128) is passed raw to gpio_pin_write() (gpio_led.c:131) unlike gpio_write() which validates GPIO_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() validates offset + i against gp->npins and rejects mapped/opened pins (gpio.c:211-226); LED maps are size 1 (mask=1, gpio_led.c:197) so map->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_ioctl is a no-op (gpio_led.c:162-169).
  • Lock order: nesting is one-way gpiodev_lock β†’ gpio_lock β†’ led_lock; the theoretical led_lock β†’ gpio_map β†’ gpio_lock inversion is defused because led_attach is only ever entered with gpio_lock already held and gpio_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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-5104

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= the copied string becomes the /dev/led/ node name (world-listable) and is kprintf'd => kmem disclosure.

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 is kernel-only (cdev_t via sys/conf.h); 64-byte struct verified against kernel layout

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= => kernel-memory-as-string disclosure published via the world-listable /dev/led/ node (gpio_led.c:206-207) and kprintf (gpio_led.c:211-212)). The 32-byte string variant over-reads the non-NUL-terminated consumer_arg.string staged in mapped_ioctl()'s 128-byte kernel stack buffer (sys_generic.c:572-574,674-686), leaking stale kernel stack the same way. NOT exercised at runtime because the misc/gpio framework is not compiled into the audit guest (no 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.