Detect truncated parity using the contiguous valid physical prefix

This commit is contained in:
Andrea Mazzoleni 2026-08-17 18:53:31 +02:00
parent ef6fa54ee2
commit 4423c8c895
4 changed files with 95 additions and 11 deletions

View file

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

View file

@ -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.
*/

View file

@ -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();

View file

@ -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");