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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0770 Β· 15 files
FileTypeDescriptionSize
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
README.md readme build/run/expected + how to reproduce
↓ download 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.

VERDICT.md verdict full narrative: code-level trace, impact chain, disassembly proof, fix
↓ download raw

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

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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED 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
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Thu Jul 9 15:43:39 UTC 2026

Confirmed kernel references

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