nlinks double-decrement on DEVFS_RULE_RESET of a rule-created link underflows size_t and permanently leaks the target devfs node (silent, leak_count-balanced)
| Field | Value |
|---|---|
| ID | DF-3006 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:L |
| CWE | CWE-670 β CWE-401 |
| File | sys/vfs/devfs/devfs_core.c |
| Lines | 526-527, 610-618, 729 (extra decrement: devfs_rules.c:222-226) |
| Area | vfs/devfs |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | kernleak |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
devfs_rule_reset_node decrements node->link_target->nlinks and then calls devfs_gc(node), whose devfs_unlinkp decrements the same counter again (the legitimate, universal decrement). nlinks underflows to (size_t)β1, so devfs_freep's NLINKSWAIT branch never frees the target node or its d_name β a permanent, silent leak: the leak_count is decremented in the same pass, so the existing 'Leaked N devfs_node elements!' warning at DEVFS_MOUNT_DEL can never fire. Root (devfsctl / DEVFS_RULE_RESET ioctl; jail devfs ruleset resets); unbounded kernel memory leak ~400 B per mount+RESET+umount cycle; jailed services churning devfs mounts with rules accumulate over time. No memory-safety consequence (the underflow drives nlinks away from 0, so the premature-free branch can never be taken). OBSERVED on stock kernel: vmstat -m devfs 888 allocs/130K β 1.66K/251K over 300 cycles; control without RESET flat; single-fix kernel flat across 900 iterations. Fix: delete the redundant decrement (row diff).
Timeline
- 2026-09-02 Discovered during pass-2 audit of devfs_core.c (GLM 5.3); leak measured + fix validated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3006 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 2.5 KB | β raw | |
| leak.sh | β | 3.0 KB | view raw | |
| leak.c | β | 2.3 KB | view raw | |
| run.sh | β | 3.0 KB | view raw | |
| run.log | β | 999 B | view raw | |
| build.log | β | 49 B | view raw | |
| env.txt | β | 280 B | view raw | |
| fix.diff | β | 440 B | view raw |
DF-3006 β nlinks double-decrement on rule-link reset leaks the target devfs node forever
- Files:
sys/vfs/devfs/devfs_rules.c:222-226(extra decrement) +sys/vfs/devfs/devfs_core.c:729,610-618(devfs_gc->devfs_unlinkpperforms the legitimate decrement) - Class: CWE-670 / resource leak via incorrect bookkeeping
- Severity: Low (memory leak; no memory-safety consequence β the size_t
underflow makes
nlinkswrap far from 0, so no premature free) - Reach: root (devfsctl / DEVFS_RULE_RESET ioctl; also any jail whose devfs ruleset is reset).
Build
sh leak.sh 300 # compiles leak.c (plain ioctls on /dev/devfs)
Run / expected
== baseline M_DEVFS usage ==
devfs 888 130K 0 390M 903
== running 300 iterations (with DEVFS_RULE_RESET => double decrement) ==
== M_DEVFS usage after bugged loop ==
devfs 1.66K 251K 0 390M 120K
== control: 300 iterations WITHOUT reset ==
== M_DEVFS usage after control loop ==
devfs 1.65K 249K 0 390M 239K
~400 bytes leaked per mount+RULE_RESET+umount cycle (sizeof(struct
devfs_node) + its d_dir.d_name kmalloc), while the identical loop without
DEVFS_RULE_RESET is flat. Full run captured in run.log.
Mechanism
- Rule
LINKcreates anNlinknode L withL->link_target = TandT->nlinks++(devfs_core.c:1894-1895). DEVFS_RULE_RESETwalks the topology callingdevfs_rule_reset_node(devfs_rules.c:213): for rule-created links it executes--node->link_target->nlinks;and thendevfs_gc(node).devfs_gc(L)->devfs_unlinkp(L)(devfs_core.c:729, 566) hits the Nlink branch and decrementstarget->nlinksa second time (devfs_core.c:614).T->nlinks(size_t) underflows to (size_t)-1 and can never reach 0 again.- At unmount, the reaper's
devfs_freep(T)seesnlinks != 0, setsDEVFS_NLINKSWAIT, and never callsobjcache_put(devfs_core.c:526-527): node + name leak permanently.leak_countstays balanced (it is decremented in the same pass), so the "Leaked N devfs_node elements!" warning at DEVFS_MOUNT_DEL does not fire β the leak is silent.
Fix
fix.diff deletes the redundant decrement in devfs_rule_reset_node;
devfs_gc/devfs_unlinkp already does exactly one decrement per removed
link, matching every other link-removal path. Validated on the patched kernel
(fix/ outputs): leak loop goes flat.
DF-3006 VERDICT
Status: reproduced β impact: silent kernel memory leak (~400 B/cycle, unbounded, root-triggerable).
Reproduction
Guest DragonFly 6.5-DEVELOPMENT #0, root. leak.sh (this pack):
leak.copens/dev/devfsand issues the stock rule ioctls:DEVFS_RULE_ADD(NAMEvn0, LINKleaklink, mntpoint/mnt/dt), then per iterationmount -t devfs df3006 /mnt/dt(node creation auto-applies the rule ->devfs_alias_create(..., rule_based=1)->target->nlinks = 1, devfs_core.c:1895),DEVFS_RULE_RESET(firesdevfs_rule_reset_node),umount /mnt/dt.- 300 bugged iterations: M_DEVFS 888 allocs/130K -> 1.66K allocs/251K
(+~780 allocations, +121K, β400 B per cycle =
sizeof(struct devfs_node)+ thed_dir.d_namekmalloc thatdevfs_freepskips underDEVFS_NLINKSWAIT). - 300 control iterations (no RESET): 251K -> 249K β flat. Deterministic difference, single variable (the RESET ioctl).
Root cause (lines)
sys/vfs/devfs/devfs_rules.c:222-226: reset decrementsnode->link_target->nlinksand then callsdevfs_gc(node).sys/vfs/devfs/devfs_core.c:729:devfs_gc->devfs_unlinkp(node).sys/vfs/devfs/devfs_core.c:610-618:devfs_unlinkpNlink branch decrementstarget->nlinksagain (this is the correct, universal decrement used by every other link-removal path).sys/vfs/devfs/devfs_core.c:526-527:devfs_freepwithnlinks != 0(size_t underflow, can never return to 0) setsDEVFS_NLINKSWAITand never frees -> node + name leaked.leak_countis decremented in the same pass (devfs_core.c:466), so theDEVFS_MOUNT_DELwarning at 1330-1334 never fires β the leak is silent.
Why not higher severity
- No memory-safety consequence: the underflow drives
nlinksaway from 0, so thetarget->nlinks == 0 && DEVFS_DESTROYEDpremature-free branch (devfs_core.c:615-618) can never be taken via this bug; the result is a pure allocation leak. - Trigger requires root (rule ioctls on /dev/devfs are privileged; jail devfs ruleset resets are system-initiated).
- Unbounded but slow: each cycle needs a mount/umount; a jailed service churning devfs mounts with rules could accumulate over time.
Fix validation
Applied fix.diff (delete the manual decrement in devfs_rule_reset_node;
devfs_gc->devfs_unlinkp provides the single legitimate decrement) in the
guest /usr/src, rebuilt with make nativekernel, installed, rebooted.
Re-ran leak.sh 300 on the patched kernel: M_DEVFS stays flat across the
bugged loop (see fix/fix_run.log), control unchanged. Fixed.
Fix verification
fixedSingle-fix kernel (fix.diff in devfs_rules.c) rebuilt and rebooted; the same 300-iteration bugged loop now leaves M_DEVFS allocations-in-use flat (1.06K/176K across 900 total iterations; +43K seen on first loop was one-time objcache magazine warm-up). Baseline leaked ~400 B/iteration linearly. FIXED.
fix_run.log
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log: vmstat -m devfs 888/130K -> 1.66K/251K with RESET (300 iters) vs flat control; fix_run.log: patched kernel 1.06K/176K FLAT across two more 300-iter bugged loops (900 total).
PoC changes
Fresh PoC: plain DEVFS_RULE_ADD/RESET/CLEAR ioctls on /dev/devfs (no devfsctl dependency); control loop isolates the RESET as the single variable.
Verified recommended fix
Delete the manual --node->link_target->nlinks in devfs_rule_reset_node; devfs_gc->devfs_unlinkp already performs the single legitimate decrement.
Verdict
Silent kernel memory leak reproduced deterministically: DEVFS_RULE_RESET of a rule-created Nlink decrements target->nlinks twice (once in devfs_rule_reset_node devfs_rules.c:225, once via devfs_gc->devfs_unlinkp devfs_core.c:614), underflowing the size_t so the unmount reaper's devfs_freep takes the DEVFS_NLINKSWAIT branch and never frees the node or its name. 300 mount+RESET+umount cycles leak +121K (~400 B/cycle) on the stock kernel; the identical loop without RESET is flat. leak_count stays balanced so the DEVFS_MOUNT_DEL warning never fires - the leak is silent. Root-triggered (rule ioctls / jail ruleset resets); no memory-safety consequence because the underflow drives nlinks away from 0 (no premature free). Fix validated: patched kernel flat across 900 iterations.
No comments yet.