# 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
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.
