HAMMER2IOC_RECLUSTER leaks the holdfp() file reference on both error returns in hammer2_ioctl_recluster() β permanent struct-file pin per failing call
| Field | Value |
|---|---|
| ID | DF-2666 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:L |
| CWE | CWE-772 Missing Release of Resource after Effective Lifetime |
| File | sys/vfs/hammer2/hammer2_ioctl.c |
| Lines | 212-236 (error returns :214, :226-231) |
| Area | vfs |
| Confidence | likely |
| Discovered | 2026-08-29 |
| Pass | 2 (GLM 5.3 second pass β surfaced during the msgops/iocom batch) |
| Bucket | hammer2 |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
hammer2_ioctl_recluster() takes a file reference with holdfp()
(ioctl.c:212) and transfers it into the kernel iocom only on the two
success branches (:217-225). hammer2_cluster_reconnect()
(hammer2_iocom.c:77-85) and kdmsg_iocom_reconnect() (kern_dmsg.c:128-131,
"We own that ref now") implement a one-way ownership transfer with no
failure path back to the caller, so both error returns in the ioctl β
VFS_ROOT() failure (:214) and the focus==NULL EINVAL branch (:226-231) β
return without fdrop(), permanently pinning one struct file (and the
vnode/socket it references) per call.
Threat model & preconditions
Root-only (caps_priv_check SYSCAP_NOVFS_IOCTL): repeated failing
recluster ioctls leak kernel memory one struct file at a time and pin
vnodes/sockets β a slow privileged resource-exhaustion vector; the
lockless cluster->focus read (:217) could observe a transient NULL
mid-rebuild, making sporadic leaks plausible on live clusters.
Proof of concept
findings/poc/DF-2666/: 200/200 calls took the success path on a healthy
mount with kern.openfiles delta exactly 0 (ref accounting balanced);
the leak branch requires a degraded iroot cluster (focus==NULL &&
nchains!=1) or a VFS_ROOT failure, neither constructible from userland
on a healthy system β code-certain, trigger not demonstrated
(not_reproduced with static proof in VERDICT.md).
Recommended fix
--- a/sys/vfs/hammer2/hammer2_ioctl.c
+++ b/sys/vfs/hammer2/hammer2_ioctl.c
@@ -229,6 +229,8 @@ hammer2_ioctl_recluster(hammer2_inode_t *ip, void *data)
}
vput(vproot);
}
+ if (error)
+ fdrop(fp); /* ref not transferred on failure */
} else {
error = EINVAL;
}
Timeline
- 2026-08-29 Discovered during the pass-2 msgops/iocom batch audit (GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2666 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.0 KB | β raw | |
| VERDICT.md | β | 4.4 KB | β raw | |
| df2666_trigger.c | β | 4.1 KB | view raw | |
| run_df2666.sh | β | 806 B | view raw | |
| build.sh | β | 290 B | view raw | |
| run.sh | β | 215 B | view raw | |
| build.log | β | 107 B | view raw | |
| run.log | β | 1.5 KB | view raw | |
| env.txt | β | 594 B | view raw | |
| fix.diff | β | 1.0 KB | view raw | |
| manifest.json | β | 1.3 KB | view raw | |
| verdict.json | β | 2.9 KB | view raw |
DF-2666 β HAMMER2IOC_RECLUSTER leaks the holdfp() file reference on error returns
Finding
hammer2_ioctl_recluster() (sys/vfs/hammer2/hammer2_ioctl.c:212-236) takes a
file reference with holdfp(curthread, recl->fd, -1) and transfers it into
the kernel iocom only on the two success branches:
fp = holdfp(curthread, recl->fd, -1);
if (fp) {
error = VFS_ROOT(ip->pmp->mp, &vproot);
if (error == 0) {
cluster = &ip->pmp->iroot->cluster;
if (cluster->focus != NULL) {
hammer2_cluster_reconnect(cluster->focus->hmp, fp); /* ref transferred */
error = 0;
} else if (cluster->nchains == 1 &&
cluster->array[0].chain != NULL) {
hammer2_cluster_reconnect(...array[0]..., fp); /* ref transferred */
error = 0;
} else {
kprintf(...); error = EINVAL; /* LEAK: no fdrop(fp) */
}
vput(vproot);
} /* LEAK: no fdrop(fp) on VFS_ROOT error */
} else {
error = EINVAL;
}
return error;
hammer2_cluster_reconnect() (hammer2_iocom.c:73-85) and its backend
kdmsg_iocom_reconnect() (kern_dmsg.c:128-131) implement a one-way
ownership contract β "The caller must ref the fp for us ... We own that ref
now" β with no failure path back to the caller. Both error returns in the
ioctl therefore leak one struct file reference per call (each pinned ref
keeps the file structure and everything it references β vnode/socket and
buffers β alive forever).
Privilege: root-only (caps_priv_check(cred, SYSCAP_NOVFS_IOCTL) at
hammer2_ioctl.c:83 gates HAMMER2IOC_RECLUSTER at :89-91). Severity: Low.
What the guest run established (stock INVARIANTS kernel #0)
- Success-path ref accounting is balanced β 200 consecutive
HAMMER2IOC_RECLUSTER calls on a healthy mount: 200Γ OK, 0Γ EINVAL,
kern.openfilesdelta 0 (each call transfers the new ref; the nextkdmsg_iocom_reconnectdrops the previousiocom->msg_fp). - The leak branch was not enterable on a healthy guest mount β
iroot->cluster.focusis non-NULL for a successfully mounted local PFS, andVFS_ROOT()does not fail there; we found no way to construct the degraded-cluster precondition (focus == NULL && nchains != 1) from userland. The leak itself is certain by code inspection. - Bonus pass-2 stress (shutdown/retract ordering) β ~1250 rapid
reconnect cycles (this run + the aborted sizing run): each cycle kills
and reaps both iocom threads, tears down the auto-LNK_CONN state through
the synthesized-failure path (kern_dmsg.c:1391-1407) β which re-enters
hammer2_autodmsg'sLNK_CONN|CREATE|REPLYblock (tcmd is derived from the state's icmd, kern_dmsg.c:1050-1058; observed as per-cycle "VOLDATA DUMP / INITIATE SPANs / CONN WAS TERMINATED" console lines) β and re-creates the threads. No wedge, no panic, unmount clean.
Files
df2666_trigger.ctrigger (mount, 200Γ RECLUSTER, kern.openfiles deltas)run_df2666.shguest-side orchestration (vn image, newfs, build, run)build.sh/run.shhost-side wrappersrun.logdecisive guest run (trigger stdout + console deltas)build.logcc outputenv.txtguest uname/envfix.diffthe fix (fdrop on failure returns)VERDICT.mdfull narrativemanifest.json,verdict.json
Build & run (from the repo root)
scp -F dfbsd-qemu/config findings/poc/DF-2666/df2666_trigger.c findings/poc/DF-2666/run_df2666.sh dfbsd:/root/poc/df2666/ dfbsd-qemu/vm.sh run_root 'sh /root/poc/df2666/run_df2666.sh'
Stock kernel: RECLUSTER_OK=200 RECLUSTER_EINVAL=0 DELTA=0 (no leak on the
healthy path; leak branch not enterable). A kernel with the leak branch
reachable would show RECLUSTER_EINVAL>0 and DELTA == RECLUSTER_EINVAL.
DF-2666 VERDICT β HAMMER2IOC_RECLUSTER holdfp() reference leak on error returns
Status: not_reproduced (leak branch not enterable on a healthy guest mount; the missing-fdrop defect itself is certain by code inspection) Impact: none demonstrated (ceiling: one permanently pinned struct file per failing call β privileged caller only) Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 (stock INVARIANTS kernel, X86_64_GENERIC, Jul 2 2026)
1. THE DEFECT (certain, static)
hammer2_ioctl_recluster() (hammer2_ioctl.c:212) takes a file reference via holdfp() and transfers it to the kernel iocom only on the two success branches (:217-225). hammer2_cluster_reconnect() (hammer2_iocom.c:77-85 -> kern_dmsg.c:132-167) implements a one-way ownership transfer ("We own that ref now", kern_dmsg.c:130) β it has no failure return and never drops the ref itself. Both error paths in the ioctl therefore leak the reference:
- VFS_ROOT() failure β fp never dropped (hammer2_ioctl.c:214,232-233)
- EINVAL branch β fp never dropped (hammer2_ioctl.c:226-231)
Each failing call permanently pins one struct file plus everything it references (vnode/inode or socket + buffers). Privilege-gated: caps_priv_check(SYSCAP_NOVFS_IOCTL) at hammer2_ioctl.c:83,89-91.
2. VERIFICATION RUN (what was tried and observed)
Two runs on the stock guest kernel (attempt 1 sized at 2000 iterations was aborted for runtime β each reconnect costs ~1s in hz/2 teardown sleeps β after ~1050 clean cycles; attempt 2 at 200 iterations completed):
OPENFILES_BEFORE=-4294967199 iters=200 (kern.openfiles is int-wrapped OPENFILES_AFTER=-4294967199 DELTA=0 on this long-lived guest; the RECLUSTER_OK=200 RECLUSTER_EINVAL=0 deltas are still live) RECLUSTER_OTHER=0 LAST_ERRNO=0 BADFD_RC=22 (Invalid argument) [expected EINVAL, no leak] UNMOUNT_OK OPENFILES_POST_UNMOUNT=-4294967201 (FINAL_DELTA=-2)
- 200/200 calls took the SUCCESS path: iroot->cluster.focus != NULL on a healthy local mount, so the ref-transfer path executed every time and the accounting balanced exactly (DELTA=0; next kdmsg_iocom_reconnect drops the previous iocom->msg_fp, :149-152).
- The leak branch requires focus==NULL && !(nchains==1 && array[0].chain) β a degraded iroot cluster β or a VFS_ROOT() failure. Neither could be constructed from userland on a healthy mount: every local PFS mount builds iroot's cluster from β₯1 live chain, and all-slaves-dead mounts fail before the ioctl is reachable. The lockless read of cluster->focus (:217) could in principle observe a transient NULL mid-rebuild, but that race was not observed (200/200 success).
- Control: recl->fd = 999999 -> holdfp() fails, EINVAL, no ref taken (not a leak β confirmed).
Conclusion: the missing fdrop is real and the fix is required, but a user-observable leak could not be demonstrated on a healthy system, hence not_reproduced / impact none / severity Low.
3. BONUS PASS-2 RESULT (shutdown/retract ordering, requested hunt)
The ~1250 reconnect cycles stress-tested exactly the ordering the orchestrator asked about: kdmsg_iocom_reconnect() kill+join of both iocom threads (kern_dmsg.c:139-144), wr-thread teardown drain + simulated failures (:543-570), auto-CONN re-initiation (hammer2_iocom.c:129 -> kern_dmsg.c:176-192), and teardown re-entry into hammer2_autodmsg's LNK_CONN|CREATE|REPLY block β empirically confirmed via the per-cycle "VOLDATA DUMP / INITIATE SPANs / CONN WAS TERMINATED" console lines (tcmd derives from the tracked state's icmd, kern_dmsg.c:1050-1058, not from the synthesized LNK_ERROR base command). No wedge, no panic, clean unmount, kern.openfiles back to baseline. This teardown-context dispatch runs WITHOUT msglk (kdmsg_state_abort releases it around the callback, kern_dmsg.c:1403-1405), so no voldata_lock/msglk lock-order inversion exists on this path either.
4. WHY NO FIX-KERNEL RUN
Fix validation is mandatory for reproduced memory-corruption findings; this finding did not reproduce (branch unenterable), and the patch is a two-line refcount fix with no behavior change on any executable path of this run (200/200 success before and after would be identical). fix_status: not_testable.
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['run.log: RECLUSTER_OK=200 RECLUSTER_EINVAL=0, OPENFILES DELTA=0 across 200 calls (success-path balance, leak branch unenterable)', "VERDICT.md section 1: static proof of the two leak paths (hammer2_ioctl.c:214/226-231) against the ownership contract (kern_dmsg.c:130 'We own that ref now')", 'fix.diff: adds fdrop(fp) when error != 0 after holdfp', 'dmesg diff (610 new lines): per-cycle VOLDATA DUMP/INITIATE SPANs/CONN WAS TERMINATED proving teardown re-entry into hammer2_autodmsg with msglk released β no panic over ~1250 cycles']
PoC changes
fresh PoC (no seed); sized down from 2000 to 200 iterations after measuring ~1 s per reconnect cycle (hz/2 teardown sleeps)
Verified recommended fix
In hammer2_ioctl_recluster(), fdrop(fp) whenever the function returns an error after a successful holdfp() (the reference is transferred into iocom->msg_fp only on the success branches).
Verdict
The missing fdrop(fp) on hammer2_ioctl_recluster()'s two error returns (VFS_ROOT failure; the focus==NULL EINVAL branch) is certain by code inspection against the one-way ref-transfer contract of hammer2_cluster_reconnect/kdmsg_iocom_reconnect, but the leak branch could not be entered on the stock guest: 200/200 RECLUSTER calls took the success path (healthy iroot cluster), kern.openfiles delta was exactly 0 (ref accounting balanced), and no userland-constructible degraded-cluster state was found that yields focus==NULL with nchains!=1. Ceiling is one permanently pinned struct file per failing call, privileged caller only (SYSCAP_NOVFS_IOCTL). Bonus: ~1250 rapid reconnect cycles stress-tested the reconnect kill/join/teardown ordering with no wedge or panic.
No comments yet.