Repository: tetratelabs/wazero
Pull request #1602 (2023-07-30 06:05:13)

sysfs: decouples FS and File from the syscall package
This decouples FS and File from the syscall package by passing through atim and mtim as numbers instead of structs. This makes it easier to integrate with `os.Chtimes` as well possible to compile with plan9.

In doing so, this unsupports the `UTIME_NOW` constant, which was problematic anyway. wasip1 defines `UTIME_NOW` as the value of the real-time clock, which isn't necessarily the same as the filesystem clock. By unsupporting `UTIME_NOW` we instead read from the correct clock.


Patch:
diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go
index 033b70cb..3a451711 100644
--- a/imports/wasi_snapshot_preview1/fs.go
+++ b/imports/wasi_snapshot_preview1/fs.go
@@ -15,7 +15,6 @@ import (
 	"github.com/tetratelabs/wazero/internal/fsapi"
 	socketapi "github.com/tetratelabs/wazero/internal/sock"
 	"github.com/tetratelabs/wazero/internal/sys"
-	"github.com/tetratelabs/wazero/internal/sysfs"
 	"github.com/tetratelabs/wazero/internal/wasip1"
 	"github.com/tetratelabs/wazero/internal/wasm"
 	sysapi "github.com/tetratelabs/wazero/sys"
@@ -503,50 +502,55 @@ func fdFilestatSetTimesFn(_ context.Context, mod api.Module, params []uint64) ex
 		return experimentalsys.EBADF
 	}
 
-	times, errno := toTimes(atim, mtim, fstFlags)
+	atim, mtim, errno := toTimes(sys.WalltimeNanos, atim, mtim, fstFlags)
 	if errno != 0 {
 		return errno
 	}
 
 	// Try to update the file timestamps by file-descriptor.
-	errno = f.File.Utimens(&times)
+	errno = f.File.Utimens(atim, mtim)
 
 	// Fall back to path based, despite it being less precise.
 	switch errno {
 	case experimentalsys.EPERM, experimentalsys.ENOSYS:
-		errno = f.FS.Utimens(f.Name, &times)
+		errno = f.FS.Utimens(f.Name, atim, mtim)
 	}
 
 	return errno
 }
 
-func toTimes(atim, mtime int64, fstFlags uint16) (times [2]syscall.Timespec, errno experimentalsys.Errno) {
+func toTimes(walltime func() int64, atim, mtim int64, fstFlags uint16) (int64, int64, experimentalsys.Errno) {
 	// times[0] == atim, times[1] == mtim
 
+	var nowTim int64
+
 	// coerce atim into a timespec
 	if set, now := fstFlags&wasip1.FstflagsAtim != 0, fstFlags&wasip1.FstflagsAtimNow != 0; set && now {
-		errno = experimentalsys.EINVAL
-		return
+		return 0, 0, experimentalsys.EINVAL
 	} else if set {
-		times[0] = syscall.NsecToTimespec(atim)
+		// atim is already correct
 	} else if now {
-		times[0].Nsec = sysfs.UTIME_NOW
+		nowTim = walltime()
+		atim = nowTim
 	} else {
-		times[0].Nsec = sysfs.UTIME_OMIT
+		atim = fsapi.UTIME_OMIT
 	}
 
 	// coerce mtim into a timespec
 	if set, now := fstFlags&wasip1.FstflagsMtim != 0, fstFlags&wasip1.FstflagsMtimNow != 0; set && now {
-		errno = experimentalsys.EINVAL
-		return
+		return 0, 0, experimentalsys.EINVAL
 	} else if set {
-		times[1] = syscall.NsecToTimespec(mtime)
+		// mtim is already correct
 	} else if now {
-		times[1].Nsec = sysfs.UTIME_NOW
+		if nowTim != 0 {
+			mtim = nowTim
+		} else {
+			mtim = walltime()
+		}
 	} else {
-		times[1].Nsec = sysfs.UTIME_OMIT
+		mtim = fsapi.UTIME_OMIT
 	}
-	return
+	return atim, mtim, 0
 }
 
 // fdPread is the WASI function named FdPreadName which reads from a file
@@ -1445,7 +1449,7 @@ func pathFilestatSetTimesFn(_ context.Context, mod api.Module, params []uint64)
 	sys := mod.(*wasm.ModuleInstance).Sys
 	fsc := sys.FS()
 
-	times, errno := toTimes(atim, mtim, fstFlags)
+	atim, mtim, errno := toTimes(sys.WalltimeNanos, atim, mtim, fstFlags)
 	if errno != 0 {
 		return errno
 	}
@@ -1457,14 +1461,14 @@ func pathFilestatSetTimesFn(_ context.Context, mod api.Module, params []uint64)
 
 	symlinkFollow := flags&wasip1.LOOKUP_SYMLINK_FOLLOW != 0
 	if symlinkFollow {
-		return preopen.Utimens(pathName, &times)
+		return preopen.Utimens(pathName, atim, mtim)
 	}
 	// Otherwise, we need to emulate don't follow by opening the file by path.
 	if f, errno := preopen.OpenFile(pathName, syscall.O_WRONLY, 0); errno != 0 {
 		return errno
 	} else {
 		defer f.Close()
-		return f.Utimens(&times)
+		return f.Utimens(atim, mtim)
 	}
 }
 
diff --git a/internal/fsapi/file.go b/internal/fsapi/file.go
index cd0ccfba..0c50175d 100644
--- a/internal/fsapi/file.go
+++ b/internal/fsapi/file.go
@@ -1,8 +1,6 @@
 package fsapi
 
 import (
-	"syscall"
-
 	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/sys"
 )
@@ -346,10 +344,9 @@ type File interface {
 	//
 	// # Parameters
 	//
-	// The `times` parameter includes the access and modification timestamps to
-	// assign. Special syscall.Timespec NSec values UTIME_NOW and UTIME_OMIT may be
-	// specified instead of real timestamps. A nil `times` parameter behaves the
-	// same as if both were set to UTIME_NOW.
+	// The `atim` and `mtim` parameters refer to access and modification time
+	// stamps as defined in sys.Stat_t. To retain one or the other, substitute
+	// it with the pseudo-timestamp UTIME_OMIT.
 	//
 	// # Errors
 	//
@@ -363,7 +360,7 @@ type File interface {
 	//     https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html
 	//   - Windows requires files to be open with fsapi.O_RDWR, which means you
 	//     cannot use this to update timestamps on a directory (EPERM).
-	Utimens(times *[2]syscall.Timespec) experimentalsys.Errno
+	Utimens(atim, mtim int64) experimentalsys.Errno
 
 	// Close closes the underlying file.
 	//
diff --git a/internal/fsapi/fs.go b/internal/fsapi/fs.go
index 50bf6168..0b0d6790 100644
--- a/internal/fsapi/fs.go
+++ b/internal/fsapi/fs.go
@@ -2,7 +2,6 @@ package fsapi
 
 import (
 	"io/fs"
-	"syscall"
 
 	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/sys"
@@ -272,11 +271,12 @@ type FS interface {
 	//
 	// # Parameters
 	//
-	// The `times` parameter includes the access and modification timestamps to
-	// assign. Special syscall.Timespec NSec values UTIME_NOW and UTIME_OMIT
-	// may be specified instead of real timestamps. A nil `times` parameter
-	// behaves the same as if both were set to UTIME_NOW. If the path is a
-	// symbolic link, the target of expanding that link is updated.
+	// If the path is a symbolic link, the target of expanding that link is
+	// updated.
+	//
+	// The `atim` and `mtim` parameters refer to access and modification time
+	// stamps as defined in sys.Stat_t. To retain one or the other, substitute
+	// it with the pseudo-timestamp UTIME_OMIT.
 	//
 	// # Errors
 	//
@@ -290,7 +290,5 @@ type FS interface {
 	//
 	//   - This is like syscall.UtimesNano and `utimensat` with `AT_FDCWD` in
 	//     POSIX. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html
-	Utimens(path string, times *[2]syscall.Timespec) experimentalsys.Errno
-	// TODO: change impl to not use syscall package,
-	// possibly by being just a pair of int64s..
+	Utimens(path string, atim, mtim int64) experimentalsys.Errno
 }
diff --git a/internal/fsapi/time.go b/internal/fsapi/time.go
new file mode 100644
index 00000000..1cfcea5d
--- /dev/null
+++ b/internal/fsapi/time.go
@@ -0,0 +1,10 @@
+package fsapi
+
+import "math"
+
+// UTIME_OMIT is a special constant for use in updating times via FS.Utimens
+// or File.Utimens. When used for atim or mtim, the value is retained.
+//
+// Note: This may be implemented via a stat when the underlying filesystem
+// does not support this value.
+const UTIME_OMIT int64 = math.MinInt64
diff --git a/internal/fsapi/unimplemented.go b/internal/fsapi/unimplemented.go
index 77567a24..b1284bc8 100644
--- a/internal/fsapi/unimplemented.go
+++ b/internal/fsapi/unimplemented.go
@@ -2,7 +2,6 @@ package fsapi
 
 import (
 	"io/fs"
-	"syscall"
 
 	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/sys"
@@ -78,7 +77,7 @@ func (UnimplementedFS) Unlink(path string) experimentalsys.Errno {
 }
 
 // Utimens implements FS.Utimens
-func (UnimplementedFS) Utimens(path string, times *[2]syscall.Timespec) experimentalsys.Errno {
+func (UnimplementedFS) Utimens(path string, atim, mtim int64) experimentalsys.Errno {
 	return experimentalsys.ENOSYS
 }
 
@@ -184,7 +183,7 @@ func (UnimplementedFile) Datasync() experimentalsys.Errno {
 }
 
 // Utimens implements File.Utimens
-func (UnimplementedFile) Utimens(*[2]syscall.Timespec) experimentalsys.Errno {
+func (UnimplementedFile) Utimens(int64, int64) experimentalsys.Errno {
 	return experimentalsys.ENOSYS
 }
 
diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go
index ad223fd6..d0cd422e 100644
--- a/internal/gojs/fs.go
+++ b/internal/gojs/fs.go
@@ -3,7 +3,6 @@ package gojs
 import (
 	"context"
 	"fmt"
-	"syscall"
 
 	"github.com/tetratelabs/wazero/api"
 	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
@@ -432,10 +431,7 @@ func (u *jsfsUtimes) invoke(ctx context.Context, mod api.Module, args ...interfa
 	callback := args[3].(funcWrapper)
 
 	fsc := mod.(*wasm.ModuleInstance).Sys.FS()
-	times := [2]syscall.Timespec{
-		syscall.NsecToTimespec(atimeSec * 1e9), syscall.NsecToTimespec(mtimeSec * 1e9),
-	}
-	errno := fsc.RootFS().Utimens(path, &times)
+	errno := fsc.RootFS().Utimens(path, atimeSec*1e9, mtimeSec*1e9)
 
 	return jsfsInvoke(ctx, mod, callback, errno)
 }
diff --git a/internal/sys/lazy.go b/internal/sys/lazy.go
index 011a5a4d..1cb01a19 100644
--- a/internal/sys/lazy.go
+++ b/internal/sys/lazy.go
@@ -1,8 +1,6 @@
 package sys
 
 import (
-	"syscall"
-
 	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/internal/fsapi"
 	"github.com/tetratelabs/wazero/sys"
@@ -103,11 +101,11 @@ func (r *lazyDir) Datasync() experimentalsys.Errno {
 }
 
 // Utimens implements the same method as documented on fsapi.File
-func (r *lazyDir) Utimens(times *[2]syscall.Timespec) experimentalsys.Errno {
+func (r *lazyDir) Utimens(atim, mtim int64) experimentalsys.Errno {
 	if f, ok := r.file(); !ok {
 		return experimentalsys.EBADF
 	} else {
-		return f.Utimens(times)
+		return f.Utimens(atim, mtim)
 	}
 }
 
diff --git a/internal/sysfs/dirfs.go b/internal/sysfs/dirfs.go
index c908d6c5..f681e97b 100644
--- a/internal/sysfs/dirfs.go
+++ b/internal/sysfs/dirfs.go
@@ -116,8 +116,8 @@ func (d *dirFS) Symlink(oldName, link string) experimentalsys.Errno {
 }
 
 // Utimens implements the same method as documented on fsapi.FS
-func (d *dirFS) Utimens(path string, times *[2]syscall.Timespec) experimentalsys.Errno {
-	return Utimens(d.join(path), times)
+func (d *dirFS) Utimens(path string, atim, mtim int64) experimentalsys.Errno {
+	return utimens(d.join(path), atim, mtim)
 }
 
 func (d *dirFS) join(path string) string {
diff --git a/internal/sysfs/file.go b/internal/sysfs/file.go
index b3285cab..b9b96076 100644
--- a/internal/sysfs/file.go
+++ b/internal/sysfs/file.go
@@ -4,6 +4,7 @@ import (
 	"io"
 	"io/fs"
 	"os"
+	"time"
 
 	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/internal/fsapi"
@@ -465,3 +466,40 @@ func pwrite(w io.WriterAt, buf []byte, off int64) (n int, errno experimentalsys.
 	n, err := w.WriteAt(buf, off)
 	return n, experimentalsys.UnwrapOSError(err)
 }
+
+func chtimes(path string, atim, mtim int64) (errno experimentalsys.Errno) { //nolint:unused
+	// When both inputs are omitted, there is nothing to change.
+	if atim == fsapi.UTIME_OMIT && mtim == fsapi.UTIME_OMIT {
+		return
+	}
+
+	// UTIME_OMIT is expensive until progress is made in Go, as it requires a
+	// stat to read-back the value to re-apply.
+	// - https://github.com/golang/go/issues/32558.
+	// - https://go-review.googlesource.com/c/go/+/219638 (unmerged)
+	var st sys.Stat_t
+	if atim == fsapi.UTIME_OMIT || mtim == fsapi.UTIME_OMIT {
+		if st, errno = stat(path); errno != 0 {
+			return
+		}
+	}
+
+	var atime, mtime time.Time
+	if atim == fsapi.UTIME_OMIT {
+		atime = epochNanosToTime(st.Atim)
+		mtime = epochNanosToTime(mtim)
+	} else if mtim == fsapi.UTIME_OMIT {
+		atime = epochNanosToTime(atim)
+		mtime = epochNanosToTime(st.Mtim)
+	} else {
+		atime = epochNanosToTime(atim)
+		mtime = epochNanosToTime(mtim)
+	}
+	return experimentalsys.UnwrapOSError(os.Chtimes(path, atime, mtime))
+}
+
+func epochNanosToTime(epochNanos int64) time.Time { //nolint:unused
+	seconds := epochNanos / 1e9
+	nanos := epochNanos % 1e9
+	return time.Unix(seconds, nanos)
+}
diff --git a/internal/sysfs/futimens.go b/internal/sysfs/futimens.go
index 9144126b..c122da79 100644
--- a/internal/sysfs/futimens.go
+++ b/internal/sysfs/futimens.go
@@ -1,53 +1,14 @@
+//go:build linux || darwin
+
 package sysfs
 
 import (
 	"syscall"
-	"time"
 	"unsafe"
 
-	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
-	"github.com/tetratelabs/wazero/sys"
+	"github.com/tetratelabs/wazero/internal/fsapi"
 )
 
-const (
-	// UTIME_NOW is a special syscall.Timespec NSec value used to set the
-	// file's timestamp to a value close to, but not greater than the current
-	// system time.
-	UTIME_NOW = _UTIME_NOW
-
-	// UTIME_OMIT is a special syscall.Timespec NSec value used to avoid
-	// setting the file's timestamp.
-	UTIME_OMIT = _UTIME_OMIT
-)
-
-// Utimens set file access and modification times on a path resolved to the
-// current working directory, at nanosecond precision.
-//
-// # Parameters
-//
-// The `times` parameter includes the access and modification timestamps to
-// assign. Special syscall.Timespec NSec values UTIME_NOW and UTIME_OMIT may be
-// specified instead of real timestamps. A nil `times` parameter behaves the
-// same as if both were set to UTIME_NOW. If the path is a symbolic link, the
-// target of expanding that link is updated.
-//
-// # Errors
-//
-// A zero sys.Errno is success. The below are expected otherwise:
-//   - sys.ENOSYS: the implementation does not support this function.
-//   - sys.EINVAL: `path` is invalid.
-//   - sys.EEXIST: `path` exists and is a directory.
-//   - sys.ENOTDIR: `path` exists and is a file.
-//
-// # Notes
-//
-//   - This is like syscall.UtimesNano and `utimensat` with `AT_FDCWD` in
-//     POSIX. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html
-func Utimens(path string, times *[2]syscall.Timespec) experimentalsys.Errno {
-	err := utimens(path, times)
-	return experimentalsys.UnwrapOSError(err)
-}
-
 func timesToPtr(times *[2]syscall.Timespec) unsafe.Pointer { //nolint:unused
 	if times != nil {
 		return unsafe.Pointer(&times[0])
@@ -55,67 +16,22 @@ func timesToPtr(times *[2]syscall.Timespec) unsafe.Pointer { //nolint:unused
 	return unsafe.Pointer(nil)
 }
 
-func utimensPortable(path string, times *[2]syscall.Timespec) error { //nolint:unused
-	// Handle when both inputs are current system time.
-	if times == nil || times[0].Nsec == UTIME_NOW && times[1].Nsec == UTIME_NOW {
-		ts := nowTimespec()
-		return syscall.UtimesNano(path, []syscall.Timespec{ts, ts})
-	}
-
+func timesToTimespecs(atim int64, mtim int64) (times *[2]syscall.Timespec) {
 	// When both inputs are omitted, there is nothing to change.
-	if times[0].Nsec == UTIME_OMIT && times[1].Nsec == UTIME_OMIT {
-		return nil
-	}
-
-	// Handle when neither input are special values
-	if times[0].Nsec != UTIME_NOW && times[1].Nsec != UTIME_NOW &&
-		times[0].Nsec != UTIME_OMIT && times[1].Nsec != UTIME_OMIT {
-		return syscall.UtimesNano(path, times[:])
+	if atim == fsapi.UTIME_OMIT && mtim == fsapi.UTIME_OMIT {
+		return
 	}
 
-	// Now, either atim or mtim is a special value, but not both.
-
-	// Now, either one of the inputs is a special value, or neither. This means
-	// we don't have a risk of re-reading the clock or re-doing stat.
-	if atim, err := normalizeTimespec(path, times, 0); err != 0 {
-		return err
-	} else if mtim, err := normalizeTimespec(path, times, 1); err != 0 {
-		return err
+	times = &[2]syscall.Timespec{}
+	if atim == fsapi.UTIME_OMIT {
+		times[0] = syscall.Timespec{Nsec: _UTIME_OMIT}
+		times[1] = syscall.NsecToTimespec(mtim)
+	} else if mtim == fsapi.UTIME_OMIT {
+		times[0] = syscall.NsecToTimespec(atim)
+		times[1] = syscall.Timespec{Nsec: _UTIME_OMIT}
 	} else {
-		return syscall.UtimesNano(path, []syscall.Timespec{atim, mtim})
-	}
-}
-
-func normalizeTimespec(path string, times *[2]syscall.Timespec, i int) (ts syscall.Timespec, err experimentalsys.Errno) { //nolint:unused
-	switch times[i].Nsec {
-	case UTIME_NOW: // declined in Go per golang/go#31880.
-		ts = nowTimespec()
-		return
-	case UTIME_OMIT:
-		// UTIME_OMIT is expensive until progress is made in Go, as it requires a
-		// stat to read-back the value to re-apply.
-		// - https://github.com/golang/go/issues/32558.
-		// - https://go-review.googlesource.com/c/go/+/219638 (unmerged)
-		var st sys.Stat_t
-		if st, err = stat(path); err != 0 {
-			return
-		}
-		switch i {
-		case 0:
-			ts = syscall.NsecToTimespec(st.Atim)
-		case 1:
-			ts = syscall.NsecToTimespec(st.Mtim)
-		default:
-			panic("BUG")
-		}
-		return
-	default: // not special
-		ts = times[i]
-		return
+		times[0] = syscall.NsecToTimespec(atim)
+		times[1] = syscall.NsecToTimespec(mtim)
 	}
-}
-
-func nowTimespec() syscall.Timespec { //nolint:unused
-	now := time.Now().UnixNano()
-	return syscall.NsecToTimespec(now)
+	return
 }
diff --git a/internal/sysfs/futimens_darwin.go b/internal/sysfs/futimens_darwin.go
index a5663ede..88e4008f 100644
--- a/internal/sysfs/futimens_darwin.go
+++ b/internal/sysfs/futimens_darwin.go
@@ -2,13 +2,14 @@ package sysfs
 
 import (
 	"syscall"
-	_ "unsafe" // for go:linkname
+	_ "unsafe"
+
+	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
 )
 
 const (
 	_AT_FDCWD            = -0x2
 	_AT_SYMLINK_NOFOLLOW = 0x0020
-	_UTIME_NOW           = -1
 	_UTIME_OMIT          = -2
 )
 
@@ -16,20 +17,25 @@ const (
 //go:linkname utimensat syscall.utimensat
 func utimensat(dirfd int, path string, times *[2]syscall.Timespec, flags int) error
 
-func utimens(path string, times *[2]syscall.Timespec) error {
+func utimens(path string, atim, mtim int64) experimentalsys.Errno {
+	times := timesToTimespecs(atim, mtim)
+	if times == nil {
+		return 0
+	}
 	var flags int
-	return utimensat(_AT_FDCWD, path, times, flags)
+	return experimentalsys.UnwrapOSError(utimensat(_AT_FDCWD, path, times, flags))
 }
 
-func futimens(fd uintptr, times *[2]syscall.Timespec) error {
+func futimens(fd uintptr, atim, mtim int64) experimentalsys.Errno {
+	times := timesToTimespecs(atim, mtim)
+	if times == nil {
+		return 0
+	}
 	_p0 := timesToPtr(times)
 
 	// Warning: futimens only exists since High Sierra (10.13).
 	_, _, e1 := syscall_syscall6(libc_futimens_trampoline_addr, fd, uintptr(_p0), 0, 0, 0, 0)
-	if e1 != 0 {
-		return e1
-	}
-	return nil
+	return experimentalsys.UnwrapOSError(e1)
 }
 
 // libc_futimens_trampoline_addr is the address of the
diff --git a/internal/sysfs/futimens_linux.go b/internal/sysfs/futimens_linux.go
index 5008ca81..3ec68537 100644
--- a/internal/sysfs/futimens_linux.go
+++ b/internal/sysfs/futimens_linux.go
@@ -3,28 +3,38 @@ package sysfs
 import (
 	"syscall"
 	"unsafe"
-	_ "unsafe" // for go:linkname
+	_ "unsafe"
+
+	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
 )
 
 const (
 	_AT_FDCWD   = -0x64
-	_UTIME_NOW  = (1 << 30) - 1
 	_UTIME_OMIT = (1 << 30) - 2
 )
 
-func utimens(path string, times *[2]syscall.Timespec) (err error) {
+func utimens(path string, atim, mtim int64) experimentalsys.Errno {
+	times := timesToTimespecs(atim, mtim)
+	if times == nil {
+		return 0
+	}
+
 	var flags int
 	var _p0 *byte
-	_p0, err = syscall.BytePtrFromString(path)
-	if err != nil {
-		return
+	_p0, err := syscall.BytePtrFromString(path)
+	if err == nil {
+		err = utimensat(_AT_FDCWD, uintptr(unsafe.Pointer(_p0)), times, flags)
 	}
-	return utimensat(_AT_FDCWD, uintptr(unsafe.Pointer(_p0)), times, flags)
+	return experimentalsys.UnwrapOSError(err)
 }
 
 // On linux, implement futimens via utimensat with the NUL path.
-func futimens(fd uintptr, times *[2]syscall.Timespec) error {
-	return utimensat(int(fd), 0 /* NUL */, times, 0)
+func futimens(fd uintptr, atim, mtim int64) experimentalsys.Errno {
+	times := timesToTimespecs(atim, mtim)
+	if times == nil {
+		return 0
+	}
+	return experimentalsys.UnwrapOSError(utimensat(int(fd), 0 /* NUL */, times, 0))
 }
 
 // utimensat is like syscall.utimensat special-cased to accept a NUL string for the path value.
diff --git a/internal/sysfs/futimens_unsupported.go b/internal/sysfs/futimens_unsupported.go
index 2816cb84..c78a16b4 100644
--- a/internal/sysfs/futimens_unsupported.go
+++ b/internal/sysfs/futimens_unsupported.go
@@ -3,22 +3,14 @@
 package sysfs
 
 import (
-	"syscall"
-
 	"github.com/tetratelabs/wazero/experimental/sys"
 )
 
-// Define values even if not used except as sentinels.
-const (
-	_UTIME_NOW  = -1
-	_UTIME_OMIT = -2
-)
-
-func utimens(path string, times *[2]syscall.Timespec) error {
-	return utimensPortable(path, times)
+func utimens(path string, atim, mtim int64) sys.Errno {
+	return chtimes(path, atim, mtim)
 }
 
-func futimens(fd uintptr, times *[2]syscall.Timespec) error {
+func futimens(fd uintptr, atim, mtim int64) error {
 	// Go exports syscall.Futimes, which is microsecond granularity, and
 	// WASI tests expect nanosecond. We don't yet have a way to invoke the
 	// futimens syscall portably.
diff --git a/internal/sysfs/futimens_windows.go b/internal/sysfs/futimens_windows.go
index 26d9c2a4..68223f5e 100644
--- a/internal/sysfs/futimens_windows.go
+++ b/internal/sysfs/futimens_windows.go
@@ -2,24 +2,17 @@ package sysfs
 
 import (
 	"syscall"
-	"time"
 
 	"github.com/tetratelabs/wazero/experimental/sys"
+	"github.com/tetratelabs/wazero/internal/fsapi"
 	"github.com/tetratelabs/wazero/internal/platform"
 )
 
-// Define values even if not used except as sentinels.
-const (
-	_UTIME_NOW              = -1
-	_UTIME_OMIT             = -2
-	SupportsSymlinkNoFollow = false
-)
-
-func utimens(path string, times *[2]syscall.Timespec) error {
-	return utimensPortable(path, times)
+func utimens(path string, atim, mtim int64) sys.Errno {
+	return chtimes(path, atim, mtim)
 }
 
-func futimens(fd uintptr, times *[2]syscall.Timespec) error {
+func futimens(fd uintptr, atim, mtim int64) error {
 	// Before Go 1.20, ERROR_INVALID_HANDLE was returned for too many reasons.
 	// Kick out so that callers can use path-based operations instead.
 	if !platform.IsAtLeastGo120 {
@@ -27,9 +20,9 @@ func futimens(fd uintptr, times *[2]syscall.Timespec) error {
 	}
 
 	// Per docs, zero isn't a valid timestamp as it cannot be differentiated
-	// from nil. In both cases, it is a marker like syscall.UTIME_OMIT.
+	// from nil. In both cases, it is a marker like fsapi.UTIME_OMIT.
 	// See https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfiletime
-	a, w := timespecToFiletime(times)
+	a, w := timespecToFiletime(atim, mtim)
 
 	if a == nil && w == nil {
 		return nil // both omitted, so nothing to change
@@ -42,32 +35,16 @@ func futimens(fd uintptr, times *[2]syscall.Timespec) error {
 	return syscall.SetFileTime(h, nil, a, w)
 }
 
-func timespecToFiletime(times *[2]syscall.Timespec) (a, w *syscall.Filetime) {
-	// Handle when both inputs are current system time.
-	if times == nil || times[0].Nsec == UTIME_NOW && times[1].Nsec == UTIME_NOW {
-		now := time.Now().UnixNano()
-		ft := syscall.NsecToFiletime(now)
-		return &ft, &ft
-	}
-
-	// Now, either one of the inputs is current time, or neither. This
-	// means we don't have a risk of re-reading the clock.
-	a = timespecToFileTime(times, 0)
-	w = timespecToFileTime(times, 1)
+func timespecToFiletime(atim, mtim int64) (a, w *syscall.Filetime) {
+	a = timespecToFileTime(atim)
+	w = timespecToFileTime(mtim)
 	return
 }
 
-func timespecToFileTime(times *[2]syscall.Timespec, i int) *syscall.Filetime {
-	if times[i].Nsec == UTIME_OMIT {
+func timespecToFileTime(tim int64) *syscall.Filetime {
+	if tim == fsapi.UTIME_OMIT {
 		return nil
 	}
-
-	var nsec int64
-	if times[i].Nsec == UTIME_NOW {
-		nsec = time.Now().UnixNano()
-	} else {
-		nsec = syscall.TimespecToNsec(times[i])
-	}
-	ft := syscall.NsecToFiletime(nsec)
+	ft := syscall.NsecToFiletime(tim)
 	return &ft
 }
diff --git a/internal/sysfs/osfile.go b/internal/sysfs/osfile.go
index 410a4d0e..ead9e49a 100644
--- a/internal/sysfs/osfile.go
+++ b/internal/sysfs/osfile.go
@@ -5,7 +5,6 @@ import (
 	"io/fs"
 	"os"
 	"runtime"
-	"syscall"
 
 	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/internal/fsapi"
@@ -244,12 +243,12 @@ func (f *osFile) Datasync() experimentalsys.Errno {
 }
 
 // Utimens implements the same method as documented on fsapi.File
-func (f *osFile) Utimens(times *[2]syscall.Timespec) experimentalsys.Errno {
+func (f *osFile) Utimens(atim, mtim int64) experimentalsys.Errno {
 	if f.closed {
 		return experimentalsys.EBADF
 	}
 
-	err := futimens(f.fd, times)
+	err := futimens(f.fd, atim, mtim)
 	return experimentalsys.UnwrapOSError(err)
 }
 
diff --git a/internal/sysfs/poll_windows.go b/internal/sysfs/poll_windows.go
index 9b1cab9f..82c8b2ba 100644
--- a/internal/sysfs/poll_windows.go
+++ b/internal/sysfs/poll_windows.go
@@ -42,9 +42,6 @@ const pollInterval = 100 * time.Millisecond
 
 // _poll implements poll on Windows, for a subset of cases.
 //
-// pollWithContext emulates the behavior of POSIX poll(2) on Windows, for a subset of cases,
-// and it supports context cancellation.
-//
 // fds may contain any number of file handles, but regular files and pipes are only processed for _POLLIN.
 // Stdin is a pipe, thus it is checked for readiness when present. Pipes are checked using PeekNamedPipe.
 // Regular files always immediately reported as ready, regardless their actual state and timeouts.
diff --git a/internal/sysfs/readfs.go b/internal/sysfs/readfs.go
index 1e96e2b4..6e5fd650 100644
--- a/internal/sysfs/readfs.go
+++ b/internal/sysfs/readfs.go
@@ -2,7 +2,6 @@ package sysfs
 
 import (
 	"io/fs"
-	"syscall"
 
 	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/internal/fsapi"
@@ -98,7 +97,7 @@ func (r *readFS) Unlink(path string) experimentalsys.Errno {
 }
 
 // Utimens implements the same method as documented on fsapi.FS
-func (r *readFS) Utimens(path string, times *[2]syscall.Timespec) experimentalsys.Errno {
+func (r *readFS) Utimens(path string, atim, mtim int64) experimentalsys.Errno {
 	return experimentalsys.EROFS
 }
 
@@ -135,7 +134,7 @@ func (r *readFile) Datasync() experimentalsys.Errno {
 }
 
 // Utimens implements the same method as documented on fsapi.File.
-func (r *readFile) Utimens(*[2]syscall.Timespec) experimentalsys.Errno {
+func (r *readFile) Utimens(int64, int64) experimentalsys.Errno {
 	return experimentalsys.EBADF
 }
 


Test patch:
diff --git a/internal/sysfs/adapter_test.go b/internal/sysfs/adapter_test.go
index f79f4bdc..265d386c 100644
--- a/internal/sysfs/adapter_test.go
+++ b/internal/sysfs/adapter_test.go
@@ -90,7 +90,7 @@ func TestAdapt_UtimesNano(t *testing.T) {
 	realPath := joinPath(tmpDir, path)
 	require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
 
-	err := testFS.Utimens(path, nil)
+	err := testFS.Utimens(path, fsapi.UTIME_OMIT, fsapi.UTIME_OMIT)
 	require.EqualErrno(t, experimentalsys.ENOSYS, err)
 }
 
diff --git a/internal/sysfs/bench_test.go b/internal/sysfs/bench_test.go
index b5c34fd1..b31a7cb8 100644
--- a/internal/sysfs/bench_test.go
+++ b/internal/sysfs/bench_test.go
@@ -5,8 +5,8 @@ import (
 	"io/fs"
 	"os"
 	"path"
-	"syscall"
 	"testing"
+	"time"
 
 	"github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/internal/fsapi"
@@ -19,14 +19,12 @@ func BenchmarkFsFileUtimesNs(b *testing.B) {
 	}
 	defer f.Close()
 
-	times := &[2]syscall.Timespec{
-		{Sec: 123, Nsec: 4 * 1e3},
-		{Sec: 123, Nsec: 4 * 1e3},
-	}
+	atim := int64(123*time.Second + 4*time.Microsecond)
+	mtim := atim
 
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
-		if errno := f.Utimens(times); errno != 0 {
+		if errno := f.Utimens(atim, mtim); errno != 0 {
 			b.Fatal(errno)
 		}
 	}
diff --git a/internal/sysfs/dirfs_test.go b/internal/sysfs/dirfs_test.go
index 078df6df..b13146a0 100644
--- a/internal/sysfs/dirfs_test.go
+++ b/internal/sysfs/dirfs_test.go
@@ -7,7 +7,6 @@ import (
 	"os"
 	"path"
 	"runtime"
-	"syscall"
 	"testing"
 	"time"
 
@@ -531,7 +530,7 @@ func TestDirFS_Utimesns(t *testing.T) {
 	require.NoError(t, err)
 
 	t.Run("doesn't exist", func(t *testing.T) {
-		err := testFS.Utimens("nope", nil)
+		err := testFS.Utimens("nope", 0, 0)
 		require.EqualErrno(t, sys.ENOENT, err)
 	})
 
@@ -540,60 +539,31 @@ func TestDirFS_Utimesns(t *testing.T) {
 	//
 	// Negative isn't tested as most platforms don't return consistent results.
 	tests := []struct {
-		name  string
-		times *[2]syscall.Timespec
+		name       string
+		atim, mtim int64
 	}{
 		{
 			name: "nil",
 		},
 		{
 			name: "a=omit,m=omit",
-			times: &[2]syscall.Timespec{
-				{Sec: 123, Nsec: UTIME_OMIT},
-				{Sec: 123, Nsec: UTIME_OMIT},
-			},
+			atim: fsapi.UTIME_OMIT,
+			mtim: fsapi.UTIME_OMIT,
 		},
 		{
-			name: "a=now,m=omit",
-			times: &[2]syscall.Timespec{
-				{Sec: 123, Nsec: UTIME_NOW},
-				{Sec: 123, Nsec: UTIME_OMIT},
-			},
+			name: "a=set,m=omit",
+			atim: int64(123*time.Second + 4*time.Microsecond),
+			mtim: fsapi.UTIME_OMIT,
 		},
 		{
-			name: "a=omit,m=now",
-			times: &[2]syscall.Timespec{
-				{Sec: 123, Nsec: UTIME_OMIT},
-				{Sec: 123, Nsec: UTIME_NOW},
-			},
-		},
-		{
-			name: "a=now,m=now",
-			times: &[2]syscall.Timespec{
-				{Sec: 123, Nsec: UTIME_NOW},
-				{Sec: 123, Nsec: UTIME_NOW},
-			},
-		},
-		{
-			name: "a=now,m=set",
-			times: &[2]syscall.Timespec{
-				{Sec: 123, Nsec: UTIME_NOW},
-				{Sec: 123, Nsec: 4 * 1e3},
-			},
-		},
-		{
-			name: "a=set,m=now",
-			times: &[2]syscall.Timespec{
-				{Sec: 123, Nsec: 4 * 1e3},
-				{Sec: 123, Nsec: UTIME_NOW},
-			},
+			name: "a=omit,m=set",
+			atim: fsapi.UTIME_OMIT,
+			mtim: int64(123*time.Second + 4*time.Microsecond),
 		},
 		{
 			name: "a=set,m=set",
-			times: &[2]syscall.Timespec{
-				{Sec: 123, Nsec: 4 * 1e3},
-				{Sec: 223, Nsec: 5 * 1e3},
-			},
+			atim: int64(123*time.Second + 4*time.Microsecond),
+			mtim: int64(223*time.Second + 5*time.Microsecond),
 		},
 	}
 
@@ -636,31 +606,25 @@ func TestDirFS_Utimesns(t *testing.T) {
 				oldSt, errno := testFS.Lstat(statPath)
 				require.EqualErrno(t, 0, errno)
 
-				errno = testFS.Utimens(path, tc.times)
+				errno = testFS.Utimens(path, tc.atim, tc.mtim)
 				require.EqualErrno(t, 0, errno)
 
 				newSt, errno := testFS.Lstat(statPath)
 				require.EqualErrno(t, 0, errno)
 
 				if platform.CompilerSupported() {
-					if tc.times != nil && tc.times[0].Nsec == UTIME_OMIT {
+					if tc.atim == fsapi.UTIME_OMIT {
 						require.Equal(t, oldSt.Atim, newSt.Atim)
-					} else if tc.times == nil || tc.times[0].Nsec == UTIME_NOW {
-						now := time.Now().UnixNano()
-						require.True(t, newSt.Atim <= now, "expected atim %d <= now %d", newSt.Atim, now)
 					} else {
-						require.Equal(t, tc.times[0].Nano(), newSt.Atim)
+						require.Equal(t, tc.atim, newSt.Atim)
 					}
 				}
 
 				// When compiler isn't supported, we can still check mtim.
-				if tc.times != nil && tc.times[1].Nsec == UTIME_OMIT {
+				if tc.mtim == fsapi.UTIME_OMIT {
 					require.Equal(t, oldSt.Mtim, newSt.Mtim)
-				} else if tc.times == nil || tc.times[1].Nsec == UTIME_NOW {
-					now := time.Now().UnixNano()
-					require.True(t, newSt.Mtim <= now, "expected mtim %d <= now %d", newSt.Mtim, now)
 				} else {
-					require.Equal(t, tc.times[1].Nano(), newSt.Mtim)
+					require.Equal(t, tc.mtim, newSt.Mtim)
 				}
 			})
 		}
diff --git a/internal/sysfs/file_test.go b/internal/sysfs/file_test.go
index 0c1d35be..4e94093f 100644
--- a/internal/sysfs/file_test.go
+++ b/internal/sysfs/file_test.go
@@ -986,10 +986,10 @@ func TestFileUtimens(t *testing.T) {
 	testUtimens(t, true)
 
 	testEBADFIfFileClosed(t, func(f fsapi.File) experimentalsys.Errno {
-		return f.Utimens(nil)
+		return f.Utimens(fsapi.UTIME_OMIT, fsapi.UTIME_OMIT)
 	})
 	testEBADFIfDirClosed(t, func(d fsapi.File) experimentalsys.Errno {
-		return d.Utimens(nil)
+		return d.Utimens(fsapi.UTIME_OMIT, fsapi.UTIME_OMIT)
 	})
 }
 
diff --git a/internal/sysfs/futimens_test.go b/internal/sysfs/futimens_test.go
index b87d46f1..ba6a3f3b 100644
--- a/internal/sysfs/futimens_test.go
+++ b/internal/sysfs/futimens_test.go
@@ -1,10 +1,11 @@
+//go:build windows || linux || darwin
+
 package sysfs
 
 import (
 	"os"
 	"path"
 	"runtime"
-	"syscall"
 	"testing"
 	"time"
 
@@ -16,7 +17,7 @@ import (
 
 func TestUtimens(t *testing.T) {
 	t.Run("doesn't exist", func(t *testing.T) {
-		err := Utimens("nope", nil)
+		err := utimens("nope", 0, 0)
 		require.EqualErrno(t, sys.ENOENT, err)
 	})
 	testUtimens(t, false)
@@ -28,74 +29,31 @@ func testUtimens(t *testing.T, futimes bool) {
 	//
 	// Negative isn't tested as most platforms don't return consistent results.
 	tests := []struct {
-		name  string
-		times *[2]syscall.Timespec
+		name       string
+		atim, mtim int64
 	}{
 		{
 			name: "nil",
 		},
 		{
 			name: "a=omit,m=omit",
-			times: &[2]syscall.Timespec{
-				{Sec: 123, Nsec: UTIME_OMIT},
-				{Sec: 123, Nsec: UTIME_OMIT},
-			},
-		},
-		{
-			name: "a=now,m=omit",
-			times: &[2]syscall.Timespec{
-				{Sec: 123, Nsec: UTIME_NOW},
-				{Sec: 123, Nsec: UTIME_OMIT},
-			},
-		},
-		{
-			name: "a=omit,m=now",
-			times: &[2]syscall.Timespec{
-				{Sec: 123, Nsec: UTIME_OMIT},
-				{Sec: 123, Nsec: UTIME_NOW},
-			},
-		},
-		{
-			name: "a=now,m=now",
-			times: &[2]syscall.Timespec{
-				{Sec: 123, Nsec: UTIME_NOW},
-				{Sec: 123, Nsec: UTIME_NOW},
-			},
-		},
-		{
-			name: "a=now,m=set",
-			times: &[2]syscall.Timespec{
-				{Sec: 123, Nsec: UTIME_NOW},
-				{Sec: 123, Nsec: 4 * 1e3},
-			},
-		},
-		{
-			name: "a=set,m=now",
-			times: &[2]syscall.Timespec{
-				{Sec: 123, Nsec: 4 * 1e3},
-				{Sec: 123, Nsec: UTIME_NOW},
-			},
+			atim: fsapi.UTIME_OMIT,
+			mtim: fsapi.UTIME_OMIT,
 		},
 		{
 			name: "a=set,m=omit",
-			times: &[2]syscall.Timespec{
-				{Sec: 123, Nsec: 4 * 1e3},
-				{Sec: 123, Nsec: UTIME_OMIT},
-			},
+			atim: int64(123*time.Second + 4*time.Microsecond),
+			mtim: fsapi.UTIME_OMIT,
 		},
 		{
 			name: "a=omit,m=set",
-			times: &[2]syscall.Timespec{
-				{Sec: 123, Nsec: UTIME_OMIT},
-				{Sec: 123, Nsec: 4 * 1e3},
-			},
+			atim: fsapi.UTIME_OMIT,
+			mtim: int64(123*time.Second + 4*time.Microsecond),
 		},
 		{
 			name: "a=set,m=set",
-			times: &[2]syscall.Timespec{
-				{Sec: 123, Nsec: 4 * 1e3},
-				{Sec: 223, Nsec: 5 * 1e3},
-			},
+			atim: int64(123*time.Second + 4*time.Microsecond),
+			mtim: int64(223*time.Second + 5*time.Microsecond),
 		},
 	}
 	for _, fileType := range []string{"dir", "file", "link"} {
@@ -136,7 +94,7 @@ func testUtimens(t *testing.T, futimes bool) {
 				require.EqualErrno(t, 0, errno)
 
 				if !futimes {
-					errno = Utimens(path, tc.times)
+					errno = utimens(path, tc.atim, tc.mtim)
 					require.EqualErrno(t, 0, errno)
 				} else {
 					flag := fsapi.O_RDWR
@@ -150,7 +108,7 @@ func testUtimens(t *testing.T, futimes bool) {
 
 					f := requireOpenFile(t, path, flag, 0)
 
-					errno = f.Utimens(tc.times)
+					errno = f.Utimens(tc.atim, tc.mtim)
 					require.EqualErrno(t, 0, f.Close())
 					require.EqualErrno(t, 0, errno)
 				}
@@ -159,24 +117,18 @@ func testUtimens(t *testing.T, futimes bool) {
 				require.EqualErrno(t, 0, errno)
 
 				if platform.CompilerSupported() {
-					if tc.times != nil && tc.times[0].Nsec == UTIME_OMIT {
+					if tc.atim == fsapi.UTIME_OMIT {
 						require.Equal(t, oldSt.Atim, newSt.Atim)
-					} else if tc.times == nil || tc.times[0].Nsec == UTIME_NOW {
-						now := time.Now().UnixNano()
-						require.True(t, newSt.Atim <= now, "expected atim %d <= now %d", newSt.Atim, now)
 					} else {
-						require.Equal(t, tc.times[0].Nano(), newSt.Atim)
+						require.Equal(t, tc.atim, newSt.Atim)
 					}
 				}
 
 				// When compiler isn't supported, we can still check mtim.
-				if tc.times != nil && tc.times[1].Nsec == UTIME_OMIT {
+				if tc.mtim == fsapi.UTIME_OMIT {
 					require.Equal(t, oldSt.Mtim, newSt.Mtim)
-				} else if tc.times == nil || tc.times[1].Nsec == UTIME_NOW {
-					now := time.Now().UnixNano()
-					require.True(t, newSt.Mtim <= now, "expected mtim %d <= now %d", newSt.Mtim, now)
 				} else {
-					require.Equal(t, tc.times[1].Nano(), newSt.Mtim)
+					require.Equal(t, tc.mtim, newSt.Mtim)
 				}
 			})
 		}
diff --git a/internal/sysfs/readfs_test.go b/internal/sysfs/readfs_test.go
index b7b2c609..6e877ad7 100644
--- a/internal/sysfs/readfs_test.go
+++ b/internal/sysfs/readfs_test.go
@@ -114,7 +114,7 @@ func TestReadFS_UtimesNano(t *testing.T) {
 	realPath := joinPath(tmpDir, path)
 	require.NoError(t, os.WriteFile(realPath, []byte{}, 0o600))
 
-	err := testFS.Utimens(path, nil)
+	err := testFS.Utimens(path, fsapi.UTIME_OMIT, fsapi.UTIME_OMIT)
 	require.EqualErrno(t, sys.EROFS, err)
 }