Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions system/lib/wasmfs/syscalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -197,15 +201,19 @@ 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<DataFile>();

// If file is nullptr, then the file was not a DataFile.
if (!file) {
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;
Expand Down Expand Up @@ -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<decltype(doOpen(0, 0, 0, 0)), unsigned int>,
"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.
Expand Down
8 changes: 4 additions & 4 deletions test/codesize/test_codesize_cxx_wasmfs.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
8 changes: 4 additions & 4 deletions test/codesize/test_codesize_files_wasmfs.json
Original file line number Diff line number Diff line change
@@ -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)",
Expand Down
79 changes: 79 additions & 0 deletions test/fs/test_access_mode.c
Original file line number Diff line number Diff line change
@@ -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 <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

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;
}
8 changes: 8 additions & 0 deletions test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down