Skip to content

Commit fc3bc46

Browse files
authored
wasip3: Fix fallback code when buffering writes (#788)
This commit fixes mistakes from #782 where the fallback code for buffering writes wasn't handled properly. Specifically when a completion event came in through `poll` it didn't fully update the internal state of the stream, such as kicking off further writes or cleaning up the internal buffer. This is fixed by refactoring the logic already present during a normal `write` to get executed in this event. Testing this is unfortunately not trivial. There aren't really all that many streams we have access to natively in Wasmtime, and TCP is "well behaved" and doesn't execute any of these fallbacks anyway. The closest way to test this is filesystem-based streams, but Wasmtime's default behavior is to lie and just do blocking operations on these streams regardless. Through changes in Wasmtime, and flags to Wasmtime, however, it's possible to Wasmtime in a mode where filesystem streams are "ill behaved" where zero-length reads don't signal readiness, triggering these fallback paths. With those changes the added test here, and minor additionally support for nonblocking reads/writes, exercise these paths and triggers the previous bug.
1 parent 6629886 commit fc3bc46

4 files changed

Lines changed: 127 additions & 38 deletions

File tree

libc-bottom-half/sources/file.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ static int file_get_read_stream(void *data, wasi_read_t *read) {
123123
#endif
124124
read->offset = &file->offset;
125125
read->timeout = 0;
126-
read->blocking = true;
126+
read->blocking = (file->oflag & O_NONBLOCK) == 0;
127127
return 0;
128128
}
129129

@@ -193,7 +193,7 @@ static int file_get_write_stream(void *data, wasi_write_t *write) {
193193
#endif
194194
write->offset = &file->offset;
195195
write->timeout = 0;
196-
write->blocking = true;
196+
write->blocking = (file->oflag & O_NONBLOCK) == 0;
197197
return 0;
198198
}
199199

libc-bottom-half/sources/file_utils.c

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,45 @@ static size_t wasip3_io_update_event(wasip3_io_state_t *state,
121121
return wasip3_io_update_code(state, event->code);
122122
}
123123

124+
/// When `event` has happened due to a completion of a pending write, this
125+
/// function will advance `state` forward.
126+
///
127+
/// This attempts to perform any follow-up writes as necessary if the pending
128+
/// write ended up coming in short. Additionally this will clear out the
129+
/// internal buffered data once it reaches
130+
/// completion.
131+
///
132+
/// Returns `true` if this stream is ready for more writes, or `false` if
133+
/// there's still a pending write in-flight.
134+
static bool wasip3_advance_pending_write(wasip3_io_state_t *state,
135+
wasip3_event_t *event) {
136+
// Update the I/O internal state given the result of the write.
137+
state->buf_start += wasip3_io_update_event(state, event);
138+
139+
// While there's remaining writes to perform, kick those off here. Once a
140+
// write blocks we bail out of this loop as there's I/O in-progress.
141+
while (!(state->flags & WASIP3_IO_DONE) &&
142+
state->buf_start != state->buf_end) {
143+
wasip3_waitable_status_t status =
144+
filesystem_stream_u8_write(state->stream, state->buf + state->buf_start,
145+
state->buf_end - state->buf_start);
146+
state->flags |= WASIP3_IO_INPROGRESS;
147+
if (status == WASIP3_WAITABLE_STATUS_BLOCKED)
148+
return false;
149+
state->buf_start += wasip3_io_update_code(state, status);
150+
}
151+
152+
// Everything should be done now at this point, meaning that the stream is
153+
// closed or we've written the entire buffer. Clean up internal state and
154+
// return to indicate there's no more pending I/O.
155+
assert((state->flags & WASIP3_IO_DONE) || state->buf_start == state->buf_end);
156+
free(state->buf);
157+
state->buf = NULL;
158+
state->buf_start = 0;
159+
state->buf_end = 0;
160+
return true;
161+
}
162+
124163
/// Attempts to resolve any pending write that may be in-progress on `write`.
125164
///
126165
/// This may notably end up issuing more writes to finish a buffered write that
@@ -154,40 +193,19 @@ static int wasip3_write_resolve_pending(wasi_write_t *write) {
154193
}
155194
}
156195

157-
// Update the I/O internal state given the result of the write.
158-
state->buf_start += wasip3_io_update_event(state, &event);
159-
160-
// While there's remaining writes to perform, kick those off here. If a
161-
// write is blocked then nonblocking mode returns as such and blocking
162-
// mode breaks out to turn this outer `while (1)` loop again to block
163-
// on the result. If the write finishes immediately then state is updated
164-
// again and then further continues.
165-
while (!(state->flags & WASIP3_IO_DONE) &&
166-
state->buf_start != state->buf_end) {
167-
wasip3_waitable_status_t status = filesystem_stream_u8_write(
168-
state->stream, state->buf + state->buf_start,
169-
state->buf_end - state->buf_start);
170-
state->flags |= WASIP3_IO_INPROGRESS;
171-
if (status == WASIP3_WAITABLE_STATUS_BLOCKED) {
172-
if (write->blocking) {
173-
break;
174-
} else {
175-
errno = EWOULDBLOCK;
176-
return -1;
177-
}
178-
}
179-
state->buf_start += wasip3_io_update_code(state, status);
180-
}
181-
182-
// We either broke out of the `while` loop normally, or we hit the `break`
183-
// in the loop which wants to continue to the top of this outer loop. Test
184-
// which it is here and act accordingly.
185-
if ((state->flags & WASIP3_IO_DONE) || state->buf_start == state->buf_end) {
186-
free(state->buf);
187-
state->buf = NULL;
188-
state->buf_start = 0;
189-
state->buf_end = 0;
196+
// Update the internal status of this stream with the `event` we have now
197+
// learned. If the stream is complete at this point then go ahead and
198+
// return.
199+
if (wasip3_advance_pending_write(state, &event))
190200
return 0;
201+
202+
// If the write isn't blocking then a pending I/O op is kicked off from
203+
// above and there's no point in turning the loop and re-polling. Bail out
204+
// here with EWOULDBLOCK.
205+
if (!write->blocking) {
206+
assert(state->flags & WASIP3_IO_INPROGRESS);
207+
errno = EWOULDBLOCK;
208+
return -1;
191209
}
192210
}
193211

@@ -684,8 +702,15 @@ static void wasip3_poll_read_ready(void *data, poll_state_t *state,
684702
static void wasip3_poll_write_ready(void *data, poll_state_t *state,
685703
wasip3_event_t *event) {
686704
wasip3_io_state_t *iostate = (wasip3_io_state_t *)data;
687-
iostate->buf_start += wasip3_io_update_event(iostate, event);
688-
__wasilibc_poll_ready(state, POLLWRNORM);
705+
706+
// Update our state with this event, and if the pending write is fully
707+
// complete then this is now ready for writing again. Otherwise there's still
708+
// a pending write so our job is complete trying to advance things a bit.
709+
if (wasip3_advance_pending_write(iostate, event)) {
710+
__wasilibc_poll_ready(state, POLLWRNORM);
711+
} else {
712+
assert(iostate->flags & WASIP3_IO_INPROGRESS);
713+
}
689714
}
690715

691716
static int wasip3_stream_poll(wasip3_io_state_t *iostate, poll_state_t *state,

test/CMakeLists.txt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ endfunction()
133133
function(register_test test_name executable_name)
134134
set(options FS NETWORK SETJMP)
135135
set(oneValueArgs CLIENT PASS_REGULAR_EXPRESSION)
136-
set(multiValueArgs ARGV ENV)
136+
set(multiValueArgs ARGV ENV ENGINE_ARG)
137137
cmake_parse_arguments(PARSE_ARGV 1 arg "${options}" "${oneValueArgs}" "${multiValueArgs}")
138138

139139
set(wasmtime_args)
@@ -148,6 +148,9 @@ function(register_test test_name executable_name)
148148
foreach(env IN LISTS arg_ENV)
149149
list(APPEND wasmtime_args --env ${env})
150150
endforeach()
151+
foreach(arg IN LISTS arg_ENGINE_ARG)
152+
list(APPEND wasmtime_args ${arg})
153+
endforeach()
151154
if (TARGET_TRIPLE MATCHES "-threads")
152155
list(APPEND wasmtime_args --wasi threads --wasm shared-memory)
153156
endif()
@@ -327,6 +330,11 @@ add_wasilibc_test(fsync.c FS)
327330
add_wasilibc_test(ftruncate.c FS)
328331
add_wasilibc_test(fts.c FS)
329332
add_wasilibc_test(fwscanf.c FS)
333+
# Note that `-Wtimeout` here is passed to force Wasmtime to avoid blocking the
334+
# main thread when doing file I/O. This is used to exercise this test's
335+
# behavior where file streams use fallback code for nonblocking reads/writes.
336+
add_wasilibc_test(fs-nonblocking.c FS ENGINE_ARG -Wtimeout=10s)
337+
set_tests_properties(fs-nonblocking.wasm PROPERTIES LABELS v8fail)
330338
add_wasilibc_test(getentropy.c)
331339
add_wasilibc_test(hello.c PASS_REGULAR_EXPRESSION "Hello, World!")
332340
add_wasilibc_test(ioctl.c FS)

test/src/fs-nonblocking.c

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#include "test.h"
2+
#include <dirent.h>
3+
#include <errno.h>
4+
#include <fcntl.h>
5+
#include <poll.h>
6+
#include <stdbool.h>
7+
#include <stdio.h>
8+
#include <stdlib.h>
9+
#include <string.h>
10+
#include <sys/stat.h>
11+
#include <unistd.h>
12+
13+
#define TEST(c) \
14+
do { \
15+
errno = 0; \
16+
if (!(c)) \
17+
t_error("%s failed (errno = %d)\n", #c, errno); \
18+
} while (0)
19+
20+
int main() {
21+
int fd;
22+
TEST((fd = open("howdy.txt", O_RDWR | O_NONBLOCK | O_CREAT, 0755)) > 0);
23+
24+
int rc, size = 0;
25+
26+
struct pollfd pollfd;
27+
int max = 32 * 1024;
28+
pollfd.fd = fd;
29+
30+
pollfd.events = POLLWRNORM;
31+
for (int i = 0; size < max && i < 100; i++) {
32+
while (size < max && (rc = write(fd, "hello", 5)) > 0)
33+
size += rc;
34+
TEST(poll(&pollfd, 1, -1) != -1);
35+
}
36+
37+
TEST(size > 0);
38+
39+
struct stat stat;
40+
TEST(fstat(fd, &stat) == 0);
41+
TEST(stat.st_size == size);
42+
43+
TEST(lseek(fd, 0, SEEK_SET) != -1);
44+
45+
char buf[5];
46+
pollfd.events = POLLRDNORM;
47+
int remaining = size;
48+
while (remaining) {
49+
while ((rc = read(fd, buf, sizeof(buf))) > 0)
50+
remaining -= rc;
51+
TEST(poll(&pollfd, 1, -1) != -1);
52+
}
53+
54+
TEST(close(fd) != -1);
55+
return t_status;
56+
}

0 commit comments

Comments
 (0)