diff --git a/system/lib/wasmfs/syscalls.cpp b/system/lib/wasmfs/syscalls.cpp index 66bfb0e2fd9ba..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,8 +133,6 @@ static __wasi_errno_t writeAtOffset(OffsetHandling setOffset, } } - // TODO: Check open file access mode for write permissions. - size_t bytesWritten = 0; for (size_t i = 0; i < iovs_len; i++) { const uint8_t* buf = iovs[i].buf; @@ -197,8 +201,6 @@ static __wasi_errno_t readAtOffset(OffsetHandling setOffset, return __WASI_ERRNO_INVAL; } - // TODO: Check open file access mode for read permissions. - auto file = lockedOpenFile.getFile()->dynCast(); // If file is nullptr, then the file was not a DataFile. @@ -206,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; @@ -569,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..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": 172782, - "a.out.nodebug.wasm.gz": 63311, - "total": 179507, - "total_gz": 66495, + "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 8c0f2f4b56336..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": 58562, - "a.out.nodebug.wasm.gz": 18269, - "total": 63720, - "total_gz": 20727, + "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..c5c0a3155ab31 --- /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 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). + +#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')