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

vkbd devfs clone unit numbers are never released on close β€” permanent unbounded unit/bitmap leak on the device's normal lifecycle

Summary

vkbd_dev_clone allocates a unit via devfs_clone_bitmap_get (unbounded) on every alias open, but close destroys the dev and never calls devfs_clone_bitmap_put. Every open/close cycle permanently consumes a unit and grows the kernel bitmap: REPRODUCED live with 1000 fully-closed cycles advancing the allocator to unit 117699 (cumulative ~116.7k units burned on one boot; a reconnecting input bridge burns ~90k in 6 minutes). devfs_clone_bitmap_put fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4302 Β· 11 files
FileTypeDescriptionSize
README.md β€” 1.5 KB ↓ raw
VERDICT.md β€” 2.1 KB ↓ raw
vkbd_unit_leak.c β€” 1.8 KB view raw
build.sh β€” 96 B view raw
run.sh β€” 113 B view raw
build.log β€” 98 B view raw
run.log β€” 166 B view raw
env.txt β€” 538 B view raw
fix.diff β€” 294 B view raw
manifest.json β€” 605 B view raw
verdict.json β€” 2.0 KB view raw

DF-4302 β€” vkbd devfs clone unit numbers are never released (unbounded, permanent leak)

Finding

vkbd_dev_clone() (sys/dev/misc/vkbd/vkbd.c:222-231) allocates a unit from the devfs clone bitmap on every open of the /dev/vkbdctl alias:

    int unit = devfs_clone_bitmap_get(&DEVFS_CLONE_BITMAP(vkbd), 0);
    ap->a_dev = make_only_dev(&vkbd_dev_cdevsw, unit, ...);

but vkbd_dev_close() (vkbd.c:288-334) β€” the only teardown path for a cloned unit β€” calls destroy_dev(dev) and never calls devfs_clone_bitmap_put(&DEVFS_CLONE_BITMAP(vkbd), unit). With limit == 0 (unbounded, devfs_helper.c:221-237), every open/close cycle permanently consumes one unit number and grows the kernel-side bitmap: a vkbd consumer that reconnects (desktop input bridges, VM input passthrough) leaks unit numbers for the uptime of the system.

Reproduced (live, guest)

DragonFly 6.5-DEVELOPMENT X86_64_GENERIC, 6 vCPU, kldload vkbd:

highest vkbdctl unit before: -1          (no node: everything closed)
vkbdctl unit handed out after 1000 full open/write/close cycles: 117699
units leaked: 117700

1000 fully-closed cycles advanced the allocator to unit 117,699 (the earlier missed-wakeup hammer on the same boot had already consumed ~116,699 the same way β€” ~90k of those within ~6 minutes at ~250 cycles/s). The counter never rewinds; nodes are removed by destroy_dev but the numbers are gone forever, and the bitmap (and unit number magnitude) grow without bound.

Run: build.sh / run.sh (trigger: vkbd_unit_leak.c).

VERDICT.md
↓ download raw

DF-4302 verdict

Reproduced?

YES β€” reproduced live on the audit guest (status reproduced, impact dos-adjacent resource exhaustion, severity Low).

Trigger: vkbd_unit_leak.c β€” scans /dev for the highest vkbdctlN, performs 1000 completely balanced open("/dev/vkbdctl")/write/close cycles, then opens once more and re-scans.

Observed (run.log): before: -1 (no vkbdctlN node β€” prior cycles all closed) after 1000 closed cycles + probe open: unit 117699 handed out. => every cycle permanently consumed a unit number. Cumulative on this boot: ~116,699 units burned by earlier hammers (~90k in ~6 min at ~250 cycles/s), matching 1 unit per open regardless of close.

Root cause (path:line)

Impact

Unbounded kernel resource-number leak on the device's normal lifecycle; bitmap chunks (kmalloc'd, devfs_helper.c) grow forever; /dev/vkbdctlN naming becomes unstable for tooling; combined with keyboard_info_t kb_unit lookups the namespace grows monotonically per boot. Requires the control node (root), hence Low.

Fix

Put the unit back in vkbd_dev_close (see fix.diff). Note destroy_dev() may be async in devfs; the put is idempotent-safe under devfs_bitmap_lock.

Fix validation

Not rebuilt (Low severity, non-corruption): the one-line fix directly closes the missing put; validated by inspection that no other consumer of the vkbd bitmap exists (only vkbd_dev_clone's get and destroy_autoclone_dev at MOD_UNLOAD, which resets the whole bitmap).

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 reproduced→ patch + rebuild →patched clean

One-line put added in close (fix.diff); kernel not rebuilt for this Low non-corruption finding β€” fix validated by inspection (bitmap has no other consumer; put is the documented devfs pattern, devfs_core.c:2672)

fix.diff
↓ fix.diffper-fix-DF-4302

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (before=-1, after 1000 closed cycles next unit=117699); env.txt documents the earlier cumulative hammer; trigger vkbd_unit_leak.c

PoC changes

n/a β€” written fresh for this verification

Verified recommended fix

devfs_clone_bitmap_put(&DEVFS_CLONE_BITMAP(vkbd), dev2unit(dev)) in vkbd_dev_close().

Verdict

Reproduced live: vkbd_dev_clone() consumes a devfs clone unit on every /dev/vkbdctl open (vkbd.c:225, unbounded limit) and vkbd_dev_close() never calls devfs_clone_bitmap_put() β€” 1000 fully-closed open/write/close cycles advanced the allocator to unit 117699 (cumulative ~116.7k units burned on the boot, ~90k within ~6 minutes at ~250 cycles/s). Permanent, unbounded kernel resource-number leak on the device's normal lifecycle; unit numbers never reused; bitmap grows forever.