From 86ecc6a35a7f4e391406607ddfa3c5199b6b85fd Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Tue, 6 Oct 2026 12:41:09 -0700 Subject: [PATCH 1/3] Fix some sanity checks for the behavior of utimesat and stat 1. Creating a file then updating its mtime with utimensat UTIME_NOW should never move the mtime backwards. 2. Creating a file, stat-ing it, then using utimesat with the mtime that stat returned, then stating it again should return identical timestamp. Both failing due to some problems with rounding. --- src/lib/libsyscall.js | 27 ++++++-- .../test_codesize_file_preload.expected.js | 21 ++++-- test/codesize/test_codesize_hello_dylink.json | 8 +-- .../test_codesize_hello_dylink_all.json | 4 +- test/test_other.py | 8 +++ test/utime/test_utime_now.c | 62 ++++++++++++++++++ test/utime/test_utime_roundtrip.c | 64 +++++++++++++++++++ 7 files changed, 175 insertions(+), 19 deletions(-) create mode 100644 test/utime/test_utime_now.c create mode 100644 test/utime/test_utime_roundtrip.c diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index 4b23b196d235f..be5351c034d3a 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -54,12 +54,21 @@ var SyscallsLibrary = { var atime = stat.atimeMs ?? stat.atime.getTime(); var mtime = stat.mtimeMs ?? stat.mtime.getTime(); var ctime = stat.ctimeMs ?? stat.ctime.getTime(); - {{{ makeSetValue('buf', C_STRUCTS.stat.st_atim.tv_sec, 'Math.floor(atime / 1000)', 'i64') }}}; - {{{ makeSetValue('buf', C_STRUCTS.stat.st_atim.tv_nsec, 'Math.floor((atime % 1000) * 1_000_000)', SIZE_TYPE) }}}; - {{{ makeSetValue('buf', C_STRUCTS.stat.st_mtim.tv_sec, 'Math.floor(mtime / 1000)', 'i64') }}}; - {{{ makeSetValue('buf', C_STRUCTS.stat.st_mtim.tv_nsec, 'Math.floor((mtime % 1000) * 1_000_000)', SIZE_TYPE) }}}; - {{{ makeSetValue('buf', C_STRUCTS.stat.st_ctim.tv_sec, 'Math.floor(ctime / 1000)', 'i64') }}}; - {{{ makeSetValue('buf', C_STRUCTS.stat.st_ctim.tv_nsec, 'Math.floor((ctime % 1000) * 1_000_000)', SIZE_TYPE) }}}; + // Round to the nearest 10 microseconds. This matches the granularity + // that __syscall_utimensat rounds down to when setting timestamps, so + // that values read via stat() round-trip through utimensat(). + var atime10Us = Math.round(atime * 100); + var mtime10Us = Math.round(mtime * 100); + var ctime10Us = Math.round(ctime * 100); + var atimeSec = Math.floor(atime10Us / 100_000); + var mtimeSec = Math.floor(mtime10Us / 100_000); + var ctimeSec = Math.floor(ctime10Us / 100_000); + {{{ makeSetValue('buf', C_STRUCTS.stat.st_atim.tv_sec, 'atimeSec', 'i64') }}}; + {{{ makeSetValue('buf', C_STRUCTS.stat.st_atim.tv_nsec, '(atime10Us - atimeSec * 100_000) * 10_000', SIZE_TYPE) }}}; + {{{ makeSetValue('buf', C_STRUCTS.stat.st_mtim.tv_sec, 'mtimeSec', 'i64') }}}; + {{{ makeSetValue('buf', C_STRUCTS.stat.st_mtim.tv_nsec, '(mtime10Us - mtimeSec * 100_000) * 10_000', SIZE_TYPE) }}}; + {{{ makeSetValue('buf', C_STRUCTS.stat.st_ctim.tv_sec, 'ctimeSec', 'i64') }}}; + {{{ makeSetValue('buf', C_STRUCTS.stat.st_ctim.tv_nsec, '(ctime10Us - ctimeSec * 100_000) * 10_000', SIZE_TYPE) }}}; {{{ makeSetValue('buf', C_STRUCTS.stat.st_ino, 'stat.ino', 'i64') }}}; return 0; }, @@ -1078,7 +1087,11 @@ var SyscallsLibrary = { var nofollow = flags & {{{ cDefs.AT_SYMLINK_NOFOLLOW }}}; path = SYSCALLS.getStr(path); path = SYSCALLS.calculateAt(dirfd, path, true); - var now = Date.now(), atime, mtime; + // Use a sub-millisecond clock for "now" if possible. This is needed to + // ensure that a later UTIME_NOW doesn't set the time earlier than an + // earlier UTIME_NOW, see test_utime_now.c + var now = globalThis.performance?.timeOrigin ? performance.timeOrigin + performance.now() : Date.now(); + var atime, mtime; if (!times) { atime = now; mtime = now; diff --git a/test/codesize/test_codesize_file_preload.expected.js b/test/codesize/test_codesize_file_preload.expected.js index d0c39ca532b58..eb4a50827ca13 100644 --- a/test/codesize/test_codesize_file_preload.expected.js +++ b/test/codesize/test_codesize_file_preload.expected.js @@ -3047,12 +3047,21 @@ var SYSCALLS = { var atime = stat.atimeMs ?? stat.atime.getTime(); var mtime = stat.mtimeMs ?? stat.mtime.getTime(); var ctime = stat.ctimeMs ?? stat.ctime.getTime(); - HEAP64[(((buf) + (40)) >> 3)] = BigInt(Math.floor(atime / 1e3)); - HEAPU32[(((buf) + (48)) >> 2)] = Math.floor((atime % 1e3) * 1e6); - HEAP64[(((buf) + (56)) >> 3)] = BigInt(Math.floor(mtime / 1e3)); - HEAPU32[(((buf) + (64)) >> 2)] = Math.floor((mtime % 1e3) * 1e6); - HEAP64[(((buf) + (72)) >> 3)] = BigInt(Math.floor(ctime / 1e3)); - HEAPU32[(((buf) + (80)) >> 2)] = Math.floor((ctime % 1e3) * 1e6); + // Round to the nearest 10 microseconds. This matches the granularity + // that __syscall_utimensat rounds down to when setting timestamps, so + // that values read via stat() round-trip through utimensat(). + var atime10Us = Math.round(atime * 100); + var mtime10Us = Math.round(mtime * 100); + var ctime10Us = Math.round(ctime * 100); + var atimeSec = Math.floor(atime10Us / 1e5); + var mtimeSec = Math.floor(mtime10Us / 1e5); + var ctimeSec = Math.floor(ctime10Us / 1e5); + HEAP64[(((buf) + (40)) >> 3)] = BigInt(atimeSec); + HEAPU32[(((buf) + (48)) >> 2)] = (atime10Us - atimeSec * 1e5) * 1e4; + HEAP64[(((buf) + (56)) >> 3)] = BigInt(mtimeSec); + HEAPU32[(((buf) + (64)) >> 2)] = (mtime10Us - mtimeSec * 1e5) * 1e4; + HEAP64[(((buf) + (72)) >> 3)] = BigInt(ctimeSec); + HEAPU32[(((buf) + (80)) >> 2)] = (ctime10Us - ctimeSec * 1e5) * 1e4; HEAP64[(((buf) + (88)) >> 3)] = BigInt(stat.ino); return 0; }, diff --git a/test/codesize/test_codesize_hello_dylink.json b/test/codesize/test_codesize_hello_dylink.json index 92a6fb2c0bf24..c8710949e930d 100644 --- a/test/codesize/test_codesize_hello_dylink.json +++ b/test/codesize/test_codesize_hello_dylink.json @@ -1,10 +1,10 @@ { - "a.out.js": 26358, - "a.out.js.gz": 11227, + "a.out.js": 26400, + "a.out.js.gz": 11255, "a.out.nodebug.wasm": 16986, "a.out.nodebug.wasm.gz": 8676, - "total": 43344, - "total_gz": 19903, + "total": 43386, + "total_gz": 19931, "sent": [ "__syscall_stat64", "emscripten_resize_heap", diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 7e99db2389059..93648e2f41f41 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 272813, + "a.out.js": 272931, "a.out.nodebug.wasm": 587951, - "total": 860764, + "total": 860882, "sent": [ "IMG_Init", "IMG_Load", diff --git a/test/test_other.py b/test/test_other.py index b44cd2219bfc4..844ee4303749d 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -5878,6 +5878,14 @@ def test_strftime_zZ(self): def test_strptime_symmetry(self): self.do_other_test('test_strptime_symmetry.c') + @also_with_nodefs_both + def test_utime_now(self): + self.do_runf('utime/test_utime_now.c', 'done\n') + + @also_with_nodefs_both + def test_utime_roundtrip(self): + self.do_runf('utime/test_utime_roundtrip.c', 'done\n') + @also_with_wasmfs def test_truncate_from_0(self): self.do_other_test('test_truncate_from_0.cpp') diff --git a/test/utime/test_utime_now.c b/test/utime/test_utime_now.c new file mode 100644 index 0000000000000..6f9da629ccb2d --- /dev/null +++ b/test/utime/test_utime_now.c @@ -0,0 +1,62 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + */ + +// Creating a file and then touching it must never move the mtime backwards. +// Run many times in a loop to prevent flakiness. + +#include +#include +#include +#include +#include + +static int cmp_timespec(const struct timespec* a, const struct timespec* b) { + if (a->tv_sec != b->tv_sec) { + return a->tv_sec < b->tv_sec ? -1 : 1; + } + if (a->tv_nsec != b->tv_nsec) { + return a->tv_nsec < b->tv_nsec ? -1 : 1; + } + return 0; +} + +int main() { + const char* path = "touchfile"; + for (int i = 0; i < 1000; i++) { + // Create the file. Filesystem assigns initial mtime. + int fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0666); + assert(fd >= 0); + close(fd); + + // stat 1 + struct stat before; + assert(stat(path, &before) == 0); + + // touch + struct timespec now[2]; + now[0].tv_nsec = UTIME_NOW; + now[1].tv_nsec = UTIME_NOW; + assert(utimensat(AT_FDCWD, path, now, 0) == 0); + + // stat 2 + struct stat after; + assert(stat(path, &after) == 0); + + // assert stat 2 >= stat 1 + if (cmp_timespec(&after.st_mtim, &before.st_mtim) < 0) { + printf("iteration %d: mtime went backwards: before=%lld.%09ld after=%lld.%09ld\n", + i, (long long)before.st_mtim.tv_sec, before.st_mtim.tv_nsec, + (long long)after.st_mtim.tv_sec, after.st_mtim.tv_nsec); + return 1; + } + + assert(unlink(path) == 0); + } + + puts("done"); + return 0; +} diff --git a/test/utime/test_utime_roundtrip.c b/test/utime/test_utime_roundtrip.c new file mode 100644 index 0000000000000..bf0b61f34b262 --- /dev/null +++ b/test/utime/test_utime_roundtrip.c @@ -0,0 +1,64 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + */ + +// Reading a file's timestamps with stat() and writing them back with +// utimensat() must produce the same timestamps when read back again. Run in a +// loop to exercise different rounding cases. + +#include +#include +#include +#include +#include + +#define ITERATIONS 2000 + +static int same_timespec(const struct timespec* a, const struct timespec* b) { + return a->tv_sec == b->tv_sec && a->tv_nsec == b->tv_nsec; +} + +static void print_timespec(const char* label, const struct timespec* ts) { + printf("%s=%lld.%09ld", label, (long long)ts->tv_sec, ts->tv_nsec); +} + +int main() { + const char* path = "roundtripfile"; + for (int i = 0; i < ITERATIONS; i++) { + // Create the file. Filesystem assigns mtime. + int fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0666); + assert(fd >= 0); + close(fd); + + struct stat before; + assert(stat(path, &before) == 0); + + // Write the timestamps back exactly as we read them. + struct timespec times[2] = {before.st_atim, before.st_mtim}; + assert(utimensat(AT_FDCWD, path, times, 0) == 0); + + + // Writing back same timestamp shouldn't have changed it. + struct stat after; + assert(stat(path, &after) == 0); + + if (!same_timespec(&before.st_atim, &after.st_atim) || + !same_timespec(&before.st_mtim, &after.st_mtim)) { + printf("iteration %d: timestamps did not round-trip: ", i); + print_timespec("atime before", &before.st_atim); + print_timespec(" after", &after.st_atim); + print_timespec(" mtime before", &before.st_mtim); + print_timespec(" after", &after.st_mtim); + printf("\n"); + return 1; + } + + assert(unlink(path) == 0); + } + + puts("done"); + return 0; +} From c78265cc95796b43857eccfa4396d859def54bdb Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Tue, 6 Oct 2026 14:17:59 -0700 Subject: [PATCH 2/3] Use emscripten_get_now() --- src/lib/libsyscall.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index be5351c034d3a..9414b2e71717f 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -1090,7 +1090,7 @@ var SyscallsLibrary = { // Use a sub-millisecond clock for "now" if possible. This is needed to // ensure that a later UTIME_NOW doesn't set the time earlier than an // earlier UTIME_NOW, see test_utime_now.c - var now = globalThis.performance?.timeOrigin ? performance.timeOrigin + performance.now() : Date.now(); + var now = emscripten_get_now(); var atime, mtime; if (!times) { atime = now; From 2c84cb35d65d046cbe94d0ad5ac912417660a1d6 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Tue, 6 Oct 2026 15:35:02 -0700 Subject: [PATCH 3/3] Revert "Use emscripten_get_now()" This reverts commit c78265cc95796b43857eccfa4396d859def54bdb. --- src/lib/libsyscall.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index 9414b2e71717f..be5351c034d3a 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -1090,7 +1090,7 @@ var SyscallsLibrary = { // Use a sub-millisecond clock for "now" if possible. This is needed to // ensure that a later UTIME_NOW doesn't set the time earlier than an // earlier UTIME_NOW, see test_utime_now.c - var now = emscripten_get_now(); + var now = globalThis.performance?.timeOrigin ? performance.timeOrigin + performance.now() : Date.now(); var atime, mtime; if (!times) { atime = now;