Disallow elastic parity split reallocation during fix to preserve persisted layout

This commit is contained in:
Andrea Mazzoleni 2026-08-28 22:43:51 +02:00
parent f6d07dfa10
commit f8319361a3
5 changed files with 38 additions and 9 deletions

View file

@ -129,6 +129,7 @@ SnapRAID is designed to recover from an interruption, including `SIGKILL`, at an
- Never run `make check` because it's too expansive
- Run `./snapraid test` for the regression test
- The `test` and `selftest` suites (in `cmdline/selftest.c` and `./snapraid test`) must be strictly memory-only without creating files or performing filesystem I/O
- If running the speed benchmark (`./snapraid -T`), always use `--speed-test-period 100` (e.g. `./snapraid -T --speed-test-period 100`) to run it faster
- The Windows build needs to be tested only when modifying Windows-specific code
- Verify error paths and recovery scenarios (the test suite covers aborted sync, UUID changes, disk failures)

View file

@ -2534,7 +2534,18 @@ int state_check(struct snapraid_state* state, int fix, block_off_t blockstart, b
/* LCOV_EXCL_STOP */
}
ret = parity_chsize(parity_ptr[l], &state->parity[l], 0, size, state->block_size, state->opt.skip_fallocate, state->opt.skip_space_holder);
/*
* Fix must preserve the split layout stored in the content file.
*
* Unlike sync, fix does not persist parity split boundary changes. Allowing a
* partially restored elastic split to become shorter would move the remaining
* logical parity into following splits, and repaired parity could then be
* written using a transient mapping that is lost on the next invocation.
*
* An exception is made for new parity levels not yet present in the content
* file (PARITY_SIZE_INVALID), which are being created from scratch.
*/
ret = parity_chsize(parity_ptr[l], &state->parity[l], 0, size, state->block_size, state->opt.skip_fallocate, state->opt.skip_space_holder, state->parity[l].split_map[0].size == PARITY_SIZE_INVALID);
if (ret == -1) {
/* LCOV_EXCL_START */
log_tag("parity_%s:%u:%s: Create error. %s.\n", es(errno), 0, lev_config_name(l), strerror(errno));

View file

@ -594,7 +594,7 @@ static int parity_split_is_fixed(struct snapraid_parity_handle* handle, unsigned
return 1;
}
int parity_chsize(struct snapraid_parity_handle* handle, struct snapraid_parity* parity, int* is_modified, data_off_t size, uint32_t block_size, int skip_fallocate, int skip_space_holder)
int parity_chsize(struct snapraid_parity_handle* handle, struct snapraid_parity* parity, int* is_modified, data_off_t size, uint32_t block_size, int skip_fallocate, int skip_space_holder, int allow_split_realloc)
{
int ret;
unsigned s;
@ -655,7 +655,18 @@ int parity_chsize(struct snapraid_parity_handle* handle, struct snapraid_parity*
log_fatal(errno, "Unexpected over resizing parity file '%s' to size %" PRIu64 " resulting in size %" PRIu64 ".\n", split->path, run, (uint64_t)split->st.st_size);
return -1;
/* LCOV_EXCL_STOP */
} else if (is_fixed && split->st.st_size < run) {
} else if ((is_fixed || !allow_split_realloc) && split->st.st_size < run) {
/*
* A fixed split must always reach its logical boundary because changing
* it would shift the logical offsets of data already present in following
* splits.
*
* Some callers also require the existing split layout to remain unchanged
* even when this split would normally be elastic. In particular, fix does
* not persist a changed split mapping. Accepting a partial allocation here
* would make the remaining size move into a following split and fix could
* then write parity using a transient layout that is lost on the next run.
*/
/* LCOV_EXCL_START */
errno = ENXIO;
log_fatal(errno, "Failed restoring parity file '%s' to size %" PRIu64 " resulting in size %" PRIu64 ".\n", split->path, run, (uint64_t)split->st.st_size);

View file

@ -93,9 +93,16 @@ int parity_create(struct snapraid_parity_handle* handle, const struct snapraid_p
/**
* Change the parity size.
* \param out_size Return the size of the parity file. The out_size is set also on error to reflect a partial resize.
*
* If allow_split_realloc is nonzero, an elastic split that cannot reach the
* requested size may keep the size actually allocated and leave the remaining
* space to following splits.
*
* If allow_split_realloc is zero, a partial allocation must fail instead of
* moving a logical split boundary. This is required by callers that cannot
* persist a changed split layout.
*/
int parity_chsize(struct snapraid_parity_handle* handle, struct snapraid_parity* parity, int* is_modified, data_off_t size, uint32_t block_size, int skip_fallocate, int skip_space_holder);
int parity_chsize(struct snapraid_parity_handle* handle, struct snapraid_parity* parity, int* is_modified, data_off_t size, uint32_t block_size, int skip_fallocate, int skip_space_holder, int allow_split_realloc);
/**
* Get the size of the parity.

View file

@ -2040,11 +2040,10 @@ int state_sync(struct snapraid_state* state, block_off_t blockstart, block_off_t
int is_modified;
/*
* Change the size of the parity file, truncating or extending it
* from this point all the DELETED blocks after the end of the parity are invalid
* and they are automatically removed when we save the new content file
* Sync may reallocate an elastic split into following splits. Any resulting
* logical split mapping is persisted by the sync state-update protocol.
*/
ret = parity_chsize(&parity_handle[l], &state->parity[l], &is_modified, size, state->block_size, state->opt.skip_fallocate, state->opt.skip_space_holder);
ret = parity_chsize(&parity_handle[l], &state->parity[l], &is_modified, size, state->block_size, state->opt.skip_fallocate, state->opt.skip_space_holder, 1);
if (ret == -1) {
/* LCOV_EXCL_START */
data_off_t out_size;