public inbox for io-uring@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH] test: add timens-abs-timer regression test
@ 2026-05-06 13:59 Maoyi Xie
  2026-05-07 17:02 ` Jens Axboe
  0 siblings, 1 reply; 2+ messages in thread
From: Maoyi Xie @ 2026-05-06 13:59 UTC (permalink / raw)
  To: Jens Axboe; +Cc: Pavel Begunkov, io-uring, Maoyi Xie

From: Maoyi Xie <maoyi.xie@ntu.edu.sg>

Add a regression test that exercises the two ABS timer paths in
io_uring with the submitter inside a CLONE_NEWTIME time namespace
that has a -10s monotonic offset:

  - IORING_OP_TIMEOUT with IORING_TIMEOUT_ABS, parsed via
    io_parse_user_time() in io_uring/timeout.c.
  - io_uring_enter with IORING_ENTER_ABS_TIMER, parsed inline in
    io_cqring_wait() in io_uring/wait.c.

The test forks once to enter the new userns, sets up uid_map
and gid_map for unprivileged root, writes the -10s monotonic
offset to /proc/self/timens_offsets, then forks again. The
grandchild is the first process actually inside the new time
namespace (unshare(CLONE_NEWTIME) does not move the caller in,
only its future children). On both ABS timer paths the
grandchild submits an absolute deadline of now + 1s and asserts
the call returns after at least 0.9s.

The test fails on a kernel without commits 9cc6bac1bebf
("io_uring/timeout: honour caller's time namespace for
IORING_TIMEOUT_ABS") and 45d2b37a37ab ("io_uring/wait: honour
caller's time namespace for IORING_ENTER_ABS_TIMER"), where
the deadline is interpreted in host view and the timer fires
after ~1ms.

The test is skipped if the kernel lacks CLONE_NEWTIME support
or the caller cannot create an unprivileged user namespace.

Signed-off-by: Maoyi Xie <maoyi.xie@ntu.edu.sg>
---
 test/Makefile           |   1 +
 test/timens-abs-timer.c | 315 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 316 insertions(+)
 create mode 100644 test/timens-abs-timer.c

diff --git a/test/Makefile b/test/Makefile
index 6a79a02..13841a9 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -286,6 +286,7 @@ test_srcs := \
 	task-restrict.c \
 	teardowns.c \
 	thread-exit.c \
+	timens-abs-timer.c \
 	timerfd-short-read.c \
 	timeout.c \
 	timeout-new.c \
diff --git a/test/timens-abs-timer.c b/test/timens-abs-timer.c
new file mode 100644
index 0000000..cd5471b
--- /dev/null
+++ b/test/timens-abs-timer.c
@@ -0,0 +1,315 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * Description: regression test for IORING_TIMEOUT_ABS and
+ * IORING_ENTER_ABS_TIMER honouring the submitter's time
+ * namespace. The kernel converts user supplied absolute time
+ * from the caller's time namespace view to host view via
+ * timens_ktime_to_host(). Without that conversion an absolute
+ * deadline submitted from inside a CLONE_NEWTIME namespace fires
+ * immediately instead of after the requested interval.
+ *
+ * The test forks a child, enters a fresh user namespace plus
+ * time namespace with a -10s monotonic offset, submits an
+ * absolute deadline of now + 1s on each path, and asserts the
+ * call returns after ~1s rather than after <100ms. The test is
+ * skipped if the kernel lacks CLONE_NEWTIME support or the
+ * caller cannot create a user namespace.
+ */
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include "helpers.h"
+#include "liburing.h"
+#include "../src/syscall.h"
+
+#ifndef CLONE_NEWTIME
+#define CLONE_NEWTIME	0x00000080
+#endif
+
+#define EXPECTED_NS	1000000000ULL	/* deadline at now + 1s */
+#define MIN_OBSERVED_NS	900000000ULL	/* fire no earlier than 0.9s */
+#define BUG_OBSERVED_NS	100000000ULL	/* bug fires under 0.1s */
+
+static int write_one(const char *path, const char *buf)
+{
+	int fd, ret;
+
+	fd = open(path, O_WRONLY);
+	if (fd < 0)
+		return -errno;
+	ret = write(fd, buf, strlen(buf));
+	close(fd);
+	if (ret < 0)
+		return -errno;
+	if ((size_t) ret != strlen(buf))
+		return -EIO;
+	return 0;
+}
+
+static int enter_unpriv_userns_timens(void)
+{
+	int ret;
+
+	ret = unshare(CLONE_NEWUSER | CLONE_NEWTIME);
+	if (ret < 0)
+		return -errno;
+
+	if (write_one("/proc/self/setgroups", "deny") < 0)
+		return -errno;
+	if (write_one("/proc/self/uid_map", "0 0 1\n") < 0)
+		return -errno;
+	if (write_one("/proc/self/gid_map", "0 0 1\n") < 0)
+		return -errno;
+
+	/* -10s monotonic offset: host_monotonic - 10s inside this ns. */
+	if (write_one("/proc/self/timens_offsets", "monotonic -10 0\n") < 0)
+		return -errno;
+
+	return 0;
+}
+
+static unsigned long long ts_to_ns(const struct timespec *ts)
+{
+	return ts->tv_sec * 1000000000ULL + ts->tv_nsec;
+}
+
+static long long elapsed_ns(const struct timespec *start)
+{
+	struct timespec now;
+
+	if (clock_gettime(CLOCK_MONOTONIC, &now) < 0)
+		return -errno;
+	return ts_to_ns(&now) - ts_to_ns(start);
+}
+
+/*
+ * Path 1: IORING_OP_TIMEOUT with IORING_TIMEOUT_ABS, parsed via
+ * io_parse_user_time() in io_uring/timeout.c.
+ */
+static int test_op_timeout_abs(void)
+{
+	struct io_uring_cqe *cqe;
+	struct io_uring_sqe *sqe;
+	struct __kernel_timespec kts;
+	struct timespec start;
+	struct io_uring ring;
+	long long elapsed;
+	int ret;
+
+	ret = io_uring_queue_init(1, &ring, 0);
+	if (ret) {
+		fprintf(stderr, "queue_init: %d\n", ret);
+		return T_EXIT_FAIL;
+	}
+
+	if (clock_gettime(CLOCK_MONOTONIC, &start) < 0) {
+		perror("clock_gettime");
+		io_uring_queue_exit(&ring);
+		return T_EXIT_FAIL;
+	}
+
+	kts.tv_sec = start.tv_sec + 1;
+	kts.tv_nsec = start.tv_nsec;
+
+	sqe = io_uring_get_sqe(&ring);
+	io_uring_prep_timeout(sqe, &kts, 0, IORING_TIMEOUT_ABS);
+
+	ret = io_uring_submit(&ring);
+	if (ret != 1) {
+		fprintf(stderr, "submit: %d\n", ret);
+		io_uring_queue_exit(&ring);
+		return T_EXIT_FAIL;
+	}
+
+	ret = io_uring_wait_cqe(&ring, &cqe);
+	if (ret) {
+		fprintf(stderr, "wait_cqe: %d\n", ret);
+		io_uring_queue_exit(&ring);
+		return T_EXIT_FAIL;
+	}
+	io_uring_cqe_seen(&ring, cqe);
+
+	elapsed = elapsed_ns(&start);
+	io_uring_queue_exit(&ring);
+
+	if (elapsed < 0) {
+		fprintf(stderr, "elapsed_ns failed\n");
+		return T_EXIT_FAIL;
+	}
+	if ((unsigned long long) elapsed < BUG_OBSERVED_NS) {
+		fprintf(stderr,
+			"IORING_TIMEOUT_ABS fired after %lld ns, expected ~%llu ns. "
+			"Likely missing timens_ktime_to_host() in io_parse_user_time().\n",
+			elapsed, EXPECTED_NS);
+		return T_EXIT_FAIL;
+	}
+	if ((unsigned long long) elapsed < MIN_OBSERVED_NS) {
+		fprintf(stderr,
+			"IORING_TIMEOUT_ABS fired early at %lld ns\n", elapsed);
+		return T_EXIT_FAIL;
+	}
+	return T_EXIT_PASS;
+}
+
+/*
+ * Path 2: io_uring_enter with IORING_ENTER_ABS_TIMER, parsed
+ * inline in io_uring/wait.c::io_cqring_wait().
+ */
+static int test_enter_abs_timer(void)
+{
+	struct io_uring_getevents_arg arg;
+	struct __kernel_timespec kts;
+	struct timespec start;
+	struct io_uring ring;
+	long long elapsed;
+	int ret;
+
+	ret = io_uring_queue_init(1, &ring, 0);
+	if (ret) {
+		fprintf(stderr, "queue_init: %d\n", ret);
+		return T_EXIT_FAIL;
+	}
+
+	if (clock_gettime(CLOCK_MONOTONIC, &start) < 0) {
+		perror("clock_gettime");
+		io_uring_queue_exit(&ring);
+		return T_EXIT_FAIL;
+	}
+
+	kts.tv_sec = start.tv_sec + 1;
+	kts.tv_nsec = start.tv_nsec;
+
+	memset(&arg, 0, sizeof(arg));
+	arg.sigmask_sz = _NSIG / 8;
+	arg.ts = (unsigned long) &kts;
+
+	ret = io_uring_enter2(ring.ring_fd, 0, 1,
+			      IORING_ENTER_GETEVENTS |
+			      IORING_ENTER_EXT_ARG |
+			      IORING_ENTER_ABS_TIMER,
+			      &arg, sizeof(arg));
+	if (ret != -ETIME) {
+		fprintf(stderr,
+			"io_uring_enter2 returned %d, expected -ETIME (%d)\n",
+			ret, -ETIME);
+		io_uring_queue_exit(&ring);
+		if (ret == -EINVAL)
+			return T_EXIT_SKIP;
+		return T_EXIT_FAIL;
+	}
+
+	elapsed = elapsed_ns(&start);
+	io_uring_queue_exit(&ring);
+
+	if (elapsed < 0) {
+		fprintf(stderr, "elapsed_ns failed\n");
+		return T_EXIT_FAIL;
+	}
+	if ((unsigned long long) elapsed < BUG_OBSERVED_NS) {
+		fprintf(stderr,
+			"IORING_ENTER_ABS_TIMER fired after %lld ns, expected ~%llu ns. "
+			"Likely missing timens_ktime_to_host() on the ABS_TIMER branch.\n",
+			elapsed, EXPECTED_NS);
+		return T_EXIT_FAIL;
+	}
+	if ((unsigned long long) elapsed < MIN_OBSERVED_NS) {
+		fprintf(stderr,
+			"IORING_ENTER_ABS_TIMER fired early at %lld ns\n", elapsed);
+		return T_EXIT_FAIL;
+	}
+	return T_EXIT_PASS;
+}
+
+/*
+ * Run the actual io_uring tests inside the new time namespace.
+ * unshare(CLONE_NEWTIME) does not move the caller into the new
+ * namespace, only its future children. So the caller sets up
+ * userns and timens, writes the offset, then forks once more to
+ * enter the new time namespace.
+ */
+static int run_tests_in_timens_grandchild(void)
+{
+	struct timespec probe;
+	int ret;
+
+	/*
+	 * Sanity check: clock_gettime should reflect the -10s offset.
+	 * If it does not, the offset was not applied and the test
+	 * would silently appear to pass on an unpatched kernel.
+	 */
+	if (clock_gettime(CLOCK_MONOTONIC, &probe) < 0) {
+		perror("clock_gettime");
+		return T_EXIT_FAIL;
+	}
+
+	ret = test_op_timeout_abs();
+	if (ret != T_EXIT_PASS)
+		return ret;
+
+	return test_enter_abs_timer();
+}
+
+static int run_in_timens(void)
+{
+	pid_t pid;
+	int status, ret;
+
+	ret = enter_unpriv_userns_timens();
+	if (ret == -EPERM || ret == -ENOSPC || ret == -EINVAL || ret == -ENOENT)
+		return T_EXIT_SKIP;
+	if (ret) {
+		fprintf(stderr, "userns/timens setup: %s\n", strerror(-ret));
+		return T_EXIT_SKIP;
+	}
+
+	pid = fork();
+	if (pid < 0) {
+		perror("fork (timens)");
+		return T_EXIT_FAIL;
+	}
+	if (pid == 0)
+		_exit(run_tests_in_timens_grandchild());
+
+	if (waitpid(pid, &status, 0) < 0) {
+		perror("waitpid (timens)");
+		return T_EXIT_FAIL;
+	}
+	if (WIFEXITED(status))
+		return WEXITSTATUS(status);
+	return T_EXIT_FAIL;
+}
+
+int main(int argc, char *argv[])
+{
+	pid_t pid;
+	int status;
+
+	if (argc > 1)
+		return T_EXIT_SKIP;
+
+	pid = fork();
+	if (pid < 0) {
+		perror("fork");
+		return T_EXIT_FAIL;
+	}
+	if (pid == 0)
+		_exit(run_in_timens());
+
+	if (waitpid(pid, &status, 0) < 0) {
+		perror("waitpid");
+		return T_EXIT_FAIL;
+	}
+	if (WIFEXITED(status))
+		return WEXITSTATUS(status);
+	return T_EXIT_FAIL;
+}

base-commit: 5dfc30a27303af1185e65d10890fdb35117bb3eb
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 2+ messages in thread

* Re: [PATCH] test: add timens-abs-timer regression test
  2026-05-06 13:59 [PATCH] test: add timens-abs-timer regression test Maoyi Xie
@ 2026-05-07 17:02 ` Jens Axboe
  0 siblings, 0 replies; 2+ messages in thread
From: Jens Axboe @ 2026-05-07 17:02 UTC (permalink / raw)
  To: Maoyi Xie; +Cc: Pavel Begunkov, io-uring, Maoyi Xie


On Wed, 06 May 2026 21:59:35 +0800, Maoyi Xie wrote:
> Add a regression test that exercises the two ABS timer paths in
> io_uring with the submitter inside a CLONE_NEWTIME time namespace
> that has a -10s monotonic offset:
> 
>   - IORING_OP_TIMEOUT with IORING_TIMEOUT_ABS, parsed via
>     io_parse_user_time() in io_uring/timeout.c.
>   - io_uring_enter with IORING_ENTER_ABS_TIMER, parsed inline in
>     io_cqring_wait() in io_uring/wait.c.
> 
> [...]

Applied, thanks!

[1/1] test: add timens-abs-timer regression test
      commit: eb8dd984881241bd206b0503a3bc2627f7ad0d09

Best regards,
-- 
Jens Axboe




^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-05-07 17:02 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-06 13:59 [PATCH] test: add timens-abs-timer regression test Maoyi Xie
2026-05-07 17:02 ` Jens Axboe

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox