From 7269925a87c481e8b80ace4c1b922eb47e280dad Mon Sep 17 00:00:00 2001 From: "Yan, Zheng" Date: Wed, 22 Jul 2026 10:09:06 +0800 Subject: [PATCH] fuse: add subvol_root_ino mount option for subvolume support Filesystems that support snapshots and subvolumes may have inodes with the same inode number across different subvolumes. When these inodes are accessed through the same FUSE mount, tools like cp(1) can incorrectly report "File X and Y are the same file" because they share the same device number (sb->s_dev). Add a new mount option "subvol_root_ino" that specifies the inode number of snapshot/subvolume root. When set, FUSE allocates unique anonymous block device ID for inodes whose orig_ino matches this value, and all inodes under that subvolume inherit the same device ID. This gives each subvolume a distinct device number in stat(2), avoiding inode number conflicts. The feature is implemented by: - Adding sub_dev to fuse_inode, to track per-subvolume device ID - Adding subvol_root_ino to fuse_conn - Extending fuse_iget and fuse_lookup_name to propagate sub_dev - Reporting fi->sub_dev in fuse_fillattr/fuse_getattr when set - Disabling NFS export_support when subvol_root_ino is enabled, since the two features are incompatible Signed-off-by: "Yan, Zheng" --- fs/fuse/dir.c | 25 +++++++++++++------- fs/fuse/fuse_i.h | 25 ++++++++++++++++---- fs/fuse/inode.c | 59 ++++++++++++++++++++++++++++++++--------------- fs/fuse/readdir.c | 3 ++- 4 files changed, 80 insertions(+), 32 deletions(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index 5ca3ebb36591e6..03096dbda777de 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -431,7 +431,8 @@ bool fuse_invalid_attr(struct fuse_attr *attr) return !fuse_valid_type(attr->mode) || !fuse_valid_size(attr->size); } -int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name, +int fuse_lookup_name(struct super_block *sb, dev_t sub_dev, + u64 nodeid, const struct qstr *name, struct fuse_entry_out *outarg, struct inode **inode) { struct fuse_mount *fm = get_fuse_mount_super(sb); @@ -468,7 +469,8 @@ int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name outarg->generation = 0; } - *inode = fuse_iget(sb, outarg->nodeid, outarg->generation, + *inode = fuse_iget(sb, sub_dev, + outarg->nodeid, outarg->generation, &outarg->attr, ATTR_TIMEOUT(outarg), attr_version, evict_ctr); err = -ENOMEM; @@ -498,7 +500,8 @@ static struct dentry *fuse_lookup(struct inode *dir, struct dentry *entry, return ERR_PTR(-EIO); locked = fuse_lock_inode(dir); - err = fuse_lookup_name(dir->i_sb, get_node_id(dir), &entry->d_name, + err = fuse_lookup_name(dir->i_sb, get_sub_dev(dir), + get_node_id(dir), &entry->d_name, &outarg, &inode); fuse_unlock_inode(dir, locked); if (err == -ENOENT) { @@ -756,7 +759,8 @@ static int fuse_create_open(struct inode *dir, struct dentry *entry, ff->fh = outopen.fh; ff->nodeid = outentry.nodeid; ff->open_flags = outopen.open_flags; - inode = fuse_iget(dir->i_sb, outentry.nodeid, outentry.generation, + inode = fuse_iget(dir->i_sb, get_sub_dev(dir), + outentry.nodeid, outentry.generation, &outentry.attr, ATTR_TIMEOUT(&outentry), 0, 0); if (!inode) { flags &= ~(O_CREAT | O_EXCL | O_TRUNC); @@ -886,8 +890,9 @@ static int create_new_entry(struct fuse_mount *fm, struct fuse_args *args, if ((outarg.attr.mode ^ mode) & S_IFMT) goto out_put_forget_req; - inode = fuse_iget(dir->i_sb, outarg.nodeid, outarg.generation, - &outarg.attr, ATTR_TIMEOUT(&outarg), 0, 0); + inode = fuse_iget(dir->i_sb, get_sub_dev(dir), outarg.nodeid, + outarg.generation, &outarg.attr, + ATTR_TIMEOUT(&outarg), 0, 0); if (!inode) { fuse_queue_forget(fm->fc, forget, outarg.nodeid, 1); return -ENOMEM; @@ -1207,8 +1212,9 @@ static void fuse_fillattr(struct inode *inode, struct fuse_attr *attr, { unsigned int blkbits; struct fuse_conn *fc = get_fuse_conn(inode); + struct fuse_inode *fi = get_fuse_inode(inode); - stat->dev = inode->i_sb->s_dev; + stat->dev = fi->sub_dev ? fi->sub_dev : inode->i_sb->s_dev; stat->ino = attr->ino; stat->mode = (inode->i_mode & S_IFMT) | (attr->mode & 07777); stat->nlink = attr->nlink; @@ -1446,6 +1452,8 @@ static int fuse_update_get_attr(struct inode *inode, struct file *file, generic_fillattr(&nop_mnt_idmap, sx_mask, inode, stat); stat->mode = fi->orig_i_mode; stat->ino = fi->orig_ino; + if (fi->sub_dev) + stat->dev = fi->sub_dev; if (test_bit(FUSE_I_BTIME, &fi->state)) { stat->btime = fi->i_btime; stat->result_mask |= STATX_BTIME; @@ -2247,8 +2255,9 @@ static int fuse_getattr(struct mnt_idmap *idmap, * If user explicitly requested *nothing* then don't * error out, but return st_dev only. */ + struct fuse_inode *fi = get_fuse_inode(inode); stat->result_mask = 0; - stat->dev = inode->i_sb->s_dev; + stat->dev = fi->sub_dev ? fi->sub_dev : inode->i_sb->s_dev; return 0; } return -EACCES; diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index 72aabf8dd5ff31..ce5ace9fe1ffde 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -136,6 +136,9 @@ struct fuse_inode { /** 64 bit inode number */ u64 orig_ino; + /** Device ID for sub-root (orig_ino == fc->subvol_root_ino) */ + dev_t sub_dev; + /** Version of last attribute change */ u64 attr_version; @@ -583,6 +586,9 @@ struct fuse_fs_context { unsigned int blksize; const char *subtype; + /* Inode number for subvolume-root */ + u64 subvol_root_ino; + /* DAX device, may be NULL */ struct dax_device *dax_dev; @@ -897,6 +903,9 @@ struct fuse_conn { /** Device ID from the root super block */ dev_t dev; + /** Inode number for subvolume (0 = disabled) */ + u64 subvol_root_ino; + /** Dentries in the control filesystem */ struct dentry *ctl_dentry[FUSE_CTL_NUM_DENTRIES]; @@ -1019,6 +1028,11 @@ static inline u64 get_node_id(struct inode *inode) return get_fuse_inode(inode)->nodeid; } +static inline dev_t get_sub_dev(struct inode *inode) +{ + return get_fuse_inode(inode)->sub_dev; +} + static inline int invalid_nodeid(u64 nodeid) { return !nodeid || nodeid == FUSE_ROOT_ID; @@ -1099,12 +1113,13 @@ extern const struct dentry_operations fuse_root_dentry_operations; /** * Get a filled in inode */ -struct inode *fuse_iget(struct super_block *sb, u64 nodeid, - int generation, struct fuse_attr *attr, - u64 attr_valid, u64 attr_version, - u64 evict_ctr); +struct inode *fuse_iget(struct super_block *sb, dev_t sub_dev, + u64 nodeid, int generation, + struct fuse_attr *attr, u64 attr_valid, + u64 attr_version, u64 evict_ctr); -int fuse_lookup_name(struct super_block *sb, u64 nodeid, const struct qstr *name, +int fuse_lookup_name(struct super_block *sb, dev_t sub_dev, + u64 nodeid, const struct qstr *name, struct fuse_entry_out *outarg, struct inode **inode); /** diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index a2d496160da4c5..a17aebc3f6c2d7 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -153,6 +153,7 @@ static void fuse_cleanup_submount_lookup(struct fuse_conn *fc, static void fuse_evict_inode(struct inode *inode) { struct fuse_inode *fi = get_fuse_inode(inode); + struct fuse_conn *fc = get_fuse_conn_super(inode->i_sb); /* Will write inode on close/munmap and in all other dirtiers */ WARN_ON(inode->i_state & I_DIRTY_INODE); @@ -160,8 +161,6 @@ static void fuse_evict_inode(struct inode *inode) truncate_inode_pages_final(&inode->i_data); clear_inode(inode); if (inode->i_sb->s_flags & SB_ACTIVE) { - struct fuse_conn *fc = get_fuse_conn(inode); - if (FUSE_IS_DAX(inode)) fuse_dax_inode_cleanup(inode); if (fi->nlookup) { @@ -191,6 +190,13 @@ static void fuse_evict_inode(struct inode *inode) WARN_ON(!list_empty(&fi->queued_writes)); fuse_dlm_cache_release_locks(fi); } + /* + * Must be done here rather than in ->free_inode(), which is not + * ordered against superblock destruction and thus must not touch + * the fuse_conn. + */ + if (fi->sub_dev && fi->orig_ino == fc->subvol_root_ino) + free_anon_bdev(fi->sub_dev); } static int fuse_reconfigure(struct fs_context *fsc) @@ -340,8 +346,6 @@ static void fuse_change_attributes_common_sx(struct inode *inode, if (!fc->default_permissions) inode->i_mode &= ~S_ISVTX; - fi->orig_ino = attr->ino; - /* * We are refreshing inode data and it is possible that another * client set suid/sgid or security.capability xattr. So clear @@ -442,8 +446,6 @@ void fuse_change_attributes_common(struct inode *inode, struct fuse_attr *attr, if (!fc->default_permissions) inode->i_mode &= ~S_ISVTX; - fi->orig_ino = attr->ino; - /* * We are refreshing inode data and it is possible that another * client set suid/sgid or security.capability xattr. So clear @@ -611,10 +613,10 @@ static int fuse_inode_set(struct inode *inode, void *_nodeidp) return 0; } -struct inode *fuse_iget(struct super_block *sb, u64 nodeid, - int generation, struct fuse_attr *attr, - u64 attr_valid, u64 attr_version, - u64 evict_ctr) +struct inode *fuse_iget(struct super_block *sb, dev_t sub_dev, + u64 nodeid, int generation, + struct fuse_attr *attr, u64 attr_valid, + u64 attr_version, u64 evict_ctr) { struct inode *inode; struct fuse_inode *fi; @@ -654,12 +656,21 @@ struct inode *fuse_iget(struct super_block *sb, u64 nodeid, if (!inode) return NULL; + fi = get_fuse_inode(inode); if ((inode->i_state & I_NEW)) { inode->i_flags |= S_NOATIME; if (!fc->writeback_cache || !S_ISREG(attr->mode)) inode->i_flags |= S_NOCMTIME; inode->i_generation = generation; fuse_init_inode(inode, attr, fc); + fi->orig_ino = attr->ino; + if (fc->subvol_root_ino && fi->orig_ino == fc->subvol_root_ino) { + int err = get_anon_bdev(&fi->sub_dev); + if (err) + pr_warn("failed to alloc anon bdev\n"); + } else { + fi->sub_dev = sub_dev; + } unlock_new_inode(inode); } else if (fuse_stale_inode(inode, generation, attr)) { /* nodeid was reused, any I/O on the old inode should fail */ @@ -670,7 +681,6 @@ struct inode *fuse_iget(struct super_block *sb, u64 nodeid, goto retry; } } - fi = get_fuse_inode(inode); spin_lock(&fi->lock); fi->nlookup++; spin_unlock(&fi->lock); @@ -984,6 +994,7 @@ enum { OPT_ALLOW_OTHER, OPT_MAX_READ, OPT_BLKSIZE, + OPT_SUBVOL_ROOT_INO, OPT_ERR }; @@ -998,6 +1009,7 @@ static const struct fs_parameter_spec fuse_fs_parameters[] = { fsparam_u32 ("max_read", OPT_MAX_READ), fsparam_u32 ("blksize", OPT_BLKSIZE), fsparam_string ("subtype", OPT_SUBTYPE), + fsparam_u64 ("subvol_root_ino", OPT_SUBVOL_ROOT_INO), {} }; @@ -1097,6 +1109,10 @@ static int fuse_parse_param(struct fs_context *fsc, struct fs_parameter *param) ctx->blksize = result.uint_32; break; + case OPT_SUBVOL_ROOT_INO: + ctx->subvol_root_ino = result.uint_64; + break; + default: return -EINVAL; } @@ -1262,7 +1278,7 @@ static struct inode *fuse_get_root_inode(struct super_block *sb, unsigned mode) attr.mode = mode; attr.ino = FUSE_ROOT_ID; attr.nlink = 1; - return fuse_iget(sb, FUSE_ROOT_ID, 0, &attr, 0, 0, 0); + return fuse_iget(sb, 0, FUSE_ROOT_ID, 0, &attr, 0, 0, 0); } struct fuse_inode_handle { @@ -1289,8 +1305,8 @@ static struct dentry *fuse_get_dentry(struct super_block *sb, if (!fc->export_support) goto out_err; - err = fuse_lookup_name(sb, handle->nodeid, &name, &outarg, - &inode); + err = fuse_lookup_name(sb, 0, handle->nodeid, &name, + &outarg, &inode); if (err && err != -ENOENT) goto out_err; if (err || !inode) { @@ -1390,7 +1406,7 @@ static struct dentry *fuse_get_parent(struct dentry *child) if (!fc->export_support) return ERR_PTR(-ESTALE); - err = fuse_lookup_name(child_inode->i_sb, get_node_id(child_inode), + err = fuse_lookup_name(child_inode->i_sb, 0, get_node_id(child_inode), &dotdot_name, &outarg, &inode); if (err) { if (err == -ENOENT) @@ -1524,8 +1540,8 @@ static void process_init_reply(struct fuse_mount *fm, struct fuse_args *args, if (flags & FUSE_ATOMIC_O_TRUNC) fc->atomic_o_trunc = 1; if (arg->minor >= 9) { - /* LOOKUP has dependency on proto version */ - if (flags & FUSE_EXPORT_SUPPORT) + if ((flags & FUSE_EXPORT_SUPPORT) && + !fc->subvol_root_ino) fc->export_support = 1; } if (flags & FUSE_BIG_WRITES) @@ -1674,6 +1690,12 @@ static struct fuse_init_args *fuse_new_init(struct fuse_mount *fm) if (fuse_uring_enabled()) flags |= FUSE_OVER_IO_URING; + if (fm->fc->subvol_root_ino) { + pr_warn("subvol_root_ino=%llu, disabling export_support\n", + fm->fc->subvol_root_ino); + flags &= ~FUSE_EXPORT_SUPPORT; + } + ia->in.flags = flags; ia->in.flags2 = flags >> 32; @@ -1890,7 +1912,7 @@ static int fuse_fill_super_submount(struct super_block *sb, return -ENOMEM; fuse_fill_attr_from_inode(&root_attr, parent_fi); - root = fuse_iget(sb, parent_fi->nodeid, 0, &root_attr, 0, 0, + root = fuse_iget(sb, 0, parent_fi->nodeid, 0, &root_attr, 0, 0, fuse_get_evict_ctr(fm->fc)); /* * This inode is just a duplicate, so it is not looked up and @@ -2032,6 +2054,7 @@ int fuse_fill_super_common(struct super_block *sb, struct fuse_fs_context *ctx) fc->destroy = ctx->destroy; fc->no_control = ctx->no_control; fc->no_force_umount = ctx->no_force_umount; + fc->subvol_root_ino = ctx->subvol_root_ino; err = -ENOMEM; root = fuse_get_root_inode(sb, ctx->rootmode); diff --git a/fs/fuse/readdir.c b/fs/fuse/readdir.c index ef231156599099..892453614889d3 100644 --- a/fs/fuse/readdir.c +++ b/fs/fuse/readdir.c @@ -231,7 +231,8 @@ static int fuse_direntplus_link(struct file *file, * which bumps nlookup inside */ } else { - inode = fuse_iget(dir->i_sb, o->nodeid, o->generation, + inode = fuse_iget(dir->i_sb, get_sub_dev(dir), + o->nodeid, o->generation, &o->attr, ATTR_TIMEOUT(o), attr_version, evict_ctr); if (!inode)