In the wasm32-wasip2 build, pread's glue uses the result of
filesystem_method_descriptor_read before checking whether the call
succeeded (libc-bottom-half/cloudlibc/src/libc/unistd/pread.c, the
__wasip2__ branch):
bool ok = filesystem_method_descriptor_read(file_handle, nbyte, offset,
&contents, &error_code);
bytes_read = contents.f0.len; // consumed before the check
memcpy(buf, contents.f0.ptr, bytes_read); // copy from it
wasip2_list_u8_free(&contents.f0); // free it
if (!ok) { ... } // checked too late
When the host returns an error, the generated binding does not write
contents at all — it is uninitialized stack memory. So on any failing
read, this code:
- reads an uninitialized length and pointer,
memcpys from that garbage pointer into the caller's buffer, and
- passes the garbage pointer to
free, corrupting the allocator heap.
Any WASI 0.2 host whose filesystem can return an error from
descriptor.read (I/O errors, busy resources, permission failures)
triggers this. Noticed by reading the glue while chasing crashes that
only occurred when reads failed.
The neighboring functions get this right — readlinkat and pwrite
both check the flag before touching the result — so pread looks like
a simple ordering slip. A one-function fix is prepared (moving the check
first, plus clamping the returned length to the requested size so an
out-of-spec host cannot overflow the caller's buffer); PR to follow.
The wasip1 and wasip3 branches of the same file are unaffected (different
mechanisms; wasip3 reads directly into the caller's buffer).
In the wasm32-wasip2 build,
pread's glue uses the result offilesystem_method_descriptor_readbefore checking whether the callsucceeded (
libc-bottom-half/cloudlibc/src/libc/unistd/pread.c, the__wasip2__branch):When the host returns an error, the generated binding does not write
contentsat all — it is uninitialized stack memory. So on any failingread, this code:
memcpys from that garbage pointer into the caller's buffer, andfree, corrupting the allocator heap.Any WASI 0.2 host whose filesystem can return an error from
descriptor.read(I/O errors, busy resources, permission failures)triggers this. Noticed by reading the glue while chasing crashes that
only occurred when reads failed.
The neighboring functions get this right —
readlinkatandpwriteboth check the flag before touching the result — so
preadlooks likea simple ordering slip. A one-function fix is prepared (moving the check
first, plus clamping the returned length to the requested size so an
out-of-spec host cannot overflow the caller's buffer); PR to follow.
The wasip1 and wasip3 branches of the same file are unaffected (different
mechanisms; wasip3 reads directly into the caller's buffer).