From c0b70329aa0cdd5fedf02bc4e1ce7435eee3e68f Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Thu, 13 Aug 2026 14:32:43 -0700 Subject: [PATCH 1/2] Fix read() on a directory fd returning EBADF instead of EISDIR POSIX requires EISDIR when read() is used on a directory. wasip2 read-via-stream fails with bad-descriptor, which we mapped to EBADF. Remap that error to EISDIR when get_type or fdstat shows a directory. Closes #865 Signed-off-by: Sebastien Tardif --- .../cloudlibc/src/libc/unistd/read.c | 7 ++++ libc-bottom-half/sources/file.c | 32 ++++++++++++++ test/CMakeLists.txt | 1 + test/src/read-dir-eisdir.c | 42 +++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 test/src/read-dir-eisdir.c diff --git a/libc-bottom-half/cloudlibc/src/libc/unistd/read.c b/libc-bottom-half/cloudlibc/src/libc/unistd/read.c index 5ce69fede..11e0d15f1 100644 --- a/libc-bottom-half/cloudlibc/src/libc/unistd/read.c +++ b/libc-bottom-half/cloudlibc/src/libc/unistd/read.c @@ -21,6 +21,13 @@ ssize_t read(int fildes, void *buf, size_t nbyte) { size_t bytes_read; __wasi_errno_t error = __wasi_fd_read(fildes, &iov, 1, &bytes_read); if (error != 0) { + __wasi_fdstat_t fds; + if ((error == __WASI_ERRNO_BADF || error == ENOTCAPABLE) && + __wasi_fd_fdstat_get(fildes, &fds) == 0 && + fds.fs_filetype == __WASI_FILETYPE_DIRECTORY) { + errno = EISDIR; + return -1; + } errno = error == ENOTCAPABLE ? EBADF : error; return -1; } diff --git a/libc-bottom-half/sources/file.c b/libc-bottom-half/sources/file.c index 6e0c22468..18c369207 100644 --- a/libc-bottom-half/sources/file.c +++ b/libc-bottom-half/sources/file.c @@ -81,6 +81,28 @@ static void file_free(void *data) { free(file); } +// True when `file` refers to a directory. Used to map the host's +// `bad-descriptor` from `read-via-stream` to POSIX EISDIR. Only called +// after a read has already failed. +static bool file_is_directory(file_t *file) { + filesystem_error_code_t error; +#ifdef __wasip2__ + filesystem_descriptor_type_t ty; + if (!filesystem_method_descriptor_get_type( + filesystem_borrow_descriptor(file->file_handle), &ty, &error)) + return false; + return ty == FILESYSTEM_DESCRIPTOR_TYPE_DIRECTORY; +#else + filesystem_descriptor_stat_t st; + if (!filesystem_method_descriptor_stat( + filesystem_borrow_descriptor(file->file_handle), &st, &error)) + return false; + bool is_dir = st.type.tag == FILESYSTEM_DESCRIPTOR_TYPE_DIRECTORY; + filesystem_descriptor_stat_free(&st); + return is_dir; +#endif +} + #ifndef __wasip2__ static int file_read_eof(void *data) { file_t *file = (file_t *)data; @@ -94,6 +116,11 @@ static int file_read_eof(void *data) { filesystem_future_result_void_error_code_drop_readable(file->read_result); file->read_result = 0; if (result.is_err) { + if (result.val.err.tag == FILESYSTEM_ERROR_CODE_BAD_DESCRIPTOR && + file_is_directory(file)) { + errno = EISDIR; + return -1; + } translate_error(&result.val.err); return -1; } @@ -115,6 +142,11 @@ static int file_get_read_stream(void *data, wasi_read_t *read) { filesystem_borrow_descriptor(file->file_handle), file->offset, &file->read_stream, &error_code); if (!ok) { + if (error_code == FILESYSTEM_ERROR_CODE_BAD_DESCRIPTOR && + file_is_directory(file)) { + errno = EISDIR; + return -1; + } translate_error(&error_code); return -1; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8fe517798..989c704b3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -488,6 +488,7 @@ add_wasilibc_test(ppoll.c FS) add_wasilibc_test(pselect.c FS) add_wasilibc_test(preadvwritev.c FS) add_wasilibc_test(preadwrite.c FS) +add_wasilibc_test(read-dir-eisdir.c FS) add_wasilibc_test(readlink.c FS) add_wasilibc_test(readv.c FS) add_wasilibc_test(rename.c FS) diff --git a/test/src/read-dir-eisdir.c b/test/src/read-dir-eisdir.c new file mode 100644 index 000000000..ef033433f --- /dev/null +++ b/test/src/read-dir-eisdir.c @@ -0,0 +1,42 @@ +#include "test.h" +#include +#include +#include +#include + +#define TEST(c) \ + do { \ + errno = 0; \ + if (!(c)) \ + t_error("%s failed (errno = %d)\n", #c, errno); \ + } while (0) + +// POSIX read() on a directory fd must fail with EISDIR, not EBADF. +// wasip2 read-via-stream currently surfaces bad-descriptor, which +// translate_error maps to EBADF. +int main(void) { + char buf[16]; + + TEST(mkdir("read-dir-eisdir", 0755) == 0); + int dirfd = open("read-dir-eisdir", O_RDONLY | O_DIRECTORY); + TEST(dirfd > 2); + + TEST(read(dirfd, buf, sizeof buf) == -1 && errno == EISDIR); + + TEST(close(dirfd) == 0); + + // Genuine bad fds stay EBADF; regular files still read. + TEST(read(-1, buf, 1) == -1 && errno == EBADF); + + int fd; + TEST((fd = open("read-dir-eisdir/f", O_RDWR | O_CREAT | O_EXCL, 0600)) > 2); + TEST(write(fd, "x", 1) == 1); + TEST(lseek(fd, 0, SEEK_SET) == 0); + TEST(read(fd, buf, 1) == 1); + TEST(buf[0] == 'x'); + TEST(close(fd) == 0); + TEST(unlink("read-dir-eisdir/f") == 0); + TEST(rmdir("read-dir-eisdir") == 0); + + return t_status; +} From 08cbec4cb65d407fafd822b2cea7655178739b00 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Thu, 13 Aug 2026 14:38:55 -0700 Subject: [PATCH 2/2] Avoid wasip3 trap when read() is used on a directory wasip3 read-via-stream returns a stream, not a result. Wasmtime traps with ErrorCode::BadDescriptor on a directory fd, so the later eof remap never runs. Check descriptor_stat before opening the stream. Signed-off-by: Sebastien Tardif --- libc-bottom-half/sources/file.c | 13 ++++++++++--- test/src/read-dir-eisdir.c | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/libc-bottom-half/sources/file.c b/libc-bottom-half/sources/file.c index 18c369207..8fcb6bf10 100644 --- a/libc-bottom-half/sources/file.c +++ b/libc-bottom-half/sources/file.c @@ -81,9 +81,10 @@ static void file_free(void *data) { free(file); } -// True when `file` refers to a directory. Used to map the host's -// `bad-descriptor` from `read-via-stream` to POSIX EISDIR. Only called -// after a read has already failed. +// True when `file` refers to a directory. Used to return POSIX EISDIR +// instead of the host's `bad-descriptor`. On wasip2 this runs only after +// `read-via-stream` fails. On wasip3 that call traps, so the check runs +// before opening the stream. static bool file_is_directory(file_t *file) { filesystem_error_code_t error; #ifdef __wasip2__ @@ -155,6 +156,12 @@ static int file_get_read_stream(void *data, wasi_read_t *read) { read->pollable = &file->read_pollable; #else if (!wasip3_io_state_present(&file->read)) { + // wasip3 `read-via-stream` returns a stream, not a result. Wasmtime + // traps with ErrorCode::BadDescriptor on a directory. Check first. + if (file_is_directory(file)) { + errno = EISDIR; + return -1; + } assert(!file->read_result); filesystem_tuple2_stream_u8_future_result_void_error_code_t result; filesystem_method_descriptor_read_via_stream( diff --git a/test/src/read-dir-eisdir.c b/test/src/read-dir-eisdir.c index ef033433f..b91f3e6cc 100644 --- a/test/src/read-dir-eisdir.c +++ b/test/src/read-dir-eisdir.c @@ -12,8 +12,8 @@ } while (0) // POSIX read() on a directory fd must fail with EISDIR, not EBADF. -// wasip2 read-via-stream currently surfaces bad-descriptor, which -// translate_error maps to EBADF. +// wasip2 read-via-stream fails with bad-descriptor (mapped to EBADF). +// wasip3 read-via-stream traps with BadDescriptor unless we check type first. int main(void) { char buf[16];