DF-0770 / df0770_hammer_flush_error.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | /* * DF-0770 — Record sync error silently dropped in hammer_sync_inode. * Variant C: metadata-heavy burst (file create + rename + delete) to trigger * flush-time B-tree allocation failure. * * The bug (sys/vfs/hammer/hammer_inode.c:3066-3073): * if (error == 0) { * tmp_error = RB_SCAN(... hammer_sync_record_callback, &cursor); * if (tmp_error < 0) * tmp_error = -error; // BUG: should be -tmp_error * if (tmp_error) * error = tmp_error; * } * * hammer_sync_record_callback returns -errno on failure (line 2876-2877 * `error = -error`). Because we are inside `if (error == 0)`, the buggy * line `tmp_error = -error` always yields 0, silently dropping the error. * hammer_sync_inode returns 0 -> hammer_sync_inode_done sets ip->error = 0 * -> hammer_vop_fsync (hammer_vnops.c:293) returns 0 to userspace. * * This variant creates a flood of new files, renames them, deletes them, * forcing many record additions and deletions in the B-tree. Combined * with filesystem pressure, this can trigger a flush-time B-tree alloc * failure where the bug's signal (fsync returning 0) is observable. * * Definitive proof of the bug is the code-level trace in VERDICT.md; * this runtime PoC is corroborating evidence. */ #include <sys/fcntl.h> #include <sys/stat.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define TARGET_DIR "/mnt/hammer/df0770" #define FILL_DIR "/mnt/hammer/fill" #define CHUNK_SZ (4 * 1024 * 1024) #define N_OPS 4000 static char buf[CHUNK_SZ]; int main(void) { char path[256], path2[256]; int fd, idx; int total_fsync = 0, fsync_success = 0, fsync_fail = 0; long total_written = 0; mkdir(TARGET_DIR, 0777); mkdir(FILL_DIR, 0777); memset(buf, 0xA5, sizeof(buf)); /* Stage 1: fill until 90% full (leave ~1GB free for B-tree ops). */ printf("[*] Stage 1: prefilling filesystem to ~90%%\n"); int fill_idx = 0; for (;;) { snprintf(path, sizeof(path), "%s/bigfill_%06d", FILL_DIR, fill_idx); fd = open(path, O_WRONLY|O_CREAT|O_EXCL, 0644); if (fd < 0) break; ssize_t n; while ((n = write(fd, buf, sizeof(buf))) > 0) total_written += n; close(fd); if (total_written > 10L * 1024 * 1024 * 1024) break; /* 10GB */ fill_idx++; } printf(" prefill done, total=%ld MB\n", total_written / (1024*1024)); sync(); sleep(1); /* Stage 2: hammer metadata ops + fsync to trigger flush-time errors. */ printf("\n[*] Stage 2: %d create+rename+fsync+delete ops\n", N_OPS); for (idx = 0; idx < N_OPS; idx++) { snprintf(path, sizeof(path), "%s/op_%06d", TARGET_DIR, idx); fd = open(path, O_WRONLY|O_CREAT|O_EXCL, 0644); if (fd < 0) { if (errno == ENOSPC || errno == EROFS) { /* FS forced ronly */ } continue; } write(fd, buf, 4096); /* rename creates a delete + add record */ snprintf(path2, sizeof(path2), "%s/op_%06d.rn", TARGET_DIR, idx); rename(path, path2); int rc = fsync(fd); total_fsync++; if (rc == 0) fsync_success++; else fsync_fail++; if (rc == 0 && (idx % 200) == 0) printf(" fsync #%d: rc=0\n", idx); else if (rc < 0) printf(" fsync #%d: errno=%d (%s)\n", idx, errno, strerror(errno)); close(fd); } printf("\n=== DF-0770 SUMMARY ===\n"); printf("total fsync calls: %d\n", total_fsync); printf(" rc=0 (success) : %d\n", fsync_success); printf(" rc<0 (error) : %d\n", fsync_fail); printf("\nDefinitive proof: code-level trace in VERDICT.md (lines 3066-3073).\n"); printf("Runtime signal: if dmesg shows 'Critical error'/'Forcing read-only'\n"); printf(" while fsync still returned 0, the dropped error reached userspace.\n"); return 0; } |