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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3006 Β· 9 files
FileTypeDescriptionSize
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_unlinkp performs the legitimate decrement)
  • Class: CWE-670 / resource leak via incorrect bookkeeping
  • Severity: Low (memory leak; no memory-safety consequence β€” the size_t underflow makes nlinks wrap 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

  1. Rule LINK creates an Nlink node L with L->link_target = T and T->nlinks++ (devfs_core.c:1894-1895).
  2. DEVFS_RULE_RESET walks the topology calling devfs_rule_reset_node (devfs_rules.c:213): for rule-created links it executes --node->link_target->nlinks; and then devfs_gc(node).
  3. devfs_gc(L) -> devfs_unlinkp(L) (devfs_core.c:729, 566) hits the Nlink branch and decrements target->nlinks a second time (devfs_core.c:614).
  4. T->nlinks (size_t) underflows to (size_t)-1 and can never reach 0 again.
  5. At unmount, the reaper's devfs_freep(T) sees nlinks != 0, sets DEVFS_NLINKSWAIT, and never calls objcache_put (devfs_core.c:526-527): node + name leak permanently. leak_count stays 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.

VERDICT.md
↓ download raw

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.c opens /dev/devfs and issues the stock rule ioctls: DEVFS_RULE_ADD (NAME vn0, LINK leaklink, mntpoint /mnt/dt), then per iteration mount -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 (fires devfs_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) + the d_dir.d_name kmalloc that devfs_freep skips under DEVFS_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 decrements node->link_target->nlinks and then calls devfs_gc(node).
  • sys/vfs/devfs/devfs_core.c:729: devfs_gc -> devfs_unlinkp(node).
  • sys/vfs/devfs/devfs_core.c:610-618: devfs_unlinkp Nlink branch decrements target->nlinks again (this is the correct, universal decrement used by every other link-removal path).
  • sys/vfs/devfs/devfs_core.c:526-527: devfs_freep with nlinks != 0 (size_t underflow, can never return to 0) sets DEVFS_NLINKSWAIT and never frees -> node + name leaked. leak_count is decremented in the same pass (devfs_core.c:466), so the DEVFS_MOUNT_DEL warning at 1330-1334 never fires β€” the leak is silent.

Why not higher severity

  • No memory-safety consequence: the underflow drives nlinks away from 0, so the target->nlinks == 0 && DEVFS_DESTROYED premature-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

fixed
baseline reproduced→ patch + rebuild →patched clean

Single-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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sat Sep 5 04:09:39 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.