Record sync error silently dropped in hammer_sync_inode β variable name typo tmp_error=-error should be -tmp_error
Summary
hammer_inode.c:3066 if(error==0) enters block. :3068 tmp_error=RB_SCAN(... hammer_sync_record_callback). Callback :2876-2877 negates errors error=-error for RB_SCAN convention. :3070 if(tmp_error<0) tmp_error=-error β BUG: should be -tmp_error. Inside if(error==0) block -error is always -0=0 so tmp_error unconditionally set to 0 ALL record sync failures silently discarded. hammer_sync_inode returns 0 hammer_sync_inode_done :2532 treats flush successful proceeds to inode record update :3080-3225 as if records committed. On crash metadata change (file size nlinks deletion) permanent while dependent records (directory entries data writes truncation records) lost. Security state revert: deleted files reappear permission changes lost truncations undone. Trigger: ENOSPC or I/O error during flush + crash/power loss. Fix: tmp_error=-tmp_error.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0770 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df0770_hammer_flush_error.c | trigger-source | runtime PoC: HAMMER v1 fs pressure test (corroboration / regression) | 3.9 KB | view raw |
| fix.diff | suggested-fix | git-apply-able one-character fix: tmp_error = -error -> -tmp_error | 400 B | view raw |
| build.sh | build-script | cc -O2 -o df0770 df0770_hammer_flush_error.c | 216 B | view raw |
| run.sh | run-script | runs ./df0770 with HAMMER FS setup prerequisites | 607 B | view raw |
| build.log | build-log | PoC compiler output (final successful build) | 13 B | view raw |
| run.log | run-log | PoC runtime output on the PATCHED kernel (#1) - regression check | 1.2 KB | view raw |
| fix_build.log | build-log | full single-fix kernel build output (make -j6 nativekernel, rc=0) | 5.6 MB | β download |
| fix_run.log | run-log | PoC runtime output on patched kernel (#1), pre-teardown | 543 B | view raw |
| disasm_compare.txt | panic-signature | before/after objdump of hammer_sync_inode fixup branch: baseline loads error (buggy), patched negates in-register tmp_error (fixed) | 1.6 KB | view raw |
| env.txt | environment | uname, kern.version, cc version, hammer sysctls, patched-kernel disasm + sha256 | 969 B | view raw |
| VERDICT.md | verdict | full narrative: code-level trace, impact chain, disassembly proof, fix | 8.3 KB | β raw |
| README.md | readme | build/run/expected + how to reproduce | 3.0 KB | β raw |
| manifest.json | manifest | this file | 3.6 KB | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0770 β Record sync error silently dropped in hammer_sync_inode
Build
cc -O2 -o df0770 df0770_hammer_flush_error.c
Run
# requires a HAMMER v1 FS mounted at /mnt/hammer, writable by the test user ./df0770
Full guest-side setup (as root, before running):
truncate -s 12G /hammer.img newfs_hammer -f -L ROOT /hammer.img vnconfig vn0 /hammer.img mkdir -p /mnt/hammer mount_hammer /dev/vn0 /mnt/hammer chmod 777 /mnt/hammer
Expected
This finding is a data-integrity logic bug (silently dropped error
propagation), not a memory-corruption primitive. The bug is at
sys/vfs/hammer/hammer_inode.c:3066-3073:
if (error == 0) {
tmp_error = RB_SCAN(hammer_rec_rb_tree, &ip->rec_tree, NULL,
hammer_sync_record_callback, &cursor);
if (tmp_error < 0)
tmp_error = -error; /* BUG: should be -tmp_error */
if (tmp_error)
error = tmp_error;
}
Because the guard if (error == 0) is true, -error always evaluates to
-0 == 0, so the buggy line unconditionally clears tmp_error. The
record-sync error returned by hammer_sync_record_callback (which negates
the errno at line 2876-2877 error = -error) is silently lost.
hammer_sync_inode then returns 0; hammer_sync_inode_done (line 2553)
sets ip->error = 0; the VOP fsync handler hammer_vop_fsync (line 293)
returns ip->error (= 0) to userspace. A failed record flush is
reported to fsync(2) as success.
Trigger
A flush-time record-sync failure (ENOSPC during B-tree allocation, or an
I/O error writing a B-tree node / UNDO record). When this happens,
hammer_flush_record_done calls hammer_critical_error which forces the
filesystem read-only β but the per-inode ip->error was already corrupted
to 0 by the bug, so:
- the immediate fsync returns 0 (success) to the application,
- the inode update at lines 3080+ proceeds (because
error == 0), - on crash the on-disk state is inconsistent: metadata changes (size, nlinks, deletion) committed while dependent records (directory entries, data writes, truncation records) lost.
Runtime vs code-level proof
The runtime PoC df0770_hammer_flush_error.c floods a HAMMER v1 filesystem
to induce ENOSPC and exercises the fsync path. On this guest the FS
reservations cause write-time ENOSPC (properly reported) before the
flush-time path is reached, so the runtime PoC is corroborating, not
deterministic. The definitive proof is the code-level trace plus the
before/after disassembly of hammer_sync_inode (see VERDICT.md).
Fix
The fix is a single-character source change at line 3070:
- tmp_error = -error;
+ tmp_error = -tmp_error;
See fix.diff. Validated by building a single-fix kernel (make -j6
nativekernel), installing, booting, and confirming via disassembly that
the buggy mov -0x128(%rbp),%eax; neg %eax (load error then negate)
became neg %eax; mov %eax,-0x128(%rbp) (negate tmp_error in register,
then store as error) β and that the patched kernel boots and the HAMMER FS
continues to operate normally.
DF-0770 β VERDICT
Verdict: REPRODUCED (code-level trace, definitive). Impact: data-integrity / silent error-drop (not memory corruption). Fix: VALIDATED by single-fix kernel build + before/after disassembly.
1. The bug
sys/vfs/hammer/hammer_inode.c:3066-3073 (in hammer_sync_inode):
if (error == 0) {
tmp_error = RB_SCAN(hammer_rec_rb_tree, &ip->rec_tree, NULL,
hammer_sync_record_callback, &cursor);
if (tmp_error < 0)
tmp_error = -error; /* BUG: should be -tmp_error */
if (tmp_error)
error = tmp_error;
}
The hammer_sync_record_callback (same file, lines 2864-2897) sets a
failure return by negating the errno:
for (;;) {
error = hammer_ip_sync_record_cursor(cursor, record);
if (error != EDEADLK) break;
...
}
...
if (error)
error = -error; /* line 2876-2877: negate for RB_SCAN convention */
done:
hammer_flush_record_done(record, error);
...
return(error);
RB_SCAN returns whatever the callback returns. On a record-sync failure
the callback returns -errno (negative). At the call site the buggy line
tmp_error = -error then tries to flip the sign back β but the outer
error is guaranteed 0 by the if (error == 0) guard, so the
assignment is always tmp_error = -0 == 0. The record-sync error is
silently discarded.
The correct code, plainly intended by the surrounding logic and matching
the error = -error pattern in the callback, is tmp_error = -tmp_error.
2. Impact chain (trace, every hop cited)
| Hop | Site | Effect |
|---|---|---|
| 1 | hammer_inode.c:3066 if (error == 0) |
guard ensures outer error is 0 |
| 2 | hammer_inode.c:3067-3068 tmp_error = RB_SCAN(... hammer_sync_record_callback ...) |
on flush failure, returns -errno |
| 3 | hammer_inode.c:3069-3070 if (tmp_error < 0) tmp_error = -error; |
BUG: -error == 0, so tmp_error is unconditionally cleared |
| 4 | hammer_inode.c:3071-3072 if (tmp_error) error = tmp_error; |
tmp_error == 0 β skipped, error stays 0 |
| 5 | hammer_inode.c:3080 if (error == 0) |
taken: inode update proceeds (B-tree cursor re-seek, inode record update at lines 3153+) |
| 6 | hammer_inode.c:3140 if (error) goto done; |
not taken |
| 7 | function returns 0 | |
| 8 | caller hammer_flusher.c:551 error = hammer_sync_inode(trans, ip); |
error == 0 |
| 9 | caller hammer_flusher.c:559-563 if (error) { ... WOULDBLOCK ... } |
not taken; WOULDBLOCK not set |
| 10 | caller hammer_flusher.c:564 hammer_sync_inode_done(ip, 0); |
called with 0 |
| 11 | hammer_inode.c:2553 ip->error = error; |
ip->error = 0 (per-inode error state corrupted to "success") |
| 12 | hammer_vnops.c:293 return (ip->error); (in hammer_vop_fsync) |
fsync(2) returns 0 to userspace despite the failed record flush |
Note: hammer_flush_record_done (called from the callback at line 2879)
does invoke hammer_critical_error (hammer_vfsops.c:892) which sets
HAMMER_MOUNT_CRITICAL_ERROR, sets hmp->ronly = 2, and forces the
filesystem read-only. So the FS goes read-only on the failure β but the
per-inode ip->error is corrupted to 0 by the bug, and the immediate
fsync syscall that triggered the failure returns success. Applications
relying on fsync to detect write errors see no error.
Concrete impact: a metadata change (size, nlinks, deletion, rename) whose dependent record (directory entry, data write, truncation record) failed to flush will have its inode record updated on disk (lines 3080+) while the dependent records are lost. On crash the on-disk state is inconsistent: deleted files reappear, truncations undone, renames half-applied.
3. Definitive disassembly proof (before / after the fix)
hammer_sync_inode is compiled into the main kernel (options HAMMER in
sys/config/X86_64_GENERIC); the hammer.ko in /boot/kernel/ is unused.
The relevant fixup branch (the if (tmp_error < 0) target) reads:
Baseline /boot/kernel.old/kernel (audit-source #0, BUGGY)
ffffffff80937a7c: 8b 85 d8 fe ff ff mov -0x128(%rbp),%eax ; load error (==0 in this path) ffffffff80937a82: f7 d8 neg %eax ; -0 == 0 β tmp_error := 0 ffffffff80937a84: e9 b1 fd ff ff jmpq ffffffff8093783a ; back to `if (tmp_error)`
Patched /boot/kernel/kernel (single-fix build #1, today, FIXED)
ffffffff80937a6c: f7 d8 neg %eax ; tmp_error := -tmp_error (in-register, from RB_SCAN) ffffffff80937a6e: 89 85 d8 fe ff ff mov %eax,-0x128(%rbp) ; error := tmp_error (store) ffffffff80937a74: e9 c7 fd ff ff jmpq ffffffff80937840 ; back to main path
The instruction-level difference is unambiguous:
- Baseline loads
errorfrom-0x128(%rbp)then negates it. Themov %r,%r/mopcode here is8b 85(load from memory). - Patched negates
%eaxdirectly (which still holdstmp_errorfrom the precedingcallq RB_SCAN) and stores the result back to theerrorslot. The store opcode is89 85(store to memory).
The compiler folds if (tmp_error < 0) tmp_error = -tmp_error; into a
single neg %eax at the fixup target, and if (tmp_error) error =
tmp_error; into the trailing mov %eax,-0x128(%rbp). Both fall through
naturally. This matches the patched source exactly.
Running kernel kern.version = DragonFly 6.5-DEVELOPMENT #1: Thu Jul 9
15:43:39 UTC 2026 confirms the patched kernel is the one executing.
4. Runtime corroboration
df0770_hammer_flush_error.c mounts a 12 GB HAMMER v1 image, prefills
to ~100 %, then issues 4000 create+rename+fsync ops. On this guest the
filesystem's space reservations cause write/open-time ENOSPC (properly
reported as errno=28) before the flush-time error path is reached, so
the runtime PoC does not deterministically trigger the bug. This is
expected: forcing a flush-time B-tree allocation failure (vs a write-time
failure) is racy and requires careful staging. Per the audit's procedure
for logic bugs that resist deterministic runtime triggering, the
code-level trace + before/after disassembly is the accepted definitive
proof.
Runtime runs (both baseline #0 and patched #1):
- baseline: HAMMER FS mounts, fills to 100 %, fsync returns 0 on healthy
writes, no
hammer_critical_errortriggered; behaviour is otherwise normal. - patched: identical runtime behaviour, no regression β the patched kernel boots cleanly, mounts and writes to the HAMMER FS, fsync behaves normally under healthy operation.
The runtime PoC therefore functions as a regression test confirming the fix does not break normal operation; the bug-vs-fix contrast is at the disassembly level.
5. Why this is not memory corruption (no escalation chain)
The primitive here is purely an error-propagation bug β a sync-time errno is dropped. It does not yield any memory-corruption primitive (no OOB write, no UAF, no type confusion, no arbitrary addressing). There is no escalation chain to develop. The realistic impact ceiling is data-integrity violation / silent corruption on HAMMER v1 filesystems that experience a flush-time record-sync failure (ENOSPC at B-tree allocation or I/O error during flush), followed by a crash before the next successful flush. This is a legitimate Medium-severity finding.
6. PoC changes
The PoC df0770_hammer_flush_error.c was authored from scratch for this
verification (no prior PoC existed in the evidence pack). It exercises
the HAMMER write/fsync path under filesystem pressure. Its purpose is
corroboration + regression coverage; the bug-vs-fix signal is the
disassembly.
7. Recommended fix
--- a/sys/vfs/hammer/hammer_inode.c
+++ b/sys/vfs/hammer/hammer_inode.c
@@ -3067,7 +3067,7 @@
tmp_error = RB_SCAN(hammer_rec_rb_tree, &ip->rec_tree, NULL,
hammer_sync_record_callback, &cursor);
if (tmp_error < 0)
- tmp_error = -error;
+ tmp_error = -tmp_error;
if (tmp_error)
error = tmp_error;
}
A one-character change (error β tmp_error) at the root cause.
Validated by git apply --check (clean) and by a full single-fix kernel
build + boot + before/after disassembly on this guest.
The DB title notes "variable name typo tmp_error=-error should be
-tmp_error" β this runner's fix.diff matches that proposal exactly.
Fix verification
fixedVALIDATED the fix. The baseline audit-source #0 kernel.old/hammer.ko at hammer_sync_inode+0x36c emits mov -0x128(%rbp),%eax; neg %eax (load error, which is 0 in the if(error==0) branch, then negate -> 0 -> tmp_error := 0 -> record-sync error dropped). The single-fix kernel #1 (built today via make -j6 nativekernel + installkernel, sha256 4f960ee8279419bb...) emits neg %eax; mov %eax,-0x128(%rbp) (negate in-register tmp_error from RB_SCAN, then store as error) -> the record-sync errno now propagates correctly to ip->error and to fsync(2). Patched kernel boots cleanly, mounts HAMMER FS, write/fsync behave normally (no regression). Runtime PoC confirms no regression on the patched kernel (no panic, no abnormal fsync returns). The runtime path cannot deterministically force a flush-time record-sync failure on this guest, so the before/after disassembly is the validation evidence; it unambiguously shows the buggy load-then-negate became the fixed negate-then-store.
baseline #0 (BUGGY) hammer_sync_inode+0x36c: ffffffff80937a7c: 8b 85 d8 fe ff ff mov -0x128(%rbp),%eax ; load error=0 ffffffff80937a82: f7 d8 neg %eax ; -0 = 0 (DROPPED) patched #1 (FIXED) hammer_sync_inode+0x35c: ffffffff80937a6c: f7 d8 neg %eax ; tmp_error = -tmp_error (propagated) ffffffff80937a6e: 89 85 d8 fe ff ff mov %eax,-0x128(%rbp) ; error = tmp_error baseline kern.version = DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (bug present at line 3070) patched kern.version = DragonFly 6.5-DEVELOPMENT #1: Thu Jul 9 15:43:39 UTC 2026 (bug fixed, line 3070 reads tmp_error = -tmp_error) HAMMER FS mount/write/fsync on patched kernel: clean, no regression
Confirmed kernel references
- sys/vfs/hammer/hammer_inode.c:3066
- sys/vfs/hammer/hammer_inode.c:3067
- sys/vfs/hammer/hammer_inode.c:3068
- sys/vfs/hammer/hammer_inode.c:3069
- sys/vfs/hammer/hammer_inode.c:3070
- sys/vfs/hammer/hammer_inode.c:3071
- sys/vfs/hammer/hammer_inode.c:3072
- sys/vfs/hammer/hammer_inode.c:2876
- sys/vfs/hammer/hammer_inode.c:2877
- sys/vfs/hammer/hammer_inode.c:2553
- sys/vfs/hammer/hammer_vnops.c:293
- sys/vfs/hammer/hammer_flusher.c:551
- sys/vfs/hammer/hammer_flusher.c:559
- sys/vfs/hammer/hammer_flusher.c:564
- sys/vfs/hammer/hammer_object.c:317
- sys/vfs/hammer/hammer_vfsops.c:892
Detail
Exploit chain
none (data-integrity logic bug, not memory corruption). The dropped error causes fsync to report success after a flush-time record-sync failure (ENOSPC at B-tree allocation or I/O error during flush). On crash the on-disk state can be inconsistent: metadata changes (size, nlinks, deletion, rename) committed while dependent records (directory entries, data writes, truncation records) lost. No memory-corruption primitive is derivable, so no escalation chain exists. hammer_flush_record_done -> hammer_critical_error still forces the FS read-only on the failure, mitigating widespread corruption, but the immediate fsync's per-inode error is corrupted to 0 and the inode update at lines 3080+ proceeds.
Evidence (decisive lines)
BASELINE /boot/kernel.old/kernel (#0, buggy) at hammer_sync_inode+0x36c: ffffffff80937a7c: 8b 85 d8 fe ff ff mov -0x128(%rbp),%eax ; load error (==0) ffffffff80937a82: f7 d8 neg %eax ; -0 == 0 -> tmp_error := 0 PATCHED /boot/kernel/kernel (#1 today, fixed): ffffffff80937a6c: f7 d8 neg %eax ; tmp_error := -tmp_error (in-register) ffffffff80937a6e: 89 85 d8 fe ff ff mov %eax,-0x128(%rbp) ; error := tmp_error sha256 baseline = 9e51d90a3f2a59225a932c39f1d258d58659bf02b6794496d05451d1d1eea7d2 sha256 patched = 4f960ee8279419bbf0c1729e42af5c83f41c4d5626ff168bb10c93845055994a kern.version (patched) = DragonFly 6.5-DEVELOPMENT #1: Thu Jul 9 15:43:39 UTC 2026 HAMMER FS mounts, writes, fsyncs normally on patched kernel (no regression).
PoC changes
findings/poc/DF-0770/ was empty when this runner started; authored the full evidence pack from scratch: df0770_hammer_flush_error.c (runtime PoC that mounts a HAMMER v1 image, prefills to ~100%, and exercises create+rename+fsync under pressure), build.sh, run.sh, README.md, VERDICT.md (full code-level trace + disassembly proof), fix.diff (one-character source fix), and the full untrimmed build/run/disasm/env logs. The runtime PoC is a corroboration/regression test; the bug-vs-fix signal is the before/after disassembly because a flush-time record-sync failure cannot be staged deterministically on this guest.
Verified recommended fix
In sys/vfs/hammer/hammer_inode.c line 3070, change tmp_error = -error; to tmp_error = -tmp_error;. This propagates the record-sync error returned by hammer_sync_record_callback (which negates errno at lines 2876-2877) back to a positive errno, which then flows into the outer error and is returned by hammer_sync_inode -> hammer_sync_inode_done -> ip->error -> hammer_vop_fsync. matches finding DB proposal (title is 'variable name typo tmp_error=-error should be -tmp_error'). The full git-apply-able diff lives in findings/poc/DF-0770/fix.diff.
Verdict
REPRODUCED (definitive code-level trace + before/after disassembly). hammer_sync_inode at sys/vfs/hammer/hammer_inode.c:3066-3073 has the buggy line tmp_error = -error inside an if (error == 0) guard; since the outer error is guaranteed 0 by the guard, the assignment unconditionally clears tmp_error, silently dropping the record-sync error returned by hammer_sync_record_callback (which negates errno at lines 2876-2877). hammer_sync_inode returns 0 -> hammer_sync_inode_done (line 2553) sets ip->error = 0 -> hammer_vop_fsync (hammer_vnops.c:293) returns 0 to userspace: a failed record flush is reported to fsync(2) as success. The runtime PoC cannot deterministically trigger a flush-time record-sync failure on this guest (HAMMER's reservations force write-time ENOSPC first), so per procedure the code-level trace + disassembly is the accepted proof. The bug is present in baseline #0 (mov -0x128(%rbp),%eax; neg %eax loads error=0 then negates to 0) and gone in the single-fix kernel #1 (neg %eax; mov %eax,-0x128(%rbp) negates in-register tmp_error then stores as error).
No comments yet.