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

gpio_unregister leaks the master device's clone unit and the shared struct gpio_driver (unlinked while sibling controllers still reference it)

Summary

Every controller attach/detach cycle leaks heap: the master cdev clone unit is never returned (no devfs_clone_bitmap_put for :573's get), and struct gpio_driver+name is LIST_REMOVE'd but never kfree'd - unconditionally, even while sibling controllers sharing the driver_name still reference it (no refcount), so the sibling's later unregister skips its unit put. Unbounded unit-space+heap creep per cycle; privileged trigger. Refs+put+conditional-free fix, compile-validated as KLD with -Werror.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4752 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 3.3 KB ↓ raw
trigger.sh β€” 1.6 KB view raw
build.sh β€” 122 B view raw
run.sh β€” 152 B view raw
build.log β€” 5.9 KB view raw
fix_build.log β€” 5.9 KB view raw
run.log β€” 656 B view raw
env.txt β€” 702 B view raw
fix.diff β€” 1.1 KB view raw
fix_gpio.c.patched β€” 15.1 KB ↓ download
manifest.json β€” 904 B view raw
verdict.json β€” 2.8 KB view raw

DF-4752 β€” gpio(4) gpio_unregister leaks the master clone unit + struct gpio_driver

File: sys/dev/misc/gpio/gpio.c (pass 2)

What

gpio_unregister() (gpio.c:587-617) has two resource-accounting defects that are distinct from the known pin-unit get-vs-put bug (DF-1845, gpio.c:596-600):

  1. Master device clone unit never returned. gpio_register() allocates a unit from DEVFS_CLONE_BITMAP(gpio) for the master cdev (gpio.c:573-575, make_dev(..., "gpio/%s%d/master", ...)). gpio_unregister() destroys that dev (gpio.c:602) but never calls devfs_clone_bitmap_put() for it β€” the unit stays allocated forever. (The pin loop right above at least attempts a bitmap call, albeit the wrong one.)

  2. struct gpio_driver + name never freed, dropped from the list while siblings may still use it. gpd is allocated once per driver name (gpio.c:559-565, shared by all controllers with the same driver_name; gpd->name = kstrdup(...)). gpio_unregister() does devfs_clone_bitmap_put(&gpd->unit_bitmap, ...); LIST_REMOVE(gpd, link); (gpio.c:609-611) with no kfree of gpd or gpd->name, and no refcount: - every unregister leaks sizeof(struct gpio_driver) + name (M_TEMP); - if two controllers share the name, the first unregister unlinks the shared gpd while the sibling is still live; the sibling's own unregister then finds no list entry and silently skips its bitmap put.

Impact

Permanent kernel heap leak per controller attach/detach cycle plus monotonic clone-unit consumption (the bitmap auto-extends β€” devfs_clone_bitmap_get() is called with limit 0, which never fails β€” so this is unbounded creep, not a hard exhaustion). Trigger is privileged (driver detach / kldunload), hence Low severity, consistent with the DF-1845/DF-1846 family.

Reproduce (hardware/config-gated)

Run ./trigger.sh on a system with device gpio + a GPIO controller (nsclpcsio/PC87366 etc.). On the audit guest the framework is not compiled in (optional gpio, not in X86_64_GENERIC), so the trigger exits 3 (SKIP) β€” defect verified by source trace; fix compile-validated (see VERDICT.md).

Fix

fix.diff (applies to sys/dev/misc/gpio/gpio.c): put the master unit back, refcount gpd, free on last reference. Compile-validated as a kernel module against the guest's 6.5-DEVELOPMENT source with -Werror (fix_build.log).

VERDICT.md
↓ download raw

DF-4752 VERDICT

Status: INCONCLUSIVE (runtime) β€” defect certain by source trace; fix compile-validated. Runtime reproduction is hardware/kernel-config-gated on the audit guest.

Impact: Low β€” permanent kernel heap leak (struct gpio_driver + name + one clone-bitmap word per cycle) on every GPIO controller detach/re-attach. Privileged trigger only (driver detach / module unload). No memory-safety consequence; same severity family as DF-1845/DF-1846.

Why it is real (source trace)

  • gpio.c:573 unit = devfs_clone_bitmap_get(&DEVFS_CLONE_BITMAP(gpio), 0); β€” master unit allocated; gpio.c:574-575 make_dev(..., unit, ... "gpio/%s%d/master", ...).
  • gpio.c:596-602 (unregister): the pin loop calls the bitmap (wrongly β€” DF-1845, known, NOT re-reported) and destroy_dev(gp->master_dev); at gpio.c:602 β€” no devfs_clone_bitmap_put for the master's unit anywhere in the function (verified by reading the whole function and by rg devfs_clone_bitmap over the file: register = 3 gets, unregister = 1 get(bug) + 1 put(driver_unit)).
  • gpio.c:552-566 (register): one gpd per driver name; second controller of the same name reuses it (master_unit = devfs_clone_bitmap_get(...)), no refcount kept.
  • gpio.c:604-613 (unregister): LIST_REMOVE(gpd, link); break; β€” gpd and gpd->name never kfreed (leak), and the unlink happens even when sibling controllers of the same name are still registered (they lose their put).
  • devfs_clone_bitmap_get(bitmap, 0) (sys/vfs/devfs/devfs_helper.c): with limit == 0 the unit > limit guard is disabled β†’ never returns -1; the bitmap auto-extends β†’ consequence is unbounded unit-space/heap creep, not allocation failure.

Verification performed on the guest

  1. Feature-gating confirmed (run.log): /dev/gpio* absent; no /boot/kernel/gpio.ko; sys/dev/misc/gpio/gpio.c is optional gpio (sys/conf/files:2119) and X86_64_GENERIC has no device gpio β†’ the framework cannot be instantiated on this guest (needs a kernel with device gpio plus a controller such as nsclpcsio/PC87366 hardware).
  2. Fix compile-validated: baseline gpio.c and fix.diff-patched gpio.c both built as KLD modules against the guest's pristine /usr/src (DragonFly 6.5-DEVELOPMENT) with -Werror: * build.log (baseline) β€” rc=0 * fix_build.log (patched) β€” rc=0
  3. sys/ tree untouched; guest /usr/src restored and hash-verified (c93a0e25… for gpio.c, 3c863567…/22808bde… for cxm.h/cxm.c) after the fix build.

Fix

fix.diff: add refs to struct gpio_driver, increment on reuse, devfs_clone_bitmap_put() the master unit after destroy_dev(), and kfree() gpd+name when the last reference drops.

Baseline vs patched

Not runtime-testable on this guest (feature absent). The patch is a pure resource-accounting change; both variants compile clean with -Werror. fix_status=inconclusive (build verified, behavior hardware-gated).

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

inconclusive
baseline no→ patch + rebuild →patched clean

fix.diff compiles clean as a KLD module (-Werror, rc=0) against guest 6.5-DEVELOPMENT source. Behavioral validation (unit returned, gpd freed, siblings safe) requires gpio(4) hardware absent from this guest.

fix_build.log (patched module link line + rc=0); build.log (baseline rc=0); fix.diff
↓ fix.diffper-fix-DF-4752

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (source trace with path:line); run.log (guest feature-absence markers); build.log vs fix_build.log (baseline and patched both rc=0, -Werror); fix.diff (refs + master-unit put + kfree on last ref)

PoC changes

No seed PoC existed; trigger.sh authored fresh and honestly exits SKIP=3 on guests without the gpio(4) framework.

Verified recommended fix

Put the master clone unit back after destroy_dev; refcount struct gpio_driver and kfree gpd+name when the last same-name controller unregisters.

Verdict

Source-certain defect: gpio_unregister (gpio.c:587-617) never returns the master device's DEVFS_CLONE_BITMAP(gpio) unit (allocated at gpio.c:573 for the 'gpio/N/master' cdev) and never frees the shared struct gpio_driver + kstrdup'd name, unlinking it at gpio.c:609-611 even while sibling controllers of the same driver_name still reference it. Permanent privileged-trigger kernel heap leak per controller detach cycle; bitmap auto-extends (limit=0) so it is unbounded creep, not exhaustion. Distinct from known DF-1845 (pin-loop get-vs-put) and DF-1846. Runtime reproduction is hardware/kernel-config-gated: the gpio(4) framework is 'optional gpio' (sys/conf/files:2119), absent from the guest's X86_64_GENERIC (no /dev/gpio*, no module), so the trigger could not be executed on the audit guest; fix.diff compile-validated as a KLD module with -Werror on the guest (fix_build.log rc=0).