Track current and previous hash generations for deallocated blocks
Some checks are pending
Build / build-mac (push) Waiting to run
Build / build-linux (push) Waiting to run
Build / build-arm32 (push) Waiting to run
Build / build-win (push) Waiting to run
Build / build-freebsd (push) Waiting to run
Build / build-musl (push) Waiting to run
CodeQL Advanced / Analyze (c-cpp) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run

This commit is contained in:
Andrea Mazzoleni 2026-09-25 23:40:58 +02:00
parent 0440d74fb5
commit b023939c6b
6 changed files with 103 additions and 61 deletions

View file

@ -495,7 +495,7 @@ struct snapraid_file* file_alloc(unsigned block_size, const char* sub, data_off_
file->mtime_nsec = mtime_nsec;
file->inode = inode;
file->flag = 0;
file->blockvec = nalloc_nofail((size_t)file->blockmax, block_sizeof());
file->blockvec = nalloc_nofail(file->blockmax, block_sizeof());
return file;
}
@ -522,7 +522,7 @@ struct snapraid_file* file_dup(struct snapraid_file* copy)
file->mtime_nsec = copy->mtime_nsec;
file->inode = copy->inode;
file->flag = copy->flag;
file->blockvec = nalloc_nofail((size_t)file->blockmax, block_sizeof());
file->blockvec = nalloc_nofail(file->blockmax, block_sizeof());
for (i = 0; i < file->blockmax; ++i) {
struct snapraid_block* block = file_block(file, i);
@ -873,39 +873,52 @@ struct snapraid_dealloc* dealloc_alloc(unsigned block_size, const char* sub, dat
exit(EXIT_FAILURE);
}
dealloc = malloc_nofail(sizeof(struct snapraid_dealloc));
dealloc = malloc_nofail(sizeof(struct snapraid_dealloc) + blockmax * sizeof(struct snapraid_dealloc_block));
dealloc->sub = strdup_nofail(sub);
dealloc->size = size;
dealloc->mtime_sec = mtime_sec;
dealloc->mtime_nsec = mtime_nsec;
dealloc->blockmax = blockmax;
dealloc->blockhash = nalloc_nofail((size_t)dealloc->blockmax, BLOCK_HASH_SIZE);
return dealloc;
}
void dealloc_import(struct snapraid_dealloc* dealloc, struct snapraid_file* file)
void dealloc_import(tommy_arrayblkof* infoarr, struct snapraid_disk* disk, struct snapraid_dealloc* dealloc, struct snapraid_file* file)
{
assert(dealloc->blockmax == file->blockmax);
/* for all the blocks of the file */
for (block_off_t i = 0; i < dealloc->blockmax; ++i) {
struct snapraid_block* block = fs_file2block_get(file, i);
unsigned char* hash = dealloc->blockhash + i * BLOCK_HASH_SIZE;
struct snapraid_dealloc_block* dealloc_block = &dealloc->block[i];
unsigned block_state = block_state_get(block);
unsigned state = block_state_get(block);
switch (state) {
switch (block_state) {
case BLOCK_STATE_BLK :
case BLOCK_STATE_REP :
case BLOCK_STATE_REBUILD :
hash_copy(hash, block->hash);
case BLOCK_STATE_REBUILD : {
block_off_t parity_pos = fs_file2par_find(disk, file, i);
snapraid_info info;
if (parity_pos == POS_NULL) {
/* LCOV_EXCL_START */
log_fatal(EINTERNAL, "Internal inconsistency: Missing parity position for dealloc block %" PRIu64 "\n", i);
exit(EXIT_FAILURE);
/* LCOV_EXCL_STOP */
}
info = info_get(infoarr, parity_pos);
dealloc_block->prev = info_get_rehash(info);
hash_copy(dealloc_block->hash, block->hash);
break;
}
case BLOCK_STATE_CHG :
hash_invalid_set(hash);
dealloc_block->prev = 0;
hash_invalid_set(dealloc_block->hash);
break;
default :
/* LCOV_EXCL_START */
log_fatal(EINTERNAL, "Internal inconsistency: State for dealloc block %" PRIu64 " state %u\n", i, state);
log_fatal(EINTERNAL, "Internal inconsistency: State for dealloc block %" PRIu64 " state %u\n", i, block_state);
exit(EXIT_FAILURE);
/* LCOV_EXCL_STOP */
}
@ -917,7 +930,6 @@ void dealloc_free(void* void_dealloc)
struct snapraid_dealloc* dealloc = void_dealloc;
free(dealloc->sub);
free(dealloc->blockhash);
free(dealloc);
}

View file

@ -368,6 +368,14 @@ struct snapraid_dir {
tommy_hashdyn_node nodeset;
};
/**
* Deallocated block.
*/
struct snapraid_dealloc_block {
unsigned char prev; /**< If the hash uses the previous hash generation. */
unsigned char hash[HASH_MAX]; /**< Hash of the block. Only BLOCK_HASH_SIZE bytes are significant. */
};
/**
* Deallocated file.
*/
@ -377,10 +385,11 @@ struct snapraid_dealloc {
int64_t mtime_sec; /**< Modification time. */
int mtime_nsec; /**< Modification time nanoseconds. In the range 0 <= x < 1,000,000,000, or STAT_NSEC_INVALID if not present. */
block_off_t blockmax; /**< Number of blocks. */
unsigned char* blockhash; /**< Hash of all blocks. */
/* nodes for data structures */
tommy_node nodelist;
struct snapraid_dealloc_block block[]; /**< Blocks of the file. */
};
/**
@ -1126,7 +1135,7 @@ void dealloc_free(void* void_dealloc);
/**
* Import file hash in the dealloc.
*/
void dealloc_import(struct snapraid_dealloc* dealloc, struct snapraid_file* file);
void dealloc_import(tommy_arrayblkof* infoarr, struct snapraid_disk* disk, struct snapraid_dealloc* dealloc, struct snapraid_file* file);
/**
* Allocate a disk.

View file

@ -224,33 +224,20 @@ static void import_dealloc(struct snapraid_state* state, const char* dir, struct
block->offset = offset;
block->size = read_size;
hash_copy(block->hash, dealloc->blockhash + i * BLOCK_HASH_SIZE);
if (dealloc->block[i].prev) {
hash_invalid_set(block->hash);
hash_copy(block->prevhash, dealloc->block[i].hash);
/* do not insert invalid hashes */
if (!hash_is_invalid(block->hash)) {
tommy_hashdyn_insert(&state->importset, &block->nodeset, block, import_block_hash(block->hash));
/* if we are in a rehash state */
if (state->prevhash != HASH_UNDEFINED) {
/*
* The deallocation record stores only one digest per block without
* recording which hash algorithm generated it. To avoid disk I/O at
* startup, index the stored digest under both algorithms.
*
* This is safe against false positives because state_import_fetch()
* validates candidate bytes with the requested algorithm upon read.
*
* Note that cross-generation matches (an old-hash deallocation supplying
* a new-hash block, or vice-versa) will miss in hash lookup and fall
* back to parity reconstruction. Only same-generation matches will succeed.
*/
hash_copy(block->prevhash, block->hash);
/* do not insert invalid hashes */
if (!hash_is_invalid(block->prevhash))
tommy_hashdyn_insert(&state->previmportset, &block->prevnodeset, block, import_block_hash(block->prevhash));
} else {
hash_invalid_set(block->prevhash);
}
} else {
hash_copy(block->hash, dealloc->block[i].hash);
hash_invalid_set(block->prevhash);
/* do not insert invalid hashes */
if (!hash_is_invalid(block->hash))
tommy_hashdyn_insert(&state->importset, &block->nodeset, block, import_block_hash(block->hash));
}
offset += read_size;

View file

@ -18,6 +18,7 @@ void state_rehash(struct snapraid_state* state)
{
block_off_t blockmax;
block_off_t i;
int has_pending_dealloc = 0;
blockmax = parity_allocated_size(state);
@ -38,8 +39,17 @@ void state_rehash(struct snapraid_state* state)
/* LCOV_EXCL_STOP */
}
/* deleted-only positions are not counted in unsynced_blocks, but their hashes still need the current generation */
for (tommy_node* node = state->disklist; node != 0; node = node->next) {
struct snapraid_disk* disk = node->data;
if (!tommy_list_empty(&disk->dealloclist)) {
has_pending_dealloc = 1;
break;
}
}
/* a rehash can start only from a fully synchronized state */
if (state->unsynced_blocks != 0) {
if (state->unsynced_blocks != 0 || has_pending_dealloc) {
/* LCOV_EXCL_START */
log_tag("summary:exit:sync_required\n");
log_fatal(EUSER, "You cannot start a rehash with an incomplete sync.\n");

View file

@ -616,7 +616,7 @@ static void scan_file_remove(struct snapraid_scan* scan, struct snapraid_file* f
if (keep_track) {
struct snapraid_dealloc* dealloc = dealloc_alloc(scan->state->block_size, file->sub, file->size, file->mtime_sec, file->mtime_nsec);
dealloc_import(dealloc, file);
dealloc_import(&scan->state->infoarr, disk, dealloc, file);
/* insert the dealloc in the dealloc containers */
tommy_list_insert_tail(&disk->dealloclist, &dealloc->nodelist, dealloc);

View file

@ -3075,11 +3075,31 @@ static void state_read_content(struct snapraid_state* state, const char* path, S
log_tag("content_info:dealloc_entry:%s:%s:%" PRIu64 ":%" PRIu64 ":%u\n", disk->name, esc_tag(dealloc->sub), dealloc->size, dealloc->mtime_sec, dealloc->mtime_nsec);
/* read all hashes */
/* read all hashes with their generation */
for (block_off_t k = 0; k < dealloc->blockmax; ++k) {
unsigned char* hash = dealloc->blockhash + k * BLOCK_HASH_SIZE;
struct snapraid_dealloc_block* block = &dealloc->block[k];
ret = sread(f, hash, BLOCK_HASH_SIZE);
c = sgetc(f);
if (c == 'c') {
block->prev = 0;
} else if (c == 'p') {
if (state->prevhash == HASH_UNDEFINED) {
/* LCOV_EXCL_START */
decoding_error(path, f);
log_fatal(EINTERNAL, "Internal inconsistency: Previous dealloc hash without previous checksum!\n");
os_abort();
/* LCOV_EXCL_STOP */
}
block->prev = 1;
} else {
/* LCOV_EXCL_START */
decoding_error(path, f);
log_fatal(ECONTENT, "Invalid dealloc hash type!\n");
os_abort();
/* LCOV_EXCL_STOP */
}
ret = sread(f, block->hash, BLOCK_HASH_SIZE);
if (ret < 0) {
/* LCOV_EXCL_START */
decoding_error(path, f);
@ -3889,7 +3909,7 @@ struct state_write_context {
block_off_t blockmax;
time_t info_oldest;
time_t info_now;
int info_has_rehash;
int has_prevhash_data;
uint64_t count_unsynced;
STREAM* f;
int first;
@ -3963,7 +3983,7 @@ static void* state_write_thread(void* arg)
block_off_t blockmax = context->blockmax;
time_t info_oldest = context->info_oldest;
time_t info_now = context->info_now;
int info_has_rehash = context->info_has_rehash;
int has_prevhash_data = context->has_prevhash_data;
STREAM* f = context->f;
uint32_t crc;
uint64_t t64;
@ -4035,10 +4055,9 @@ static void* state_write_thread(void* arg)
/* LCOV_EXCL_STOP */
}
/* previous hash only present */
/* previous hash is also needed by deallocated blocks after active rehash positions disappear */
if (state->prevhash != HASH_UNDEFINED) {
/* if at least one rehash tag found, we have to save the previous hash */
if (info_has_rehash) {
if (has_prevhash_data) {
sputc('C', f);
if (state->prevhash == HASH_MURMUR3) {
sputc('u', f);
@ -4393,8 +4412,13 @@ static void* state_write_thread(void* arg)
if (context->first)
log_tag("content_info:dealloc_entry:%s:%s:%" PRIu64 ":%" PRIu64 ":%u\n", disk->name, esc_tag(dealloc->sub), dealloc->size, dealloc->mtime_sec, dealloc->mtime_nsec);
/* deallocated hashes are already contiguous in memory */
swrite(dealloc->blockhash, (size_t)dealloc->blockmax * BLOCK_HASH_SIZE, f);
/* write all hashes with their generation */
for (block_off_t k = 0; k < dealloc->blockmax; ++k) {
struct snapraid_dealloc_block* block = &dealloc->block[k];
sputc(block->prev ? 'p' : 'c', f);
swrite(block->hash, BLOCK_HASH_SIZE, f);
}
}
if (serror(f)) {
@ -4567,6 +4591,7 @@ static void state_write_content(struct snapraid_state* state, struct state_write
time_t content_mtime;
int info_has_rehash;
int state_has_rebuild;
int has_dealloc;
int mapping_idx;
block_off_t idx;
uint32_t crc;
@ -4669,22 +4694,21 @@ static void state_write_content(struct snapraid_state* state, struct state_write
}
int content_version = 3;
has_dealloc = 0;
/*
* Force at least version 3 as we want to always store the parity size.
* If there is a REBUILD block or a dealloc list, force version 4.
*/
if (state_has_rebuild) {
content_version = 4;
} else {
for (i = state->disklist; i != 0; i = i->next) {
struct snapraid_disk* disk = i->data;
if (!tommy_list_empty(&disk->dealloclist)) {
content_version = 4;
break;
}
for (i = state->disklist; i != 0; i = i->next) {
struct snapraid_disk* disk = i->data;
if (!tommy_list_empty(&disk->dealloclist)) {
has_dealloc = 1;
break;
}
}
if (state_has_rebuild || has_dealloc)
content_version = 4;
fs_single_thread(state, 0);
@ -4735,7 +4759,7 @@ static void state_write_content(struct snapraid_state* state, struct state_write
context->blockmax = blockmax;
context->info_oldest = info_oldest;
context->info_now = info_now;
context->info_has_rehash = info_has_rehash;
context->has_prevhash_data = info_has_rehash || has_dealloc;
context->count_unsynced = count_unsynced;
context->f = f;
context->first = first;
@ -4914,7 +4938,7 @@ static void state_write_content(struct snapraid_state* state, struct state_write
context->blockmax = blockmax;
context->info_oldest = info_oldest;
context->info_now = info_now;
context->info_has_rehash = info_has_rehash;
context->has_prevhash_data = info_has_rehash || has_dealloc;
context->count_unsynced = count_unsynced;
context->f = f;
context->first = 1;