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

hammer2_vop_nrename masks backend ENOENT as success and commits rename bookkeeping (tip nlink--/DELETING, iparent/name_key rewrite, cache_rename) against an unrenamed media topology

Summary

When hammer2_xop_nrename cannot find the source dirent (a race the backend itself documents: 'XXX shouldn't happen, but does under fsstress' -> ENOENT), the frontend converts ENOENT to success and still performs every success side-effect although the backend changed nothing on media: rewrites ip->meta.name_len/name_key and iparent, runs the unlink finisher on the overwrite target tip β€” decrementing its persistent nlinks and marking it ISUNLINKED/DELETING while its dirent survives β€” and commits cache_rename, diverging namecache from media. rename_stress 4-thread churn 120s did not hit the window (verdict not_reproduced, race-bound trigger, code path certain). Filesystem metadata corruption: tip drivable to nlinks 0/DELETING while still reachable; stale iparent/name_key (broken '..' after remount). Remove-the-masking fix proposed.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5820 Β· 3 files
FileTypeDescriptionSize
rename_stress.c β€” 1.3 KB view raw
README.md β€” 2.4 KB ↓ raw
verdict.json β€” 2.4 KB view raw

DF-5820 β€” hammer2_vop_nrename masks backend ENOENT as success and commits the rename bookkeeping anyway

What this pack contains

  • rename_stress.c β€” 4-thread rename churn harness used to try to hit the documented backend ENOENT race (120 s total on the guest; no hit).
  • Static analysis below (code path is certain; the trigger is the race the backend itself documents: hammer2_xops.c:621 "XXX shouldn't happen, but does under fsstress").

Code path (certain)

  1. Backend hammer2_xop_nrename fails to find the source dirent and feeds HAMMER2_ERROR_ENOENT (sys/vfs/hammer2/hammer2_xops.c:620-628).
  2. Frontend hammer2_vop_nrename converts it to success: error = hammer2_error_to_errno(error); ... if (error == ENOENT) error = 0; (hammer2_vnops.c:2165-2166)
  3. With error now 0 the frontend still performs every success side effect although the media topology was NOT renamed (the backend bailed before deleting the source dirent and before touching the target): - rewrites ip->meta.name_len/name_key (:2174-2179), - rewrites ip->meta.iparent = tdip->meta.inum (:2180-2183), - runs hammer2_inode_unlink_finisher(tip, &vprecycle) on the overwrite target (:2194-2196) β€” decrementing tip's persistent nlinks and marking it ISUNLINKED/DELETING while its dirent still exists on media, - commits cache_rename() (:2247-2248), so the namecache diverges from the media until reclaim/remount.

Divergence consequences: link-count corruption on tip (an inode can be driven to nlinks 0 / DELETING while still reachable through its surviving dirent), stale iparent/name_key on ip (breaks .. resolution after remount), and namespace divergence (source name resolvable on media after reboot but not in the live cache).

Stress attempt result

/tmp/rename_stress /dfpoc (4 threads, 30 s + 90 s) on the hammer2 root filesystem produced zero xop_nrename: ... ENOENT console messages β€” the race window was not hit; the finding remains code-certain but runtime-unproven on this guest (status not_reproduced).

Fix

Propagate the ENOENT instead of masking it: delete the if (error == ENOENT) error = 0; at hammer2_vnops.c:2165-2166 (or restrict the mask to the exact benign sub-case, if one is ever identified, and skip all :2174-2251 success side effects when the backend reports ENOENT).

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable: trigger is a rare backend lookup race (upstream 'fsstress' class); not hit in 120s of targeted churn.

dmesg grep count 0 after both stress runs.
per-fix-DF-5820

Confirmed kernel references

Detail

Evidence (decisive lines)

README.md full path:line trace; rename_stress.c harness; stress attempt produced 0 backend-ENOENT console hits in 120s.

PoC changes

wrote a 4-thread rename-churn harness (rename_stress.c) to try to hit the documented race; window not hit on a single-master guest.

Verified recommended fix

Do not mask ENOENT from the rename xop: remove 'if (error == ENOENT) error = 0;' at hammer2_vnops.c:2165-2166 so the VOP returns the error and skips all success side-effects.

Verdict

hammer2_vop_nrename converts the rename-backend's ENOENT (source dirent not found; the backend itself documents this happens under fsstress, hammer2_xops.c:621-628) into success at hammer2_vnops.c:2165-2166 and then performs every success side-effect on a media topology that was NOT renamed: rewrites ip->meta.name_len/name_key (:2174-2179) and ip->meta.iparent (:2180-2183), runs hammer2_inode_unlink_finisher on the overwrite target tip (:2194-2196, decrementing its persistent nlinks and marking it ISUNLINKED/DELETING while its dirent survives), and commits cache_rename (:2247-2248), diverging namecache from media. Code path certain; 120s of 4-thread rename churn on the guest did not hit the underlying race (zero 'xop_nrename: ... ENOENT' console messages), so it stays runtime-unproven here.