From c7c9b7f8dee5047c58a56bb4860715df716f2373 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 3 Jul 2026 16:43:29 +0200 Subject: [PATCH 1/4] WasmFS: enforce open file access mode on read/write writeAtOffset and readAtOffset had a TODO to check the open file access mode but did not, so writing to an O_RDONLY fd (or reading from an O_WRONLY fd) silently succeeded instead of failing with EBADF as POSIX requires. Return __WASI_ERRNO_BADF in those cases, matching the existing check in the mmap path. --- system/lib/wasmfs/syscalls.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/system/lib/wasmfs/syscalls.cpp b/system/lib/wasmfs/syscalls.cpp index 66bfb0e2fd9ba..dccc97db0d721 100644 --- a/system/lib/wasmfs/syscalls.cpp +++ b/system/lib/wasmfs/syscalls.cpp @@ -127,7 +127,11 @@ static __wasi_errno_t writeAtOffset(OffsetHandling setOffset, } } - // TODO: Check open file access mode for write permissions. + // A file opened for reading only (O_RDONLY) cannot be written. POSIX write(2) + // returns EBADF when the file descriptor is not open for writing. + if ((lockedOpenFile.getFlags() & O_ACCMODE) == O_RDONLY) { + return __WASI_ERRNO_BADF; + } size_t bytesWritten = 0; for (size_t i = 0; i < iovs_len; i++) { @@ -197,7 +201,11 @@ static __wasi_errno_t readAtOffset(OffsetHandling setOffset, return __WASI_ERRNO_INVAL; } - // TODO: Check open file access mode for read permissions. + // A file opened for writing only (O_WRONLY) cannot be read. POSIX read(2) + // returns EBADF when the file descriptor is not open for reading. + if ((lockedOpenFile.getFlags() & O_ACCMODE) == O_WRONLY) { + return __WASI_ERRNO_BADF; + } auto file = lockedOpenFile.getFile()->dynCast(); From 2b8b4fd582875a6725c2e54b6840bf56b1c10d26 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 7 Jul 2026 18:16:16 +0200 Subject: [PATCH 2/4] feedback and fixes --- system/lib/wasmfs/syscalls.cpp | 27 ++++++++++--------- test/codesize/test_codesize_cxx_wasmfs.json | 8 +++--- test/codesize/test_codesize_files_wasmfs.json | 8 +++--- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/system/lib/wasmfs/syscalls.cpp b/system/lib/wasmfs/syscalls.cpp index dccc97db0d721..3b70211d152de 100644 --- a/system/lib/wasmfs/syscalls.cpp +++ b/system/lib/wasmfs/syscalls.cpp @@ -111,6 +111,12 @@ static __wasi_errno_t writeAtOffset(OffsetHandling setOffset, return __WASI_ERRNO_ISDIR; } + // A file opened for reading only (O_RDONLY) cannot be written. POSIX write(2) + // returns EBADF when the file descriptor is not open for writing. + if ((lockedOpenFile.getFlags() & O_ACCMODE) == O_RDONLY) { + return __WASI_ERRNO_BADF; + } + auto lockedFile = file->locked(); if (setOffset == OffsetHandling::OpenFileState) { @@ -127,12 +133,6 @@ static __wasi_errno_t writeAtOffset(OffsetHandling setOffset, } } - // A file opened for reading only (O_RDONLY) cannot be written. POSIX write(2) - // returns EBADF when the file descriptor is not open for writing. - if ((lockedOpenFile.getFlags() & O_ACCMODE) == O_RDONLY) { - return __WASI_ERRNO_BADF; - } - size_t bytesWritten = 0; for (size_t i = 0; i < iovs_len; i++) { const uint8_t* buf = iovs[i].buf; @@ -201,12 +201,6 @@ static __wasi_errno_t readAtOffset(OffsetHandling setOffset, return __WASI_ERRNO_INVAL; } - // A file opened for writing only (O_WRONLY) cannot be read. POSIX read(2) - // returns EBADF when the file descriptor is not open for reading. - if ((lockedOpenFile.getFlags() & O_ACCMODE) == O_WRONLY) { - return __WASI_ERRNO_BADF; - } - auto file = lockedOpenFile.getFile()->dynCast(); // If file is nullptr, then the file was not a DataFile. @@ -214,6 +208,12 @@ static __wasi_errno_t readAtOffset(OffsetHandling setOffset, return __WASI_ERRNO_ISDIR; } + // A file opened for writing only (O_WRONLY) cannot be read. POSIX read(2) + // returns EBADF when the file descriptor is not open for reading. + if ((lockedOpenFile.getFlags() & O_ACCMODE) == O_WRONLY) { + return __WASI_ERRNO_BADF; + } + auto lockedFile = file->locked(); size_t bytesRead = 0; @@ -577,7 +577,8 @@ static __wasi_fd_t doOpen(path::ParsedParent parsed, int wasmfs_create_file(const char* pathname, mode_t mode, backend_t backend) { static_assert(std::is_same_v, "unexpected conversion from result of doOpen to int"); - return doOpen(path::parseParent(pathname), O_CREAT | O_EXCL, mode, backend); + return doOpen( + path::parseParent(pathname), O_CREAT | O_EXCL | O_RDWR, mode, backend); } // TODO: Test this with non-AT_FDCWD values. diff --git a/test/codesize/test_codesize_cxx_wasmfs.json b/test/codesize/test_codesize_cxx_wasmfs.json index 6f82fe5e1de0e..d5d29aeef34e2 100644 --- a/test/codesize/test_codesize_cxx_wasmfs.json +++ b/test/codesize/test_codesize_cxx_wasmfs.json @@ -1,10 +1,10 @@ { "a.out.js": 6725, "a.out.js.gz": 3184, - "a.out.nodebug.wasm": 172782, - "a.out.nodebug.wasm.gz": 63311, - "total": 179507, - "total_gz": 66495, + "a.out.nodebug.wasm": 172832, + "a.out.nodebug.wasm.gz": 63334, + "total": 179557, + "total_gz": 66518, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_files_wasmfs.json b/test/codesize/test_codesize_files_wasmfs.json index 8c0f2f4b56336..ce26e1ccfad02 100644 --- a/test/codesize/test_codesize_files_wasmfs.json +++ b/test/codesize/test_codesize_files_wasmfs.json @@ -1,10 +1,10 @@ { "a.out.js": 5158, "a.out.js.gz": 2458, - "a.out.nodebug.wasm": 58562, - "a.out.nodebug.wasm.gz": 18269, - "total": 63720, - "total_gz": 20727, + "a.out.nodebug.wasm": 58616, + "a.out.nodebug.wasm.gz": 18380, + "total": 63774, + "total_gz": 20838, "sent": [ "a (emscripten_date_now)", "b (emscripten_err)", From 953c943b43af1d44778c019af4bee836ca2ec118 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 7 Jul 2026 19:00:59 +0200 Subject: [PATCH 3/4] test_access_mode.c --- test/codesize/test_codesize_cxx_wasmfs.json | 8 +- test/codesize/test_codesize_files_wasmfs.json | 8 +- test/fs/test_access_mode.c | 79 +++++++++++++++++++ test/test_core.py | 8 ++ 4 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 test/fs/test_access_mode.c diff --git a/test/codesize/test_codesize_cxx_wasmfs.json b/test/codesize/test_codesize_cxx_wasmfs.json index d5d29aeef34e2..27884b4e8620d 100644 --- a/test/codesize/test_codesize_cxx_wasmfs.json +++ b/test/codesize/test_codesize_cxx_wasmfs.json @@ -1,10 +1,10 @@ { "a.out.js": 6725, "a.out.js.gz": 3184, - "a.out.nodebug.wasm": 172832, - "a.out.nodebug.wasm.gz": 63334, - "total": 179557, - "total_gz": 66518, + "a.out.nodebug.wasm": 172829, + "a.out.nodebug.wasm.gz": 63329, + "total": 179554, + "total_gz": 66513, "sent": [ "__cxa_throw", "_abort_js", diff --git a/test/codesize/test_codesize_files_wasmfs.json b/test/codesize/test_codesize_files_wasmfs.json index ce26e1ccfad02..f5c6388f137c7 100644 --- a/test/codesize/test_codesize_files_wasmfs.json +++ b/test/codesize/test_codesize_files_wasmfs.json @@ -1,10 +1,10 @@ { "a.out.js": 5158, "a.out.js.gz": 2458, - "a.out.nodebug.wasm": 58616, - "a.out.nodebug.wasm.gz": 18380, - "total": 63774, - "total_gz": 20838, + "a.out.nodebug.wasm": 58606, + "a.out.nodebug.wasm.gz": 18406, + "total": 63764, + "total_gz": 20864, "sent": [ "a (emscripten_date_now)", "b (emscripten_err)", diff --git a/test/fs/test_access_mode.c b/test/fs/test_access_mode.c new file mode 100644 index 0000000000000..5a8b8773cfa03 --- /dev/null +++ b/test/fs/test_access_mode.c @@ -0,0 +1,79 @@ +/* + * Copyright 2026 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. + */ + +// Verify that the kernel enforces the open file access mode on read/write, as +// POSIX requires: writing to an O_RDONLY descriptor and reading from an +// O_WRONLY descriptor must fail with EBADF. This should behave identically +// across all of our filesystems (MEMFS, WASMFS, NODEFS, NODERAWFS). + +#include +#include +#include +#include +#include +#include + +int main(void) { + const char* msg = "hello"; + const size_t len = strlen(msg); + char buf[16]; + + // Create a file with some content. + int fd = open("testfile", O_WRONLY | O_CREAT | O_TRUNC, 0666); + assert(fd >= 0); + assert(write(fd, msg, len) == (ssize_t)len); + assert(close(fd) == 0); + + // O_RDONLY: reads are allowed, writes must fail with EBADF. + fd = open("testfile", O_RDONLY); + assert(fd >= 0); + + errno = 0; + assert(write(fd, msg, len) == -1); + assert(errno == EBADF); + + errno = 0; + assert(pwrite(fd, msg, len, 0) == -1); + assert(errno == EBADF); + + errno = 0; + memset(buf, 0, sizeof buf); + assert(read(fd, buf, sizeof buf) == (ssize_t)len); + assert(errno == 0); + assert(strcmp(buf, msg) == 0); + assert(close(fd) == 0); + + // O_WRONLY: writes are allowed, reads must fail with EBADF. + fd = open("testfile", O_WRONLY); + assert(fd >= 0); + + errno = 0; + assert(read(fd, buf, sizeof buf) == -1); + assert(errno == EBADF); + + errno = 0; + assert(pread(fd, buf, sizeof buf, 0) == -1); + assert(errno == EBADF); + + errno = 0; + assert(write(fd, msg, len) == (ssize_t)len); + assert(errno == 0); + assert(close(fd) == 0); + + // O_RDWR: both reads and writes are allowed. + fd = open("testfile", O_RDWR); + assert(fd >= 0); + errno = 0; + memset(buf, 0, sizeof buf); + assert(read(fd, buf, sizeof buf) == (ssize_t)len); + assert(write(fd, msg, len) == (ssize_t)len); + assert(errno == 0); + assert(close(fd) == 0); + + printf("done\n"); + return 0; +} diff --git a/test/test_core.py b/test/test_core.py index fdfba927f5e77..a0a60da823507 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -5933,6 +5933,14 @@ def test_fs_write(self): self.set_setting("FORCE_FILESYSTEM") self.do_runf_out_file('fs/test_fs_write.c') + @with_all_fs + def test_fs_access_mode(self): + # Writing to an O_RDONLY fd and reading from an O_WRONLY fd must fail with + # EBADF, consistently across all filesystems. + if self.get_setting('WASMFS'): + self.set_setting('FORCE_FILESYSTEM') + self.do_runf('fs/test_access_mode.c', 'done\n') + @also_with_noderawfs def test_fs_emptyPath(self): self.do_runf_out_file('fs/test_emptyPath.c') From 4ca2edc11179903e1a36cb87a310ba0a0d1e8cc9 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 7 Jul 2026 19:16:42 +0200 Subject: [PATCH 4/4] feedback --- test/fs/test_access_mode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fs/test_access_mode.c b/test/fs/test_access_mode.c index 5a8b8773cfa03..c5c0a3155ab31 100644 --- a/test/fs/test_access_mode.c +++ b/test/fs/test_access_mode.c @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -// Verify that the kernel enforces the open file access mode on read/write, as +// Verify that the file system enforces the open file access mode on read/write, as // POSIX requires: writing to an O_RDONLY descriptor and reading from an // O_WRONLY descriptor must fail with EBADF. This should behave identically // across all of our filesystems (MEMFS, WASMFS, NODEFS, NODERAWFS).