From 4423c8c8954b8436ddad92e9f4f5c9e45394e822 Mon Sep 17 00:00:00 2001 From: Andrea Mazzoleni Date: Mon, 17 Aug 2026 18:53:31 +0200 Subject: [PATCH] Detect truncated parity using the contiguous valid physical prefix --- cmdline/parity.c | 26 ++++++++++++++++++++++++++ cmdline/parity.h | 9 +++++++++ cmdline/selftest.c | 43 +++++++++++++++++++++++++++++++++++++++++++ cmdline/sync.c | 28 +++++++++++++++++----------- 4 files changed, 95 insertions(+), 11 deletions(-) diff --git a/cmdline/parity.c b/cmdline/parity.c index f6d2182d..6cc9e24f 100644 --- a/cmdline/parity.c +++ b/cmdline/parity.c @@ -127,6 +127,32 @@ void parity_size(struct snapraid_parity_handle* handle, data_off_t* out_size) *out_size = size; } +void parity_valid_size(struct snapraid_parity_handle* handle, data_off_t* out_size) +{ + unsigned s; + data_off_t size; + + /* compute the contiguous valid prefix of the logical parity layout */ + size = 0; + + for (s = 0; s < handle->split_mac; ++s) { + struct snapraid_split_handle* split = &handle->split_map[s]; + data_off_t run = split->valid_size; + + /* don't count physical data outside the logical split */ + if (run > split->size) + run = split->size; + + size += run; + + /* later splits cannot compensate for a hole in this one */ + if (run < split->size) + break; + } + + *out_size = size; +} + int parity_create(struct snapraid_parity_handle* handle, const struct snapraid_parity* parity, unsigned level, int mode, uint32_t block_size, data_off_t limit_size) { unsigned s; diff --git a/cmdline/parity.h b/cmdline/parity.h index 14cfeef2..0b9750d8 100644 --- a/cmdline/parity.h +++ b/cmdline/parity.h @@ -94,6 +94,15 @@ int parity_chsize(struct snapraid_parity_handle* handle, struct snapraid_parity* */ void parity_size(struct snapraid_parity_handle* handle, data_off_t* out_size); +/** + * Get the size of the contiguous valid prefix of the parity. + * + * This returns the logical prefix covered by valid data in all consecutive splits. + * For example, with two 100 GiB splits, if the first is truncated to 90 GiB + * and the second is fully valid, the valid prefix is 90 GiB, not 190 GiB. + */ +void parity_valid_size(struct snapraid_parity_handle* handle, data_off_t* out_size); + /** * Open an already existing parity file. */ diff --git a/cmdline/selftest.c b/cmdline/selftest.c index 5ba0a7ab..1dd8dc4d 100644 --- a/cmdline/selftest.c +++ b/cmdline/selftest.c @@ -14,6 +14,7 @@ #include "state.h" #include "support.h" #include "stream.h" +#include "parity.h" #include "tommyds/tommyhash.h" #include "tommyds/tommyarray.h" #include "tommyds/tommyarrayblkof.h" @@ -297,6 +298,47 @@ static void test_crc32c(void) } } +static void test_parity(void) +{ + static const struct { + unsigned split_mac; + data_off_t size[2]; + data_off_t valid_size[2]; + data_off_t expected; + } test[] = { + { 1, { 100, 0 }, { 100, 0 }, 100 }, + { 1, { 100, 0 }, { 90, 0 }, 90 }, + { 1, { 100, 0 }, { 99, 0 }, 99 }, + { 2, { 100, 100 }, { 90, 100 }, 90 }, + { 2, { 100, 100 }, { 100, 70 }, 170 }, + { 2, { 100, 100 }, { 110, 100 }, 200 }, + { 2, { 100, 100 }, { 0, 100 }, 0 }, + { 0, { 0, 0 }, { 0, 0 }, 0 } + }; + unsigned i; + + for (i = 0; test[i].split_mac != 0; ++i) { + struct snapraid_parity_handle handle; + data_off_t size; + unsigned s; + + memset(&handle, 0, sizeof(handle)); + handle.split_mac = test[i].split_mac; + for (s = 0; s < handle.split_mac; ++s) { + handle.split_map[s].size = test[i].size[s]; + handle.split_map[s].valid_size = test[i].valid_size[s]; + } + + parity_valid_size(&handle, &size); + if (size != test[i].expected) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed parity valid size test\n"); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + } +} + /** * Size of tommy data structures. */ @@ -1079,6 +1121,7 @@ void selftest(void) } test_hash(); test_crc32c(); + test_parity(); test_tommy(); test_stream(); test_wnmatch(); diff --git a/cmdline/sync.c b/cmdline/sync.c index 45186757..3a35add1 100644 --- a/cmdline/sync.c +++ b/cmdline/sync.c @@ -1525,7 +1525,8 @@ int state_sync(struct snapraid_state* state, block_off_t blockstart, block_off_t { block_off_t blockmax; block_off_t used_paritymax; - block_off_t file_paritymax; + data_off_t used_parity_size; + data_off_t file_parity_size; data_off_t size; int ret; struct snapraid_parity_handle parity_handle[LEV_MAX]; @@ -1540,9 +1541,10 @@ int state_sync(struct snapraid_state* state, block_off_t blockstart, block_off_t /* minimum size of the parity files we expect */ used_paritymax = parity_used_size(state); + used_parity_size = used_paritymax * (data_off_t)state->block_size; /* effective size of the parity files */ - file_paritymax = 0; + file_parity_size = 0; if (blockstart > blockmax) { /* LCOV_EXCL_START */ @@ -1570,26 +1572,30 @@ int state_sync(struct snapraid_state* state, block_off_t blockstart, block_off_t } /* number of block in the parity file */ - parity_size(&parity_handle[l], &out_size); + parity_valid_size(&parity_handle[l], &out_size); parityblocks = out_size / state->block_size; /* if the file is too small */ - if (parityblocks < used_paritymax) { + if (out_size < used_parity_size) { log_fatal(ESOFT, "WARNING! The %s parity has only %" PRIu64 " blocks instead of %" PRIu64 ".\n", lev_name(l), parityblocks, used_paritymax); } - /* keep the smallest parity number of blocks */ - if (l == 0 || file_paritymax > parityblocks) - file_paritymax = parityblocks; + /* keep the smallest valid parity size */ + if (l == 0 || file_parity_size > out_size) + file_parity_size = out_size; } - /* if we do a full parity realloc or computation, having a wrong parity size is expected */ - if (!state->opt.force_realloc && !state->opt.force_full) { + /* + * A full parity rebuild or reallocation from zero reconstructs truncated + * parity. Tail-only reallocation still relies on the existing prefix, so a + * truncation before parity_tail remains fatal. + */ + if (!state->opt.force_full && !(state->opt.force_realloc && state->opt.parity_tail == 0)) { /* if the parities are too small */ - if (file_paritymax < used_paritymax) { + if (file_parity_size < used_parity_size) { /* LCOV_EXCL_START */ log_fatal(ESOFT, "DANGER! One or more the parity files are smaller than expected!\n"); - if (file_paritymax != 0) { + if (file_parity_size != 0) { log_fatal(ESOFT, "If this happens because you are using an old content file,\n"); log_fatal(ESOFT, "you can 'sync' anyway using 'snapraid --force-full sync'\n"); log_fatal(ESOFT, "to force a full rebuild of the parity.\n");