Support filesystems with invalid inode numbers on individual files
Some checks are pending
Build / build-mac (push) Waiting to run
Build / build-linux (push) Waiting to run
Build / build-win (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-07-30 16:20:26 +02:00
parent 5f724b047b
commit 3a5a3b23ee
8 changed files with 89 additions and 57 deletions

View file

@ -48,6 +48,9 @@ SnapRAID HISTORY
controllers and USB enclosures.
* Added support for specifying case-insensitive SMART attribute names
(instead of only numeric IDs) in the `smartignore` configuration option.
* Added support for filesystems where only specific files fail to report a
64-bit inode number (such as ReFS when 128-bit file IDs cannot be mapped),
allowing SnapRAID to continue processing those individual files without inodes.
14.10 2026/08
=============

View file

@ -660,7 +660,6 @@ static int file_post(struct snapraid_state* state, int fix, block_off_t i, struc
struct snapraid_file* collide_file;
struct snapraid_file* file;
block_off_t file_pos;
uint64_t inode;
disk = handle[j].disk;
if (!disk) {
@ -774,10 +773,12 @@ static int file_post(struct snapraid_state* state, int fix, block_off_t i, struc
log_tag("status:recovered:%s:%s\n", disk->name, esc_tag(file->sub));
msg_info("recovered %s\n", fmt_term(disk, file->sub));
inode = handle[j].st.st_ino;
/* search for the corresponding inode */
collide_file = tommy_hashdyn_search(&disk->inodeset, file_inode_compare_to_arg, &inode, file_inode_hash(inode));
uint64_t inode = handle[j].st.st_ino; /* don't know the exact type of st_ino and we cannot pass it by pointer in the search */
if (inode != INODE_INVALID)
collide_file = tommy_hashdyn_search(&disk->inodeset, file_inode_compare_to_arg, &inode, file_inode_hash(inode));
else
collide_file = 0;
/*
* If the inode is already in the database and it refers to a different file name,

View file

@ -213,14 +213,6 @@ struct snapraid_block {
*/
#define FILE_IS_UNSYNCED 0x100
/**
* If the file is without inode.
* It could happen in file-systems where inodes are not persistent,
* or when restoring a full disk with "fix".
* In such cases we have to clear any stored duplicate inode.
* After the scan process completes, no file should have this flag set.
*/
#define FILE_IS_WITHOUT_INODE 0x200
/**
* The file is deleted.

View file

@ -604,7 +604,7 @@ static void scan_file_refresh(struct snapraid_scan* scan, const char* sub, struc
st->st_nlink = synced_st.st_nlink;
}
if (st->st_ino != synced_st.st_ino) {
if (st->st_ino != INODE_INVALID && synced_st.st_ino != INODE_INVALID && st->st_ino != synced_st.st_ino) {
log_tag("%s:%u:%s:%s: Uncached inode change error.\n", es(ESOFT), 0, disk->name, esc_tag(sub));
log_fatal(ESOFT, "DANGER! Detected uncached inode change from %" PRIu64 " to %" PRIu64 " for file '%s'\n",
(uint64_t)st->st_ino, (uint64_t)synced_st.st_ino, sub);
@ -633,7 +633,7 @@ static void scan_file_insert(struct snapraid_scan* scan, struct snapraid_file* f
struct snapraid_disk* disk = scan->disk;
/* insert the file in the containers */
if (!file_flag_has(file, FILE_IS_WITHOUT_INODE))
if (file->inode != INODE_INVALID)
tommy_hashdyn_insert(&disk->inodeset, &file->nodeset, file, file_inode_hash(file->inode));
stamp_lock(disk);
@ -652,9 +652,13 @@ static void scan_file_remove(struct snapraid_scan* scan, struct snapraid_file* f
struct snapraid_disk* disk = scan->disk;
/* remove the file from the containers */
if (!file_flag_has(file, FILE_IS_WITHOUT_INODE))
if (file->inode != INODE_INVALID)
tommy_hashdyn_remove_existing(&disk->inodeset, &file->nodeset);
stamp_lock(disk);
tommy_hashdyn_remove_existing(&disk->pathset, &file->pathset);
tommy_hashdyn_remove_existing(&disk->stampset, &file->stampset);
stamp_unlock(disk);
/*
* Keep track of the removed file (that won't be added later)
@ -670,10 +674,6 @@ static void scan_file_remove(struct snapraid_scan* scan, struct snapraid_file* f
tommy_list_insert_tail(&disk->dealloclist, &dealloc->nodelist, dealloc);
}
stamp_lock(disk);
tommy_hashdyn_remove_existing(&disk->stampset, &file->stampset);
stamp_unlock(disk);
/*
* Deallocate the file from the parity.
*
@ -763,9 +763,11 @@ static void scan_file(struct snapraid_scan* scan, int is_diff, const char* sub,
* Always search with the new inode, in the all new inodes found until now,
* with the eventual presence of also the past inodes
*/
uint64_t inode = st->st_ino;
file = tommy_hashdyn_search(&disk->inodeset, file_inode_compare_to_arg, &inode, file_inode_hash(inode));
uint64_t inode = st->st_ino; /* don't know the exact type of st_ino and we cannot pass it by pointer in the search */
if (inode != INODE_INVALID)
file = tommy_hashdyn_search(&disk->inodeset, file_inode_compare_to_arg, &inode, file_inode_hash(inode));
else
file = 0;
/* identify moved files with past inodes and hardlinks with the new inodes */
if (file) {
@ -901,19 +903,19 @@ static void scan_file(struct snapraid_scan* scan, int is_diff, const char* sub,
* otherwise, it will get removed
*/
/* here the inode must be present */
if (file->inode == INODE_INVALID) {
/* LCOV_EXCL_START */
log_fatal(EINTERNAL, "Internal inconsistency in inode for file '%s%s' as unexpected missing\n", disk->dir, sub);
os_abort();
/* LCOV_EXCL_STOP */
}
/* remove from the inode set */
tommy_hashdyn_remove_existing(&disk->inodeset, &file->nodeset);
/*
* Clear the inode
* this is not really needed for correct functionality
* because we are going to set FILE_IS_WITHOUT_INODE
* but it's easier for debugging to have invalid inodes set to 0
*/
file->inode = 0;
/* mark as missing inode */
file_flag_set(file, FILE_IS_WITHOUT_INODE);
/* clear the inode */
file->inode = INODE_INVALID;
/* go further to find it by name */
}
@ -931,20 +933,18 @@ static void scan_file(struct snapraid_scan* scan, int is_diff, const char* sub,
if (is_file_already_present) {
/* if the file is without an inode */
if (file_flag_has(file, FILE_IS_WITHOUT_INODE)) {
if (file->inode == INODE_INVALID) {
/* set it now */
file->inode = st->st_ino;
/* insert in the set */
tommy_hashdyn_insert(&disk->inodeset, &file->nodeset, file, file_inode_hash(file->inode));
/* unmark as missing inode */
file_flag_clear(file, FILE_IS_WITHOUT_INODE);
/* insert in the set if valid */
if (file->inode != INODE_INVALID)
tommy_hashdyn_insert(&disk->inodeset, &file->nodeset, file, file_inode_hash(file->inode));
} else {
/* here the inode has to be different, otherwise we would have found it before */
if (file->inode == st->st_ino) {
/* LCOV_EXCL_START */
log_fatal(EINTERNAL, "Internal inconsistency in inode '%" PRIu64 "' for files '%s%s' as unexpected matching\n", file->inode, disk->dir, sub);
log_fatal(EINTERNAL, "Internal inconsistency in inode '%" PRIu64 "' for file '%s%s' as unexpected matching\n", file->inode, disk->dir, sub);
os_abort();
/* LCOV_EXCL_STOP */
}
@ -987,7 +987,7 @@ static void scan_file(struct snapraid_scan* scan, int is_diff, const char* sub,
}
/* if when processing the disk we used the past inodes values */
if (has_past_inodes) {
if (has_past_inodes && file->inode != INODE_INVALID && st->st_ino != INODE_INVALID) {
/*
* If persistent inodes are supported, we are sure that the inode number
* is now different, because otherwise the file would have been found
@ -1736,13 +1736,11 @@ static void* scan_disk(void* arg)
node = node->next;
/* remove from the inode set */
tommy_hashdyn_remove_existing(&disk->inodeset, &file->nodeset);
if (file->inode != INODE_INVALID)
tommy_hashdyn_remove_existing(&disk->inodeset, &file->nodeset);
/* clear the inode */
file->inode = 0;
/* mark as missing inode */
file_flag_set(file, FILE_IS_WITHOUT_INODE);
file->inode = INODE_INVALID;
}
}

View file

@ -2364,7 +2364,8 @@ static void state_read_content(struct snapraid_state* state, const char* path, S
file = file_alloc(state->block_size, sub, v_size, v_mtime_sec, v_mtime_nsec, v_inode, 0);
/* insert the file in the file containers */
tommy_hashdyn_insert(&disk->inodeset, &file->nodeset, file, file_inode_hash(file->inode));
if (file->inode != INODE_INVALID)
tommy_hashdyn_insert(&disk->inodeset, &file->nodeset, file, file_inode_hash(file->inode));
tommy_hashdyn_insert(&disk->pathset, &file->pathset, file, file_path_hash(file->sub));
tommy_hashdyn_insert(&disk->stampset, &file->stampset, file, file_stamp_hash(file->size, file->mtime_sec, file->mtime_nsec));
tommy_list_insert_tail(&disk->filelist, &file->nodelist, file);

View file

@ -191,7 +191,7 @@ static int state_hash_process(struct snapraid_state* state, block_off_t blocksta
if (handle[j].st.st_size != file->size
|| handle[j].st.st_mtime != file->mtime_sec
|| STAT_NSEC(&handle[j].st) != file->mtime_nsec
|| handle[j].st.st_ino != file->inode
|| (handle[j].st.st_ino != INODE_INVALID && file->inode != INODE_INVALID && handle[j].st.st_ino != file->inode)
) {
if (handle[j].st.st_size != file->size) {
log_tag("error:%" PRIu64 ":%s:%s: Unexpected size change\n", blockcur, disk->name, esc_tag(file->sub));
@ -564,7 +564,7 @@ static void sync_data_reader(struct snapraid_worker* worker, struct snapraid_tas
if (handle->st.st_size != task->file->size
|| handle->st.st_mtime != task->file->mtime_sec
|| STAT_NSEC(&handle->st) != task->file->mtime_nsec
|| handle->st.st_ino != task->file->inode
|| (handle->st.st_ino != INODE_INVALID && task->file->inode != INODE_INVALID && handle->st.st_ino != task->file->inode)
) {
log_tag("error:%" PRIu64 ":%s:%s: Unexpected attribute change\n", blockcur, disk->name, esc_tag(task->file->sub));
if (handle->st.st_size != task->file->size) {

View file

@ -38,6 +38,10 @@
#define FILE_INVALID_FILE_ID ((ULONGLONG)-1LL)
#endif
#ifndef FILE_UNSUPPORTED_FILE_ID
#define FILE_UNSUPPORTED_FILE_ID 0
#endif
/**
* Direct access to RtlGenRandom().
* This function is accessible only with LoadLibrary() and it's available from Windows XP.
@ -464,16 +468,44 @@ static int windows_info2stat(const BY_HANDLE_FILE_INFORMATION* info, const FILE_
*
* From Microsoft "Application Compatibility with ReFS"
* http://download.microsoft.com/download/C/B/3/CB3561DC-6BF6-443D-B5B9-9676ACDF7F75/Application%20Compatibility%20with%20ReFS.docx
* "64-bit file identifier can be obtained from GetFileInformationByHandle in"
* "the nFileIndexHigh and nFileIndexLow members. This API is an extended version"
* "that includes 128-bit file identifiers. If GetFileInformationByHandle returns"
* "FILE_INVALID_FILE_ID, the identifier may only be described in 128 bit form."
* 64-bit file identifier can be obtained from GetFileInformationByHandle in
* the nFileIndexHigh and nFileIndexLow members. This API is an extended version
* that includes 128-bit file identifiers. If GetFileInformationByHandle returns
* FILE_INVALID_FILE_ID, the identifier may only be described in 128 bit form.
*
* From Microsoft "[MS-FSCC]: File System Control Codes - 11/21/2025 v60.0 - 6 Appendix B: Product Behavior"
* https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d4bc551b-7aaf-4b4f-ba0e-3a75e7c528f0
*
* 64bitFileID Generate Stable Unique
* FAT Yes No No
* EXFAT Yes No No
* FAT32 Yes No No
* UDFS Yes Yes Yes
* NTFS Yes Yes Yes
* ReFS Yes Yes Yes
*
* NTFS computes the 64-bit file ID as follows: the low 48 bits are the index of the
* file's primary record in the master file table (MFT); the remaining 16 bits are
* a sequence number. Therefore, it is possible, though rare, that a different file
* can have the same 64-bit file ID as a file on that volume had in the past.
*
* ReFS maps a subset of the possible 128-bit file ID values to a 64-bit value using
* a reversible algorithm; for values outside of this subset, ReFS sets the 64-bit
* file ID to -1.
*
* From Microsoft "[MS-FSCC]: File System Control Codes - 11/21/2025 v60.0 - 2.1.9 64-bit file ID"
* https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/2d3333fe-fc98-4a6f-98a2-4bb805aff407?utm_source=chatgpt.com
*
* A 64-bit file ID value uniquely identifies a file within a given volume. This
* identifier is generated and stored by the file system. The identifier SHOULD
* be unique to the volume and stable until the file is deleted.
* For file systems that do not support a 64-bit file ID, this field MUST be
* set to 0, and MUST be ignored.
* For files for which a unique 64-bit file ID cannot be established, this
* field MUST be set to 0xFFFFFFFFFFFFFFFF, and MUST be ignored.
*/
if (st->st_ino == (uint64_t)FILE_INVALID_FILE_ID) {
errno = EINVAL;
os_syslog(OS_LVL_INFO, "invalid inode number! Is this ReFS?");
return -1;
}
if (st->st_ino == (uint64_t)FILE_INVALID_FILE_ID || st->st_ino == (uint64_t)FILE_UNSUPPORTED_FILE_ID)
st->st_ino = INODE_INVALID;
return 0;
}

View file

@ -195,6 +195,11 @@ int os_validate_exec_input(const char* str);
/****************************************************************************/
/* fs */
/**
* Invalid inode number
*/
#define INODE_INVALID 0
/**
* Physical offset not yet read.
*/