* [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