* ntsync: absolute MONOTONIC timeout ignores time namespace offset
@ 2026-05-18 10:21 Maoyi Xie
2026-05-21 3:44 ` Elizabeth Figura
2026-05-21 15:39 ` Maoyi Xie
0 siblings, 2 replies; 3+ messages in thread
From: Maoyi Xie @ 2026-05-18 10:21 UTC (permalink / raw)
To: zfigura, arnd, Greg Kroah-Hartman; +Cc: wine-devel, linux-kernel
[-- Attachment #1: Type: text/plain, Size: 1668 bytes --]
Hi all,
While reading drivers/misc/ntsync.c I noticed something that
might be a time-namespace bug. I would appreciate it if you
could take a look and let me know whether this is a real bug,
and whether it is worth fixing.
ntsync_schedule() takes the user-supplied absolute timeout
and hands it to schedule_hrtimeout_range_clock() with
HRTIMER_MODE_ABS. For the default CLOCK_MONOTONIC path, it
does not call timens_ktime_to_host() first.
A process inside a CLOCK_MONOTONIC time namespace computes
the absolute timeout in its own clock view. The kernel reads
the same value against the host clock. The two differ by the
namespace offset.
The other absolute-timeout consumers (timerfd, posix-timers,
alarmtimer, posix-stubs, futex) all run user-supplied
absolute MONOTONIC ktime through timens_ktime_to_host()
before hrtimer. ntsync was added post-5.17 and seems to have
missed that step.
/dev/ntsync is mode 0666, so any user in a time namespace
that can open it is affected. The user-visible impact is
wrong timeout behaviour for Wine inside a container that
sets a CLOCK_MONOTONIC offset.
Attached PoC: baseline run elapsed ~100 ms; the --in-timens
run inside a -10s namespace elapsed ~0 ms.
A candidate fix would call timens_ktime_to_host(clock, timeout)
in the else branch (i.e., when NTSYNC_WAIT_REALTIME is not set)
before passing the value to schedule_hrtimeout_range_clock().
If this is intentional or already known, please disregard.
Otherwise, I am happy to send a [PATCH] or to leave the fix to you.
Thank you for your time, and sorry for the noise if this is not actually
a bug or has already been spotted.
Thanks,
Maoyi Xie
https://maoyixie.com/
[-- Attachment #2: poc_ntsync_timens.log --]
[-- Type: application/octet-stream, Size: 1514 bytes --]
# bug24 PoC verification on linux-7.1-rc1 (mainline vanilla)
#
# Test platform:
# qemu-system-x86_64 -enable-kvm -smp 4 -m 4G
# bzImage: vm/build-7.1-rc1-vanilla/arch/x86/boot/bzImage
# rootfs: vm/bookworm.qcow2 (debian bookworm)
# ntsync.ko: built CONFIG_NTSYNC=m from the same tree
#
# /dev/ntsync registers with mode 0666, so any user inside the VM
# (and any container exposing /dev/ntsync) can open it.
### Unpatched (linux-7.1-rc1 vanilla) ###
$ uname -a
Linux nsprobe 7.1.0-rc1-00001-g196d9a9bfa9b #2 SMP PREEMPT_DYNAMIC
$ insmod /root/ntsync.ko
$ ls -la /dev/ntsync
crw-rw-rw- 1 root root 10, 258 May 17 16:19 /dev/ntsync
$ ./poc_ntsync_timens # baseline (no timens)
[parent] baseline (no timens)
[wait] ret=-1 errno=110 (Connection timed out) elapsed=100 ms
$ ./poc_ntsync_timens --in-timens # inside CLOCK_MONOTONIC -10s
[child] in CLOCK_MONOTONIC -10s time namespace
[wait] ret=-1 errno=110 (Connection timed out) elapsed=0 ms
### Patched (linux-7.1-rc1 + 0001-ntsync-timens-fix.patch) ###
$ uname -a
Linux nsprobe 7.1.0-rc1-00001-g196d9a9bfa9b-dirty #3 SMP PREEMPT_DYNAMIC
$ insmod /root/ntsync.ko
$ ./poc_ntsync_timens # baseline (no timens)
[parent] baseline (no timens)
[wait] ret=-1 errno=110 (Connection timed out) elapsed=100 ms
$ ./poc_ntsync_timens --in-timens # inside CLOCK_MONOTONIC -10s
[child] in CLOCK_MONOTONIC -10s time namespace
[wait] ret=-1 errno=110 (Connection timed out) elapsed=100 ms
[-- Attachment #3: poc_ntsync_timens.c --]
[-- Type: application/octet-stream, Size: 4074 bytes --]
/*
* PoC: ntsync NTSYNC_IOC_WAIT_ANY ignores the CLOCK_MONOTONIC
* time namespace offset.
*
* ntsync passes a user-supplied absolute timeout to hrtimer
* with HRTIMER_MODE_ABS. It does not call timens_ktime_to_host()
* first. Inside a CLOCK_MONOTONIC time namespace, the user view
* and the host view of "now" differ by the namespace offset.
*
* Build: cc -O0 -g poc_ntsync_timens.c -o poc_ntsync_timens
* Run: ./poc_ntsync_timens # baseline
* ./poc_ntsync_timens --in-timens # inside -10s timens
*
* Baseline: elapsed ~ 100 ms
* In-timens: elapsed ~ 0 ms
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <linux/types.h>
#ifndef CLONE_NEWTIME
#define CLONE_NEWTIME 0x00000080
#endif
/* Mirror of include/uapi/linux/ntsync.h. */
struct ntsync_sem_args { __u32 count; __u32 max; };
struct ntsync_wait_args {
__u64 timeout;
__u64 objs;
__u32 count;
__u32 index;
__u32 flags;
__u32 owner;
__u32 alert;
__u32 pad;
};
#define NTSYNC_IOC_CREATE_SEM _IOW ('N', 0x80, struct ntsync_sem_args)
#define NTSYNC_IOC_WAIT_ANY _IOWR('N', 0x82, struct ntsync_wait_args)
static int run_wait(void)
{
int fd = open("/dev/ntsync", O_RDWR | O_CLOEXEC);
if (fd < 0) {
fprintf(stderr, "open /dev/ntsync: %s\n", strerror(errno));
return 1;
}
struct ntsync_sem_args sa = { .count = 0, .max = 1 };
int sem_fd = ioctl(fd, NTSYNC_IOC_CREATE_SEM, &sa);
if (sem_fd < 0) {
fprintf(stderr, "ioctl CREATE_SEM: %s\n", strerror(errno));
return 1;
}
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
__u64 now_ns = (__u64)ts.tv_sec * 1000000000ULL + ts.tv_nsec;
__u64 timeout_ns = now_ns + 100ULL * 1000000ULL; /* +100ms */
struct ntsync_wait_args wa = {
.timeout = timeout_ns,
.objs = (__u64)(uintptr_t)&sem_fd,
.count = 1,
.index = 0,
.flags = 0, /* CLOCK_MONOTONIC */
.owner = 0,
.alert = 0,
};
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
int ret = ioctl(fd, NTSYNC_IOC_WAIT_ANY, &wa);
int err = errno;
clock_gettime(CLOCK_MONOTONIC, &t1);
long ms = (t1.tv_sec - t0.tv_sec) * 1000 +
(t1.tv_nsec - t0.tv_nsec) / 1000000;
printf("[wait] ret=%d errno=%d (%s) elapsed=%ld ms\n",
ret, err, strerror(err), ms);
close(sem_fd);
close(fd);
return 0;
}
static int set_timens(long monotonic_offset_sec)
{
if (unshare(CLONE_NEWUSER) < 0) {
fprintf(stderr, "unshare(CLONE_NEWUSER): %s\n", strerror(errno));
return 1;
}
int fd;
if ((fd = open("/proc/self/setgroups", O_WRONLY)) >= 0) {
write(fd, "deny", 4);
close(fd);
}
if ((fd = open("/proc/self/uid_map", O_WRONLY)) >= 0) {
dprintf(fd, "0 %d 1", getuid());
close(fd);
}
if ((fd = open("/proc/self/gid_map", O_WRONLY)) >= 0) {
dprintf(fd, "0 %d 1", getgid());
close(fd);
}
if (unshare(CLONE_NEWTIME) < 0) {
fprintf(stderr, "unshare(CLONE_NEWTIME): %s\n", strerror(errno));
return 1;
}
fd = open("/proc/self/timens_offsets", O_WRONLY);
if (fd < 0) {
fprintf(stderr, "open timens_offsets: %s\n", strerror(errno));
return 1;
}
char buf[64];
int n = snprintf(buf, sizeof(buf), "monotonic %ld 0\n",
monotonic_offset_sec);
if (write(fd, buf, n) < 0) {
fprintf(stderr, "write timens_offsets: %s\n", strerror(errno));
close(fd);
return 1;
}
close(fd);
return 0;
}
int main(int argc, char **argv)
{
int in_timens = 0;
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--in-timens"))
in_timens = 1;
}
if (in_timens) {
if (set_timens(-10) != 0)
return 1;
/* fork so child re-execs into the new time namespace */
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) {
printf("[child] in CLOCK_MONOTONIC -10s time namespace\n");
return run_wait();
}
int status;
waitpid(pid, &status, 0);
return WEXITSTATUS(status);
}
printf("[parent] baseline (no timens)\n");
return run_wait();
}
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: ntsync: absolute MONOTONIC timeout ignores time namespace offset
2026-05-18 10:21 ntsync: absolute MONOTONIC timeout ignores time namespace offset Maoyi Xie
@ 2026-05-21 3:44 ` Elizabeth Figura
2026-05-21 15:39 ` Maoyi Xie
1 sibling, 0 replies; 3+ messages in thread
From: Elizabeth Figura @ 2026-05-21 3:44 UTC (permalink / raw)
To: arnd, Greg Kroah-Hartman, Maoyi Xie; +Cc: wine-devel, linux-kernel
On Monday, 18 May 2026 05:21:08 CDT Maoyi Xie wrote:
> Hi all,
>
> While reading drivers/misc/ntsync.c I noticed something that
> might be a time-namespace bug. I would appreciate it if you
> could take a look and let me know whether this is a real bug,
> and whether it is worth fixing.
>
> ntsync_schedule() takes the user-supplied absolute timeout
> and hands it to schedule_hrtimeout_range_clock() with
> HRTIMER_MODE_ABS. For the default CLOCK_MONOTONIC path, it
> does not call timens_ktime_to_host() first.
>
> A process inside a CLOCK_MONOTONIC time namespace computes
> the absolute timeout in its own clock view. The kernel reads
> the same value against the host clock. The two differ by the
> namespace offset.
>
> The other absolute-timeout consumers (timerfd, posix-timers,
> alarmtimer, posix-stubs, futex) all run user-supplied
> absolute MONOTONIC ktime through timens_ktime_to_host()
> before hrtimer. ntsync was added post-5.17 and seems to have
> missed that step.
>
> /dev/ntsync is mode 0666, so any user in a time namespace
> that can open it is affected. The user-visible impact is
> wrong timeout behaviour for Wine inside a container that
> sets a CLOCK_MONOTONIC offset.
>
> Attached PoC: baseline run elapsed ~100 ms; the --in-timens
> run inside a -10s namespace elapsed ~0 ms.
>
> A candidate fix would call timens_ktime_to_host(clock, timeout)
> in the else branch (i.e., when NTSYNC_WAIT_REALTIME is not set)
> before passing the value to schedule_hrtimeout_range_clock().
>
> If this is intentional or already known, please disregard.
I was not aware of time namespaces, so no, this was not intentional, and there's no reason to keep it this way. I don't remember at this point if I wrote the timeout logic using any other code as a model... you'd think I would have used futex, but evidently I either didn't or missed the timens call. Thank you for catching this.
> Otherwise, I am happy to send a [PATCH] or to leave the fix to you.
No strong feelings here, so I suppose whoever gets to it first ;-)
> Thank you for your time, and sorry for the noise if this is not actually
> a bug or has already been spotted.
>
> Thanks,
> Maoyi Xie
> https://maoyixie.com/
>
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: ntsync: absolute MONOTONIC timeout ignores time namespace offset
2026-05-18 10:21 ntsync: absolute MONOTONIC timeout ignores time namespace offset Maoyi Xie
2026-05-21 3:44 ` Elizabeth Figura
@ 2026-05-21 15:39 ` Maoyi Xie
1 sibling, 0 replies; 3+ messages in thread
From: Maoyi Xie @ 2026-05-21 15:39 UTC (permalink / raw)
To: Elizabeth Figura, Thomas Gleixner
Cc: Anna-Maria Behnsen, Frederic Weisbecker, Arnd Bergmann,
Greg Kroah-Hartman, linux-kernel
Hi all,
Elizabeth confirmed this bug yesterday. Before I send a fix I would
like to check the export plan with the time namespace folks.
The fix calls timens_ktime_to_host(clock, timeout) on the non-REALTIME
branch in ntsync_schedule(). The helper is an inline in
<linux/time_namespace.h>. It references init_time_ns and
do_timens_ktime_to_host. Neither is exported.
All current users of the inline (timerfd, posix-timers, alarmtimer,
posix-stubs, futex, io_uring) are built-in. ntsync is tristate
(default m), so it is the first module that needs the helper.
Without the exports, modpost reports both symbols unresolved and
ntsync.ko does not load.
I was planning to send:
1/2 time/namespace: export init_time_ns and do_timens_ktime_to_host
2/2 ntsync: honour caller's time namespace for absolute MONOTONIC
timeouts
I also considered these but they look weaker:
- Flip CONFIG_NTSYNC to bool. Breaks module users.
- Add a new exported wrapper that hides init_time_ns. Cleaner
ABI but adds a helper for one caller.
If the export plan is OK I will send the series. If you prefer
something else please let me know and I will follow that.
Thanks for taking a look,
Maoyi
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-05-21 15:39 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-05-18 10:21 ntsync: absolute MONOTONIC timeout ignores time namespace offset Maoyi Xie
2026-05-21 3:44 ` Elizabeth Figura
2026-05-21 15:39 ` Maoyi Xie
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®