From fc8746d7b21ebfaa26cbe59c28b28be3b8447cab Mon Sep 17 00:00:00 2001 From: Evgeniy Gorbanyov Date: Mon, 19 Oct 2026 19:23:37 +0600 Subject: [PATCH] wasi: fix integer overflow in iovec bounds checks Guest-controlled counts were multiplied by the serialized element size in uint32_t. A length such as 0x20000000 wraps the product to 0, so the bounds check accepts the array and deserialization reads past the end of the WebAssembly memory. Multiply in size_t and reject an overflowing product before reading the array. The same check covers fd_read, fd_write, fd_pread, fd_pwrite, poll_oneoff, sock_recv, and sock_send. Signed-off-by: Evgeniy Gorbanyov --- src/node_wasi.cc | 42 +++++++++++++++++---------- test/wasi/test-wasi-iovec-overflow.js | 36 +++++++++++++++++++++++ 2 files changed, 62 insertions(+), 16 deletions(-) create mode 100644 test/wasi/test-wasi-iovec-overflow.js diff --git a/src/node_wasi.cc b/src/node_wasi.cc index 6c2adc826571..6f2cf9e0932e 100644 --- a/src/node_wasi.cc +++ b/src/node_wasi.cc @@ -26,6 +26,16 @@ inline void Debug(const WASI& wasi, Args&&... args) { } \ } while (0) +// elem_size * count is computed in size_t. A uint32_t product wraps +// (for example 0x20000000 * 8 == 0) and then skips the bounds check. +#define CHECK_ARRAY_BOUNDS_OR_RETURN(mem_size, offset, elem_size, count) \ + do { \ + if (!uvwasi_serdes_check_array_bounds( \ + (offset), (mem_size), (elem_size), (count))) { \ + return UVWASI_EOVERFLOW; \ + } \ + } while (0) + using v8::Array; using v8::ArrayBuffer; using v8::BigInt; @@ -664,8 +674,8 @@ uint32_t WASI::FdPread(WASI& wasi, iovs_len, offset, nread_ptr); - CHECK_BOUNDS_OR_RETURN( - memory.size, iovs_ptr, iovs_len * UVWASI_SERDES_SIZE_iovec_t); + CHECK_ARRAY_BOUNDS_OR_RETURN( + memory.size, iovs_ptr, UVWASI_SERDES_SIZE_iovec_t, iovs_len); CHECK_BOUNDS_OR_RETURN(memory.size, nread_ptr, UVWASI_SERDES_SIZE_size_t); std::vector iovs(iovs_len); uvwasi_errno_t err; @@ -724,8 +734,8 @@ uint32_t WASI::FdPwrite(WASI& wasi, iovs_len, offset, nwritten_ptr); - CHECK_BOUNDS_OR_RETURN( - memory.size, iovs_ptr, iovs_len * UVWASI_SERDES_SIZE_ciovec_t); + CHECK_ARRAY_BOUNDS_OR_RETURN( + memory.size, iovs_ptr, UVWASI_SERDES_SIZE_ciovec_t, iovs_len); CHECK_BOUNDS_OR_RETURN(memory.size, nwritten_ptr, UVWASI_SERDES_SIZE_size_t); std::vector iovs(iovs_len); uvwasi_errno_t err; @@ -752,8 +762,8 @@ uint32_t WASI::FdRead(WASI& wasi, uint32_t iovs_len, uint32_t nread_ptr) { Debug(wasi, "fd_read(%d, %d, %d, %d)\n", fd, iovs_ptr, iovs_len, nread_ptr); - CHECK_BOUNDS_OR_RETURN( - memory.size, iovs_ptr, iovs_len * UVWASI_SERDES_SIZE_iovec_t); + CHECK_ARRAY_BOUNDS_OR_RETURN( + memory.size, iovs_ptr, UVWASI_SERDES_SIZE_iovec_t, iovs_len); CHECK_BOUNDS_OR_RETURN(memory.size, nread_ptr, UVWASI_SERDES_SIZE_size_t); std::vector iovs(iovs_len); uvwasi_errno_t err; @@ -853,8 +863,8 @@ uint32_t WASI::FdWrite(WASI& wasi, iovs_ptr, iovs_len, nwritten_ptr); - CHECK_BOUNDS_OR_RETURN( - memory.size, iovs_ptr, iovs_len * UVWASI_SERDES_SIZE_ciovec_t); + CHECK_ARRAY_BOUNDS_OR_RETURN( + memory.size, iovs_ptr, UVWASI_SERDES_SIZE_ciovec_t, iovs_len); CHECK_BOUNDS_OR_RETURN(memory.size, nwritten_ptr, UVWASI_SERDES_SIZE_size_t); std::vector iovs(iovs_len); uvwasi_errno_t err; @@ -1126,10 +1136,10 @@ uint32_t WASI::PollOneoff(WASI& wasi, out_ptr, nsubscriptions, nevents_ptr); - CHECK_BOUNDS_OR_RETURN( - memory.size, in_ptr, nsubscriptions * UVWASI_SERDES_SIZE_subscription_t); - CHECK_BOUNDS_OR_RETURN( - memory.size, out_ptr, nsubscriptions * UVWASI_SERDES_SIZE_event_t); + CHECK_ARRAY_BOUNDS_OR_RETURN( + memory.size, in_ptr, UVWASI_SERDES_SIZE_subscription_t, nsubscriptions); + CHECK_ARRAY_BOUNDS_OR_RETURN( + memory.size, out_ptr, UVWASI_SERDES_SIZE_event_t, nsubscriptions); CHECK_BOUNDS_OR_RETURN(memory.size, nevents_ptr, UVWASI_SERDES_SIZE_size_t); std::vector in(nsubscriptions); std::vector out(nsubscriptions); @@ -1209,8 +1219,8 @@ uint32_t WASI::SockRecv(WASI& wasi, ri_flags, ro_datalen_ptr, ro_flags_ptr); - CHECK_BOUNDS_OR_RETURN( - memory.size, ri_data_ptr, ri_data_len * UVWASI_SERDES_SIZE_iovec_t); + CHECK_ARRAY_BOUNDS_OR_RETURN( + memory.size, ri_data_ptr, UVWASI_SERDES_SIZE_iovec_t, ri_data_len); CHECK_BOUNDS_OR_RETURN(memory.size, ro_datalen_ptr, 4); CHECK_BOUNDS_OR_RETURN(memory.size, ro_flags_ptr, 4); std::vector ri_data(ri_data_len); @@ -1251,8 +1261,8 @@ uint32_t WASI::SockSend(WASI& wasi, si_data_len, si_flags, so_datalen_ptr); - CHECK_BOUNDS_OR_RETURN( - memory.size, si_data_ptr, si_data_len * UVWASI_SERDES_SIZE_ciovec_t); + CHECK_ARRAY_BOUNDS_OR_RETURN( + memory.size, si_data_ptr, UVWASI_SERDES_SIZE_ciovec_t, si_data_len); CHECK_BOUNDS_OR_RETURN( memory.size, so_datalen_ptr, UVWASI_SERDES_SIZE_size_t); std::vector si_data(si_data_len); diff --git a/test/wasi/test-wasi-iovec-overflow.js b/test/wasi/test-wasi-iovec-overflow.js new file mode 100644 index 000000000000..9ee838eabb42 --- /dev/null +++ b/test/wasi/test-wasi-iovec-overflow.js @@ -0,0 +1,36 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { WASI } = require('wasi'); + +// deps/uvwasi/include/wasi_types.h +const UVWASI_ESUCCESS = 0; +const UVWASI_EOVERFLOW = 61; + +// _start calls fd_read and stores each errno: +// 0: iovs_len 0x20000000 (0x20000000 * 8 wraps uint32_t to 0) +// 4: iovs_len 0x20000001 (wraps to 8, which fits in one page) +// 8: iovs_len 0 +const bytes = Uint8Array.from([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x0c, 0x02, 0x60, 0x00, 0x00, 0x60, 0x04, + 0x7f, 0x7f, 0x7f, 0x7f, 0x01, 0x7f, 0x02, 0x22, 0x01, 0x16, 0x77, 0x61, 0x73, 0x69, 0x5f, 0x73, + 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x70, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x31, + 0x07, 0x66, 0x64, 0x5f, 0x72, 0x65, 0x61, 0x64, 0x00, 0x01, 0x03, 0x02, 0x01, 0x00, 0x05, 0x03, + 0x01, 0x00, 0x01, 0x07, 0x13, 0x02, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x06, + 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x00, 0x01, 0x0a, 0x39, 0x01, 0x37, 0x00, 0x41, 0x00, 0x41, + 0x00, 0x41, 0x00, 0x41, 0x80, 0x80, 0x80, 0x80, 0x02, 0x41, 0x10, 0x10, 0x00, 0x36, 0x02, 0x00, + 0x41, 0x04, 0x41, 0x00, 0x41, 0x00, 0x41, 0x81, 0x80, 0x80, 0x80, 0x02, 0x41, 0x10, 0x10, 0x00, + 0x36, 0x02, 0x00, 0x41, 0x08, 0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0x41, 0x10, 0x10, 0x00, 0x36, + 0x02, 0x00, 0x0b, +]); + +(async () => { + const wasi = new WASI({ version: 'preview1', returnOnExit: true }); + const { instance } = await WebAssembly.instantiate(bytes, wasi.getImportObject()); + assert.strictEqual(wasi.start(instance), 0); + + const view = new DataView(instance.exports.memory.buffer); + assert.strictEqual(view.getUint32(0, true), UVWASI_EOVERFLOW); + assert.strictEqual(view.getUint32(4, true), UVWASI_EOVERFLOW); + assert.strictEqual(view.getUint32(8, true), UVWASI_ESUCCESS); +})().then(common.mustCall());