Repository: tetratelabs/wazero
Pull request #1597 (2023-07-28 00:13:18)

sysfs: changes PollRead to accept int32 timeoutMillis
This changes `PollRead` to accept int32 timeoutMillis instead of using a pointer. I made a mistake earlier on this API, which wasn't being direct about units and also using nil where it isn't used that way at the syscall layer.

See https://github.com/tetratelabs/wazero/pull/1596


Patch:
diff --git a/imports/wasi_snapshot_preview1/poll.go b/imports/wasi_snapshot_preview1/poll.go
index aed08b7b..b6dc758e 100644
--- a/imports/wasi_snapshot_preview1/poll.go
+++ b/imports/wasi_snapshot_preview1/poll.go
@@ -174,7 +174,7 @@ func pollOneoffFn(_ context.Context, mod api.Module, params []uint64) sys.Errno
 			return sys.EBADF
 		}
 		// Wait for the timeout to expire, or for some data to become available on Stdin.
-		stdinReady, errno := stdin.File.PollRead(&timeout)
+		stdinReady, errno := stdin.File.PollRead(int32(timeout.Milliseconds()))
 		if errno != 0 {
 			return errno
 		}
diff --git a/internal/fsapi/dir.go b/internal/fsapi/dir.go
index 6e99b1b5..a68a3c47 100644
--- a/internal/fsapi/dir.go
+++ b/internal/fsapi/dir.go
@@ -3,7 +3,6 @@ package fsapi
 import (
 	"fmt"
 	"io/fs"
-	"time"
 
 	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/sys"
@@ -89,7 +88,7 @@ func (DirFile) Pread([]byte, int64) (int, experimentalsys.Errno) {
 }
 
 // PollRead implements File.PollRead
-func (DirFile) PollRead(*time.Duration) (ready bool, errno experimentalsys.Errno) {
+func (DirFile) PollRead(int32) (ready bool, errno experimentalsys.Errno) {
 	return false, experimentalsys.ENOSYS
 }
 
diff --git a/internal/fsapi/file.go b/internal/fsapi/file.go
index f40d4155..84d466a7 100644
--- a/internal/fsapi/file.go
+++ b/internal/fsapi/file.go
@@ -2,7 +2,6 @@ package fsapi
 
 import (
 	"syscall"
-	"time"
 
 	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/sys"
@@ -16,10 +15,10 @@ import (
 //
 // # Errors
 //
-// All methods that can return an error return a Errno, which is zero
+// All methods that can return an error return a sys.Errno, which is zero
 // on success.
 //
-// Restricting to Errno matches current WebAssembly host functions,
+// Restricting to sys.Errno matches current WebAssembly host functions,
 // which are constrained to well-known error codes. For example, `GOOS=js` maps
 // hard coded values and panics otherwise. More commonly, WASI maps syscall
 // errors to u32 numeric values.
@@ -86,9 +85,9 @@ type File interface {
 	//
 	// # Errors
 	//
-	// A zero Errno is success. The below are expected otherwise:
-	//   - ENOSYS: the implementation does not support this function.
-	//   - EBADF: the file or directory was closed.
+	// A zero sys.Errno is success. The below are expected otherwise:
+	//   - sys.ENOSYS: the implementation does not support this function.
+	//   - sys.EBADF: the file or directory was closed.
 	//
 	// # Notes
 	//
@@ -109,9 +108,9 @@ type File interface {
 	//
 	// # Errors
 	//
-	// A zero Errno is success. The below are expected otherwise:
-	//   - ENOSYS: the implementation does not support this function.
-	//   - EBADF: the file or directory was closed.
+	// A zero sys.Errno is success. The below are expected otherwise:
+	//   - sys.ENOSYS: the implementation does not support this function.
+	//   - sys.EBADF: the file or directory was closed.
 	//
 	// # Notes
 	//
@@ -124,9 +123,9 @@ type File interface {
 	//
 	// # Errors
 	//
-	// A zero Errno is success. The below are expected otherwise:
-	//   - ENOSYS: the implementation does not support this function.
-	//   - EBADF: the file or directory was closed.
+	// A zero sys.Errno is success. The below are expected otherwise:
+	//   - sys.ENOSYS: the implementation does not support this function.
+	//   - sys.EBADF: the file or directory was closed.
 	//
 	// # Notes
 	//
@@ -142,10 +141,10 @@ type File interface {
 	//
 	// # Errors
 	//
-	// A zero Errno is success. The below are expected otherwise:
-	//   - ENOSYS: the implementation does not support this function.
-	//   - EBADF: the file or directory was closed or not readable.
-	//   - EISDIR: the file was a directory.
+	// A zero sys.Errno is success. The below are expected otherwise:
+	//   - sys.ENOSYS: the implementation does not support this function.
+	//   - sys.EBADF: the file or directory was closed or not readable.
+	//   - sys.EISDIR: the file was a directory.
 	//
 	// # Notes
 	//
@@ -160,11 +159,11 @@ type File interface {
 	//
 	// # Errors
 	//
-	// A zero Errno is success. The below are expected otherwise:
-	//   - ENOSYS: the implementation does not support this function.
-	//   - EBADF: the file or directory was closed or not readable.
-	//   - EINVAL: the offset was negative.
-	//   - EISDIR: the file was a directory.
+	// A zero sys.Errno is success. The below are expected otherwise:
+	//   - sys.ENOSYS: the implementation does not support this function.
+	//   - sys.EBADF: the file or directory was closed or not readable.
+	//   - sys.EINVAL: the offset was negative.
+	//   - sys.EISDIR: the file was a directory.
 	//
 	// # Notes
 	//
@@ -195,10 +194,10 @@ type File interface {
 	//
 	// # Errors
 	//
-	// A zero Errno is success. The below are expected otherwise:
-	//   - ENOSYS: the implementation does not support this function.
-	//   - EBADF: the file or directory was closed or not readable.
-	//   - EINVAL: the offset was negative.
+	// A zero sys.Errno is success. The below are expected otherwise:
+	//   - sys.ENOSYS: the implementation does not support this function.
+	//   - sys.EBADF: the file or directory was closed or not readable.
+	//   - sys.EINVAL: the offset was negative.
 	//
 	// # Notes
 	//
@@ -210,21 +209,24 @@ type File interface {
 	//
 	// # Parameters
 	//
-	// The `timeout` parameter when nil blocks up to forever.
+	// The `timeoutMillis` parameter is how long to block for data to become
+	// readable, or interrupted, in milliseconds. There are two special values:
+	//   - zero returns immediately
+	//   - any negative value blocks any amount of time
 	//
 	// # Errors
 	//
-	// A zero Errno is success. The below are expected otherwise:
-	//   - ENOSYS: the implementation does not support this function.
+	// A zero sys.Errno is success. The below are expected otherwise:
+	//   - sys.ENOSYS: the implementation does not support this function.
+	//   - sys.EINTR: the call was interrupted prior to data being readable.
 	//
 	// # Notes
 	//
 	//   - This is like `poll` in POSIX, for a single file.
 	//     See https://pubs.opengroup.org/onlinepubs/9699919799/functions/poll.html
 	//   - No-op files, such as those which read from /dev/null, should return
-	//     immediately true to avoid hangs (because data will never become
-	//     available).
-	PollRead(timeout *time.Duration) (ready bool, errno experimentalsys.Errno)
+	//     immediately true, as data will never become readable.
+	PollRead(timeoutMillis int32) (ready bool, errno experimentalsys.Errno)
 
 	// Readdir reads the contents of the directory associated with file and
 	// returns a slice of up to n Dirent values in an arbitrary order. This is
@@ -235,10 +237,10 @@ type File interface {
 	//
 	// # Errors
 	//
-	// A zero Errno is success. The below are expected otherwise:
-	//   - ENOSYS: the implementation does not support this function.
-	//   - EBADF: the file was closed or not a directory.
-	//   - ENOENT: the directory could not be read (e.g. deleted).
+	// A zero sys.Errno is success. The below are expected otherwise:
+	//   - sys.ENOSYS: the implementation does not support this function.
+	//   - sys.EBADF: the file was closed or not a directory.
+	//   - sys.ENOENT: the directory could not be read (e.g. deleted).
 	//
 	// # Notes
 	//
@@ -255,9 +257,9 @@ type File interface {
 	//
 	// # Errors
 	//
-	// A zero Errno is success. The below are expected otherwise:
-	//   - ENOSYS: the implementation does not support this function.
-	//   - EBADF: the file was closed, not writeable, or a directory.
+	// A zero sys.Errno is success. The below are expected otherwise:
+	//   - sys.ENOSYS: the implementation does not support this function.
+	//   - sys.EBADF: the file was closed, not writeable, or a directory.
 	//
 	// # Notes
 	//
@@ -270,11 +272,11 @@ type File interface {
 	//
 	// # Errors
 	//
-	// A zero Errno is success. The below are expected otherwise:
-	//   - ENOSYS: the implementation does not support this function.
-	//   - EBADF: the file or directory was closed or not writeable.
-	//   - EINVAL: the offset was negative.
-	//   - EISDIR: the file was a directory.
+	// A zero sys.Errno is success. The below are expected otherwise:
+	//   - sys.ENOSYS: the implementation does not support this function.
+	//   - sys.EBADF: the file or directory was closed or not writeable.
+	//   - sys.EINVAL: the offset was negative.
+	//   - sys.EISDIR: the file was a directory.
 	//
 	// # Notes
 	//
@@ -286,11 +288,11 @@ type File interface {
 	//
 	// # Errors
 	//
-	// A zero Errno is success. The below are expected otherwise:
-	//   - ENOSYS: the implementation does not support this function.
-	//   - EBADF: the file or directory was closed.
-	//   - EINVAL: the `size` is negative.
-	//   - EISDIR: the file was a directory.
+	// A zero sys.Errno is success. The below are expected otherwise:
+	//   - sys.ENOSYS: the implementation does not support this function.
+	//   - sys.EBADF: the file or directory was closed.
+	//   - sys.EINVAL: the `size` is negative.
+	//   - sys.EISDIR: the file was a directory.
 	//
 	// # Notes
 	//
@@ -303,8 +305,8 @@ type File interface {
 	//
 	// # Errors
 	//
-	// A zero Errno is success. The below are expected otherwise:
-	//   - EBADF: the file or directory was closed.
+	// A zero sys.Errno is success. The below are expected otherwise:
+	//   - sys.EBADF: the file or directory was closed.
 	//
 	// # Notes
 	//
@@ -319,8 +321,8 @@ type File interface {
 	//
 	// # Errors
 	//
-	// A zero Errno is success. The below are expected otherwise:
-	//   - EBADF: the file or directory was closed.
+	// A zero sys.Errno is success. The below are expected otherwise:
+	//   - sys.EBADF: the file or directory was closed.
 	//
 	// # Notes
 	//
@@ -343,9 +345,9 @@ type File interface {
 	//
 	// # Errors
 	//
-	// A zero Errno is success. The below are expected otherwise:
-	//   - ENOSYS: the implementation does not support this function.
-	//   - EBADF: the file or directory was closed.
+	// A zero sys.Errno is success. The below are expected otherwise:
+	//   - sys.ENOSYS: the implementation does not support this function.
+	//   - sys.EBADF: the file or directory was closed.
 	//
 	// # Notes
 	//
@@ -357,7 +359,7 @@ type File interface {
 
 	// Close closes the underlying file.
 	//
-	// A zero Errno is returned if unimplemented or success.
+	// A zero sys.Errno is returned if unimplemented or success.
 	//
 	// # Notes
 	//
diff --git a/internal/fsapi/unimplemented.go b/internal/fsapi/unimplemented.go
index b31cbd13..883463dd 100644
--- a/internal/fsapi/unimplemented.go
+++ b/internal/fsapi/unimplemented.go
@@ -3,7 +3,6 @@ package fsapi
 import (
 	"io/fs"
 	"syscall"
-	"time"
 
 	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/sys"
@@ -155,7 +154,7 @@ func (UnimplementedFile) Readdir(int) (dirents []Dirent, errno experimentalsys.E
 }
 
 // PollRead implements File.PollRead
-func (UnimplementedFile) PollRead(*time.Duration) (ready bool, errno experimentalsys.Errno) {
+func (UnimplementedFile) PollRead(int32) (ready bool, errno experimentalsys.Errno) {
 	return false, experimentalsys.ENOSYS
 }
 
diff --git a/internal/sys/stdio.go b/internal/sys/stdio.go
index b153e32c..0d80e155 100644
--- a/internal/sys/stdio.go
+++ b/internal/sys/stdio.go
@@ -3,7 +3,6 @@ package sys
 import (
 	"io"
 	"os"
-	"time"
 
 	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/internal/fsapi"
@@ -50,7 +49,7 @@ func (noopStdinFile) Read([]byte) (int, experimentalsys.Errno) {
 }
 
 // PollRead implements the same method as documented on fsapi.File
-func (noopStdinFile) PollRead(*time.Duration) (ready bool, errno experimentalsys.Errno) {
+func (noopStdinFile) PollRead(int32) (ready bool, errno experimentalsys.Errno) {
 	return true, 0 // always ready to read nothing
 }
 
diff --git a/internal/sysfs/osfile.go b/internal/sysfs/osfile.go
index e39c9256..4fe10261 100644
--- a/internal/sysfs/osfile.go
+++ b/internal/sysfs/osfile.go
@@ -6,7 +6,6 @@ import (
 	"os"
 	"runtime"
 	"syscall"
-	"time"
 
 	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/internal/fsapi"
@@ -183,17 +182,22 @@ func (f *osFile) Seek(offset int64, whence int) (newOffset int64, errno experime
 }
 
 // PollRead implements the same method as documented on fsapi.File
-func (f *osFile) PollRead(timeout *time.Duration) (ready bool, errno experimentalsys.Errno) {
+func (f *osFile) PollRead(timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
 	fdSet := platform.FdSet{}
 	fd := int(f.fd)
 	fdSet.Set(fd)
 	nfds := fd + 1 // See https://man7.org/linux/man-pages/man2/select.2.html#:~:text=condition%20has%20occurred.-,nfds,-This%20argument%20should
-	count, err := _select(nfds, &fdSet, nil, nil, timeout)
+
+	// Coerce negative timeout to -1 as that's defined in POSIX
+	if timeoutMillis < 0 {
+		timeoutMillis = -1
+	}
+	ready, err := _select(nfds, &fdSet, nil, nil, timeoutMillis)
 	if errno = experimentalsys.UnwrapOSError(err); errno != 0 {
 		// Defer validation overhead until we've already had an error.
 		errno = fileError(f, f.closed, errno)
 	}
-	return count > 0, errno
+	return ready, errno
 }
 
 // Readdir implements File.Readdir. Notably, this uses "Readdir", not
diff --git a/internal/sysfs/select.go b/internal/sysfs/select.go
index ac0861fd..6494ec3d 100644
--- a/internal/sysfs/select.go
+++ b/internal/sysfs/select.go
@@ -1,27 +1,25 @@
 package sysfs
 
 import (
-	"time"
-
+	"github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/internal/platform"
 )
 
-// _select exposes the select(2) syscall. This is named as such to avoid
-// colliding with they keyword select while not exporting the function.
-//
-// # Notes on Parameters
-//
-//	For convenience, we expose a pointer to a time.Duration instead of a pointer to a syscall.Timeval.
-//	It must be a pointer because `nil` means "wait forever".
+// _select waits until one or more of the file descriptors become ready for
+// reading or writing.
 //
-//	However, notice that select(2) may mutate the pointed Timeval on some platforms,
-//	for instance if the call returns early.
+// # Parameters
 //
-//	This implementation *will not* update the pointed time.Duration value accordingly.
+// The `timeoutMillis` parameter is how long to block for an event, or
+// interrupted, in milliseconds. There are two special values:
+//   - zero returns immediately
+//   - any negative value blocks any amount of time
 //
-//	See also: https://github.com/golang/sys/blob/master/unix/syscall_unix_test.go#L606-L617
+// A zero sys.Errno is success. The below are expected otherwise:
+//   - sys.ENOSYS: the implementation does not support this function.
+//   - sys.EINTR: the call was interrupted prior to an event.
 //
-// # Notes on the Syscall
+// # Impact of blocking
 //
 //	Because this is a blocking syscall, it will also block the carrier thread of the goroutine,
 //	preventing any means to support context cancellation directly.
@@ -31,6 +29,14 @@ import (
 //	e.g. the read-end of a pipe or an eventfd on Linux.
 //	When the context is canceled, we may unblock a Select call by writing to the fd, causing it to return immediately.
 //	This however requires to do a bit of housekeeping to hide the "special" FD from the end-user.
-func _select(n int, r, w, e *platform.FdSet, timeout *time.Duration) (int, error) {
-	return syscall_select(n, r, w, e, timeout)
+//
+// # Notes
+//
+//   - This is like `select` in POSIX except it returns if any are ready
+//     instead of a specific file descriptor. See
+//     https://pubs.opengroup.org/onlinepubs/9699919799/functions/select.html
+//   - This is named _select to avoid collision on the select keyword, while
+//     not exporting the function.
+func _select(n int, r, w, e *platform.FdSet, timeoutNanos int32) (ready bool, errno sys.Errno) {
+	return syscall_select(n, r, w, e, timeoutNanos)
 }
diff --git a/internal/sysfs/select_darwin.go b/internal/sysfs/select_darwin.go
index eabf4f45..24e03742 100644
--- a/internal/sysfs/select_darwin.go
+++ b/internal/sysfs/select_darwin.go
@@ -2,22 +2,23 @@ package sysfs
 
 import (
 	"syscall"
-	"time"
 	"unsafe"
 
+	"github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/internal/platform"
 )
 
-// syscall_select invokes select on Darwin, with the given timeout Duration.
-// We implement our own version instead of relying on syscall.Select because the latter
-// only returns the error and discards the result.
-func syscall_select(n int, r, w, e *platform.FdSet, timeout *time.Duration) (int, error) {
+// syscall_select implements _select on Darwin
+//
+// Note: We implement our own version instead of relying on syscall.Select
+// because the latter only returns the error and discards the result.
+func syscall_select(n int, r, w, e *platform.FdSet, timeoutNanos int32) (ready bool, errno sys.Errno) {
 	var t *syscall.Timeval
-	if timeout != nil {
-		tv := syscall.NsecToTimeval(timeout.Nanoseconds())
+	if timeoutNanos >= 0 {
+		tv := syscall.NsecToTimeval(int64(timeoutNanos))
 		t = &tv
 	}
-	result, _, errno := syscall_syscall6(
+	r1, _, err := syscall_syscall6(
 		libc_select_trampoline_addr,
 		uintptr(n),
 		uintptr(unsafe.Pointer(r)),
@@ -25,11 +26,7 @@ func syscall_select(n int, r, w, e *platform.FdSet, timeout *time.Duration) (int
 		uintptr(unsafe.Pointer(e)),
 		uintptr(unsafe.Pointer(t)),
 		0)
-	res := int(result)
-	if errno == 0 {
-		return res, nil
-	}
-	return res, errno
+	return r1 > 0, sys.UnwrapOSError(err)
 }
 
 // libc_select_trampoline_addr is the address of the
diff --git a/internal/sysfs/select_linux.go b/internal/sysfs/select_linux.go
index aae5e48f..09707898 100644
--- a/internal/sysfs/select_linux.go
+++ b/internal/sysfs/select_linux.go
@@ -2,17 +2,18 @@ package sysfs
 
 import (
 	"syscall"
-	"time"
 
+	"github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/internal/platform"
 )
 
-// syscall_select invokes select on Unix (unless Darwin), with the given timeout Duration.
-func syscall_select(n int, r, w, e *platform.FdSet, timeout *time.Duration) (int, error) {
+// syscall_select implements _select on Linux
+func syscall_select(n int, r, w, e *platform.FdSet, timeoutNanos int32) (bool, sys.Errno) {
 	var t *syscall.Timeval
-	if timeout != nil {
-		tv := syscall.NsecToTimeval(timeout.Nanoseconds())
+	if timeoutNanos >= 0 {
+		tv := syscall.NsecToTimeval(int64(timeoutNanos))
 		t = &tv
 	}
-	return syscall.Select(n, (*syscall.FdSet)(r), (*syscall.FdSet)(w), (*syscall.FdSet)(e), t)
+	n, err := syscall.Select(n, (*syscall.FdSet)(r), (*syscall.FdSet)(w), (*syscall.FdSet)(e), t)
+	return n > 0, sys.UnwrapOSError(err)
 }
diff --git a/internal/sysfs/select_unsupported.go b/internal/sysfs/select_unsupported.go
index 400df900..9b8a320f 100644
--- a/internal/sysfs/select_unsupported.go
+++ b/internal/sysfs/select_unsupported.go
@@ -3,12 +3,10 @@
 package sysfs
 
 import (
-	"time"
-
 	"github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/internal/platform"
 )
 
-func syscall_select(n int, r, w, e *platform.FdSet, timeout *time.Duration) (int, error) {
-	return -1, sys.ENOSYS
+func syscall_select(n int, r, w, e *platform.FdSet, timeoutNanos int32) (ready bool, errno sys.Errno) {
+	return false, sys.ENOSYS
 }
diff --git a/internal/sysfs/select_windows.go b/internal/sysfs/select_windows.go
index b5c1a1bd..dde41ca7 100644
--- a/internal/sysfs/select_windows.go
+++ b/internal/sysfs/select_windows.go
@@ -16,7 +16,7 @@ const pollInterval = 100 * time.Millisecond
 // zeroDuration is the zero value for time.Duration. It is used in selectAllHandles.
 var zeroDuration = time.Duration(0)
 
-// syscall_select emulates the select syscall on Windows, for a subset of cases.
+// syscall_select implements _select on Windows, for a subset of cases.
 //
 // r, w, e may contain any number of file handles, but regular files and pipes are only processed for r (Read).
 // Stdin is a pipe, thus it is checked for readiness when present. Pipes are checked using PeekNamedPipe.
@@ -27,21 +27,26 @@ var zeroDuration = time.Duration(0)
 //
 // Note: ideas taken from https://stackoverflow.com/questions/6839508/test-if-stdin-has-input-for-c-windows-and-or-linux
 // PeekNamedPipe: https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe
-func syscall_select(n int, r, w, e *platform.FdSet, timeout *time.Duration) (int, error) {
+func syscall_select(n int, r, w, e *platform.FdSet, timeoutNanos int32) (ready bool, errno sys.Errno) {
+	var timeout *time.Duration
+	if timeoutNanos >= 0 {
+		duration := time.Duration(timeoutNanos)
+		timeout = &duration
+	}
+
+	// TODO: This impl was left alone because it will change soon.
+	// See https://github.com/tetratelabs/wazero/pull/1596
 	if n == 0 {
 		// Don't block indefinitely.
 		if timeout == nil {
-			return -1, sys.ENOSYS
+			return false, sys.ENOSYS
 		}
 		time.Sleep(*timeout)
-		return 0, nil
+		return false, 0
 	}
 
-	n, errno := selectAllHandles(context.TODO(), r, w, e, timeout)
-	if errno == 0 {
-		return n, nil
-	}
-	return n, errno
+	n, errno = selectAllHandles(context.TODO(), r, w, e, timeout)
+	return n > 0, errno
 }
 
 // selectAllHandles emulates a general-purpose POSIX select on Windows.


Test patch:
diff --git a/imports/wasi_snapshot_preview1/poll_test.go b/imports/wasi_snapshot_preview1/poll_test.go
index ff88e358..bb942f66 100644
--- a/imports/wasi_snapshot_preview1/poll_test.go
+++ b/imports/wasi_snapshot_preview1/poll_test.go
@@ -555,8 +555,12 @@ type neverReadyTtyStdinFile struct {
 }
 
 // PollRead implements the same method as documented on fsapi.File
-func (neverReadyTtyStdinFile) PollRead(timeout *time.Duration) (ready bool, errno experimentalsys.Errno) {
-	time.Sleep(*timeout)
+func (neverReadyTtyStdinFile) PollRead(timeoutMillis int32) (ready bool, errno experimentalsys.Errno) {
+	switch {
+	case timeoutMillis <= 0:
+		return
+	}
+	time.Sleep(time.Duration(timeoutMillis) * time.Millisecond)
 	return false, 0
 }
 
@@ -567,6 +571,6 @@ type pollStdinFile struct {
 }
 
 // PollRead implements the same method as documented on fsapi.File
-func (p *pollStdinFile) PollRead(*time.Duration) (ready bool, errno experimentalsys.Errno) {
+func (p *pollStdinFile) PollRead(int32) (ready bool, errno experimentalsys.Errno) {
 	return p.ready, 0
 }
diff --git a/internal/sysfs/file_test.go b/internal/sysfs/file_test.go
index 6023fa9b..a6a36642 100644
--- a/internal/sysfs/file_test.go
+++ b/internal/sysfs/file_test.go
@@ -9,7 +9,6 @@ import (
 	"runtime"
 	"testing"
 	gofstest "testing/fstest"
-	"time"
 
 	experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
 	"github.com/tetratelabs/wazero/internal/fsapi"
@@ -330,10 +329,10 @@ func TestFilePollRead(t *testing.T) {
 	rF, err := NewStdioFile(true, r)
 	require.NoError(t, err)
 	buf := make([]byte, 10)
-	timeout := time.Duration(0) // return immediately
+	timeout := int32(0) // return immediately
 
 	// When there's nothing in the pipe, it isn't ready.
-	ready, errno := rF.PollRead(&timeout)
+	ready, errno := rF.PollRead(timeout)
 	require.EqualErrno(t, 0, errno)
 	require.False(t, ready)
 
@@ -343,7 +342,7 @@ func TestFilePollRead(t *testing.T) {
 	require.NoError(t, err)
 
 	// We should now be able to poll ready
-	ready, errno = rF.PollRead(&timeout)
+	ready, errno = rF.PollRead(timeout)
 	require.EqualErrno(t, 0, errno)
 	require.True(t, ready)
 
diff --git a/internal/sysfs/select_test.go b/internal/sysfs/select_test.go
index 4cbf84cb..5294bb99 100644
--- a/internal/sysfs/select_test.go
+++ b/internal/sysfs/select_test.go
@@ -12,50 +12,50 @@ import (
 )
 
 func TestSelect(t *testing.T) {
-	t.Run("should return immediately with no fds and duration 0", func(t *testing.T) {
+	t.Run("should return immediately with no fds and timeoutNanos 0", func(t *testing.T) {
 		for {
-			dur := time.Duration(0)
-			n, err := _select(0, nil, nil, nil, &dur)
-			if err == sys.EINTR {
+			timeoutNanos := int32(0)
+			ready, errno := _select(0, nil, nil, nil, timeoutNanos)
+			if errno == sys.EINTR {
 				t.Logf("Select interrupted")
 				continue
 			}
-			require.NoError(t, err)
-			require.Equal(t, 0, n)
+			require.EqualErrno(t, 0, errno)
+			require.False(t, ready)
 			break
 		}
 	})
 
 	t.Run("should wait for the given duration", func(t *testing.T) {
-		dur := 250 * time.Millisecond
+		timeoutNanos := int32(250 * time.Millisecond)
 		var took time.Duration
 		for {
 			// On some platforms (e.g. Linux), the passed-in timeval is
 			// updated by select(2). We are not accounting for this
 			// in our implementation.
 			start := time.Now()
-			n, err := _select(0, nil, nil, nil, &dur)
+			ready, errno := _select(0, nil, nil, nil, timeoutNanos)
 			took = time.Since(start)
-			if err == sys.EINTR {
+			if errno == sys.EINTR {
 				t.Logf("Select interrupted after %v", took)
 				continue
 			}
-			require.NoError(t, err)
-			require.Equal(t, 0, n)
+			require.EqualErrno(t, 0, errno)
+			require.False(t, ready)
 			break
 		}
 
 		// On some platforms the actual timeout might be arbitrarily
 		// less than requested.
-		if took < dur {
+		if tookNanos := int32(took.Nanoseconds()); tookNanos < timeoutNanos {
 			if runtime.GOOS == "linux" {
 				// Linux promises to only return early if a file descriptor
 				// becomes ready (not applicable here), or the call
 				// is interrupted by a signal handler (explicitly retried in the loop above),
 				// or the timeout expires.
-				t.Errorf("Select: slept for %v, expected %v", took, dur)
+				t.Errorf("Select: slept for %v, expected %v", tookNanos, timeoutNanos)
 			} else {
-				t.Logf("Select: slept for %v, requested %v", took, dur)
+				t.Logf("Select: slept for %v, requested %v", tookNanos, timeoutNanos)
 			}
 		}
 	})
@@ -74,13 +74,13 @@ func TestSelect(t *testing.T) {
 		rFdSet.Set(fd)
 
 		for {
-			n, err := _select(fd+1, rFdSet, nil, nil, nil)
-			if err == sys.EINTR {
+			ready, errno := _select(fd+1, rFdSet, nil, nil, -1)
+			if errno == sys.EINTR {
 				t.Log("Select interrupted")
 				continue
 			}
-			require.NoError(t, err)
-			require.Equal(t, 1, n)
+			require.EqualErrno(t, 0, errno)
+			require.True(t, ready)
 			break
 		}
 	})
diff --git a/internal/sysfs/select_windows_test.go b/internal/sysfs/select_windows_test.go
index 1347fb00..30d7a56a 100644
--- a/internal/sysfs/select_windows_test.go
+++ b/internal/sysfs/select_windows_test.go
@@ -38,18 +38,18 @@ func TestSelect_Windows(t *testing.T) {
 		close(ch)
 	}
 
-	t.Run("syscall_select returns sys.ENOSYS when n == 0 and duration is nil", func(t *testing.T) {
-		n, errno := syscall_select(0, nil, nil, nil, nil)
-		require.Equal(t, -1, n)
+	t.Run("syscall_select returns sys.ENOSYS when n == 0 and timeoutNanos is negative", func(t *testing.T) {
+		ready, errno := syscall_select(0, nil, nil, nil, -1)
 		require.EqualErrno(t, sys.ENOSYS, errno)
+		require.False(t, ready)
 	})
 
-	t.Run("syscall_select propagates error when peekAllPipes returns an error", func(t *testing.T) {
+	t.Run("syscall_select propagates error when peekAllPipes returns an negative", func(t *testing.T) {
 		fdSet := platform.FdSet{}
 		fdSet.Pipes().Set(-1)
-		n, errno := syscall_select(0, &fdSet, nil, nil, nil)
-		require.Equal(t, -1, n)
+		ready, errno := syscall_select(0, &fdSet, nil, nil, -1)
 		require.EqualErrno(t, sys.ENOSYS, errno)
+		require.False(t, ready)
 	})
 
 	t.Run("peekNamedPipe should report the correct state of incoming data in the pipe", func(t *testing.T) {