From 78e87549ca52c200900eaa00f3078526f1eb1343 Mon Sep 17 00:00:00 2001 From: Andrea Mazzoleni Date: Wed, 26 Aug 2026 08:36:19 +0200 Subject: [PATCH] Improve directory scan filesystem boundary detection using statx() for mount and subvolume IDs --- cmdline/app.h | 23 +++++++++++ cmdline/elem.c | 1 - cmdline/elem.h | 1 - cmdline/mingwapp.c | 14 +++++++ cmdline/scan.c | 43 ++++++++++++++++++- cmdline/state.c | 9 ++-- cmdline/unixapp.c | 101 +++++++++++++++++++++++++++++++++++++++++++++ configure.ac | 2 +- 8 files changed, 184 insertions(+), 10 deletions(-) diff --git a/cmdline/app.h b/cmdline/app.h index b5af88ba..a0df1941 100644 --- a/cmdline/app.h +++ b/cmdline/app.h @@ -11,6 +11,8 @@ */ #include "tommyds/tommylist.h" +#define STATIC_ASSERT(name, cond) typedef char static_assert_ ## name[(cond) ? 1 : -1] + #define EINTERNAL EFAULT /**< Internal assertion failed. */ #define EDATA EIO /**< Silent data corruption. */ #define ESOFT EINVAL /**< Software error, like permission denied. */ @@ -45,6 +47,27 @@ static inline int hardlink(const char* a, const char* b) */ int devuuid(uint64_t device_id, const char* device_path, char* uuid, size_t size); +/** + * Filesystem identity for directory boundary checks. + */ +struct fsidentity { + uint64_t device; /**< Device identifier (st_dev). */ + uint64_t mnt_id; /**< Mount identifier (stx_mnt_id or 0). */ + uint64_t subvol; /**< Subvolume identifier (stx_subvol or 0). */ +}; + +/** + * Get the filesystem identity of a path. + * + * The caller already provides the normal stat information. + * This function must NOT repeat lstat()/stat() for normal metadata. + * + * mnt_id and subvol are set to zero when unavailable. + * + * Return 0 on success, -1 on error. + */ +int fsidentity(const char* path, const struct stat* st, struct fsidentity* identity); + /** * Check if the underline file-system support persistent inodes. * Return -1 on error, 0 on success. diff --git a/cmdline/elem.c b/cmdline/elem.c index fde89a9a..d56aed87 100644 --- a/cmdline/elem.c +++ b/cmdline/elem.c @@ -870,7 +870,6 @@ struct snapraid_disk* disk_alloc(const char* name, const char* dir, uint64_t dev disk->fstype[0] = 0; disk->fslabel[0] = 0; disk->mount_device = dev; - disk->dir_device = dev; disk->tick = 0; disk->cached_blocks = 0; disk->progress_file = 0; diff --git a/cmdline/elem.h b/cmdline/elem.h index e4a1f097..fe0ede2e 100644 --- a/cmdline/elem.h +++ b/cmdline/elem.h @@ -402,7 +402,6 @@ struct snapraid_disk { char fslabel[FSINFO_MAX]; /**< Filesystem label */ uint64_t mount_device; /**< Device identifier of the mount_point. */ - uint64_t dir_device; /**< Device identifier of the effective dir. */ block_off_t total_blocks; /**< Number of total blocks. */ block_off_t free_blocks; /**< Number of free blocks at the last sync. */ diff --git a/cmdline/mingwapp.c b/cmdline/mingwapp.c index 1762886c..1520110d 100644 --- a/cmdline/mingwapp.c +++ b/cmdline/mingwapp.c @@ -170,6 +170,20 @@ int fsinfo(const char* path, int* has_persistent_inode, int* has_syncronized_har return 0; } +/****************************************************************************/ +/* identity */ + +int fsidentity(const char* path, const struct stat* st, struct fsidentity* identity) +{ + (void)path; + + identity->device = st->st_dev; + identity->mnt_id = 0; + identity->subvol = 0; + + return 0; +} + /****************************************************************************/ /* snapshot */ diff --git a/cmdline/scan.c b/cmdline/scan.c index 998f0c99..ab17ae03 100644 --- a/cmdline/scan.c +++ b/cmdline/scan.c @@ -40,6 +40,7 @@ struct snapraid_scan { tommy_list link_insert_list; /**< Links to insert. */ tommy_list dir_insert_list; /**< Dirs to insert. */ tommy_list local_filter_list; /**< Filter list specific for the disk. */ + struct fsidentity dir_identity; /**< Effective scan root identity. */ /* nodes for data structures */ tommy_node node; @@ -64,6 +65,9 @@ static struct snapraid_scan* scan_alloc(struct snapraid_state* state, struct sna tommy_list_init(&scan->link_insert_list); tommy_list_init(&scan->dir_insert_list); tommy_list_init(&scan->local_filter_list); + scan->dir_identity.device = 0; + scan->dir_identity.mnt_id = 0; + scan->dir_identity.subvol = 0; scan->is_diff = is_diff; scan->need_write = 0; @@ -1611,13 +1615,31 @@ static int scan_sub(struct snapraid_scan* scan, int level, int is_diff, char* pa if (!st) st = DSTAT(path_next, dd, &st_buf); + struct fsidentity identity; + if (fsidentity(path_next, st, &identity) != 0) { + /* LCOV_EXCL_START */ + /* restore removing additions */ + path_next[path_len] = 0; + sub_next[sub_len] = 0; + log_tag("%s:%u:%s:%s: Statx error. %s.\n", es(errno), 0, disk->name, esc_tag(path_next), strerror(errno)); + log_fatal(errno, "Error accessing directory '%s'. %s.\n", path_next, strerror(errno)); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + /* - * In Unix don't follow mount points in different devices + * In Unix don't follow mount points in different devices or subvolumes * in Windows we are already skipping them reporting them as special files */ - if ((uint64_t)st->st_dev != disk->dir_device) { + if (identity.device != scan->dir_identity.device) { log_tag("%s:%u:%s:%s: Ignoring mount point.\n", es(ESOFT), 0, disk->name, esc_tag(path_next)); log_error(ESOFT, "WARNING! Ignoring mount point '%s' because it appears to be in a different device\n", path_next); + } else if (identity.mnt_id != scan->dir_identity.mnt_id) { + log_tag("%s:%u:%s:%s: Ignoring mount point.\n", es(ESOFT), 0, disk->name, esc_tag(path_next)); + log_error(ESOFT, "WARNING! Ignoring mount point '%s' because it belongs to a different mount\n", path_next); + } else if (identity.subvol != scan->dir_identity.subvol) { + log_tag("%s:%u:%s:%s: Ignoring nested subvolume.\n", es(ESOFT), 0, disk->name, esc_tag(path_next)); + log_error(ESOFT, "WARNING! Ignoring nested subvolume '%s' because it belongs to a different subvolume\n", path_next); } else #endif { @@ -1727,6 +1749,23 @@ static void* scan_disk(void* arg) start = os_tick_ms(); + struct stat st; + if (lstat(disk->dir, &st) != 0) { + /* LCOV_EXCL_START */ + log_tag("%s:%u:%s:%s: Stat error. %s.\n", es(errno), 0, disk->name, esc_tag(disk->dir), strerror(errno)); + log_fatal(errno, "Error accessing directory '%s'. %s.\n", disk->dir, strerror(errno)); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + + if (fsidentity(disk->dir, &st, &scan->dir_identity) != 0) { + /* LCOV_EXCL_START */ + log_tag("%s:%u:%s:%s: Statx error. %s.\n", es(errno), 0, disk->name, esc_tag(disk->dir), strerror(errno)); + log_fatal(errno, "Error accessing directory '%s'. %s.\n", disk->dir, strerror(errno)); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + scan_dir(scan, 0, scan->is_diff, disk->dir, ""); if (!scan->is_diff) diff --git a/cmdline/state.c b/cmdline/state.c index 93aa652a..01d07e1d 100644 --- a/cmdline/state.c +++ b/cmdline/state.c @@ -278,7 +278,7 @@ static void state_config_check(struct snapraid_state* state, const char* path, t continue; #ifdef _WIN32 - if (disk->dir_device == 0) { + if (disk->mount_device == 0) { /* LCOV_EXCL_START */ log_fatal(ESOFT, "Disk '%s' has a zero serial number.\n", disk->dir); log_fatal(ESOFT, "This is not necessarily wrong, but for using SnapRAID\n"); @@ -304,7 +304,7 @@ static void state_config_check(struct snapraid_state* state, const char* path, t /* LCOV_EXCL_START */ log_fatal(ESOFT, "Disks '%s' and '%s' are on the same device.\n", disk->mount_point, other->mount_point); #ifdef _WIN32 - log_fatal(ESOFT, "Both have the serial number '%" PRIx64 "'.\n", disk->dir_device); + log_fatal(ESOFT, "Both have the serial number '%" PRIx64 "'.\n", disk->mount_device); log_fatal(ESOFT, "Try using the 'VolumeID' tool by 'Mark Russinovich'\n"); log_fatal(ESOFT, "to change one of the disk serial.\n"); #endif @@ -336,7 +336,7 @@ static void state_config_check(struct snapraid_state* state, const char* path, t /* LCOV_EXCL_START */ log_fatal(ESOFT, "Disk '%s' and %s '%s' are on the same device.\n", disk->mount_point, lev_name(l), state->parity[l].split_map[s].path); #ifdef _WIN32 - log_fatal(ESOFT, "Both have the serial number '%" PRIx64 "'.\n", disk->dir_device); + log_fatal(ESOFT, "Both have the serial number '%" PRIx64 "'.\n", disk->mount_device); log_fatal(ESOFT, "Try using the 'VolumeID' tool by 'Mark Russinovich'\n"); log_fatal(ESOFT, "to change one of the disk serial.\n"); #endif @@ -352,7 +352,7 @@ static void state_config_check(struct snapraid_state* state, const char* path, t /* LCOV_EXCL_START */ log_fatal(ESOFT, "Disk '%s' and pool '%s' are on the same device.\n", disk->mount_point, state->pool); #ifdef _WIN32 - log_fatal(ESOFT, "Both have the serial number '%" PRIx64 "'.\n", disk->dir_device); + log_fatal(ESOFT, "Both have the serial number '%" PRIx64 "'.\n", disk->mount_device); log_fatal(ESOFT, "Try using the 'VolumeID' tool by 'Mark Russinovich'\n"); log_fatal(ESOFT, "to change one of the disk serial.\n"); #endif @@ -6041,7 +6041,6 @@ static int state_snapshot_dir(struct fssnapshot_struct* fss, const char* name, s return -1; pathcpy(disk->dir, sizeof(disk->dir), vol); - disk->dir_device = st.st_dev; } return 0; diff --git a/cmdline/unixapp.c b/cmdline/unixapp.c index ba07895a..c1724b32 100644 --- a/cmdline/unixapp.c +++ b/cmdline/unixapp.c @@ -2138,6 +2138,107 @@ int fsinfo(const char* path, int* has_persistent_inode, int* has_syncronized_har return 0; } +/****************************************************************************/ +/* identity */ + +#if HAVE_STATX + +#ifndef STATX_MNT_ID +#define STATX_MNT_ID 0x00001000U +#endif +#ifndef STATX_SUBVOL +#define STATX_SUBVOL 0x00008000U +#endif + +struct snapraid_statx_timestamp_abi { + int64_t tv_sec; + uint32_t tv_nsec; + int32_t reserved; +}; + +struct snapraid_statx_abi { + /* 0x00 */ + uint32_t stx_mask; + uint32_t stx_blksize; + uint64_t stx_attributes; + + /* 0x10 */ + uint32_t stx_nlink; + uint32_t stx_uid; + uint32_t stx_gid; + uint16_t stx_mode; + uint16_t spare0; + + /* 0x20 */ + uint64_t stx_ino; + uint64_t stx_size; + uint64_t stx_blocks; + uint64_t stx_attributes_mask; + + /* 0x40 */ + struct snapraid_statx_timestamp_abi stx_atime; + struct snapraid_statx_timestamp_abi stx_btime; + struct snapraid_statx_timestamp_abi stx_ctime; + struct snapraid_statx_timestamp_abi stx_mtime; + + /* 0x80 */ + uint32_t stx_rdev_major; + uint32_t stx_rdev_minor; + uint32_t stx_dev_major; + uint32_t stx_dev_minor; + + /* 0x90 */ + uint64_t stx_mnt_id; + uint32_t stx_dio_mem_align; + uint32_t stx_dio_offset_align; + + /* 0xa0 */ + uint64_t stx_subvol; + + /* ABI continues to 0x100. */ + unsigned char tail[0x100 - 0xa8]; +}; + +STATIC_ASSERT(statx_abi_size, sizeof(struct snapraid_statx_abi) == 0x100); +STATIC_ASSERT(statx_abi_mnt_id, offsetof(struct snapraid_statx_abi, stx_mnt_id) == 0x90); +STATIC_ASSERT(statx_abi_subvol, offsetof(struct snapraid_statx_abi, stx_subvol) == 0xa0); +#endif + +int fsidentity(const char* path, const struct stat* st, struct fsidentity* identity) +{ + (void)path; + + identity->device = st->st_dev; + identity->mnt_id = 0; + identity->subvol = 0; + +#if HAVE_STATX + struct statx stx; + + /* query mount ID and subvolume ID without following symlinks */ + if (statx(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW, STATX_MNT_ID | STATX_SUBVOL, &stx) != 0) { + /* + * Fall back if statx is unsupported by the kernel (ENOSYS) or blocked + * by a seccomp/security sandbox filter (EPERM/EACCES in older Docker/OCI). + */ + if (errno == ENOSYS || errno == EPERM || errno == EACCES) + return 0; + return -1; + } + + /* copy to fixed ABI representation to support libc headers lacking named fields */ + struct snapraid_statx_abi abi; + memcpy(&abi, &stx, sizeof(abi)); + + if ((abi.stx_mask & STATX_MNT_ID) != 0) + identity->mnt_id = abi.stx_mnt_id; + if ((abi.stx_mask & STATX_SUBVOL) != 0) + identity->subvol = abi.stx_subvol; +#endif + + return 0; +} + /****************************************************************************/ /* snapshot */ diff --git a/configure.ac b/configure.ac index 790cef0a..beb4d5dd 100644 --- a/configure.ac +++ b/configure.ac @@ -125,7 +125,7 @@ AC_CHECK_FUNCS([ftruncate fallocate access]) AC_CHECK_FUNCS([fsync posix_fadvise sync_file_range syncfs]) AC_CHECK_FUNCS([getc_unlocked ferror_unlocked]) AC_CHECK_FUNCS([futimes futimens futimesat localtime_r lutimes utimensat]) -AC_CHECK_FUNCS([fstatat flock renameat]) +AC_CHECK_FUNCS([fstatat flock renameat statx]) AC_CHECK_FUNCS([mach_absolute_time]) AC_CHECK_FUNCS([backtrace backtrace_symbols]) AC_SEARCH_LIBS([clock_gettime], [rt])