summaryrefslogtreecommitdiffstats
path: root/fs/utimes.c
diff options
context:
space:
mode:
authorJinke Han <jinkehan@didiglobal.com>2026-09-08 15:37:42 +0800
committerIngo Molnar <mingo@kernel.org>2026-09-17 09:48:42 +0200
commita5f7a5bb3b7f28ba7e4fa246775b29a0e5537255 (patch)
tree73fef824f9a846cf935869d0dbc5070ef6e2f741 /fs/utimes.c
downloadlinux-stable-a5f7a5bb3b7f28ba7e4fa246775b29a0e5537255.tar.gz
linux-stable-a5f7a5bb3b7f28ba7e4fa246775b29a0e5537255.zip
x86/kprobes: Fix crash when probing CS CALL instructionsgrafted
When using eBPF to probe CS CALL instructions within a function, a crash can be triggered. The eBPF tool probes offset 257 of the __hrtimer_run_queues() function: <__hrtimer_run_queues+249>: nopl 0x0(%rax,%rax,1) <__hrtimer_run_queues+254>: mov %r14,%rdi <__hrtimer_run_queues+257>: cs call <__x86_indirect_thunk_r12> <__hrtimer_run_queues+263>: mov %eax,%r12d <__hrtimer_run_queues+266>: xchg %ax,%ax <__hrtimer_run_queues+268>: mov %r13,%rdi Which triggers this crash: BUG: unable to handle page fault for address: 00000000000f41c9 #PF: supervisor write access in kernel mode #PF: error_code(0x0002) - not-present page PGD 0 P4D 0 Oops: 0002 [#1] SMP NOPTI CPU: 1 PID: 0 Comm: swapper/1 Kdump: loaded Tainted: P RIP: 0010:__hrtimer_run_queues+0x106/0x230 Note that __hrtimer_run_queues+0x106 is __hrtimer_run_queues+262, which is at the 6th byte of the above CS CALL instruction. Since the CS CALL instruction occupies 6 bytes, the exception occurred in the middle of that call instruction. The root cause is that when using eBPF tools to probe in the middle of a function, a kprobe with INT3 is used as the underlying implementation. During single-step emulation of the original CALL instruction, int3_emulate_call() assumes that the probed CALL instruction is 5 bytes long. However, the actual CS-prefixed CALL instruction occupies 6 bytes, so it constructs an incorrect exception return address. When the CPU returns from the kprobe handler, the next instruction to be executed is at the address of the last byte of that CS CALL instruction. Coincidentally, starting from that address, the CPU fetches and decodes a completely different instruction, which ultimately triggers a kernel crash. Fix the issue by using the actual instruction length obtained from the instruction decoder when constructing the exception return address, rather than relying on the hardcoded CALL_INSN_SIZE macro. [ mingo: Refined the changelog ] Fixes: 6256e668b7af ("x86/kprobes: Use int3 instead of debug trap for single-step") Suggested-by: Masami Hiramatsu (Google) <mhiramat@kernel.org> Signed-off-by: Jinke Han <jinkehan@didiglobal.com> Signed-off-by: Ingo Molnar <mingo@kernel.org> Reviewed-by: Masami Hiramatsu (Google) <mhiramat@kernel.org> Acked-by: Yafang Shao <laoar.shao@gmail.com> Acked-by: Borislav Petkov <bp@alien8.de> Cc: Peter Zijlstra <peterz@infradead.org> Link: https://patch.msgid.link/20260908073742.GA10517@didi-ThinkCentre-M920t-N000
Diffstat (limited to 'fs/utimes.c')
-rw-r--r--fs/utimes.c294
1 files changed, 294 insertions, 0 deletions
diff --git a/fs/utimes.c b/fs/utimes.c
new file mode 100644
index 000000000..e22664e41
--- /dev/null
+++ b/fs/utimes.c
@@ -0,0 +1,294 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/file.h>
+#include <linux/mount.h>
+#include <linux/namei.h>
+#include <linux/utime.h>
+#include <linux/syscalls.h>
+#include <linux/uaccess.h>
+#include <linux/compat.h>
+#include <asm/unistd.h>
+#include <linux/filelock.h>
+#include "internal.h"
+
+static bool nsec_valid(long nsec)
+{
+ if (nsec == UTIME_OMIT || nsec == UTIME_NOW)
+ return true;
+
+ return nsec >= 0 && nsec <= 999999999;
+}
+
+int vfs_utimes(const struct path *path, struct timespec64 *times)
+{
+ int error;
+ struct iattr newattrs;
+ struct inode *inode = path->dentry->d_inode;
+ struct delegated_inode delegated_inode = { };
+
+ if (times) {
+ if (!nsec_valid(times[0].tv_nsec) ||
+ !nsec_valid(times[1].tv_nsec))
+ return -EINVAL;
+ if (times[0].tv_nsec == UTIME_NOW &&
+ times[1].tv_nsec == UTIME_NOW)
+ times = NULL;
+ }
+
+ error = mnt_want_write(path->mnt);
+ if (error)
+ goto out;
+
+ newattrs.ia_valid = ATTR_CTIME | ATTR_MTIME | ATTR_ATIME;
+ if (times) {
+ if (times[0].tv_nsec == UTIME_OMIT)
+ newattrs.ia_valid &= ~ATTR_ATIME;
+ else if (times[0].tv_nsec != UTIME_NOW) {
+ newattrs.ia_atime = times[0];
+ newattrs.ia_valid |= ATTR_ATIME_SET;
+ }
+
+ if (times[1].tv_nsec == UTIME_OMIT)
+ newattrs.ia_valid &= ~ATTR_MTIME;
+ else if (times[1].tv_nsec != UTIME_NOW) {
+ newattrs.ia_mtime = times[1];
+ newattrs.ia_valid |= ATTR_MTIME_SET;
+ }
+ /*
+ * Tell setattr_prepare(), that this is an explicit time
+ * update, even if neither ATTR_ATIME_SET nor ATTR_MTIME_SET
+ * were used.
+ */
+ newattrs.ia_valid |= ATTR_TIMES_SET;
+ } else {
+ newattrs.ia_valid |= ATTR_TOUCH;
+ }
+retry_deleg:
+ inode_lock(inode);
+ error = notify_change(mnt_idmap(path->mnt), path->dentry, &newattrs,
+ &delegated_inode);
+ inode_unlock(inode);
+ if (is_delegated(&delegated_inode)) {
+ error = break_deleg_wait(&delegated_inode);
+ if (!error)
+ goto retry_deleg;
+ }
+
+ mnt_drop_write(path->mnt);
+out:
+ return error;
+}
+EXPORT_SYMBOL_GPL(vfs_utimes);
+
+static int do_utimes_path(int dfd, const char __user *filename,
+ struct timespec64 *times, int flags)
+{
+ struct path path;
+ int lookup_flags = 0, error;
+
+ if (flags & ~(AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH))
+ return -EINVAL;
+
+ 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)
+ return error;
+ error = vfs_utimes(&path, times);
+ path_put(&path);
+ if (retry_estale(error, lookup_flags)) {
+ lookup_flags |= LOOKUP_REVAL;
+ goto retry;
+ }
+ return error;
+}
+
+static int do_utimes_fd(int fd, struct timespec64 *times, int flags)
+{
+ if (flags)
+ return -EINVAL;
+
+ CLASS(fd, f)(fd);
+ if (fd_empty(f))
+ return -EBADF;
+ return vfs_utimes(&fd_file(f)->f_path, times);
+}
+
+/*
+ * do_utimes - change times on filename or file descriptor
+ * @dfd: open file descriptor, -1 or AT_FDCWD
+ * @filename: path name or NULL
+ * @times: new times or NULL
+ * @flags: zero or more flags (only AT_SYMLINK_NOFOLLOW for the moment)
+ *
+ * If filename is NULL and dfd refers to an open file, then operate on
+ * the file. Otherwise look up filename, possibly using dfd as a
+ * starting point.
+ *
+ * If times==NULL, set access and modification to current time,
+ * must be owner or have write permission.
+ * Else, update from *times, must be owner or super user.
+ */
+long do_utimes(int dfd, const char __user *filename, struct timespec64 *times,
+ int flags)
+{
+ if (filename == NULL && dfd != AT_FDCWD)
+ return do_utimes_fd(dfd, times, flags);
+ return do_utimes_path(dfd, filename, times, flags);
+}
+
+SYSCALL_DEFINE4(utimensat, int, dfd, const char __user *, filename,
+ struct __kernel_timespec __user *, utimes, int, flags)
+{
+ struct timespec64 tstimes[2];
+
+ if (utimes) {
+ if ((get_timespec64(&tstimes[0], &utimes[0]) ||
+ get_timespec64(&tstimes[1], &utimes[1])))
+ return -EFAULT;
+
+ /* Nothing to do, we must not even check the path. */
+ if (tstimes[0].tv_nsec == UTIME_OMIT &&
+ tstimes[1].tv_nsec == UTIME_OMIT)
+ return 0;
+ }
+
+ return do_utimes(dfd, filename, utimes ? tstimes : NULL, flags);
+}
+
+#ifdef __ARCH_WANT_SYS_UTIME
+/*
+ * futimesat(), utimes() and utime() are older versions of utimensat()
+ * that are provided for compatibility with traditional C libraries.
+ * On modern architectures, we always use libc wrappers around
+ * utimensat() instead.
+ */
+static long do_futimesat(int dfd, const char __user *filename,
+ struct __kernel_old_timeval __user *utimes)
+{
+ struct __kernel_old_timeval times[2];
+ struct timespec64 tstimes[2];
+
+ if (utimes) {
+ if (copy_from_user(&times, utimes, sizeof(times)))
+ return -EFAULT;
+
+ /* This test is needed to catch all invalid values. If we
+ would test only in do_utimes we would miss those invalid
+ values truncated by the multiplication with 1000. Note
+ that we also catch UTIME_{NOW,OMIT} here which are only
+ valid for utimensat. */
+ if (times[0].tv_usec >= 1000000 || times[0].tv_usec < 0 ||
+ times[1].tv_usec >= 1000000 || times[1].tv_usec < 0)
+ return -EINVAL;
+
+ tstimes[0].tv_sec = times[0].tv_sec;
+ tstimes[0].tv_nsec = 1000 * times[0].tv_usec;
+ tstimes[1].tv_sec = times[1].tv_sec;
+ tstimes[1].tv_nsec = 1000 * times[1].tv_usec;
+ }
+
+ return do_utimes(dfd, filename, utimes ? tstimes : NULL, 0);
+}
+
+
+SYSCALL_DEFINE3(futimesat, int, dfd, const char __user *, filename,
+ struct __kernel_old_timeval __user *, utimes)
+{
+ return do_futimesat(dfd, filename, utimes);
+}
+
+SYSCALL_DEFINE2(utimes, char __user *, filename,
+ struct __kernel_old_timeval __user *, utimes)
+{
+ return do_futimesat(AT_FDCWD, filename, utimes);
+}
+
+SYSCALL_DEFINE2(utime, char __user *, filename, struct utimbuf __user *, times)
+{
+ struct timespec64 tv[2];
+
+ if (times) {
+ if (get_user(tv[0].tv_sec, &times->actime) ||
+ get_user(tv[1].tv_sec, &times->modtime))
+ return -EFAULT;
+ tv[0].tv_nsec = 0;
+ tv[1].tv_nsec = 0;
+ }
+ return do_utimes(AT_FDCWD, filename, times ? tv : NULL, 0);
+}
+#endif
+
+#ifdef CONFIG_COMPAT_32BIT_TIME
+/*
+ * Not all architectures have sys_utime, so implement this in terms
+ * of sys_utimes.
+ */
+#ifdef __ARCH_WANT_SYS_UTIME32
+SYSCALL_DEFINE2(utime32, const char __user *, filename,
+ struct old_utimbuf32 __user *, t)
+{
+ struct timespec64 tv[2];
+
+ if (t) {
+ if (get_user(tv[0].tv_sec, &t->actime) ||
+ get_user(tv[1].tv_sec, &t->modtime))
+ return -EFAULT;
+ tv[0].tv_nsec = 0;
+ tv[1].tv_nsec = 0;
+ }
+ return do_utimes(AT_FDCWD, filename, t ? tv : NULL, 0);
+}
+#endif
+
+SYSCALL_DEFINE4(utimensat_time32, unsigned int, dfd, const char __user *, filename, struct old_timespec32 __user *, t, int, flags)
+{
+ struct timespec64 tv[2];
+
+ if (t) {
+ if (get_old_timespec32(&tv[0], &t[0]) ||
+ get_old_timespec32(&tv[1], &t[1]))
+ return -EFAULT;
+
+ if (tv[0].tv_nsec == UTIME_OMIT && tv[1].tv_nsec == UTIME_OMIT)
+ return 0;
+ }
+ return do_utimes(dfd, filename, t ? tv : NULL, flags);
+}
+
+#ifdef __ARCH_WANT_SYS_UTIME32
+static long do_compat_futimesat(unsigned int dfd, const char __user *filename,
+ struct old_timeval32 __user *t)
+{
+ struct timespec64 tv[2];
+
+ if (t) {
+ if (get_user(tv[0].tv_sec, &t[0].tv_sec) ||
+ get_user(tv[0].tv_nsec, &t[0].tv_usec) ||
+ get_user(tv[1].tv_sec, &t[1].tv_sec) ||
+ get_user(tv[1].tv_nsec, &t[1].tv_usec))
+ return -EFAULT;
+ if (tv[0].tv_nsec >= 1000000 || tv[0].tv_nsec < 0 ||
+ tv[1].tv_nsec >= 1000000 || tv[1].tv_nsec < 0)
+ return -EINVAL;
+ tv[0].tv_nsec *= 1000;
+ tv[1].tv_nsec *= 1000;
+ }
+ return do_utimes(dfd, filename, t ? tv : NULL, 0);
+}
+
+SYSCALL_DEFINE3(futimesat_time32, unsigned int, dfd,
+ const char __user *, filename,
+ struct old_timeval32 __user *, t)
+{
+ return do_compat_futimesat(dfd, filename, t);
+}
+
+SYSCALL_DEFINE2(utimes_time32, const char __user *, filename, struct old_timeval32 __user *, t)
+{
+ return do_compat_futimesat(AT_FDCWD, filename, t);
+}
+#endif
+#endif