Since commit a528d35e8bfc ("statx: Add a system call to make enhanced file info available") kernel has statx(2) system call. This patch implements FUSE_STATX as a superset of FUSE_GETATTR, which enables user-space file-systems to report statx information; in particular, creation (birth) time. The newly added struct fuse_statx_timestamp has same members as struct statx_timestamp, where 'sec' is __s64 and padded with reserved field. Likewise, layout of struct fuse_statx follows struct statx, including trailing spare place-holders, making it 240 bytes in size, and struct fuse_statx_out 256 bytes in size. Due to difference in size (strcut fuse_attr is only 88 bytes), FUSE_GETATTR is preferred over FUSE_STATX unless request_mask has STATX_BTIME active. With this patch: $ stat --format %w /path/to/fuse/fs/file 2021-03-19 10:42:07.284912429 +0200 Tested with glibc-2.32 and coreutils-8.32 over 'voluta' user-space file-system. Signed-off-by: Shachar Sharon <synarete@xxxxxxxxx> --- fs/fuse/dir.c | 161 ++++++++++++++++++++++++++++++++++---- fs/fuse/fuse_i.h | 3 + include/uapi/linux/fuse.h | 48 +++++++++++- 3 files changed, 194 insertions(+), 18 deletions(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index 06a18700a845..aa33669b9fda 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -111,6 +111,11 @@ static u64 attr_timeout(struct fuse_attr_out *o) return time_to_jiffies(o->attr_valid, o->attr_valid_nsec); } +static u64 statx_timeout(struct fuse_statx_out *o) +{ + return time_to_jiffies(o->attr_valid, o->attr_valid_nsec); +} + u64 entry_attr_timeout(struct fuse_entry_out *o) { return time_to_jiffies(o->attr_valid, o->attr_valid_nsec); @@ -1022,8 +1027,116 @@ static void fuse_fillattr(struct inode *inode, struct fuse_attr *attr, stat->blksize = 1 << blkbits; } -static int fuse_do_getattr(struct inode *inode, struct kstat *stat, - struct file *file) +static void fuse_fillattr2(struct inode *inode, struct fuse_attr *attr, + struct fuse_statx *stx, struct kstat *stat) +{ + fuse_fillattr(inode, attr, stat); + stat->result_mask = stx->mask; + stat->attributes = stx->attributes; + stat->attributes_mask = stx->attributes_mask; + if (stx->mask & STATX_BTIME) { + stat->btime.tv_sec = stx->btime.sec; + stat->btime.tv_nsec = stx->btime.nsec; + } +} + +static void fuse_statx_to_attr(struct fuse_statx *stx, struct fuse_attr *attr) +{ + attr->ino = stx->ino; + attr->size = stx->size; + attr->blocks = stx->blocks; + attr->atime = stx->atime.sec; + attr->mtime = stx->mtime.sec; + attr->ctime = stx->ctime.sec; + attr->atimensec = stx->atime.nsec; + attr->mtimensec = stx->mtime.nsec; + attr->ctimensec = stx->ctime.nsec; + attr->mode = stx->mode; + attr->nlink = stx->nlink; + attr->uid = stx->uid; + attr->gid = stx->gid; + attr->rdev = MKDEV(stx->rdev_major, stx->rdev_minor); + attr->blksize = stx->blksize; + attr->flags = stx->flags; +} + +static int fuse_require_valid_attr(struct inode *inode, struct fuse_attr *attr) +{ + int ret = 0; + + if (fuse_invalid_attr(attr) || (inode->i_mode ^ attr->mode) & S_IFMT) { + fuse_make_bad(inode); + ret = -EIO; + } + return ret; +} + +static void fuse_getattr_inargs(struct inode *inode, struct file *file, + u32 *getattr_flags, u64 *fh) +{ + /* Directories have separate file-handle space */ + if (file && S_ISREG(inode->i_mode)) { + struct fuse_file *ff = file->private_data; + + *getattr_flags |= FUSE_GETATTR_FH; + *fh = ff->fh; + } +} + +static int __fuse_do_statx(struct inode *inode, struct kstat *stat, + struct file *file, u32 request_mask) +{ + int err; + struct fuse_attr attr; + struct fuse_statx_in inarg; + struct fuse_statx_out outarg; + struct fuse_mount *fm = get_fuse_mount(inode); + FUSE_ARGS(args); + u64 attr_version; + + attr_version = fuse_get_attr_version(fm->fc); + + memset(&inarg, 0, sizeof(inarg)); + memset(&outarg, 0, sizeof(outarg)); + fuse_getattr_inargs(inode, file, &inarg.getattr_flags, &inarg.fh); + inarg.mask = request_mask | STATX_BASIC_STATS; + args.opcode = FUSE_STATX; + args.nodeid = get_node_id(inode); + args.in_numargs = 1; + args.in_args[0].size = sizeof(inarg); + args.in_args[0].value = &inarg; + args.out_numargs = 1; + args.out_args[0].size = sizeof(outarg); + args.out_args[0].value = &outarg; + err = fuse_simple_request(fm, &args); + if (err == -ENOSYS) + goto no_statx; + + if (err) + return err; + + if ((outarg.attr.mask & STATX_BASIC_STATS) != STATX_BASIC_STATS) + goto no_statx; + + fuse_statx_to_attr(&outarg.attr, &attr); + err = fuse_require_valid_attr(inode, &attr); + if (err) + return err; + + fuse_change_attributes(inode, &attr, + statx_timeout(&outarg), attr_version); + if (stat) + fuse_fillattr2(inode, &attr, &outarg.attr, stat); + + return err; + +no_statx: + fm->fc->no_statx = 1; + return -EINVAL; +} + +static int __fuse_do_getattr(struct inode *inode, struct kstat *stat, + struct file *file) { int err; struct fuse_getattr_in inarg; @@ -1036,13 +1149,7 @@ static int fuse_do_getattr(struct inode *inode, struct kstat *stat, memset(&inarg, 0, sizeof(inarg)); memset(&outarg, 0, sizeof(outarg)); - /* Directories have separate file-handle space */ - if (file && S_ISREG(inode->i_mode)) { - struct fuse_file *ff = file->private_data; - - inarg.getattr_flags |= FUSE_GETATTR_FH; - inarg.fh = ff->fh; - } + fuse_getattr_inargs(inode, file, &inarg.getattr_flags, &inarg.fh); args.opcode = FUSE_GETATTR; args.nodeid = get_node_id(inode); args.in_numargs = 1; @@ -1053,11 +1160,8 @@ static int fuse_do_getattr(struct inode *inode, struct kstat *stat, args.out_args[0].value = &outarg; err = fuse_simple_request(fm, &args); if (!err) { - if (fuse_invalid_attr(&outarg.attr) || - (inode->i_mode ^ outarg.attr.mode) & S_IFMT) { - fuse_make_bad(inode); - err = -EIO; - } else { + err = fuse_require_valid_attr(inode, &outarg.attr); + if (!err) { fuse_change_attributes(inode, &outarg.attr, attr_timeout(&outarg), attr_version); @@ -1068,11 +1172,31 @@ static int fuse_do_getattr(struct inode *inode, struct kstat *stat, return err; } +static int fuse_do_getattr(struct inode *inode, struct kstat *stat, + struct file *file, u32 request_mask) +{ + struct fuse_conn *fc = get_fuse_conn(inode); + int err; + + if (fc->no_statx || fc->minor < 34 || !(request_mask & STATX_BTIME)) + goto getattr; + + err = __fuse_do_statx(inode, stat, file, request_mask); + if (err && fc->no_statx) + goto getattr; + + return err; + +getattr: + return __fuse_do_getattr(inode, stat, file); +} + static int fuse_update_get_attr(struct inode *inode, struct file *file, struct kstat *stat, u32 request_mask, unsigned int flags) { struct fuse_inode *fi = get_fuse_inode(inode); + struct fuse_conn *fc = get_fuse_conn(inode); int err = 0; bool sync; @@ -1082,12 +1206,14 @@ static int fuse_update_get_attr(struct inode *inode, struct file *file, sync = false; else if (request_mask & READ_ONCE(fi->inval_mask)) sync = true; + else if (!fc->no_statx && (request_mask & STATX_BTIME)) + sync = true; else sync = time_before64(fi->i_time, get_jiffies_64()); if (sync) { forget_all_cached_acls(inode); - err = fuse_do_getattr(inode, stat, file); + err = fuse_do_getattr(inode, stat, file, request_mask); } else if (stat) { generic_fillattr(&init_user_ns, inode, stat); stat->mode = fi->orig_i_mode; @@ -1235,7 +1361,7 @@ static int fuse_perm_getattr(struct inode *inode, int mask) return -ECHILD; forget_all_cached_acls(inode); - return fuse_do_getattr(inode, NULL, NULL); + return fuse_do_getattr(inode, NULL, NULL, STATX_BASIC_STATS); } /* @@ -1789,7 +1915,8 @@ static int fuse_setattr(struct user_namespace *mnt_userns, struct dentry *entry, * ia_mode calculation may have used stale i_mode. * Refresh and recalculate. */ - ret = fuse_do_getattr(inode, NULL, file); + ret = fuse_do_getattr(inode, NULL, file, + STATX_BASIC_STATS); if (ret) return ret; diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index 63d97a15ffde..15cd1968aac9 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -755,6 +755,9 @@ struct fuse_conn { /* Auto-mount submounts announced by the server */ unsigned int auto_submounts:1; + /** Is statx not implemented by fs? */ + unsigned int no_statx:1; + /** The number of requests waiting for completion */ atomic_t num_waiting; diff --git a/include/uapi/linux/fuse.h b/include/uapi/linux/fuse.h index 54442612c48b..36ac3c40b922 100644 --- a/include/uapi/linux/fuse.h +++ b/include/uapi/linux/fuse.h @@ -179,6 +179,9 @@ * 7.33 * - add FUSE_HANDLE_KILLPRIV_V2, FUSE_WRITE_KILL_SUIDGID, FATTR_KILL_SUIDGID * - add FUSE_OPEN_KILL_SUIDGID + * + * 7.34 + * - add FUSE_STATX */ #ifndef _LINUX_FUSE_H @@ -214,7 +217,7 @@ #define FUSE_KERNEL_VERSION 7 /** Minor version number of this interface */ -#define FUSE_KERNEL_MINOR_VERSION 33 +#define FUSE_KERNEL_MINOR_VERSION 34 /** The node ID of the root inode */ #define FUSE_ROOT_ID 1 @@ -499,6 +502,7 @@ enum fuse_opcode { FUSE_COPY_FILE_RANGE = 47, FUSE_SETUPMAPPING = 48, FUSE_REMOVEMAPPING = 49, + FUSE_STATX = 50, /* CUSE specific operations */ CUSE_INIT = 4096, @@ -957,4 +961,46 @@ struct fuse_removemapping_one { #define FUSE_REMOVEMAPPING_MAX_ENTRY \ (PAGE_SIZE / sizeof(struct fuse_removemapping_one)) +struct fuse_statx_timestamp { + int64_t sec; + uint32_t nsec; + int32_t reserved; +}; + +struct fuse_statx { + uint32_t mask; + uint32_t blksize; + uint64_t attributes; + uint32_t nlink; + uint32_t uid; + uint32_t gid; + uint32_t mode; + uint64_t ino; + uint64_t size; + uint64_t blocks; + uint64_t attributes_mask; + struct fuse_statx_timestamp atime; + struct fuse_statx_timestamp btime; + struct fuse_statx_timestamp ctime; + struct fuse_statx_timestamp mtime; + uint32_t rdev_major; + uint32_t rdev_minor; + uint32_t flags; + uint32_t padding; + uint64_t spare[12]; +}; + +struct fuse_statx_in { + uint32_t getattr_flags; + uint32_t mask; + uint64_t fh; +}; + +struct fuse_statx_out { + uint64_t attr_valid; + uint32_t attr_valid_nsec; + uint32_t dummy; + struct fuse_statx attr; +}; + #endif /* _LINUX_FUSE_H */ -- 2.26.3