From 82be7b07b381134e188a8b6b0c1ae299a295cda8 Mon Sep 17 00:00:00 2001 From: Andrea Mazzoleni Date: Fri, 28 Aug 2026 18:08:29 +0200 Subject: [PATCH] Persist reallocated mapping before parity resize in force-realloc sync --- Makefile.am | 33 +++++++++++++++++++++++++++++ cmdline/locate.c | 5 +++++ cmdline/snapraid.c | 9 +++++++- cmdline/state.h | 1 + cmdline/sync.c | 53 +++++++++++++++++++++++++++++----------------- 5 files changed, 81 insertions(+), 20 deletions(-) diff --git a/Makefile.am b/Makefile.am index c5927b4b..7175ddce 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1282,6 +1282,39 @@ endif # !HAVE_MEMORY_CHECKER $(MSG) Sync after all the fixes $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) check +#### FORCE-REALLOC CRASH TESTS #### + $(MSG) Test interrupted force-realloc post-resize crash + dd bs=1024 count=1024 if=/dev/urandom of=bench/disk1/REALLOC_CRASH1 + dd bs=1024 count=1024 if=/dev/urandom of=bench/disk1/REALLOC_CRASH2 + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync + rm bench/disk1/REALLOC_CRASH1 + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) locate > bench/realloc_crash_before.txt + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) -R --test-kill-after-resize sync + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) locate > bench/realloc_crash_after.txt + ! diff bench/realloc_crash_before.txt bench/realloc_crash_after.txt > /dev/null + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) locate > bench/realloc_crash_final.txt + diff bench/realloc_crash_after.txt bench/realloc_crash_final.txt + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) check + rm bench/disk1/REALLOC_CRASH2 + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync + $(MSG) Test interrupted force-realloc-tail post-resize crash + dd bs=1024 count=1024 if=/dev/urandom of=bench/disk1/REALLOC_TAIL1 + dd bs=1024 count=1024 if=/dev/urandom of=bench/disk1/REALLOC_TAIL2 + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync + rm bench/disk1/REALLOC_TAIL1 + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) locate > bench/realloc_tail_crash_before.txt + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) -W 10M --test-kill-after-resize sync + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) locate > bench/realloc_tail_crash_after.txt + ! diff bench/realloc_tail_crash_before.txt bench/realloc_tail_crash_after.txt > /dev/null + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) locate > bench/realloc_tail_crash_final.txt + diff bench/realloc_tail_crash_after.txt bench/realloc_tail_crash_final.txt + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) check + rm bench/disk1/REALLOC_TAIL2 + $(TESTENV) $(SNAPRAID) $(CHECKFLAGS) -c $(CONF) sync endif # HAVE_THREAD_CHECKER $(MSG) Regression test completed with SUCCESS! $(MSG) Please ignore any error message printed above, they are expected! diff --git a/cmdline/locate.c b/cmdline/locate.c index 8883f15a..b7be0960 100644 --- a/cmdline/locate.c +++ b/cmdline/locate.c @@ -250,8 +250,13 @@ void state_locate_mark_tail_blocks_for_resync(struct snapraid_state* state, uint /* * Explicit force-realloc overrides an in-place REBUILD request. * REP makes the block eligible for the normal reallocation path. + * + * This transition changes persistent block-state semantics. Mark the + * state dirty explicitly rather than relying on the following scan to + * incidentally do so. */ block_state_set(block, BLOCK_STATE_REP); + state->need_write = 1; } } } diff --git a/cmdline/snapraid.c b/cmdline/snapraid.c index 5adca22d..281047f4 100644 --- a/cmdline/snapraid.c +++ b/cmdline/snapraid.c @@ -257,6 +257,7 @@ void log_close(const char* file) #define OPT_TEST_SPEED_DISKS_NUMBER 308 #define OPT_TEST_SPEED_BLOCKS_SIZE 309 #define OPT_TEST_KILL_BEFORE_SYNC 310 +#define OPT_TEST_KILL_AFTER_RESIZE 311 #if HAVE_GETOPT_LONG @@ -312,7 +313,10 @@ static struct option long_options[] = { /* After syncing, do not write the new content file */ { "test-kill-after-sync", 0, 0, OPT_TEST_KILL_AFTER_SYNC }, - /* Do not execure syncing after writing the new initial content file */ + /* Do not execute syncing after resizing parity files, before the normal post-resize content write */ + { "test-kill-after-resize", 0, 0, OPT_TEST_KILL_AFTER_RESIZE }, + + /* Do not execute syncing after writing the new initial content file */ { "test-kill-before-sync", 0, 0, OPT_TEST_KILL_BEFORE_SYNC }, /* Exit with failure if after check/fix there ARE NOT unrecoverable errors. */ @@ -860,6 +864,9 @@ int snapraid_main(int argc, char* argv[]) case OPT_TEST_KILL_AFTER_SYNC : opt.kill_after_sync = 1; break; + case OPT_TEST_KILL_AFTER_RESIZE : + opt.kill_after_resize = 1; + break; case OPT_TEST_KILL_BEFORE_SYNC : opt.kill_before_sync = 1; break; diff --git a/cmdline/state.h b/cmdline/state.h index 5801c339..04cab782 100644 --- a/cmdline/state.h +++ b/cmdline/state.h @@ -87,6 +87,7 @@ struct snapraid_option { int skip_content_access; /**< Skip the content access for commands that don't need it. */ int kill_after_sync; /**< Kill the process after sync without saving the final state. */ int kill_before_sync; /**< Kill the process before sync after saving the initial state. */ + int kill_after_resize; /**< Kill the process after parity resize before the normal post-resize state write. */ int force_murmur3; /**< Force Murmur3 choice. */ int force_spooky2; /**< Force Spooky2 choice. */ int force_order; /**< Force sorting order. One of the SORT_* defines. */ diff --git a/cmdline/sync.c b/cmdline/sync.c index 49dac003..9b840999 100644 --- a/cmdline/sync.c +++ b/cmdline/sync.c @@ -140,10 +140,14 @@ * * 1. Scan creates CHG, REP and DELETED pending states. * For --force-full, BLK blocks are marked REBUILD before parity resizing. + * For --force-realloc, the scan also constructs the new data-to-parity + * allocation before physical parity is resized. * - * 2. The pending content state is durably written before parity is changed. + * 2. The pending content state and, when reallocating, the new allocation + * are durably written before physical parity is changed. * A crash after this point therefore leaves enough information to - * identify every parity position that may have been modified or needs rebuild. + * identify every parity position that may be modified or rebuilt without + * relying on a physical layout referenced only by an older content file. * * 3. Sync reads current data and computes new parity. Blocks may already be * changed to BLK/EMPTY in memory while parity writes are still pending. @@ -1966,30 +1970,36 @@ int state_sync(struct snapraid_state* state, block_off_t blockstart, block_off_t if (!skip_sync) { /* - * Persist the full-rebuild intent before changing physical parity. + * Persist the logical state required to recover from a crash before changing + * the physical parity layout. * - * parity_chsize() may enlarge a truncated parity file. While the parity handle - * remains open, physical_reach_size intentionally does not advance merely because - * of allocation: it is a physical read/truncation extent, not a parity-validity - * marker. + * For --force-full, BLK -> REBUILD records that the existing allocation must + * be preserved but its physical parity must be regenerated. * - * physical_reach_size is not persisted separately. After a crash and reopen, the - * physical EOF becomes the reconstructed extent, so the previous short EOF can - * no longer be used as evidence that positions beyond it still require work. + * For --force-realloc, state_scan() has already completed the requested + * reallocation before state_sync() is entered. The in-memory content state + * therefore contains the new data-to-parity mapping, with REP/CHG/DELETED + * states describing parity that is not yet synchronized. * - * For --force-full, that rebuild requirement therefore has to be persisted in - * the content state before resizing. BLK -> REBUILD records that physical - * parity must be regenerated independently of what EOF or physical_reach_size says. + * That post-scan state must be durable before parity_chsize() runs. + * Reallocation can move data backward and reduce the required parity size. + * If parity were truncated first and the process terminated before the new + * mapping was persisted, the old content file could still reference parity + * positions that had already been physically discarded. * - * This ordering is about preserving rebuild intent across a crash. It is not - * needed because bytes below physical_reach_size are assumed valid: parity - * correctness is determined separately by block state and parity verification. + * The opposite crash ordering is safe: if the new content state is written + * and the process terminates before resizing or rewriting parity, its + * REP/CHG/DELETED states prevent the old physical parity from being trusted + * as synchronized with the new mapping. */ - if (state->opt.force_full) { + if (state->opt.force_full) state_sync_mark_rebuild(state); - if (!state->opt.skip_content_write && state->need_write) - state_write(state); + if ((state->opt.force_full || state->opt.force_realloc) + && !state->opt.skip_content_write + && state->need_write + ) { + state_write(state); } msg_progress("Resizing...\n"); @@ -2018,6 +2028,11 @@ int state_sync(struct snapraid_state* state, block_off_t blockstart, block_off_t state->need_write = 1; } + if (state->opt.kill_after_resize) { + log_fatal(EUSER, "WARNING! Killing due --test-kill-after-resize option.\n"); + exit(EXIT_SUCCESS); + } + /* after resizing parity files, refresh again the free info */ state_refresh(state);