Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions libc-bottom-half/sources/file.c
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ static int file_get_read_stream(void *data, wasi_read_t *read) {
#endif
read->offset = &file->offset;
read->timeout = 0;
read->blocking = true;
read->blocking = (file->oflag & O_NONBLOCK) == 0;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this mean read/write can start returning EWOULDBLOCK for regular files?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It does, yeah. I'm not sure how useful that is in practice, but it's the only way I could think of to test this PR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I suppose if an application requested O_NONBLOCK, then getting EWOULDBLOCK is exactly what it asked for. Nonetheless, I wonder how many programs are actually prepared to handle that for regular files, given ~40 years of assumptions that the flag is ignored for those.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Er sorry meant to think about this a bit more before landing. Do you feel that this should get backed out? I don't know how to test anything related to "ill behaving streams" otherwise since files are basically the only host-defined stream that doesn't handle zero-length reads/writes like TCP does.

Copy link
Copy Markdown
Member

@badeend badeend Apr 22, 2026

Choose a reason for hiding this comment

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

The Linux man page mentions:

Note that this flag has no effect for regular files and block devices; that is, I/O operations will (briefly) block when device activity is required, regardless of whether O_NONBLOCK is set.

And also:

Since O_NONBLOCK semantics might eventually be implemented, applications should not depend upon blocking behavior when specifying this flag for regular files and block devices.

So yeah,. there's that.. :)


I feel this leaks testing concerns into user-visible behavior so I'm leaning toward keeping the fix but backing out the nonblocking behavior for regular files. I don't have a super strong well-funded opinion for it though.

That would indeed leave some of the new code untested. As long as the test suite uses off-the-shelf, unmodified Wasm engines, we're unlikely to hit all code paths anyway. A longer-term solution might be some kind of host-side "chaos mode" that deterministically generates all kinds of odd-but-technically-correct stream behaviors. That could also be more generally useful for other languages and toolchains targeting the component model directly. I realize that is completely out of scope for this PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Hm yeah ok I agree and it seems prudent. I've posted #796 to disable this, and for further testing -- in addition to a theoretical chaos mode this'd be a good use of #766 I believe.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ah right, this could indeed be a nice use case for component composition

return 0;
}

Expand Down Expand Up @@ -193,7 +193,7 @@ static int file_get_write_stream(void *data, wasi_write_t *write) {
#endif
write->offset = &file->offset;
write->timeout = 0;
write->blocking = true;
write->blocking = (file->oflag & O_NONBLOCK) == 0;
return 0;
}

Expand Down
98 changes: 63 additions & 35 deletions libc-bottom-half/sources/file_utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,48 @@ static size_t wasip3_io_update_event(wasip3_io_state_t *state,
return wasip3_io_update_code(state, event->code);
}

/// When `event` has happened due to a completion of a pending write, this
/// function will advance `state` forward.
///
/// This attempts to perform any follow-up writes as necessary if the pending
/// write ended up coming in short. Additionally this will clear out the
/// internal buffered data once it reaches
/// completion.
///
/// Returns `true` if this stream is ready for more writes, or `false` if
/// there's still a pending write in-flight.
static bool wasip3_advance_pending_write(wasip3_io_state_t *state,
wasip3_event_t *event) {
// Update the I/O internal state given the result of the write.
state->buf_start += wasip3_io_update_event(state, event);

// While there's remaining writes to perform, kick those off here. If a
// write is blocked then nonblocking mode returns as such and blocking
// mode breaks out to turn this outer `while (1)` loop again to block
// on the result. If the write finishes immediately then state is updated
// again and then further continues.
while (!(state->flags & WASIP3_IO_DONE) &&
state->buf_start != state->buf_end) {
wasip3_waitable_status_t status =
filesystem_stream_u8_write(state->stream, state->buf + state->buf_start,
state->buf_end - state->buf_start);
state->flags |= WASIP3_IO_INPROGRESS;
if (status == WASIP3_WAITABLE_STATUS_BLOCKED)
return false;
state->buf_start += wasip3_io_update_code(state, status);
}

// We either broke out of the `while` loop normally, or we hit the `break`
// in the loop which wants to continue to the top of this outer loop. Test
Comment thread
alexcrichton marked this conversation as resolved.
Outdated
// which it is here and act accordingly.
assert((state->flags & WASIP3_IO_DONE) || state->buf_start == state->buf_end);
free(state->buf);
state->buf = NULL;
state->buf_start = 0;
state->buf_end = 0;
return true;
}

/// Attempts to resolve any pending write that may be in-progress on `write`.
///
/// This may notably end up issuing more writes to finish a buffered write that
Expand Down Expand Up @@ -154,40 +196,19 @@ static int wasip3_write_resolve_pending(wasi_write_t *write) {
}
}

// Update the I/O internal state given the result of the write.
state->buf_start += wasip3_io_update_event(state, &event);

// While there's remaining writes to perform, kick those off here. If a
// write is blocked then nonblocking mode returns as such and blocking
// mode breaks out to turn this outer `while (1)` loop again to block
// on the result. If the write finishes immediately then state is updated
// again and then further continues.
while (!(state->flags & WASIP3_IO_DONE) &&
state->buf_start != state->buf_end) {
wasip3_waitable_status_t status = filesystem_stream_u8_write(
state->stream, state->buf + state->buf_start,
state->buf_end - state->buf_start);
state->flags |= WASIP3_IO_INPROGRESS;
if (status == WASIP3_WAITABLE_STATUS_BLOCKED) {
if (write->blocking) {
break;
} else {
errno = EWOULDBLOCK;
return -1;
}
}
state->buf_start += wasip3_io_update_code(state, status);
}

// We either broke out of the `while` loop normally, or we hit the `break`
// in the loop which wants to continue to the top of this outer loop. Test
// which it is here and act accordingly.
if ((state->flags & WASIP3_IO_DONE) || state->buf_start == state->buf_end) {
free(state->buf);
state->buf = NULL;
state->buf_start = 0;
state->buf_end = 0;
// Update the internal status of this stream with the `event` we have now
// learned. If the stream is complete at this point then go ahead and
// return.
if (wasip3_advance_pending_write(state, &event))
return 0;

// If the write isn't blocking then a pending I/O op is kicked off from
// above and there's no point in turning the loop and re-polling. Bail out
// here with EWOULDBLOCK.
if (!write->blocking) {
assert(state->flags & WASIP3_IO_INPROGRESS);
errno = EWOULDBLOCK;
return -1;
}
}

Expand Down Expand Up @@ -663,8 +684,15 @@ static void wasip3_poll_read_ready(void *data, poll_state_t *state,
static void wasip3_poll_write_ready(void *data, poll_state_t *state,
wasip3_event_t *event) {
wasip3_io_state_t *iostate = (wasip3_io_state_t *)data;
iostate->buf_start += wasip3_io_update_event(iostate, event);
__wasilibc_poll_ready(state, POLLWRNORM);

// Update our state with this event, and if the pending write is fully
// complete then this is now ready for writing again. Otherwise there's still
// a pending write so our job is complete trying to advance things a bit.
if (wasip3_advance_pending_write(iostate, event)) {
__wasilibc_poll_ready(state, POLLWRNORM);
} else {
assert(iostate->flags & WASIP3_IO_INPROGRESS);
}
}

static int wasip3_stream_poll(wasip3_io_state_t *iostate, poll_state_t *state,
Expand Down
10 changes: 9 additions & 1 deletion test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ endfunction()
function(register_test test_name executable_name)
set(options FS NETWORK SETJMP)
set(oneValueArgs CLIENT PASS_REGULAR_EXPRESSION)
set(multiValueArgs ARGV ENV)
set(multiValueArgs ARGV ENV ENGINE_ARG)
cmake_parse_arguments(PARSE_ARGV 1 arg "${options}" "${oneValueArgs}" "${multiValueArgs}")

set(wasmtime_args)
Expand All @@ -148,6 +148,9 @@ function(register_test test_name executable_name)
foreach(env IN LISTS arg_ENV)
list(APPEND wasmtime_args --env ${env})
endforeach()
foreach(arg IN LISTS arg_ENGINE_ARG)
list(APPEND wasmtime_args ${arg})
endforeach()
if (TARGET_TRIPLE MATCHES "-threads")
list(APPEND wasmtime_args --wasi threads --wasm shared-memory)
endif()
Expand Down Expand Up @@ -327,6 +330,11 @@ add_wasilibc_test(fsync.c FS)
add_wasilibc_test(ftruncate.c FS)
add_wasilibc_test(fts.c FS)
add_wasilibc_test(fwscanf.c FS)
# Note that `-Wtimeout` here is passed to force Wasmtime to avoid blocking the
# main thread when doing file I/O. This is used to exercise this test's
# behavior where file streams use fallback code for nonblocking reads/writes.
add_wasilibc_test(fs-nonblocking.c FS ENGINE_ARG -Wtimeout=10s)
set_tests_properties(fs-nonblocking.wasm PROPERTIES LABELS v8fail)
add_wasilibc_test(getentropy.c)
add_wasilibc_test(hello.c PASS_REGULAR_EXPRESSION "Hello, World!")
add_wasilibc_test(ioctl.c FS)
Expand Down
56 changes: 56 additions & 0 deletions test/src/fs-nonblocking.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#include "test.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#define TEST(c) \
do { \
errno = 0; \
if (!(c)) \
t_error("%s failed (errno = %d)\n", #c, errno); \
} while (0)

int main() {
int fd;
TEST((fd = open("howdy.txt", O_RDWR | O_NONBLOCK | O_CREAT, 0755)) > 0);

int rc, size = 0;

struct pollfd pollfd;
int max = 32 * 1024;
pollfd.fd = fd;

pollfd.events = POLLWRNORM;
for (int i = 0; size < max && i < 100; i++) {
while (size < max && (rc = write(fd, "hello", 5)) > 0)
size += rc;
TEST(poll(&pollfd, 1, -1) != -1);
}

TEST(size > 0);

struct stat stat;
TEST(fstat(fd, &stat) == 0);
TEST(stat.st_size == size);

TEST(lseek(fd, 0, SEEK_SET) != -1);

char buf[5];
pollfd.events = POLLRDNORM;
int remaining = size;
while (remaining) {
while ((rc = read(fd, buf, sizeof(buf))) > 0)
remaining -= rc;
TEST(poll(&pollfd, 1, -1) != -1);
}

TEST(close(fd) != -1);
return t_status;
}