From 130f59563d00bae29feb98360c06b5bbd980722c Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Wed, 8 Jul 2026 14:44:34 -0700 Subject: [PATCH 1/3] Fix doReadv/doWritev discarding partial count on EAGAIN On a non-blocking stream a later iovec can would-block after earlier iovecs already transferred data. FS.read/FS.write throw EAGAIN, which escaped doReadv/doWritev and failed the whole readv/writev even though bytes were transferred. POSIX gather I/O returns the transferred count and never fails after partial success. Catch EAGAIN/EWOULDBLOCK from a subsequent iovec and return the accumulated count when ret > 0. --- src/lib/libwasi.js | 28 +++++++++++++- test/fs/test_readv_eagain.c | 75 +++++++++++++++++++++++++++++++++++++ test/test_core.py | 3 ++ 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 test/fs/test_readv_eagain.c diff --git a/src/lib/libwasi.js b/src/lib/libwasi.js index c058362e28157..d9e204632b39e 100644 --- a/src/lib/libwasi.js +++ b/src/lib/libwasi.js @@ -202,7 +202,19 @@ var WasiLibrary = { var ptr = {{{ makeGetValue('iov', C_STRUCTS.iovec.iov_base, '*') }}}; var len = {{{ makeGetValue('iov', C_STRUCTS.iovec.iov_len, '*') }}}; iov += {{{ C_STRUCTS.iovec.__size__ }}}; - var curr = FS.read(stream, HEAP8, ptr, len, offset); + var curr; + try { + curr = FS.read(stream, HEAP8, ptr, len, offset); + } catch (e) { + // On a non-blocking stream a subsequent read may would-block after we + // already gathered data. POSIX readv is a single gather-read: return + // what we have rather than failing the whole call. + if (ret > 0 && e instanceof FS.ErrnoError && + (e.errno == {{{ cDefs.EAGAIN }}} || e.errno == {{{ cDefs.EWOULDBLOCK }}})) { + break; + } + throw e; + } if (curr < 0) return -1; ret += curr; if (curr < len) break; // nothing more to read @@ -219,7 +231,19 @@ var WasiLibrary = { var ptr = {{{ makeGetValue('iov', C_STRUCTS.iovec.iov_base, '*') }}}; var len = {{{ makeGetValue('iov', C_STRUCTS.iovec.iov_len, '*') }}}; iov += {{{ C_STRUCTS.iovec.__size__ }}}; - var curr = FS.write(stream, HEAP8, ptr, len, offset); + var curr; + try { + curr = FS.write(stream, HEAP8, ptr, len, offset); + } catch (e) { + // On a non-blocking stream a subsequent write may would-block after we + // already sent data. POSIX writev is a single gather-write: return + // what we have rather than failing the whole call. + if (ret > 0 && e instanceof FS.ErrnoError && + (e.errno == {{{ cDefs.EAGAIN }}} || e.errno == {{{ cDefs.EWOULDBLOCK }}})) { + break; + } + throw e; + } if (curr < 0) return -1; ret += curr; if (curr < len) { diff --git a/test/fs/test_readv_eagain.c b/test/fs/test_readv_eagain.c new file mode 100644 index 0000000000000..75b1c7e38ec34 --- /dev/null +++ b/test/fs/test_readv_eagain.c @@ -0,0 +1,75 @@ +/* + * Copyright 2024 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + */ + +// Regression test: a partial gather-read/write on a non-blocking stream must +// return the bytes already transferred rather than propagating EAGAIN from a +// later iovec. + +#include +#include +#include +#include +#include +#include +#include +#include + +int main() { + EM_ASM({ + var eagain = $0; + + // Device that satisfies the first read fully, then would-blocks. + var readCalls = 0; + var rdev = FS.makedev(64, 0); + FS.registerDevice(rdev, { + read: (stream, buffer, offset, length, pos) => { + if (++readCalls == 1) { + for (var i = 0; i < length; i++) buffer[offset + i] = 65 + i; + return length; + } + throw new FS.ErrnoError(eagain); + } + }); + FS.mkdev('/rdev', rdev); + + // Device that accepts the first write fully, then would-blocks. + var writeCalls = 0; + var wdev = FS.makedev(64, 1); + FS.registerDevice(wdev, { + write: (stream, buffer, offset, length, pos) => { + if (++writeCalls == 1) return length; + throw new FS.ErrnoError(eagain); + } + }); + FS.mkdev('/wdev', wdev); + }, EAGAIN); + + int rfd = open("/rdev", O_RDONLY); + assert(rfd >= 0); + char buf0[4] = {0}; + char buf1[4] = {0}; + struct iovec riov[] = {{.iov_base = buf0, .iov_len = 4}, + {.iov_base = buf1, .iov_len = 4}}; + ssize_t nread = readv(rfd, riov, 2); + // First iovec filled; second would-block -> partial success, not EAGAIN. + assert(nread == 4); + assert(memcmp(buf0, "ABCD", 4) == 0); + close(rfd); + + int wfd = open("/wdev", O_WRONLY); + assert(wfd >= 0); + char out0[4] = "ABCD"; + char out1[4] = "EFGH"; + struct iovec wiov[] = {{.iov_base = out0, .iov_len = 4}, + {.iov_base = out1, .iov_len = 4}}; + ssize_t nwrite = writev(wfd, wiov, 2); + assert(nwrite == 4); + close(wfd); + + printf("done\n"); + return 0; +} diff --git a/test/test_core.py b/test/test_core.py index 772ab8067278a..56c39630382a0 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -6037,6 +6037,9 @@ def test_fs_readv(self): def test_fs_writev(self): self.do_runf('fs/test_writev.c', 'done\n', cflags=['-sFORCE_FILESYSTEM']) + def test_fs_readv_eagain(self): + self.do_runf('fs/test_readv_eagain.c', 'done\n', cflags=['-sFORCE_FILESYSTEM']) + def test_fs_64bit(self): if self.get_setting('WASMFS'): self.set_setting('FORCE_FILESYSTEM') From 2f3cabe9b259e7834d170d35466fa2f32e51d09a Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Wed, 8 Jul 2026 14:59:14 -0700 Subject: [PATCH 2/3] Declare curr inside the try block in doReadv/doWritev --- src/lib/libwasi.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/libwasi.js b/src/lib/libwasi.js index d9e204632b39e..d668d35ba3179 100644 --- a/src/lib/libwasi.js +++ b/src/lib/libwasi.js @@ -202,9 +202,8 @@ var WasiLibrary = { var ptr = {{{ makeGetValue('iov', C_STRUCTS.iovec.iov_base, '*') }}}; var len = {{{ makeGetValue('iov', C_STRUCTS.iovec.iov_len, '*') }}}; iov += {{{ C_STRUCTS.iovec.__size__ }}}; - var curr; try { - curr = FS.read(stream, HEAP8, ptr, len, offset); + var curr = FS.read(stream, HEAP8, ptr, len, offset); } catch (e) { // On a non-blocking stream a subsequent read may would-block after we // already gathered data. POSIX readv is a single gather-read: return @@ -231,9 +230,8 @@ var WasiLibrary = { var ptr = {{{ makeGetValue('iov', C_STRUCTS.iovec.iov_base, '*') }}}; var len = {{{ makeGetValue('iov', C_STRUCTS.iovec.iov_len, '*') }}}; iov += {{{ C_STRUCTS.iovec.__size__ }}}; - var curr; try { - curr = FS.write(stream, HEAP8, ptr, len, offset); + var curr = FS.write(stream, HEAP8, ptr, len, offset); } catch (e) { // On a non-blocking stream a subsequent write may would-block after we // already sent data. POSIX writev is a single gather-write: return From 23be3846df502064a699168e30da24c07e56ba54 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Wed, 8 Jul 2026 15:18:39 -0700 Subject: [PATCH 3/3] rebaseline --- test/codesize/test_codesize_cxx_ctors1.json | 8 ++++---- test/codesize/test_codesize_cxx_ctors2.json | 8 ++++---- test/codesize/test_codesize_cxx_except.json | 8 ++++---- test/codesize/test_codesize_cxx_except_wasm.json | 8 ++++---- .../test_codesize_cxx_except_wasm_legacy.json | 8 ++++---- test/codesize/test_codesize_cxx_lto.json | 8 ++++---- test/codesize/test_codesize_cxx_mangle.json | 8 ++++---- test/codesize/test_codesize_cxx_noexcept.json | 8 ++++---- test/codesize/test_codesize_file_preload.expected.js | 12 +++++++++++- test/codesize/test_codesize_file_preload.json | 8 ++++---- test/codesize/test_codesize_files_js_fs.json | 8 ++++---- test/codesize/test_codesize_hello_dylink.json | 8 ++++---- test/codesize/test_codesize_hello_dylink_all.json | 4 ++-- 13 files changed, 57 insertions(+), 47 deletions(-) diff --git a/test/codesize/test_codesize_cxx_ctors1.json b/test/codesize/test_codesize_cxx_ctors1.json index d9b5dd91f246f..b22e5eb1f8086 100644 --- a/test/codesize/test_codesize_cxx_ctors1.json +++ b/test/codesize/test_codesize_cxx_ctors1.json @@ -1,10 +1,10 @@ { - "a.out.js": 19183, - "a.out.js.gz": 7971, + "a.out.js": 19321, + "a.out.js.gz": 8000, "a.out.nodebug.wasm": 132556, "a.out.nodebug.wasm.gz": 49942, - "total": 151739, - "total_gz": 57913, + "total": 151877, + "total_gz": 57942, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_ctors2.json b/test/codesize/test_codesize_cxx_ctors2.json index 6dcbdafb9fe47..301036e4802b7 100644 --- a/test/codesize/test_codesize_cxx_ctors2.json +++ b/test/codesize/test_codesize_cxx_ctors2.json @@ -1,10 +1,10 @@ { - "a.out.js": 19160, - "a.out.js.gz": 7957, + "a.out.js": 19298, + "a.out.js.gz": 7986, "a.out.nodebug.wasm": 131982, "a.out.nodebug.wasm.gz": 49601, - "total": 151142, - "total_gz": 57558, + "total": 151280, + "total_gz": 57587, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_cxx_except.json b/test/codesize/test_codesize_cxx_except.json index 05b1566e8146f..42081ddbcaea7 100644 --- a/test/codesize/test_codesize_cxx_except.json +++ b/test/codesize/test_codesize_cxx_except.json @@ -1,10 +1,10 @@ { - "a.out.js": 23158, - "a.out.js.gz": 8955, + "a.out.js": 23296, + "a.out.js.gz": 8993, "a.out.nodebug.wasm": 172466, "a.out.nodebug.wasm.gz": 57476, - "total": 195624, - "total_gz": 66431, + "total": 195762, + "total_gz": 66469, "sent": [ "__cxa_begin_catch", "__cxa_end_catch", diff --git a/test/codesize/test_codesize_cxx_except_wasm.json b/test/codesize/test_codesize_cxx_except_wasm.json index 2c3f865598586..3820db7dcf330 100644 --- a/test/codesize/test_codesize_cxx_except_wasm.json +++ b/test/codesize/test_codesize_cxx_except_wasm.json @@ -1,10 +1,10 @@ { - "a.out.js": 18982, - "a.out.js.gz": 7888, + "a.out.js": 19120, + "a.out.js.gz": 7923, "a.out.nodebug.wasm": 147881, "a.out.nodebug.wasm.gz": 55369, - "total": 166863, - "total_gz": 63257, + "total": 167001, + "total_gz": 63292, "sent": [ "_abort_js", "_tzset_js", diff --git a/test/codesize/test_codesize_cxx_except_wasm_legacy.json b/test/codesize/test_codesize_cxx_except_wasm_legacy.json index c93a5a2a92dd4..832db5bab35e8 100644 --- a/test/codesize/test_codesize_cxx_except_wasm_legacy.json +++ b/test/codesize/test_codesize_cxx_except_wasm_legacy.json @@ -1,10 +1,10 @@ { - "a.out.js": 19060, - "a.out.js.gz": 7913, + "a.out.js": 19198, + "a.out.js.gz": 7948, "a.out.nodebug.wasm": 145687, "a.out.nodebug.wasm.gz": 54986, - "total": 164747, - "total_gz": 62899, + "total": 164885, + "total_gz": 62934, "sent": [ "_abort_js", "_tzset_js", diff --git a/test/codesize/test_codesize_cxx_lto.json b/test/codesize/test_codesize_cxx_lto.json index a7bcf95dae831..295e106a3a791 100644 --- a/test/codesize/test_codesize_cxx_lto.json +++ b/test/codesize/test_codesize_cxx_lto.json @@ -1,10 +1,10 @@ { - "a.out.js": 18526, - "a.out.js.gz": 7665, + "a.out.js": 18665, + "a.out.js.gz": 7692, "a.out.nodebug.wasm": 102063, "a.out.nodebug.wasm.gz": 39527, - "total": 120589, - "total_gz": 47192, + "total": 120728, + "total_gz": 47219, "sent": [ "a (emscripten_resize_heap)", "b (_setitimer_js)", diff --git a/test/codesize/test_codesize_cxx_mangle.json b/test/codesize/test_codesize_cxx_mangle.json index 3b58a1da9e8c2..9b8640fda16a8 100644 --- a/test/codesize/test_codesize_cxx_mangle.json +++ b/test/codesize/test_codesize_cxx_mangle.json @@ -1,10 +1,10 @@ { - "a.out.js": 23208, - "a.out.js.gz": 8976, + "a.out.js": 23346, + "a.out.js.gz": 9013, "a.out.nodebug.wasm": 238895, "a.out.nodebug.wasm.gz": 79814, - "total": 262103, - "total_gz": 88790, + "total": 262241, + "total_gz": 88827, "sent": [ "__cxa_begin_catch", "__cxa_end_catch", diff --git a/test/codesize/test_codesize_cxx_noexcept.json b/test/codesize/test_codesize_cxx_noexcept.json index 9dee5ccc5df7b..16311f9372b88 100644 --- a/test/codesize/test_codesize_cxx_noexcept.json +++ b/test/codesize/test_codesize_cxx_noexcept.json @@ -1,10 +1,10 @@ { - "a.out.js": 19183, - "a.out.js.gz": 7971, + "a.out.js": 19321, + "a.out.js.gz": 8000, "a.out.nodebug.wasm": 134556, "a.out.nodebug.wasm.gz": 50787, - "total": 153739, - "total_gz": 58758, + "total": 153877, + "total_gz": 58787, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_file_preload.expected.js b/test/codesize/test_codesize_file_preload.expected.js index 97ef63fcb0f09..54bd501f7bd2d 100644 --- a/test/codesize/test_codesize_file_preload.expected.js +++ b/test/codesize/test_codesize_file_preload.expected.js @@ -519,7 +519,17 @@ var onPreRuns = []; var ptr = HEAPU32[((iov) >> 2)]; var len = HEAPU32[(((iov) + (4)) >> 2)]; iov += 8; - var curr = FS.write(stream, HEAP8, ptr, len, offset); + try { + var curr = FS.write(stream, HEAP8, ptr, len, offset); + } catch (e) { + // On a non-blocking stream a subsequent write may would-block after we + // already sent data. POSIX writev is a single gather-write: return + // what we have rather than failing the whole call. + if (ret > 0 && e instanceof FS.ErrnoError && (e.errno == 6 || e.errno == 6)) { + break; + } + throw e; + } if (curr < 0) return -1; ret += curr; if (curr < len) { diff --git a/test/codesize/test_codesize_file_preload.json b/test/codesize/test_codesize_file_preload.json index 8f618f019aa71..9361957f78abd 100644 --- a/test/codesize/test_codesize_file_preload.json +++ b/test/codesize/test_codesize_file_preload.json @@ -1,10 +1,10 @@ { - "a.out.js": 22239, - "a.out.js.gz": 9251, + "a.out.js": 22308, + "a.out.js.gz": 9277, "a.out.nodebug.wasm": 1666, "a.out.nodebug.wasm.gz": 945, - "total": 23905, - "total_gz": 10196, + "total": 23974, + "total_gz": 10222, "sent": [ "a (fd_write)" ], diff --git a/test/codesize/test_codesize_files_js_fs.json b/test/codesize/test_codesize_files_js_fs.json index 3ff56a1152b6e..aee1fe867e699 100644 --- a/test/codesize/test_codesize_files_js_fs.json +++ b/test/codesize/test_codesize_files_js_fs.json @@ -1,10 +1,10 @@ { - "a.out.js": 17831, - "a.out.js.gz": 7311, + "a.out.js": 17969, + "a.out.js.gz": 7342, "a.out.nodebug.wasm": 381, "a.out.nodebug.wasm.gz": 258, - "total": 18212, - "total_gz": 7569, + "total": 18350, + "total_gz": 7600, "sent": [ "a (fd_write)", "b (fd_read)", diff --git a/test/codesize/test_codesize_hello_dylink.json b/test/codesize/test_codesize_hello_dylink.json index bca58fb7b3b36..65d886933cf0f 100644 --- a/test/codesize/test_codesize_hello_dylink.json +++ b/test/codesize/test_codesize_hello_dylink.json @@ -1,10 +1,10 @@ { - "a.out.js": 26190, - "a.out.js.gz": 11198, + "a.out.js": 26260, + "a.out.js.gz": 11231, "a.out.nodebug.wasm": 17861, "a.out.nodebug.wasm.gz": 9019, - "total": 44051, - "total_gz": 20217, + "total": 44121, + "total_gz": 20250, "sent": [ "__syscall_stat64", "emscripten_resize_heap", diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 637eb03509e47..c5054db0c57f8 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 268279, + "a.out.js": 268425, "a.out.nodebug.wasm": 587978, - "total": 856257, + "total": 856403, "sent": [ "IMG_Init", "IMG_Load",