* [RFC PATCH 1/3] fs: add filename_utimensat()
2026-10-30 12:39 [RFC PATCH 0/3] io_uring: add IORING_OP_UTIMENSAT Méven Car
@ 2026-10-30 12:39 ` Méven Car
2026-10-30 12:39 ` [RFC PATCH 2/3] io_uring: add support for utimensat Méven Car
2026-10-30 12:39 ` [RFC PATCH 3/3] io_uring: set times inline when the filesystem cannot sleep Méven Car
2 siblings, 0 replies; 6+ messages in thread
From: Méven Car @ 2026-10-30 12:39 UTC (permalink / raw)
To: Jens Axboe
Cc: Alexander Viro, Christian Brauner, Jan Kara, Hugh Dickins,
Baolin Wang, Andrew Morton, io-uring, linux-fsdevel, linux-mm,
linux-kernel
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] 6+ messages in thread* [RFC PATCH 2/3] io_uring: add support for utimensat
2026-10-30 12:39 [RFC PATCH 0/3] io_uring: add IORING_OP_UTIMENSAT Méven Car
2026-10-30 12:39 ` [RFC PATCH 1/3] fs: add filename_utimensat() Méven Car
@ 2026-10-30 12:39 ` Méven Car
2026-10-31 12:40 ` sashiko-bot
2026-10-30 12:39 ` [RFC PATCH 3/3] io_uring: set times inline when the filesystem cannot sleep Méven Car
2 siblings, 1 reply; 6+ messages in thread
From: Méven Car @ 2026-10-30 12:39 UTC (permalink / raw)
To: Jens Axboe
Cc: Alexander Viro, Christian Brauner, Jan Kara, Hugh Dickins,
Baolin Wang, Andrew Morton, io-uring, linux-fsdevel, linux-mm,
linux-kernel
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 sqe->fd itself, and
two UTIME_OMIT times complete with 0 without a lookup. Unlike the
syscall, a NULL path or AT_EMPTY_PATH also works on an O_PATH
descriptor.
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 | 99 +++++++++++++++++++++++++++++++++++
io_uring/fs.h | 4 ++
io_uring/opdef.c | 8 +++
4 files changed, 113 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..85e0a1cb6b47 100644
--- a/io_uring/fs.c
+++ b/io_uring/fs.c
@@ -304,3 +304,102 @@ 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;
+ 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;
+}
+
+static int __io_utimensat(struct io_utimensat *ut, struct filename *name)
+{
+ int flags = ut->flags & (AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH);
+
+ return filename_utimensat(ut->dfd, name, ut->times, flags);
+}
+
+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(filename_kernel, name)("");
+ ret = __io_utimensat(ut, name);
+ } else if (!(ut->flags & IO_UTIMENSAT_NOOP)) {
+ CLASS(filename_complete_delayed, name)(&ut->filename);
+ ret = __io_utimensat(ut, name);
+ }
+
+ 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] 6+ messages in thread* Re: [RFC PATCH 2/3] io_uring: add support for utimensat
2026-10-30 12:39 ` [RFC PATCH 2/3] io_uring: add support for utimensat Méven Car
@ 2026-10-31 12:40 ` sashiko-bot
0 siblings, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-10-31 12:40 UTC (permalink / raw)
To: Méven Car, Jens Axboe
Cc: Alexander Viro, Christian Brauner, Jan Kara, Hugh Dickins,
Baolin Wang, Andrew Morton, linux-mm
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] io_uring: missing flags check and wrong NULL path handling in io_utimensat()
--
commit 8d82d4a2e30073e0345719074be6f5140b1c8829
Author: Méven Car <meven29@gmail.com>
io_uring: add support for utimensat
This commit adds the IORING_OP_UTIMENSAT opcode to io_uring to support
updating file timestamps asynchronously via io-wq. It copies the path and
timespec arguments during submission prep and invokes filename_utimensat()
when issued.
> diff --git a/io_uring/fs.c b/io_uring/fs.c
> index 26ea841a22e70..85e0a1cb6b479 100644
> --- a/io_uring/fs.c
> +++ b/io_uring/fs.c
> @@ -304,3 +304,102 @@ 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;
> + 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;
> +}
> +
> +static int __io_utimensat(struct io_utimensat *ut, struct filename *name)
> +{
> + int flags = ut->flags & (AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH);
> +
> + return filename_utimensat(ut->dfd, name, ut->times, flags);
> +}
> +
> +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(filename_kernel, name)("");
> + ret = __io_utimensat(ut, name);
[Severity: Medium]
When sqe->addr is NULL (the IO_UTIMENSAT_NO_PATH case), should
io_utimensat_prep() and io_utimensat() follow do_utimes_fd() instead of
allocating an empty filename and routing through filename_utimensat()?
In fs/utimes.c, do_utimes() dispatches a NULL filename (with dfd != AT_FDCWD)
to do_utimes_fd():
fs/utimes.c:do_utimes_fd() {
if (flags)
return -EINVAL;
CLASS(fd, f)(fd);
if (fd_empty(f))
return -EBADF;
return vfs_utimes(&fd_file(f)->f_path, times);
}
Routing IO_UTIMENSAT_NO_PATH through getname_kernel("") -> __io_utimensat() ->
filename_utimensat() -> filename_lookup() differs from utimensat(2) in several
ways:
1. In io_utimensat_prep(), non-zero sqe->utimes_flags (AT_SYMLINK_NOFOLLOW or
AT_EMPTY_PATH) are accepted when fname is NULL, whereas do_utimes_fd() rejects
any non-zero flags with -EINVAL.
2. In filename_lookup() -> path_lookupat(), an empty string returns from
link_path_walk() with nd->last_type == LAST_ROOT without calling step_into(),
so LOOKUP_FOLLOW is not applied to dfd itself. If sqe->fd is an O_PATH |
O_NOFOLLOW descriptor referring to a symlink and sqe->addr is NULL, this
modifies the symlink itself even when AT_SYMLINK_NOFOLLOW is not set.
3. In path_lookupat(), path_init() sets ND_JUMPED and complete_walk() invokes
d_op->d_weak_revalidate() on filesystems such as NFS and 9p. If sqe->fd refers
to an open file that has been unlinked, d_weak_revalidate() fails and
filename_utimensat() returns -ESTALE (after an extra server round-trip),
whereas do_utimes_fd() calls vfs_utimes() directly on fd_file(f)->f_path and
succeeds.
4. Every IO_UTIMENSAT_NO_PATH call allocates a 4KB struct filename from
names_cachep and records a synthetic empty filename in audit_getname().
Also, the commit message states:
"Unlike the syscall, a NULL path or AT_EMPTY_PATH also works on an O_PATH
descriptor."
Doesn't the utimensat(2) syscall already support AT_EMPTY_PATH with a non-NULL
empty path on an O_PATH descriptor via do_utimes_path() ->
filename_utimensat() -> filename_lookup(), while only rejecting O_PATH on a
NULL path in do_utimes_fd()?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030123945.1704118-1-meven@kde.org?part=2
^ permalink raw reply [flat|nested] 6+ messages in thread
* [RFC PATCH 3/3] io_uring: set times inline when the filesystem cannot sleep
2026-10-30 12:39 [RFC PATCH 0/3] io_uring: add IORING_OP_UTIMENSAT Méven Car
2026-10-30 12:39 ` [RFC PATCH 1/3] fs: add filename_utimensat() Méven Car
2026-10-30 12:39 ` [RFC PATCH 2/3] io_uring: add support for utimensat Méven Car
@ 2026-10-30 12:39 ` Méven Car
2026-10-31 12:40 ` sashiko-bot
2 siblings, 1 reply; 6+ messages in thread
From: Méven Car @ 2026-10-30 12:39 UTC (permalink / raw)
To: Jens Axboe
Cc: Alexander Viro, Christian Brauner, Jan Kara, Hugh Dickins,
Baolin Wang, Andrew Morton, io-uring, linux-fsdevel, linux-mm,
linux-kernel
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 | 3 ++-
fs/namespace.c | 20 +++++++++++++++++++
fs/utimes.c | 48 +++++++++++++++++++++++++++++++++++++---------
include/linux/fs.h | 1 +
io_uring/fs.c | 26 +++++++++++++++----------
mm/shmem.c | 3 ++-
6 files changed, 80 insertions(+), 21 deletions(-)
diff --git a/fs/internal.h b/fs/internal.h
index 985b2bc5895e..e991b63bb714 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -64,7 +64,7 @@ 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_tmpfile(struct mnt_idmap *idmap,
const struct path *parentpath,
struct file *file, umode_t mode);
@@ -86,6 +86,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..f70ad9003fa9 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 */
+static 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,40 @@ 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;
+ }
error = notify_change(mnt_idmap(path->mnt), path->dentry, &newattrs,
&delegated_inode);
inode_unlock(inode);
if (is_delegated(&delegated_inode)) {
+ if (nowait) {
+ iput(delegated_inode.di_inode);
+ delegated_inode.di_inode = NULL;
+ error = -EAGAIN;
+ goto drop;
+ }
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 +116,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 +138,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 85e0a1cb6b47..79f6f39ce267 100644
--- a/io_uring/fs.c
+++ b/io_uring/fs.c
@@ -356,38 +356,44 @@ int io_utimensat_prep(struct io_kiocb *req, const struct io_uring_sqe *sqe)
if (ut->dfd == AT_FDCWD)
return -EFAULT;
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;
}
-static int __io_utimensat(struct io_utimensat *ut, struct filename *name)
+static int __io_utimensat(struct io_utimensat *ut, struct filename *name,
+ bool force_nonblock)
{
int flags = ut->flags & (AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH);
- return filename_utimensat(ut->dfd, name, ut->times, flags);
+ return filename_utimensat(ut->dfd, name, ut->times, flags,
+ force_nonblock);
}
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(filename_kernel, name)("");
- ret = __io_utimensat(ut, name);
+ ret = __io_utimensat(ut, name, force_nonblock);
+ if (ret == -EAGAIN && force_nonblock)
+ return -EAGAIN;
} else if (!(ut->flags & IO_UTIMENSAT_NOOP)) {
CLASS(filename_complete_delayed, name)(&ut->filename);
- ret = __io_utimensat(ut, name);
+ ret = __io_utimensat(ut, name, 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] 6+ messages in thread* Re: [RFC PATCH 3/3] io_uring: set times inline when the filesystem cannot sleep
2026-10-30 12:39 ` [RFC PATCH 3/3] io_uring: set times inline when the filesystem cannot sleep Méven Car
@ 2026-10-31 12:40 ` sashiko-bot
0 siblings, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-10-31 12:40 UTC (permalink / raw)
To: Méven Car, Jens Axboe
Cc: Alexander Viro, Christian Brauner, Jan Kara, Hugh Dickins,
Baolin Wang, Andrew Morton, linux-mm
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] fs: utimes: direct di_inode access and iput() in __vfs_utimes() under nowait
- [Medium] fs: utimes: blocking lookup and audit before FS_SETATTR_TIMES_NOWAIT check
- [Medium] io_uring: fs: missing flags==0 check for NULL path in io_utimensat_prep()
--
commit 1030cd9e18d185bfa4d7791c093821be89bb0205
Author: Méven Car <meven29@gmail.com>
io_uring: set times inline when the filesystem cannot sleep
This commit adds non-blocking inline execution support for
IORING_OP_UTIMENSAT when the target filesystem sets FS_SETATTR_TIMES_NOWAIT
(enabled for tmpfs), falling back to io-wq on -EAGAIN.
> diff --git a/fs/utimes.c b/fs/utimes.c
> index 8072f05ac8dcc..f70ad9003fa9e 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 */
> +static int __vfs_utimes(const struct path *path, struct timespec64 *times,
> + bool nowait)
> {
[ ... ]
> @@ -63,24 +73,40 @@ 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;
> + }
> error = notify_change(mnt_idmap(path->mnt), path->dentry, &newattrs,
> &delegated_inode);
> inode_unlock(inode);
> if (is_delegated(&delegated_inode)) {
> + if (nowait) {
> + iput(delegated_inode.di_inode);
> + delegated_inode.di_inode = NULL;
> + error = -EAGAIN;
> + goto drop;
> + }
[Severity: Medium]
Does this break the build when CONFIG_FILE_LOCKING is disabled? In
include/linux/filelock.h, struct delegated_inode only defines di_inode under
CONFIG_FILE_LOCKING, and is an empty struct otherwise:
include/linux/filelock.h:
#else /* !CONFIG_FILE_LOCKING */
struct delegated_inode { };
static inline bool is_delegated(struct delegated_inode *di)
{
return false;
}
Because fs/utimes.c is built unconditionally, referencing
delegated_inode.di_inode in __vfs_utimes() will fail to compile when
CONFIG_FILE_LOCKING=n.
Also, when CONFIG_FILE_LOCKING=y, calling iput(delegated_inode.di_inode) on
the nowait path invokes might_sleep() and can sleep if the file was
concurrently unlinked after inode_unlock(inode).
If __vfs_utimes() passes nowait ? NULL : &delegated_inode to notify_change()
instead, try_break_deleg() will initiate the non-blocking break and return
-EWOULDBLOCK (-EAGAIN) without taking an extra inode reference or populating
delegated_inode:
include/linux/filelock.h:try_break_deleg()
ret = break_deleg(inode, flags | LEASE_BREAK_NONBLOCK);
if (ret == -EWOULDBLOCK && di) {
di->di_inode = inode;
ihold(inode);
}
return ret;
Would passing NULL for delegated_inode when nowait is true avoid both the
di_inode access and the iput() call here?
> 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 +116,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);
[Severity: Medium]
Can filename_lookup() sleep or perform duplicate work on the nowait path
before __vfs_utimes() checks FS_SETATTR_TIMES_NOWAIT?
Inside filename_lookup() -> path_lookupat() -> complete_walk(), LOOKUP_CACHED
is cleared before try_to_unlazy(), and if ND_JUMPED is set (such as for empty
paths, '..', '/', mountpoint crossings, or trailing symlinks) on a filesystem
with DCACHE_OP_WEAK_REVALIDATE (e.g. NFS, 9p, or overlayfs),
d_op->d_weak_revalidate() is called in blocking mode without LOOKUP_RCU or
LOOKUP_CACHED:
fs/namei.c:complete_walk()
nd->flags &= ~LOOKUP_CACHED;
if (!try_to_unlazy(nd))
return -ECHILD;
...
if (likely(!(nd->state & ND_JUMPED)))
return 0;
if (likely(!(dentry->d_flags & DCACHE_OP_WEAK_REVALIDATE)))
return 0;
status = dentry->d_op->d_weak_revalidate(dentry, nd->flags);
In addition, when path_lookupat() succeeds on a cached dentry of any
filesystem lacking FS_SETATTR_TIMES_NOWAIT (such as ext4, xfs, or btrfs),
filename_lookup() calls audit_inode() before __vfs_utimes() rejects the
filesystem with -EAGAIN. When auditing is active, audit_inode() ->
audit_copy_fcaps() -> get_vfs_caps_from_disk() can sleep reading xattrs from
disk, records a duplicate audit entry, and bumps name->refcnt (forcing
putname_to_delayed() to allocate a second struct filename), while path_put()
also runs on the inline submission path.
> diff --git a/io_uring/fs.c b/io_uring/fs.c
> index 85e0a1cb6b479..79f6f39ce267a 100644
> --- a/io_uring/fs.c
> +++ b/io_uring/fs.c
> @@ -356,38 +356,44 @@ int io_utimensat_prep(struct io_kiocb *req, const struct io_uring_sqe *sqe)
> if (ut->dfd == AT_FDCWD)
> return -EFAULT;
> ut->flags |= IO_UTIMENSAT_NO_PATH;
> - goto out;
> + return 0;
> }
[Severity: Medium]
When fname is NULL and ut->dfd != AT_FDCWD, should io_utimensat_prep() reject
non-zero ut->flags with -EINVAL to match do_utimes_fd() in fs/utimes.c?
In fs/utimes.c, a NULL filename with a valid fd is routed to do_utimes_fd(),
which enforces:
fs/utimes.c:do_utimes_fd()
if (flags)
return -EINVAL;
whereas io_utimensat_prep() allows AT_SYMLINK_NOFOLLOW or AT_EMPTY_PATH when
sqe->addr is NULL and passes them through __io_utimensat() to
filename_utimensat(). Also, since IO_UTIMENSAT_NO_PATH operates directly on
ut->dfd, could io_utimensat() use fdget()/CLASS(fd) and __vfs_utimes() like
do_utimes_fd() instead of allocating a struct filename via
CLASS(filename_kernel, name)("") and doing a path lookup on Both the inline
and io-wq attempts?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030123945.1704118-1-meven@kde.org?part=3
^ permalink raw reply [flat|nested] 6+ messages in thread