# 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`): ```c 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: ```c 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 `error` from `-0x128(%rbp)` then negates it. The `mov %r,%r/m` opcode here is `8b 85` (load from memory). - **Patched** negates `%eax` directly (which still holds `tmp_error` from the preceding `callq RB_SCAN`) and stores the result back to the `error` slot. The store opcode is `89 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_error` triggered; 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 ```diff --- 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.