Skip to content

return ERRNO_AGAIN from fd_{read,write} when appropriate#13111

Merged
alexcrichton merged 4 commits intobytecodealliance:mainfrom
dicej:p1-adapter-eagain
Apr 16, 2026
Merged

return ERRNO_AGAIN from fd_{read,write} when appropriate#13111
alexcrichton merged 4 commits intobytecodealliance:mainfrom
dicej:p1-adapter-eagain

Conversation

@dicej
Copy link
Copy Markdown
Contributor

@dicej dicej commented Apr 15, 2026

The WASIp1 fd_read and fd_write functions should return Err(ERRNO_AGAIN) rather Ok(0) if a non-blocking operation fails to transfer any bytes. The docs aren't particularly clear about this other than saying these functions are "similar to [readv,writev] in POSIX", but if any functions ought to ever return ERRNO_AGAIN, it's these.

I'm guessing this hasn't been noticed (or at least not reported) until now because most guests use blocking I/O for files, but Go uses non-blocking I/O to support concurrent I/O across multiple goroutines, and it only knows to call poll_oneoff (rather than e.g. assume EOF) if it gets an EAGAIN.

@dicej dicej requested a review from alexcrichton April 15, 2026 20:35
@dicej dicej requested a review from a team as a code owner April 15, 2026 20:35
@alexcrichton
Copy link
Copy Markdown
Member

Could you detail a bit more about how this surfaced and came here? I'm effectively not certain this is the best way to fix the underlying issue. The interaction with wasmtime-wasi, files, and nonblocking I/O is ... weird. IIRC wasmtime-wasi just ignores requests for nonblocking I/O on files and always does blocking I/O, and for example that might be the best fix here for the adapter. I think your conclusion here is correct that the adapter isn't doing the right thing with nonblocking I/O, but my thinking is that it's probably wrong for it to try at all.

One example is that nonblocking writes look pretty buggy on the surface:

  • Closed streams return 0, which here with this PR would turn into EAGAIN
  • Nonblocking writes do a blocking flush

And that may not be the full extent of possible issues here...

@dicej
Copy link
Copy Markdown
Contributor Author

dicej commented Apr 15, 2026

The symptom that prompted this PR is that a Go WASIp1 module which attempts to read a file immediately gets what it thinks is an EOF instead of the content when wrapped in a WASIp2 (or WASIp3+WASIp2) component using the p1 adapter. When I investigated, I found that Go explicitly sets all file descriptors to non-blocking using fd_fdstat_set_flags when opening them and expects errno == EAGAIN rather than errno == 0 to indicate non-blocking backpressure.

So we either need to ignore the FDFLAGS_NONBLOCK flag in the adapter's fd_fdstat_set_flags (i.e. force blocking always) or return ERRNO_AGAIN from fd_{read,write} when appropriate. I don't care which, personally.

@dicej
Copy link
Copy Markdown
Contributor Author

dicej commented Apr 15, 2026

Closed streams return 0, which here with this PR would turn into EAGAIN

Aren't they supposed to return StreamErr::Closed?

@dicej
Copy link
Copy Markdown
Contributor Author

dicej commented Apr 15, 2026

Closed streams return 0, which here with this PR would turn into EAGAIN

Aren't they supposed to return StreamErr::Closed?

Nevermind, I see what you're pointing to now. Yes, I'll need to fix that, but it's fixable.

The WASIp1 `fd_read` and `fd_write` functions should return `Err(ERRNO_AGAIN)`
rather Ok(0) if a non-blocking operation fails to transfer any bytes.  [The
docs](https://github.com/WebAssembly/WASI/blob/wasi-0.1/preview1/docs.md) aren't
particularly clear about this other than saying these functions are "similar to
[readv,writev] in POSIX", but if any functions ought to ever return
`ERRNO_AGAIN`, it's these.

I'm guessing this hasn't been noticed (or at least not reported) until now
because most guests use blocking I/O for files, but Go uses non-blocking I/O to
support concurrent I/O across multiple goroutines, and it only knows to call
`poll_oneoff` (rather than e.g. assume EOF) if it gets an `EAGAIN`.
@dicej dicej force-pushed the p1-adapter-eagain branch from 540f3e3 to 10a9b0b Compare April 15, 2026 21:40
Copy link
Copy Markdown
Member

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose in the sake of being conservative it's probably best to apply a fix like this (returning the right codes) instead of behaviorally changing by ignoring nonblocking flags. Another risk to consider here though is that the adapter may have different behavior than the native implementation of WASIp1, which is something we've historically avoided. Could you add a test which exercises this at a higher level? That test should automatically be run by the various implementations with-and-without the adapter which can help validate the behavior in each situation.

If we end up going this route (e.g. a test behaves well across implementations), could you update the read path to look more like the write path though? For example could the BlockingMode::read function return a result-with-wasi-errno instead of the StreamError it does today? That would move the handling of the 0 return value there to look simliar to the write path.

@dicej dicej requested a review from a team as a code owner April 16, 2026 19:00
@dicej dicej requested review from cfallin and removed request for a team April 16, 2026 19:00
@alexcrichton alexcrichton added this pull request to the merge queue Apr 16, 2026
Merged via the queue into bytecodealliance:main with commit d60f178 Apr 16, 2026
48 checks passed
dicej added a commit to dicej/componentize-go that referenced this pull request Apr 16, 2026
Reading from files via e.g. `os.ReadFile` was broken due to a bug in the
`wasi_snapshot_preview1` adapter _and_ another bug in my patched version of Go.
The following PR and commit fix those issues:

- bytecodealliance/wasmtime#13111
- dicej/go@1f14282

I've updated the adapter using a build from
bytecodealliance/wasmtime@d60f178
and pointed the Go URL to the new release URL.  I've also updated the download
directory to force a new download for users which may already have the old
version.

Finally, I've updated the Go wrapper to point to the `v0.3.2` release (which
doesn't exist yet, but will soon).  Previously, we were pointing to the `canary`
release on the `main` branch, but it's annoying to switch back and forth when
tagging releases, so let's just leave it set to whatever the latest release is
for now.
dicej added a commit to bytecodealliance/componentize-go that referenced this pull request Apr 16, 2026
Reading from files via e.g. `os.ReadFile` was broken due to a bug in the
`wasi_snapshot_preview1` adapter _and_ another bug in my patched version of Go.
The following PR and commit fix those issues:

- bytecodealliance/wasmtime#13111
- dicej/go@1f14282

I've updated the adapter using a build from
bytecodealliance/wasmtime@d60f178
and pointed the Go URL to the new release URL.  I've also updated the download
directory to force a new download for users which may already have the old
version.

Finally, I've updated the Go wrapper to point to the `v0.3.2` release (which
doesn't exist yet, but will soon).  Previously, we were pointing to the `canary`
release on the `main` branch, but it's annoying to switch back and forth when
tagging releases, so let's just leave it set to whatever the latest release is
for now.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants