Improve directory scan filesystem boundary detection using statx() for mount and subvolume IDs

This commit is contained in:
Andrea Mazzoleni 2026-08-26 08:36:19 +02:00
parent 0416366e2d
commit 78e87549ca
8 changed files with 184 additions and 10 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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])