* [RFC PATCH v2 1/3] fs: add filename_utimensat()
2026-11-04 11:06 [RFC PATCH v2 0/3] io_uring: add IORING_OP_UTIMENSAT Méven Car
@ 2026-11-04 11:06 ` Méven Car
2026-11-09 11:05 ` Jan Kara
2026-11-04 11:06 ` [RFC PATCH v2 2/3] io_uring: add support for utimensat Méven Car
2026-11-04 11:06 ` [RFC PATCH v2 3/3] io_uring: set times inline when the filesystem cannot sleep Méven Car
2 siblings, 1 reply; 7+ messages in thread
From: Méven Car @ 2026-11-04 11:06 UTC (permalink / raw)
To: Jens Axboe
Cc: io-uring, linux-fsdevel, linux-mm, Alexander Viro,
Christian Brauner, Jan Kara, Hugh Dickins, Baolin Wang,
Andrew Morton, Méven Car
From: Méven Car <meven@kde.org>
Copy utilities that preserve timestamps, like cp -p or a file manager,
set the times of every copy after its last write. To do that through
io_uring, the request needs the utimensat() lookup on a name that was
imported at submission time.
io_uring takes the path of a request when it is submitted and looks it
up later, from a worker thread. The path ops it already has call the
filename_*() helpers of fs/namei.c with a struct filename. utimensat(2)
had no such helper: do_utimes_path() took the user pointer and imported
the name itself.
Split the lookup and the vfs_utimes() call out of do_utimes_path() into
filename_utimensat(), declared in fs/internal.h next to the other
filename_*() helpers. do_utimes_path() now only imports the name.
Signed-off-by: Méven Car <meven@kde.org>
---
fs/internal.h | 2 ++
fs/utimes.c | 12 +++++++++---
2 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/fs/internal.h b/fs/internal.h
index c658c8a5ebd5..985b2bc5895e 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -63,6 +63,8 @@ int filename_mknodat(int dfd, struct filename *name, umode_t mode, unsigned int
int filename_symlinkat(struct filename *from, int newdfd, struct filename *to);
int filename_linkat(int olddfd, struct filename *old, int newdfd,
struct filename *new, int flags);
+int filename_utimensat(int dfd, struct filename *name,
+ struct timespec64 *times, int flags);
int vfs_tmpfile(struct mnt_idmap *idmap,
const struct path *parentpath,
struct file *file, umode_t mode);
diff --git a/fs/utimes.c b/fs/utimes.c
index e22664e4115f..8072f05ac8dc 100644
--- a/fs/utimes.c
+++ b/fs/utimes.c
@@ -79,8 +79,8 @@ int vfs_utimes(const struct path *path, struct timespec64 *times)
}
EXPORT_SYMBOL_GPL(vfs_utimes);
-static int do_utimes_path(int dfd, const char __user *filename,
- struct timespec64 *times, int flags)
+int filename_utimensat(int dfd, struct filename *name,
+ struct timespec64 *times, int flags)
{
struct path path;
int lookup_flags = 0, error;
@@ -91,7 +91,6 @@ static int do_utimes_path(int dfd, const char __user *filename,
if (!(flags & AT_SYMLINK_NOFOLLOW))
lookup_flags |= LOOKUP_FOLLOW;
- CLASS(filename_uflags, name)(filename, flags);
retry:
error = filename_lookup(dfd, name, lookup_flags, &path, NULL);
if (error)
@@ -105,6 +104,13 @@ static int do_utimes_path(int dfd, const char __user *filename,
return error;
}
+static int do_utimes_path(int dfd, const char __user *filename,
+ struct timespec64 *times, int flags)
+{
+ CLASS(filename_uflags, name)(filename, flags);
+ return filename_utimensat(dfd, name, times, flags);
+}
+
static int do_utimes_fd(int fd, struct timespec64 *times, int flags)
{
if (flags)
--
2.56.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* Re: [RFC PATCH v2 1/3] fs: add filename_utimensat()
2026-11-04 11:06 ` [RFC PATCH v2 1/3] fs: add filename_utimensat() Méven Car
@ 2026-11-09 11:05 ` Jan Kara
2026-11-10 14:12 ` Christian Brauner
0 siblings, 1 reply; 7+ messages in thread
From: Jan Kara @ 2026-11-09 11:05 UTC (permalink / raw)
To: Méven Car
Cc: Jens Axboe, io-uring, linux-fsdevel, linux-mm, Alexander Viro,
Christian Brauner, Jan Kara, Hugh Dickins, Baolin Wang,
Andrew Morton, Méven Car
On Wed 04-11-26 12:06:02, Méven Car wrote:
> From: Méven Car <meven@kde.org>
>
> Copy utilities that preserve timestamps, like cp -p or a file manager,
> set the times of every copy after its last write. To do that through
> io_uring, the request needs the utimensat() lookup on a name that was
> imported at submission time.
>
> io_uring takes the path of a request when it is submitted and looks it
> up later, from a worker thread. The path ops it already has call the
> filename_*() helpers of fs/namei.c with a struct filename. utimensat(2)
> had no such helper: do_utimes_path() took the user pointer and imported
> the name itself.
>
> Split the lookup and the vfs_utimes() call out of do_utimes_path() into
> filename_utimensat(), declared in fs/internal.h next to the other
> filename_*() helpers. do_utimes_path() now only imports the name.
>
> Signed-off-by: Méven Car <meven@kde.org>
Looks ok. Feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
Honza
> ---
> fs/internal.h | 2 ++
> fs/utimes.c | 12 +++++++++---
> 2 files changed, 11 insertions(+), 3 deletions(-)
>
> diff --git a/fs/internal.h b/fs/internal.h
> index c658c8a5ebd5..985b2bc5895e 100644
> --- a/fs/internal.h
> +++ b/fs/internal.h
> @@ -63,6 +63,8 @@ int filename_mknodat(int dfd, struct filename *name, umode_t mode, unsigned int
> int filename_symlinkat(struct filename *from, int newdfd, struct filename *to);
> int filename_linkat(int olddfd, struct filename *old, int newdfd,
> struct filename *new, int flags);
> +int filename_utimensat(int dfd, struct filename *name,
> + struct timespec64 *times, int flags);
> int vfs_tmpfile(struct mnt_idmap *idmap,
> const struct path *parentpath,
> struct file *file, umode_t mode);
> diff --git a/fs/utimes.c b/fs/utimes.c
> index e22664e4115f..8072f05ac8dc 100644
> --- a/fs/utimes.c
> +++ b/fs/utimes.c
> @@ -79,8 +79,8 @@ int vfs_utimes(const struct path *path, struct timespec64 *times)
> }
> EXPORT_SYMBOL_GPL(vfs_utimes);
>
> -static int do_utimes_path(int dfd, const char __user *filename,
> - struct timespec64 *times, int flags)
> +int filename_utimensat(int dfd, struct filename *name,
> + struct timespec64 *times, int flags)
> {
> struct path path;
> int lookup_flags = 0, error;
> @@ -91,7 +91,6 @@ static int do_utimes_path(int dfd, const char __user *filename,
> if (!(flags & AT_SYMLINK_NOFOLLOW))
> lookup_flags |= LOOKUP_FOLLOW;
>
> - CLASS(filename_uflags, name)(filename, flags);
> retry:
> error = filename_lookup(dfd, name, lookup_flags, &path, NULL);
> if (error)
> @@ -105,6 +104,13 @@ static int do_utimes_path(int dfd, const char __user *filename,
> return error;
> }
>
> +static int do_utimes_path(int dfd, const char __user *filename,
> + struct timespec64 *times, int flags)
> +{
> + CLASS(filename_uflags, name)(filename, flags);
> + return filename_utimensat(dfd, name, times, flags);
> +}
> +
> static int do_utimes_fd(int fd, struct timespec64 *times, int flags)
> {
> if (flags)
> --
> 2.56.0
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: [RFC PATCH v2 1/3] fs: add filename_utimensat()
2026-11-09 11:05 ` Jan Kara
@ 2026-11-10 14:12 ` Christian Brauner
0 siblings, 0 replies; 7+ messages in thread
From: Christian Brauner @ 2026-11-10 14:12 UTC (permalink / raw)
To: Jan Kara
Cc: Méven Car, Jens Axboe, io-uring, linux-fsdevel, linux-mm,
Alexander Viro, Hugh Dickins, Baolin Wang, Andrew Morton,
Méven Car
On Mon, Nov 09, 2026 at 12:05:18PM +0100, Jan Kara wrote:
> On Wed 04-11-26 12:06:02, Méven Car wrote:
> > From: Méven Car <meven@kde.org>
> >
> > Copy utilities that preserve timestamps, like cp -p or a file manager,
> > set the times of every copy after its last write. To do that through
> > io_uring, the request needs the utimensat() lookup on a name that was
> > imported at submission time.
> >
> > io_uring takes the path of a request when it is submitted and looks it
> > up later, from a worker thread. The path ops it already has call the
> > filename_*() helpers of fs/namei.c with a struct filename. utimensat(2)
> > had no such helper: do_utimes_path() took the user pointer and imported
> > the name itself.
> >
> > Split the lookup and the vfs_utimes() call out of do_utimes_path() into
> > filename_utimensat(), declared in fs/internal.h next to the other
> > filename_*() helpers. do_utimes_path() now only imports the name.
> >
> > Signed-off-by: Méven Car <meven@kde.org>
>
> Looks ok. Feel free to add:
>
> Reviewed-by: Jan Kara <jack@suse.cz>
Seems fine.
^ permalink raw reply [flat|nested] 7+ messages in thread
* [RFC PATCH v2 2/3] io_uring: add support for utimensat
2026-11-04 11:06 [RFC PATCH v2 0/3] io_uring: add IORING_OP_UTIMENSAT Méven Car
2026-11-04 11:06 ` [RFC PATCH v2 1/3] fs: add filename_utimensat() Méven Car
@ 2026-11-04 11:06 ` Méven Car
2026-11-04 11:06 ` [RFC PATCH v2 3/3] io_uring: set times inline when the filesystem cannot sleep Méven Car
2 siblings, 0 replies; 7+ messages in thread
From: Méven Car @ 2026-11-04 11:06 UTC (permalink / raw)
To: Jens Axboe
Cc: io-uring, linux-fsdevel, linux-mm, Alexander Viro,
Christian Brauner, Jan Kara, Hugh Dickins, Baolin Wang,
Andrew Morton, Méven Car
From: Méven Car <meven@kde.org>
A program that copies many files sets the times of every copy after
its last write. On NFS and SMB each of those calls is a round trip to
the server, and with io_uring it is one of the steps of the copy that
still have to leave the ring and run one file at a time.
IORING_OP_UTIMENSAT behaves like utimensat(2). sqe->fd is the directory
fd, sqe->addr the path, sqe->addr2 a pointer to two struct
__kernel_timespec, and sqe->utimes_flags takes AT_SYMLINK_NOFOLLOW and
AT_EMPTY_PATH. An addr2 of zero sets both times to the current time, as
a NULL times argument does. The times and the path are copied at
submission.
As with utimensat(2), a NULL path sets the times of the file that
sqe->fd refers to, without a lookup, and takes no flags. Two UTIME_OMIT
times complete with 0 without a lookup.
Like the other path ops, the request is issued from io-wq, apart from
the no-op, because the lookup and notify_change() can sleep. A fixed
file is not accepted as the directory fd. A failed request fails the
rest of its link.
Signed-off-by: Méven Car <meven@kde.org>
---
include/uapi/linux/io_uring.h | 2 +
io_uring/fs.c | 97 +++++++++++++++++++++++++++++++++++
io_uring/fs.h | 4 ++
io_uring/opdef.c | 8 +++
4 files changed, 111 insertions(+)
diff --git a/include/uapi/linux/io_uring.h b/include/uapi/linux/io_uring.h
index b42b66b424a7..6a98c9282e0b 100644
--- a/include/uapi/linux/io_uring.h
+++ b/include/uapi/linux/io_uring.h
@@ -76,6 +76,7 @@ struct io_uring_sqe {
__u32 install_fd_flags;
__u32 nop_flags;
__u32 pipe_flags;
+ __u32 utimes_flags;
};
__u64 user_data; /* data to be passed back at completion time */
/* pack this to avoid bogus arm OABI complaints */
@@ -319,6 +320,7 @@ enum io_uring_op {
IORING_OP_PIPE,
IORING_OP_NOP128,
IORING_OP_URING_CMD128,
+ IORING_OP_UTIMENSAT,
/* this goes last, obviously */
IORING_OP_LAST,
diff --git a/io_uring/fs.c b/io_uring/fs.c
index 26ea841a22e7..71164bddd958 100644
--- a/io_uring/fs.c
+++ b/io_uring/fs.c
@@ -304,3 +304,100 @@ void io_link_cleanup(struct io_kiocb *req)
dismiss_delayed_filename(&sl->oldpath);
dismiss_delayed_filename(&sl->newpath);
}
+
+/* Internal flags, above the AT_ flags of the SQE */
+#define IO_UTIMENSAT_NOOP BIT(30) /* both times UTIME_OMIT */
+#define IO_UTIMENSAT_NO_PATH BIT(31) /* NULL path: the dfd itself */
+
+struct io_utimensat {
+ struct file *file;
+ int dfd;
+ unsigned int flags;
+ struct delayed_filename filename;
+ /* NULL sets both times to the current time */
+ struct timespec64 *times;
+ struct timespec64 ts[2];
+};
+
+int io_utimensat_prep(struct io_kiocb *req, const struct io_uring_sqe *sqe)
+{
+ struct io_utimensat *ut = io_kiocb_to_cmd(req, struct io_utimensat);
+ struct __kernel_timespec __user *uts;
+ const char __user *fname;
+ int err;
+
+ if (sqe->len || sqe->buf_index || sqe->splice_fd_in || sqe->addr3 ||
+ sqe->__pad2[0])
+ return -EINVAL;
+ if (unlikely(req->flags & REQ_F_FIXED_FILE))
+ return -EBADF;
+
+ ut->dfd = READ_ONCE(sqe->fd);
+ ut->flags = READ_ONCE(sqe->utimes_flags);
+ if (ut->flags & ~(AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH))
+ return -EINVAL;
+
+ uts = u64_to_user_ptr(READ_ONCE(sqe->addr2));
+ ut->times = NULL;
+ if (uts) {
+ if (get_timespec64(&ut->ts[0], &uts[0]) ||
+ get_timespec64(&ut->ts[1], &uts[1]))
+ return -EFAULT;
+ ut->times = ut->ts;
+ if (ut->ts[0].tv_nsec == UTIME_OMIT &&
+ ut->ts[1].tv_nsec == UTIME_OMIT) {
+ ut->flags |= IO_UTIMENSAT_NOOP;
+ return 0;
+ }
+ }
+
+ fname = u64_to_user_ptr(READ_ONCE(sqe->addr));
+ if (!fname) {
+ if (ut->dfd == AT_FDCWD)
+ return -EFAULT;
+ if (ut->flags)
+ return -EINVAL;
+ ut->flags |= IO_UTIMENSAT_NO_PATH;
+ goto out;
+ }
+ err = delayed_getname_uflags(&ut->filename, fname, ut->flags);
+ if (unlikely(err))
+ return err;
+ req->flags |= REQ_F_NEED_CLEANUP;
+out:
+ req->flags |= REQ_F_FORCE_ASYNC;
+ return 0;
+}
+
+int io_utimensat(struct io_kiocb *req, unsigned int issue_flags)
+{
+ struct io_utimensat *ut = io_kiocb_to_cmd(req, struct io_utimensat);
+ int ret = 0;
+
+ WARN_ON_ONCE((issue_flags & IO_URING_F_NONBLOCK) &&
+ !(ut->flags & IO_UTIMENSAT_NOOP));
+
+ if (ut->flags & IO_UTIMENSAT_NO_PATH) {
+ CLASS(fd, f)(ut->dfd);
+
+ ret = -EBADF;
+ if (!fd_empty(f))
+ ret = vfs_utimes(&fd_file(f)->f_path, ut->times);
+ } else if (!(ut->flags & IO_UTIMENSAT_NOOP)) {
+ CLASS(filename_complete_delayed, name)(&ut->filename);
+ ret = filename_utimensat(ut->dfd, name, ut->times, ut->flags);
+ }
+
+ req->flags &= ~REQ_F_NEED_CLEANUP;
+ if (ret < 0)
+ req_set_fail(req);
+ io_req_set_res(req, ret, 0);
+ return IOU_COMPLETE;
+}
+
+void io_utimensat_cleanup(struct io_kiocb *req)
+{
+ struct io_utimensat *ut = io_kiocb_to_cmd(req, struct io_utimensat);
+
+ dismiss_delayed_filename(&ut->filename);
+}
diff --git a/io_uring/fs.h b/io_uring/fs.h
index 0bb5efe3d6bb..2812dc3b4b2f 100644
--- a/io_uring/fs.h
+++ b/io_uring/fs.h
@@ -18,3 +18,7 @@ int io_symlinkat(struct io_kiocb *req, unsigned int issue_flags);
int io_linkat_prep(struct io_kiocb *req, const struct io_uring_sqe *sqe);
int io_linkat(struct io_kiocb *req, unsigned int issue_flags);
void io_link_cleanup(struct io_kiocb *req);
+
+int io_utimensat_prep(struct io_kiocb *req, const struct io_uring_sqe *sqe);
+int io_utimensat(struct io_kiocb *req, unsigned int issue_flags);
+void io_utimensat_cleanup(struct io_kiocb *req);
diff --git a/io_uring/opdef.c b/io_uring/opdef.c
index cf3aa2242cd7..383a9b9d1df4 100644
--- a/io_uring/opdef.c
+++ b/io_uring/opdef.c
@@ -591,6 +591,10 @@ const struct io_issue_def io_issue_defs[] = {
.prep = io_uring_cmd_prep,
.issue = io_uring_cmd,
},
+ [IORING_OP_UTIMENSAT] = {
+ .prep = io_utimensat_prep,
+ .issue = io_utimensat,
+ },
};
const struct io_cold_def io_cold_defs[] = {
@@ -849,6 +853,10 @@ const struct io_cold_def io_cold_defs[] = {
.sqe_copy = io_uring_cmd_sqe_copy,
.cleanup = io_uring_cmd_cleanup,
},
+ [IORING_OP_UTIMENSAT] = {
+ .name = "UTIMENSAT",
+ .cleanup = io_utimensat_cleanup,
+ },
};
const char *io_uring_get_opcode(u8 opcode)
--
2.56.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* [RFC PATCH v2 3/3] io_uring: set times inline when the filesystem cannot sleep
2026-11-04 11:06 [RFC PATCH v2 0/3] io_uring: add IORING_OP_UTIMENSAT Méven Car
2026-11-04 11:06 ` [RFC PATCH v2 1/3] fs: add filename_utimensat() Méven Car
2026-11-04 11:06 ` [RFC PATCH v2 2/3] io_uring: add support for utimensat Méven Car
@ 2026-11-04 11:06 ` Méven Car
2026-11-10 14:19 ` Christian Brauner
2 siblings, 1 reply; 7+ messages in thread
From: Méven Car @ 2026-11-04 11:06 UTC (permalink / raw)
To: Jens Axboe
Cc: io-uring, linux-fsdevel, linux-mm, Alexander Viro,
Christian Brauner, Jan Kara, Hugh Dickins, Baolin Wang,
Andrew Morton, Méven Car
From: Méven Car <meven@kde.org>
IORING_OP_UTIMENSAT always went to io-wq. On tmpfs the setattr only
changes the inode in memory, so the handoff to a worker cost more than
the work: 2000 files took 7 ms with utimensat(2) and 20 ms through the
ring.
Try the request inline first. The lookup uses LOOKUP_CACHED, and the
freeze protection and the inode lock are only tried, the former with
the new mnt_want_write_trylock(). A filesystem whose ->setattr() never
sleeps for a change of times alone says so with
FS_SETATTR_TIMES_NOWAIT, which tmpfs sets. Any other filesystem, an
uncached path, a contended lock or a delegation returns -EAGAIN, and
io_uring sends the request to io-wq as before.
Signed-off-by: Méven Car <meven@kde.org>
---
fs/internal.h | 5 ++++-
fs/namespace.c | 20 ++++++++++++++++++++
fs/utimes.c | 45 +++++++++++++++++++++++++++++++++++----------
include/linux/fs.h | 1 +
io_uring/fs.c | 22 ++++++++++++++--------
mm/shmem.c | 3 ++-
6 files changed, 76 insertions(+), 20 deletions(-)
diff --git a/fs/internal.h b/fs/internal.h
index 985b2bc5895e..2370faec54f5 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -64,7 +64,9 @@ int filename_symlinkat(struct filename *from, int newdfd, struct filename *to);
int filename_linkat(int olddfd, struct filename *old, int newdfd,
struct filename *new, int flags);
int filename_utimensat(int dfd, struct filename *name,
- struct timespec64 *times, int flags);
+ struct timespec64 *times, int flags, bool nowait);
+int __vfs_utimes(const struct path *path, struct timespec64 *times,
+ bool nowait);
int vfs_tmpfile(struct mnt_idmap *idmap,
const struct path *parentpath,
struct file *file, umode_t mode);
@@ -86,6 +88,7 @@ extern int sb_prepare_remount_readonly(struct super_block *);
extern void __init mnt_init(void);
int mnt_get_write_access_file(struct file *file);
+int mnt_want_write_trylock(struct vfsmount *m);
void mnt_put_write_access_file(struct file *file);
extern void dissolve_on_fput(struct vfsmount *);
diff --git a/fs/namespace.c b/fs/namespace.c
index ae5dc64f8b45..f4aadd4e1958 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -499,6 +499,26 @@ int mnt_want_write(struct vfsmount *m)
}
EXPORT_SYMBOL_GPL(mnt_want_write);
+/**
+ * mnt_want_write_trylock - get write access to a mount without waiting
+ * @m: the mount on which to take a write
+ *
+ * Like mnt_want_write(), but returns -EAGAIN instead of waiting when the
+ * filesystem is frozen. When the write operation is finished,
+ * mnt_drop_write() must be called.
+ */
+int mnt_want_write_trylock(struct vfsmount *m)
+{
+ int ret;
+
+ if (!sb_start_write_trylock(m->mnt_sb))
+ return -EAGAIN;
+ ret = mnt_get_write_access(m);
+ if (ret)
+ sb_end_write(m->mnt_sb);
+ return ret;
+}
+
/**
* mnt_get_write_access_file - get write access to a file's mount
* @file: the file who's mount on which to take a write
diff --git a/fs/utimes.c b/fs/utimes.c
index 8072f05ac8dc..a0d67b8e96a1 100644
--- a/fs/utimes.c
+++ b/fs/utimes.c
@@ -18,7 +18,9 @@ static bool nsec_valid(long nsec)
return nsec >= 0 && nsec <= 999999999;
}
-int vfs_utimes(const struct path *path, struct timespec64 *times)
+/* With @nowait, returns -EAGAIN instead of sleeping */
+int __vfs_utimes(const struct path *path, struct timespec64 *times,
+ bool nowait)
{
int error;
struct iattr newattrs;
@@ -34,9 +36,17 @@ int vfs_utimes(const struct path *path, struct timespec64 *times)
times = NULL;
}
- error = mnt_want_write(path->mnt);
- if (error)
- goto out;
+ if (nowait) {
+ if (!(inode->i_sb->s_type->fs_flags & FS_SETATTR_TIMES_NOWAIT))
+ return -EAGAIN;
+ error = mnt_want_write_trylock(path->mnt);
+ if (error)
+ goto out;
+ } else {
+ error = mnt_want_write(path->mnt);
+ if (error)
+ goto out;
+ }
newattrs.ia_valid = ATTR_CTIME | ATTR_MTIME | ATTR_ATIME;
if (times) {
@@ -63,24 +73,35 @@ int vfs_utimes(const struct path *path, struct timespec64 *times)
newattrs.ia_valid |= ATTR_TOUCH;
}
retry_deleg:
- inode_lock(inode);
+ if (!nowait) {
+ inode_lock(inode);
+ } else if (!inode_trylock(inode)) {
+ error = -EAGAIN;
+ goto drop;
+ }
+ /* Without a delegated_inode, a delegation gives -EWOULDBLOCK */
error = notify_change(mnt_idmap(path->mnt), path->dentry, &newattrs,
- &delegated_inode);
+ nowait ? NULL : &delegated_inode);
inode_unlock(inode);
if (is_delegated(&delegated_inode)) {
error = break_deleg_wait(&delegated_inode);
if (!error)
goto retry_deleg;
}
-
+drop:
mnt_drop_write(path->mnt);
out:
return error;
}
+
+int vfs_utimes(const struct path *path, struct timespec64 *times)
+{
+ return __vfs_utimes(path, times, false);
+}
EXPORT_SYMBOL_GPL(vfs_utimes);
int filename_utimensat(int dfd, struct filename *name,
- struct timespec64 *times, int flags)
+ struct timespec64 *times, int flags, bool nowait)
{
struct path path;
int lookup_flags = 0, error;
@@ -90,13 +111,17 @@ int filename_utimensat(int dfd, struct filename *name,
if (!(flags & AT_SYMLINK_NOFOLLOW))
lookup_flags |= LOOKUP_FOLLOW;
+ if (nowait)
+ lookup_flags |= LOOKUP_CACHED;
retry:
error = filename_lookup(dfd, name, lookup_flags, &path, NULL);
if (error)
return error;
- error = vfs_utimes(&path, times);
+ error = __vfs_utimes(&path, times, nowait);
path_put(&path);
+ if (nowait && error == -ESTALE)
+ return -EAGAIN;
if (retry_estale(error, lookup_flags)) {
lookup_flags |= LOOKUP_REVAL;
goto retry;
@@ -108,7 +133,7 @@ static int do_utimes_path(int dfd, const char __user *filename,
struct timespec64 *times, int flags)
{
CLASS(filename_uflags, name)(filename, flags);
- return filename_utimensat(dfd, name, times, flags);
+ return filename_utimensat(dfd, name, times, flags, false);
}
static int do_utimes_fd(int fd, struct timespec64 *times, int flags)
diff --git a/include/linux/fs.h b/include/linux/fs.h
index f9d1e05e8ae6..d1800fba15d8 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -2296,6 +2296,7 @@ struct file_system_type {
#define FS_POWER_FREEZE 256 /* Always freeze on suspend/hibernate */
#define FS_USERNS_MOUNT_RESTRICTED 512 /* Restrict mount in userns if not already visible */
#define FS_USERNS_DELEGATABLE 1024 /* Can be mounted inside userns from outside */
+#define FS_SETATTR_TIMES_NOWAIT 2048 /* ->setattr() of times alone never sleeps */
#define FS_RENAME_DOES_D_MOVE 32768 /* FS will handle d_move() during rename() internally. */
int (*init_fs_context)(struct fs_context *);
const struct fs_parameter_spec *parameters;
diff --git a/io_uring/fs.c b/io_uring/fs.c
index 71164bddd958..664ea918ae3b 100644
--- a/io_uring/fs.c
+++ b/io_uring/fs.c
@@ -358,34 +358,40 @@ int io_utimensat_prep(struct io_kiocb *req, const struct io_uring_sqe *sqe)
if (ut->flags)
return -EINVAL;
ut->flags |= IO_UTIMENSAT_NO_PATH;
- goto out;
+ return 0;
}
err = delayed_getname_uflags(&ut->filename, fname, ut->flags);
if (unlikely(err))
return err;
req->flags |= REQ_F_NEED_CLEANUP;
-out:
- req->flags |= REQ_F_FORCE_ASYNC;
return 0;
}
int io_utimensat(struct io_kiocb *req, unsigned int issue_flags)
{
struct io_utimensat *ut = io_kiocb_to_cmd(req, struct io_utimensat);
+ bool force_nonblock = issue_flags & IO_URING_F_NONBLOCK;
int ret = 0;
- WARN_ON_ONCE((issue_flags & IO_URING_F_NONBLOCK) &&
- !(ut->flags & IO_UTIMENSAT_NOOP));
-
if (ut->flags & IO_UTIMENSAT_NO_PATH) {
CLASS(fd, f)(ut->dfd);
ret = -EBADF;
if (!fd_empty(f))
- ret = vfs_utimes(&fd_file(f)->f_path, ut->times);
+ ret = __vfs_utimes(&fd_file(f)->f_path, ut->times,
+ force_nonblock);
+ if (ret == -EAGAIN && force_nonblock)
+ return -EAGAIN;
} else if (!(ut->flags & IO_UTIMENSAT_NOOP)) {
CLASS(filename_complete_delayed, name)(&ut->filename);
- ret = filename_utimensat(ut->dfd, name, ut->times, ut->flags);
+ ret = filename_utimensat(ut->dfd, name, ut->times, ut->flags,
+ force_nonblock);
+ if (ret == -EAGAIN && force_nonblock) {
+ ret = putname_to_delayed(&ut->filename,
+ no_free_ptr(name));
+ if (likely(!ret))
+ return -EAGAIN;
+ }
}
req->flags &= ~REQ_F_NEED_CLEANUP;
diff --git a/mm/shmem.c b/mm/shmem.c
index 897fa2b61346..405b39fc5d0e 100644
--- a/mm/shmem.c
+++ b/mm/shmem.c
@@ -5310,7 +5310,8 @@ static struct file_system_type shmem_fs_type = {
.parameters = shmem_fs_parameters,
#endif
.kill_sb = kill_anon_super,
- .fs_flags = FS_USERNS_MOUNT | FS_ALLOW_IDMAP | FS_MGTIME,
+ .fs_flags = FS_USERNS_MOUNT | FS_ALLOW_IDMAP | FS_MGTIME |
+ FS_SETATTR_TIMES_NOWAIT,
};
#if defined(CONFIG_SYSFS) && defined(CONFIG_TMPFS)
--
2.56.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* Re: [RFC PATCH v2 3/3] io_uring: set times inline when the filesystem cannot sleep
2026-11-04 11:06 ` [RFC PATCH v2 3/3] io_uring: set times inline when the filesystem cannot sleep Méven Car
@ 2026-11-10 14:19 ` Christian Brauner
0 siblings, 0 replies; 7+ messages in thread
From: Christian Brauner @ 2026-11-10 14:19 UTC (permalink / raw)
To: Méven Car
Cc: Jens Axboe, io-uring, linux-fsdevel, linux-mm, Alexander Viro,
Jan Kara, Hugh Dickins, Baolin Wang, Andrew Morton,
Méven Car
On Wed, Nov 04, 2026 at 12:06:04PM +0100, Méven Car wrote:
> From: Méven Car <meven@kde.org>
>
> IORING_OP_UTIMENSAT always went to io-wq. On tmpfs the setattr only
> changes the inode in memory, so the handoff to a worker cost more than
> the work: 2000 files took 7 ms with utimensat(2) and 20 ms through the
> ring.
>
> Try the request inline first. The lookup uses LOOKUP_CACHED, and the
> freeze protection and the inode lock are only tried, the former with
> the new mnt_want_write_trylock(). A filesystem whose ->setattr() never
> sleeps for a change of times alone says so with
> FS_SETATTR_TIMES_NOWAIT, which tmpfs sets. Any other filesystem, an
> uncached path, a contended lock or a delegation returns -EAGAIN, and
> io_uring sends the request to io-wq as before.
>
> Signed-off-by: Méven Car <meven@kde.org>
> ---
> fs/internal.h | 5 ++++-
> fs/namespace.c | 20 ++++++++++++++++++++
We do not add a bunch of generic APIs in a io_uring patch.
What exactly is the the idea here? for tmpfs io-wq handoff costs three
times the setattr.
So it tries the request inline with LOOKUP_CACHED, inode_trylock(), a
new mnt_want_write_trylock(), no delegation breaking, and bails with
EAGAIN unless the filesystem type sets a new FS_SETATTR_TIMES_NOWAIT
fs_flags bit. Bah.. and then only tmpfs. ext4, xfs and btrfs can't
because ->setattr() starts a transaction.
The inline path still runs the whole notify_change()
security_inode_setattr(), fsnotify_change(), and
security_inode_post_setattr(). That's all blocking material.
Every request on a filesystem without the bit does a cached lookup with
d_weak_revalidate() and audit_inode() side effects, fails in __vfs_utimes(),
and then io-wq looks the path up again. Eew.
Not acceptable in this form and for sure not with all this new
machinery.
^ permalink raw reply [flat|nested] 7+ messages in thread