From 53b592067630fda20304492ced5b5b64e934a69b Mon Sep 17 00:00:00 2001 From: Kleis Auke Wolthuizen Date: Thu, 17 Jul 2025 21:21:52 +0200 Subject: [PATCH 1/7] [WasmFS] Fix absolute path access under NODERAWFS Resolves: #24830. Resolves: #24836. --- src/lib/libwasmfs_node.js | 6 ++-- system/include/emscripten/wasmfs.h | 7 +++- system/lib/wasmfs/backends/node_backend.cpp | 8 +++-- system/lib/wasmfs/backends/noderawfs_root.cpp | 4 +++ system/lib/wasmfs/wasmfs.cpp | 29 +++++++++++++-- system/lib/wasmfs/wasmfs.h | 3 ++ test/fs/test_fs_dev_random.c | 6 ++-- test/test_core.py | 7 ++-- test/test_other.py | 36 +++++++++++++++---- 9 files changed, 85 insertions(+), 21 deletions(-) diff --git a/src/lib/libwasmfs_node.js b/src/lib/libwasmfs_node.js index bf749467ffdc6..c0165c6bb4eef 100644 --- a/src/lib/libwasmfs_node.js +++ b/src/lib/libwasmfs_node.js @@ -31,7 +31,7 @@ addToLibrary({ if (wasmfsNodeIsWindows) { // Node.js on Windows never represents permission bit 'x', so // propagate read bits to execute bits - stat.mode |= (stat.mode & {{{ cDefs.S_IRUSR | cDefs.S_IRGRP | cDefs.S_IROTH }}}) >> 2; + stat.mode |= (stat.mode & {{{ cDefs.S_IRUGO }}}) >> 2; } return stat; }, @@ -206,7 +206,7 @@ addToLibrary({ return wasmfsTry(() => { // TODO: Cache open file descriptors to guarantee that opened files will // still exist when we try to access them. - let nread = fs.readSync(fd, new Int8Array(HEAPU8.buffer, buf_p, len), 0, len, pos); + let nread = fs.readSync(fd, HEAPU8, buf_p, len, pos); {{{ makeSetValue('nread_p', 0, 'nread', 'i32') }}}; // implicitly return 0 }); @@ -217,7 +217,7 @@ addToLibrary({ return wasmfsTry(() => { // TODO: Cache open file descriptors to guarantee that opened files will // still exist when we try to access them. - let nwritten = fs.writeSync(fd, new Int8Array(HEAPU8.buffer, buf_p, len), 0, len, pos); + let nwritten = fs.writeSync(fd, HEAPU8, buf_p, len, pos); {{{ makeSetValue('nwritten_p', 0, 'nwritten', 'i32') }}}; // implicitly return 0 }); diff --git a/system/include/emscripten/wasmfs.h b/system/include/emscripten/wasmfs.h index 024d0119ad09e..744458661f416 100644 --- a/system/include/emscripten/wasmfs.h +++ b/system/include/emscripten/wasmfs.h @@ -77,7 +77,7 @@ backend_t wasmfs_create_memory_backend(void); // backend_t wasmfs_create_fetch_backend(const char* _Nonnull base_url, uint32_t chunk_size); -backend_t wasmfs_create_node_backend(const char* _Nonnull root); +backend_t wasmfs_create_node_backend(const char* _Nonnull mount_path); // Note: this cannot be called on the browser main thread because it might // deadlock while waiting for the OPFS dedicated worker thread to be spawned. @@ -111,6 +111,11 @@ void wasmfs_flush(void); // default backend is used. backend_t wasmfs_create_root_dir(void); +// A hook users can do to create the working directory. Overriding this allows +// the user to set a particular backend as the CWD. If this is not set then the +// root_backend is used. +backend_t wasmfs_create_working_dir(backend_t root_backend); + // A hook users can do to run code during WasmFS startup. This hook happens // before file preloading, so user code could create backends and mount them, // which would then affect in which backend the preloaded files are loaded (the diff --git a/system/lib/wasmfs/backends/node_backend.cpp b/system/lib/wasmfs/backends/node_backend.cpp index a3a7ad19b32f3..5daabd5556055 100644 --- a/system/lib/wasmfs/backends/node_backend.cpp +++ b/system/lib/wasmfs/backends/node_backend.cpp @@ -191,7 +191,9 @@ class NodeDirectory : public Directory { if (_wasmfs_node_get_mode(childPath.c_str(), &mode)) { return nullptr; } - if (S_ISREG(mode)) { + // Allow reading from character device files (e.g. `/dev/random`, + // `/dev/urandom`) + if (S_ISREG(mode) || S_ISCHR(mode)) { return std::make_shared(mode, getBackend(), childPath); } else if (S_ISDIR(mode)) { return std::make_shared(mode, getBackend(), childPath); @@ -300,8 +302,8 @@ class NodeBackend : public Backend { extern "C" { -backend_t wasmfs_create_node_backend(const char* root) { - return wasmFS.addBackend(std::make_unique(root)); +backend_t wasmfs_create_node_backend(const char* mount_path) { + return wasmFS.addBackend(std::make_unique(mount_path)); } void EMSCRIPTEN_KEEPALIVE _wasmfs_node_record_dirent( diff --git a/system/lib/wasmfs/backends/noderawfs_root.cpp b/system/lib/wasmfs/backends/noderawfs_root.cpp index 6ed7af81daf56..32788b60ba908 100644 --- a/system/lib/wasmfs/backends/noderawfs_root.cpp +++ b/system/lib/wasmfs/backends/noderawfs_root.cpp @@ -6,5 +6,9 @@ #include "emscripten/wasmfs.h" backend_t wasmfs_create_root_dir(void) { + return wasmfs_create_node_backend(""); +} + +backend_t wasmfs_create_working_dir(backend_t root_backend) { return wasmfs_create_node_backend("."); } diff --git a/system/lib/wasmfs/wasmfs.cpp b/system/lib/wasmfs/wasmfs.cpp index 7a771a8ff4bd0..b4fb7b0d563d4 100644 --- a/system/lib/wasmfs/wasmfs.cpp +++ b/system/lib/wasmfs/wasmfs.cpp @@ -37,7 +37,8 @@ __attribute__((init_priority(100))) WasmFS wasmFS; __attribute__((weak)) extern "C" void wasmfs_before_preload(void) {} // Set up global data structures and preload files. -WasmFS::WasmFS() : rootDirectory(initRootDirectory()), cwd(rootDirectory) { +WasmFS::WasmFS() + : rootDirectory(initRootDirectory()), cwd(initCWD(rootDirectory)) { wasmfs_before_preload(); preloadFiles(); } @@ -87,8 +88,8 @@ WasmFS::~WasmFS() { rootDirectory->locked().setParent(nullptr); } -// Special backends that want to install themselves as the root use this hook. -// Otherwise, we use the default backends. +// Hooks for backends that want to install themselves as the root or CWD. +// Otherwise, we use the default memory backends. __attribute__((weak)) extern "C" backend_t wasmfs_create_root_dir(void) { #ifdef WASMFS_CASE_INSENSITIVE return createIgnoreCaseBackend([]() { return createMemoryBackend(); }); @@ -97,6 +98,11 @@ __attribute__((weak)) extern "C" backend_t wasmfs_create_root_dir(void) { #endif } +__attribute__((weak)) extern "C" backend_t +wasmfs_create_working_dir(backend_t root_backend) { + return root_backend; +} + std::shared_ptr WasmFS::initRootDirectory() { auto rootBackend = wasmfs_create_root_dir(); auto rootDirectory = @@ -126,6 +132,23 @@ std::shared_ptr WasmFS::initRootDirectory() { return rootDirectory; } +std::shared_ptr +WasmFS::initCWD(const std::shared_ptr& root) { + auto rootBackend = root->getBackend(); + auto cwdBackend = wasmfs_create_working_dir(rootBackend); + if (rootBackend == cwdBackend) { + return root; + } + + auto cwd = cwdBackend->createDirectory(S_IRUGO | S_IXUGO | S_IWUGO); + auto lockedCwd = cwd->locked(); + + // The root directory is the parent of the CWD. + lockedCwd.setParent(root); + + return cwd; +} + // Initialize files specified by the --preload-file option. // Set up directories and files in wasmFS$preloadedDirs and // wasmFS$preloadedFiles from JS. This function will be called before any file diff --git a/system/lib/wasmfs/wasmfs.h b/system/lib/wasmfs/wasmfs.h index 682d53064ee77..d369239a769c0 100644 --- a/system/lib/wasmfs/wasmfs.h +++ b/system/lib/wasmfs/wasmfs.h @@ -32,6 +32,9 @@ class WasmFS { // dev/stderr. Refers to the same std streams in the open file table. std::shared_ptr initRootDirectory(); + // Private method to initialize CWD once. + std::shared_ptr initCWD(const std::shared_ptr& root); + // Initialize files specified by --preload-file option. void preloadFiles(); diff --git a/test/fs/test_fs_dev_random.c b/test/fs/test_fs_dev_random.c index 4200f0bdc8139..a88a04fc75619 100644 --- a/test/fs/test_fs_dev_random.c +++ b/test/fs/test_fs_dev_random.c @@ -13,12 +13,14 @@ int main() { int nread; fp = fopen("/dev/random", "r"); - nread = fread(&data, 1, byte_count, fp); + assert(fp != NULL); + nread = fread(data, 1, byte_count, fp); assert(nread == byte_count); fclose(fp); fp = fopen("/dev/urandom", "r"); - nread = fread(&data, 1, byte_count, fp); + assert(fp != NULL); + nread = fread(data, 1, byte_count, fp); assert(nread == byte_count); fclose(fp); diff --git a/test/test_core.py b/test/test_core.py index dfdbdb1eba9ed..e190eb54ce570 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -6149,9 +6149,12 @@ def test_unistd_unlink(self): # Several differences/bugs on non-linux including https://github.com/nodejs/node/issues/18014 # TODO: NODERAWFS in WasmFS - if '-DNODERAWFS' in self.cflags and os.geteuid() == 0: + if '-DNODERAWFS' in self.cflags: # 0 if root user - self.cflags += ['-DSKIP_ACCESS_TESTS'] + if os.geteuid() == 0: + self.cflags += ['-DSKIP_ACCESS_TESTS'] + if self.get_setting('WASMFS'): + self.skipTest('https://github.com/emscripten-core/emscripten/issues/18112') self.do_runf('unistd/unlink.c', 'success') diff --git a/test/test_other.py b/test/test_other.py index 936dff4783e1c..7c33c07af1f1b 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -9255,16 +9255,43 @@ def test_noderawfs_disables_embedding(self): self.assert_fail(base + ['--preload-file', 'somefile'], expected) self.assert_fail(base + ['--embed-file', 'somefile'], expected) + @crossplatform + @also_with_wasmfs def test_noderawfs_access_abspath(self): create_file('foo', 'bar') create_file('access.c', r''' + #include + #include #include int main(int argc, char** argv) { - return access(argv[1], F_OK); + printf("testing access to %s\n", argv[1]); + int rtn = access(argv[1], F_OK); + assert(rtn == 0); + return 0; } ''') self.do_runf('access.c', cflags=['-sNODERAWFS'], args=[os.path.abspath('foo')]) + @crossplatform + @also_with_wasmfs + def test_noderawfs_open_abspath(self): + create_file('foo', 'bar') + create_file('open.c', r''' + #include + #include + #include + #include + int main(int argc, char** argv) { + printf("testing open to %s\n", argv[1]); + int fd = open(argv[1], O_RDONLY, 0644); + assert(fd >= 0); + int rtn = close(fd); + assert(rtn == 0); + return 0; + } + ''') + self.do_runf('open.c', cflags=['-sNODERAWFS'], args=[os.path.abspath('foo')]) + def test_noderawfs_readfile_prerun(self): create_file('foo', 'bar') self.add_pre_run("console.log(FS.readFile('foo', { encoding: 'utf8' }));") @@ -13210,11 +13237,10 @@ def test_unistd_chown(self): self.set_setting('WASMFS') self.do_run_in_out_file_test('wasmfs/wasmfs_chown.c') - @wasmfs_all_backends def test_wasmfs_getdents(self): # Run only in WASMFS for now. self.set_setting('FORCE_FILESYSTEM') - self.do_run_in_out_file_test('wasmfs/wasmfs_getdents.c') + self.do_run_in_out_file_test('wasmfs/wasmfs_getdents.c', cflags=['-sWASMFS']) def test_wasmfs_jsfile(self): self.set_setting('WASMFS') @@ -13801,15 +13827,11 @@ def test_fs_icase(self): @crossplatform @with_all_fs def test_std_filesystem(self): - if self.get_setting('NODERAWFS') and self.get_setting('WASMFS'): - self.skipTest('https://github.com/emscripten-core/emscripten/issues/24830') self.do_other_test('test_std_filesystem.cpp') @crossplatform @with_all_fs def test_std_filesystem_tempdir(self): - if self.get_setting('NODERAWFS') and self.get_setting('WASMFS'): - self.skipTest('https://github.com/emscripten-core/emscripten/issues/24830') self.do_other_test('test_std_filesystem_tempdir.cpp', cflags=['-g']) def test_strict_js_closure(self): From 0a76c59cc82c5e18f1e3d2f596f11c7bee853ea0 Mon Sep 17 00:00:00 2001 From: Kleis Auke Wolthuizen Date: Tue, 24 Mar 2026 10:17:10 +0100 Subject: [PATCH 2/7] [WiP] Try to fix Windows CI --- src/lib/libwasmfs_node.js | 23 ++++++++++++++----- system/lib/wasmfs/backend.h | 3 +++ .../wasmfs/backends/ignore_case_backend.cpp | 8 +++++++ system/lib/wasmfs/backends/memory_backend.cpp | 6 +++++ system/lib/wasmfs/backends/node_backend.cpp | 13 +++++++++++ system/lib/wasmfs/backends/node_backend.h | 3 +++ system/lib/wasmfs/backends/opfs_backend.cpp | 8 +++++++ system/lib/wasmfs/js_impl_backend.h | 6 +++++ system/lib/wasmfs/paths.cpp | 6 +++-- .../wasmfs/proxied_async_js_impl_backend.h | 8 +++++++ 10 files changed, 76 insertions(+), 8 deletions(-) diff --git a/src/lib/libwasmfs_node.js b/src/lib/libwasmfs_node.js index c0165c6bb4eef..ff94b68a77458 100644 --- a/src/lib/libwasmfs_node.js +++ b/src/lib/libwasmfs_node.js @@ -6,6 +6,7 @@ addToLibrary({ $wasmfsNodeIsWindows: !!process.platform.match(/^win/), + $nodePath: "require('node:path')", $wasmfsNodeConvertNodeCode__deps: ['$ERRNO_CODES'], $wasmfsNodeConvertNodeCode: (e) => { @@ -157,15 +158,15 @@ addToLibrary({ }, _wasmfs_node_truncate__i53abi: true, - _wasmfs_node_truncate__deps : ['$wasmfsTry'], - _wasmfs_node_truncate : (path_p, len) => { + _wasmfs_node_truncate__deps: ['$wasmfsTry'], + _wasmfs_node_truncate: (path_p, len) => { if (isNaN(len)) return -{{{ cDefs.EOVERFLOW }}}; return wasmfsTry(() => fs.truncateSync(UTF8ToString(path_p), len)); }, _wasmfs_node_ftruncate__i53abi: true, - _wasmfs_node_ftruncate__deps : ['$wasmfsTry'], - _wasmfs_node_ftruncate : (fd, len) => { + _wasmfs_node_ftruncate__deps: ['$wasmfsTry'], + _wasmfs_node_ftruncate: (fd, len) => { if (isNaN(len)) return -{{{ cDefs.EOVERFLOW }}}; return wasmfsTry(() => fs.ftruncateSync(fd, len)); }, @@ -212,8 +213,8 @@ addToLibrary({ }); }, - _wasmfs_node_write__deps : ['$wasmfsTry'], - _wasmfs_node_write : (fd, buf_p, len, pos, nwritten_p) => { + _wasmfs_node_write__deps: ['$wasmfsTry'], + _wasmfs_node_write: (fd, buf_p, len, pos, nwritten_p) => { return wasmfsTry(() => { // TODO: Cache open file descriptors to guarantee that opened files will // still exist when we try to access them. @@ -223,4 +224,14 @@ addToLibrary({ }); }, + _wasmfs_node_path_is_abs__deps: ['$nodePath'], + _wasmfs_node_path_is_abs: (path_p) => { + return nodePath.isAbsolute(UTF8ToString(path_p)); + }, + + _wasmfs_node_path_get_root__deps: ['$nodePath'], + _wasmfs_node_path_get_root: (abs_path_p, target_p, bufsize) => { + return stringToUTF8(nodePath.parse(UTF8ToString(abs_path_p)).root, target_p, bufsize); + }, + }); diff --git a/system/lib/wasmfs/backend.h b/system/lib/wasmfs/backend.h index d8e584c01136a..31761a37764d5 100644 --- a/system/lib/wasmfs/backend.h +++ b/system/lib/wasmfs/backend.h @@ -21,6 +21,9 @@ class Backend { virtual std::shared_ptr createDirectory(mode_t mode) = 0; virtual std::shared_ptr createSymlink(std::string target) = 0; + virtual bool isAbsolutePath(std::string_view path) = 0; + virtual std::string getRootPath(std::string_view abs_path) = 0; + virtual ~Backend() = default; }; diff --git a/system/lib/wasmfs/backends/ignore_case_backend.cpp b/system/lib/wasmfs/backends/ignore_case_backend.cpp index ba1f1f4e0b72a..46540ddf438e0 100644 --- a/system/lib/wasmfs/backends/ignore_case_backend.cpp +++ b/system/lib/wasmfs/backends/ignore_case_backend.cpp @@ -225,6 +225,14 @@ class IgnoreCaseBackend : public Backend { std::shared_ptr createSymlink(std::string target) override { return virtualize(backend->createSymlink(target), this); } + + bool isAbsolutePath(std::string_view path) override { + return path.front() == '/'; + } + + std::string getRootPath(std::string_view abs_path) override { + return "/"; + } }; // Create an ignore case backend by supplying another backend. diff --git a/system/lib/wasmfs/backends/memory_backend.cpp b/system/lib/wasmfs/backends/memory_backend.cpp index 6752b3bbbb5bd..5c9ffa4a33582 100644 --- a/system/lib/wasmfs/backends/memory_backend.cpp +++ b/system/lib/wasmfs/backends/memory_backend.cpp @@ -104,6 +104,12 @@ class MemoryBackend : public Backend { std::shared_ptr createSymlink(std::string target) override { return std::make_shared(target, this); } + bool isAbsolutePath(std::string_view path) override { + return path.front() == '/'; + } + std::string getRootPath(std::string_view abs_path) override { + return "/"; + } }; backend_t createMemoryBackend() { diff --git a/system/lib/wasmfs/backends/node_backend.cpp b/system/lib/wasmfs/backends/node_backend.cpp index 5daabd5556055..d54704c5ad004 100644 --- a/system/lib/wasmfs/backends/node_backend.cpp +++ b/system/lib/wasmfs/backends/node_backend.cpp @@ -296,6 +296,19 @@ class NodeBackend : public Backend { std::shared_ptr createSymlink(std::string target) override { WASMFS_UNREACHABLE("TODO: implement NodeBackend::createSymlink"); } + + bool isAbsolutePath(std::string_view path) override { + return _wasmfs_node_path_is_abs(std::string(path).c_str()); + } + + std::string getRootPath(std::string_view abs_path) override { + char buf[PATH_MAX]; + auto path = std::string(abs_path); + if (_wasmfs_node_path_get_root(path.c_str(), buf, PATH_MAX) < 0) { + WASMFS_UNREACHABLE("getRootPath cannot fail"); + } + return std::string(buf); + } }; // TODO: symlink diff --git a/system/lib/wasmfs/backends/node_backend.h b/system/lib/wasmfs/backends/node_backend.h index 7e6e496b7c73f..ca276e8d2de89 100644 --- a/system/lib/wasmfs/backends/node_backend.h +++ b/system/lib/wasmfs/backends/node_backend.h @@ -52,4 +52,7 @@ int _wasmfs_node_read( int _wasmfs_node_write( int fd, const void* buf, uint32_t len, uint32_t pos, uint32_t* nwritten); +int _wasmfs_node_path_is_abs(const char* path); +int _wasmfs_node_path_get_root(const char* abs_path, const char *buf, int bufsize); + } // extern "C" diff --git a/system/lib/wasmfs/backends/opfs_backend.cpp b/system/lib/wasmfs/backends/opfs_backend.cpp index f5a014f5d83b5..32c3432cbc054 100644 --- a/system/lib/wasmfs/backends/opfs_backend.cpp +++ b/system/lib/wasmfs/backends/opfs_backend.cpp @@ -393,6 +393,14 @@ class OPFSBackend : public Backend { // Symlinks not supported. return nullptr; } + + bool isAbsolutePath(std::string_view path) override { + return path.front() == '/'; + } + + std::string getRootPath(std::string_view abs_path) override { + return "/"; + } }; } // anonymous namespace diff --git a/system/lib/wasmfs/js_impl_backend.h b/system/lib/wasmfs/js_impl_backend.h index 38d4b59c61bdc..2a638baefef6e 100644 --- a/system/lib/wasmfs/js_impl_backend.h +++ b/system/lib/wasmfs/js_impl_backend.h @@ -120,6 +120,12 @@ class JSImplBackend : public Backend { std::shared_ptr createSymlink(std::string target) override { return std::make_shared(target, this); } + bool isAbsolutePath(std::string_view path) override { + return path.front() == '/'; + } + std::string getRootPath(std::string_view abs_path) override { + return "/"; + } }; extern "C" { diff --git a/system/lib/wasmfs/paths.cpp b/system/lib/wasmfs/paths.cpp index 3d1fbf4c081e8..6033c996cf291 100644 --- a/system/lib/wasmfs/paths.cpp +++ b/system/lib/wasmfs/paths.cpp @@ -69,10 +69,12 @@ ParsedParent doParseParent(std::string_view path, return {-ENOENT}; } + auto currBackend = curr->getBackend(); + // Handle absolute paths. - if (path.front() == '/') { + if (currBackend->isAbsolutePath(path)) { curr = wasmFS.getRootDirectory(); - path.remove_prefix(1); + path.remove_prefix(currBackend->getRootPath(path).size()); } // Ignore trailing '/'. diff --git a/system/lib/wasmfs/proxied_async_js_impl_backend.h b/system/lib/wasmfs/proxied_async_js_impl_backend.h index e91ec561ef5ff..0c603244b394b 100644 --- a/system/lib/wasmfs/proxied_async_js_impl_backend.h +++ b/system/lib/wasmfs/proxied_async_js_impl_backend.h @@ -161,6 +161,14 @@ class ProxiedAsyncJSBackend : public Backend { std::shared_ptr createSymlink(std::string target) override { return std::make_shared(target, this); } + + bool isAbsolutePath(std::string_view path) override { + return path.front() == '/'; + } + + std::string getRootPath(std::string_view abs_path) override { + return "/"; + } }; } // namespace wasmfs From 4e6a56848ea772aa267399b279d1053d7a4c63ca Mon Sep 17 00:00:00 2001 From: Kleis Auke Wolthuizen Date: Tue, 24 Mar 2026 10:26:46 +0100 Subject: [PATCH 3/7] Simplify --- src/lib/libsigs.js | 1 + src/lib/libwasmfs_node.js | 9 ++------- system/lib/wasmfs/backend.h | 3 +-- system/lib/wasmfs/backends/ignore_case_backend.cpp | 8 ++------ system/lib/wasmfs/backends/memory_backend.cpp | 7 ++----- system/lib/wasmfs/backends/node_backend.cpp | 10 +++------- system/lib/wasmfs/backends/node_backend.h | 3 +-- system/lib/wasmfs/backends/opfs_backend.cpp | 8 ++------ system/lib/wasmfs/js_impl_backend.h | 7 ++----- system/lib/wasmfs/paths.cpp | 6 +++--- system/lib/wasmfs/proxied_async_js_impl_backend.h | 8 ++------ test/test_other.py | 4 +++- 12 files changed, 24 insertions(+), 50 deletions(-) diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index bb990b3f9c142..7182a4733b380 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -411,6 +411,7 @@ sigs = { _wasmfs_node_insert_directory__sig: 'ipi', _wasmfs_node_insert_file__sig: 'ipi', _wasmfs_node_open__sig: 'ipp', + _wasmfs_node_path_get_root__sig: 'ippi', _wasmfs_node_read__sig: 'iipiip', _wasmfs_node_readdir__sig: 'ipp', _wasmfs_node_readlink__sig: 'ippi', diff --git a/src/lib/libwasmfs_node.js b/src/lib/libwasmfs_node.js index ff94b68a77458..62a099734f094 100644 --- a/src/lib/libwasmfs_node.js +++ b/src/lib/libwasmfs_node.js @@ -224,14 +224,9 @@ addToLibrary({ }); }, - _wasmfs_node_path_is_abs__deps: ['$nodePath'], - _wasmfs_node_path_is_abs: (path_p) => { - return nodePath.isAbsolute(UTF8ToString(path_p)); - }, - _wasmfs_node_path_get_root__deps: ['$nodePath'], - _wasmfs_node_path_get_root: (abs_path_p, target_p, bufsize) => { - return stringToUTF8(nodePath.parse(UTF8ToString(abs_path_p)).root, target_p, bufsize); + _wasmfs_node_path_get_root: (path_p, target_p, bufsize) => { + return stringToUTF8(nodePath.parse(UTF8ToString(path_p)).root, target_p, bufsize); }, }); diff --git a/system/lib/wasmfs/backend.h b/system/lib/wasmfs/backend.h index 31761a37764d5..6b96189464f5e 100644 --- a/system/lib/wasmfs/backend.h +++ b/system/lib/wasmfs/backend.h @@ -21,8 +21,7 @@ class Backend { virtual std::shared_ptr createDirectory(mode_t mode) = 0; virtual std::shared_ptr createSymlink(std::string target) = 0; - virtual bool isAbsolutePath(std::string_view path) = 0; - virtual std::string getRootPath(std::string_view abs_path) = 0; + virtual std::string getRootPath(std::string_view path) = 0; virtual ~Backend() = default; }; diff --git a/system/lib/wasmfs/backends/ignore_case_backend.cpp b/system/lib/wasmfs/backends/ignore_case_backend.cpp index 46540ddf438e0..992f9f4788d06 100644 --- a/system/lib/wasmfs/backends/ignore_case_backend.cpp +++ b/system/lib/wasmfs/backends/ignore_case_backend.cpp @@ -226,12 +226,8 @@ class IgnoreCaseBackend : public Backend { return virtualize(backend->createSymlink(target), this); } - bool isAbsolutePath(std::string_view path) override { - return path.front() == '/'; - } - - std::string getRootPath(std::string_view abs_path) override { - return "/"; + std::string getRootPath(std::string_view path) override { + return path.front() == '/' ? "/" : ""; } }; diff --git a/system/lib/wasmfs/backends/memory_backend.cpp b/system/lib/wasmfs/backends/memory_backend.cpp index 5c9ffa4a33582..975d0ccf8cf3f 100644 --- a/system/lib/wasmfs/backends/memory_backend.cpp +++ b/system/lib/wasmfs/backends/memory_backend.cpp @@ -104,11 +104,8 @@ class MemoryBackend : public Backend { std::shared_ptr createSymlink(std::string target) override { return std::make_shared(target, this); } - bool isAbsolutePath(std::string_view path) override { - return path.front() == '/'; - } - std::string getRootPath(std::string_view abs_path) override { - return "/"; + std::string getRootPath(std::string_view path) override { + return path.front() == '/' ? "/" : ""; } }; diff --git a/system/lib/wasmfs/backends/node_backend.cpp b/system/lib/wasmfs/backends/node_backend.cpp index d54704c5ad004..73776adb434f4 100644 --- a/system/lib/wasmfs/backends/node_backend.cpp +++ b/system/lib/wasmfs/backends/node_backend.cpp @@ -297,14 +297,10 @@ class NodeBackend : public Backend { WASMFS_UNREACHABLE("TODO: implement NodeBackend::createSymlink"); } - bool isAbsolutePath(std::string_view path) override { - return _wasmfs_node_path_is_abs(std::string(path).c_str()); - } - - std::string getRootPath(std::string_view abs_path) override { + std::string getRootPath(std::string_view path) override { char buf[PATH_MAX]; - auto path = std::string(abs_path); - if (_wasmfs_node_path_get_root(path.c_str(), buf, PATH_MAX) < 0) { + auto pathStr = std::string(path); + if (_wasmfs_node_path_get_root(pathStr.c_str(), buf, PATH_MAX) < 0) { WASMFS_UNREACHABLE("getRootPath cannot fail"); } return std::string(buf); diff --git a/system/lib/wasmfs/backends/node_backend.h b/system/lib/wasmfs/backends/node_backend.h index ca276e8d2de89..f8318c940d16d 100644 --- a/system/lib/wasmfs/backends/node_backend.h +++ b/system/lib/wasmfs/backends/node_backend.h @@ -52,7 +52,6 @@ int _wasmfs_node_read( int _wasmfs_node_write( int fd, const void* buf, uint32_t len, uint32_t pos, uint32_t* nwritten); -int _wasmfs_node_path_is_abs(const char* path); -int _wasmfs_node_path_get_root(const char* abs_path, const char *buf, int bufsize); +int _wasmfs_node_path_get_root(const char* path, const char *buf, int bufsize); } // extern "C" diff --git a/system/lib/wasmfs/backends/opfs_backend.cpp b/system/lib/wasmfs/backends/opfs_backend.cpp index 32c3432cbc054..81287d49003d6 100644 --- a/system/lib/wasmfs/backends/opfs_backend.cpp +++ b/system/lib/wasmfs/backends/opfs_backend.cpp @@ -394,12 +394,8 @@ class OPFSBackend : public Backend { return nullptr; } - bool isAbsolutePath(std::string_view path) override { - return path.front() == '/'; - } - - std::string getRootPath(std::string_view abs_path) override { - return "/"; + std::string getRootPath(std::string_view path) override { + return path.front() == '/' ? "/" : ""; } }; diff --git a/system/lib/wasmfs/js_impl_backend.h b/system/lib/wasmfs/js_impl_backend.h index 2a638baefef6e..a2b5cd425faa8 100644 --- a/system/lib/wasmfs/js_impl_backend.h +++ b/system/lib/wasmfs/js_impl_backend.h @@ -120,11 +120,8 @@ class JSImplBackend : public Backend { std::shared_ptr createSymlink(std::string target) override { return std::make_shared(target, this); } - bool isAbsolutePath(std::string_view path) override { - return path.front() == '/'; - } - std::string getRootPath(std::string_view abs_path) override { - return "/"; + std::string getRootPath(std::string_view path) override { + return path.front() == '/' ? "/" : ""; } }; diff --git a/system/lib/wasmfs/paths.cpp b/system/lib/wasmfs/paths.cpp index 6033c996cf291..8262d12b3003a 100644 --- a/system/lib/wasmfs/paths.cpp +++ b/system/lib/wasmfs/paths.cpp @@ -69,12 +69,12 @@ ParsedParent doParseParent(std::string_view path, return {-ENOENT}; } - auto currBackend = curr->getBackend(); + auto rootPath = curr->getBackend()->getRootPath(path); // Handle absolute paths. - if (currBackend->isAbsolutePath(path)) { + if (!rootPath.empty()) { curr = wasmFS.getRootDirectory(); - path.remove_prefix(currBackend->getRootPath(path).size()); + path.remove_prefix(rootPath.size()); } // Ignore trailing '/'. diff --git a/system/lib/wasmfs/proxied_async_js_impl_backend.h b/system/lib/wasmfs/proxied_async_js_impl_backend.h index 0c603244b394b..aab1b95ebaffc 100644 --- a/system/lib/wasmfs/proxied_async_js_impl_backend.h +++ b/system/lib/wasmfs/proxied_async_js_impl_backend.h @@ -162,12 +162,8 @@ class ProxiedAsyncJSBackend : public Backend { return std::make_shared(target, this); } - bool isAbsolutePath(std::string_view path) override { - return path.front() == '/'; - } - - std::string getRootPath(std::string_view abs_path) override { - return "/"; + std::string getRootPath(std::string_view path) override { + return path.front() == '/' ? "/" : ""; } }; diff --git a/test/test_other.py b/test/test_other.py index 7c33c07af1f1b..a82139b75e735 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -13827,7 +13827,9 @@ def test_fs_icase(self): @crossplatform @with_all_fs def test_std_filesystem(self): - self.do_other_test('test_std_filesystem.cpp') + # build with `-fexceptions` to catch `filesystem_error`. Otherwise, the follwing exception may occur: + # "filesystem_error was thrown in -fno-exceptions mode" + self.do_other_test('test_std_filesystem.cpp', cflags=['-fexceptions']) @crossplatform @with_all_fs From 7263eff3a1ad2ce2e512449d13c4853e6201bdd9 Mon Sep 17 00:00:00 2001 From: Kleis Auke Wolthuizen Date: Tue, 24 Mar 2026 11:18:12 +0100 Subject: [PATCH 4/7] [WiP] Try to fix Windows CI (2) --- src/lib/libwasmfs_node.js | 2 +- test/other/test_std_filesystem.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/libwasmfs_node.js b/src/lib/libwasmfs_node.js index 62a099734f094..74ce3858118bd 100644 --- a/src/lib/libwasmfs_node.js +++ b/src/lib/libwasmfs_node.js @@ -5,7 +5,7 @@ */ addToLibrary({ - $wasmfsNodeIsWindows: !!process.platform.match(/^win/), + $wasmfsNodeIsWindows: "!!process.platform.match(/^win/)", $nodePath: "require('node:path')", $wasmfsNodeConvertNodeCode__deps: ['$ERRNO_CODES'], diff --git a/test/other/test_std_filesystem.cpp b/test/other/test_std_filesystem.cpp index 41cc2275d08d4..894a129bdf5f7 100644 --- a/test/other/test_std_filesystem.cpp +++ b/test/other/test_std_filesystem.cpp @@ -13,7 +13,7 @@ int main() { std::cout << "remove(): " << fs::remove(tmp / foo) << '\n'; // success std::cout << "remove(): " << fs::remove(tmp / foo) << '\n'; // fail - std::filesystem::create_directories(tmp / "abcdef/example"); + std::filesystem::create_directories(tmp / "abcdef" / "example"); const std::uintmax_t n{fs::remove_all(tmp / "abcdef")}; std::cout << "remove_all(): " << n << " files or directories\n"; } From fd7f0a9258b8d2ca8086e7c737d08fead27ddbc8 Mon Sep 17 00:00:00 2001 From: Kleis Auke Wolthuizen Date: Tue, 24 Mar 2026 11:24:04 +0100 Subject: [PATCH 5/7] [WiP] Try to fix Closure when building with `-sENVIRONMENT=web` Details: ``` building:ERROR: Closure compiler run failed: building:ERROR: /tmp/emscripten_temp_j4p9p4rt/vips-es6.jso3.js:5791:28: ERROR - [JSC_UNDEFINED_VARIABLE] variable process is undeclared 5791| var wasmfsNodeIsWindows = !!process.platform.match(/^win/); ^^^^^^^ building:ERROR: /tmp/emscripten_temp_j4p9p4rt/vips-es6.jso3.js:6006:15: ERROR - [JSC_UNDEFINED_VARIABLE] variable require is undeclared 6006| var nodePath = require("node:path"); ^^^^^^^ 2 error(s), 0 warning(s) ``` --- src/lib/libwasmfs_node.js | 28 ++++++++++++++++++++++++---- test/test_other.py | 25 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/lib/libwasmfs_node.js b/src/lib/libwasmfs_node.js index 74ce3858118bd..475c0e4880674 100644 --- a/src/lib/libwasmfs_node.js +++ b/src/lib/libwasmfs_node.js @@ -5,8 +5,28 @@ */ addToLibrary({ - $wasmfsNodeIsWindows: "!!process.platform.match(/^win/)", - $nodePath: "require('node:path')", +#if !ENVIRONMENT_MAY_BE_NODE + _wasmfs_node_readdir: (path_p, vec) => {}, + _wasmfs_node_get_mode: (path_p, mode_p) => {}, + _wasmfs_node_stat_size: (path_p, size_p) => {}, + _wasmfs_node_fstat_size: (fd, size_p) => {}, + _wasmfs_node_insert_file: (path_p, mode) => {}, + _wasmfs_node_insert_directory: (path_p, mode) => {}, + _wasmfs_node_unlink: (path_p) => {}, + _wasmfs_node_rmdir: (path_p) => {}, + _wasmfs_node_truncate: (path_p, len) => {}, + _wasmfs_node_ftruncate: (fd, len) => {}, + _wasmfs_node_open: (path_p, mode_p) => {}, + _wasmfs_node_rename: (from_path_p, to_path_p) => {}, + _wasmfs_node_symlink: (target_path_p, linkpath_path_p) => {}, + _wasmfs_node_readlink: (path_p, target_p, bufsize) => {}, + _wasmfs_node_close: (fd) => {}, + _wasmfs_node_read: (fd, buf_p, len, pos, nread_p) => {}, + _wasmfs_node_write: (fd, buf_p, len, pos, nwritten_p) => {}, + _wasmfs_node_path_get_root: (path_p, target_p, bufsize) => {}, +#else + $wasmfsNodeIsWindows: "!!globalThis.process?.platform.match(/^win/)", + $nodePath: "ENVIRONMENT_IS_NODE ? require('node:path') : undefined", $wasmfsNodeConvertNodeCode__deps: ['$ERRNO_CODES'], $wasmfsNodeConvertNodeCode: (e) => { @@ -194,7 +214,7 @@ addToLibrary({ }); }, - _wasmfs_node_close__deps: [], + _wasmfs_node_close__deps: ['$wasmfsTry'], _wasmfs_node_close: (fd) => { return wasmfsTry(() => { fs.closeSync(fd); @@ -228,5 +248,5 @@ addToLibrary({ _wasmfs_node_path_get_root: (path_p, target_p, bufsize) => { return stringToUTF8(nodePath.parse(UTF8ToString(path_p)).root, target_p, bufsize); }, - +#endif }); diff --git a/test/test_other.py b/test/test_other.py index a82139b75e735..fea596148b975 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -9292,6 +9292,31 @@ def test_noderawfs_open_abspath(self): ''') self.do_runf('open.c', cflags=['-sNODERAWFS'], args=[os.path.abspath('foo')]) + def test_wasmfs_nodefs_stubs(self): + # This is essentially equivalent to building with `-sWASMFS -sNODERAWFS`, except that + # the Wasm binary can also be used on the web. Ensure that this use case is supported. + create_file('main.c', r''' + #include + #include + + EM_JS(bool, is_node, (), { return ENVIRONMENT_IS_NODE; }); + + backend_t wasmfs_create_root_dir() { + return is_node() ? wasmfs_create_node_backend("") + : wasmfs_create_memory_backend(); + } + + backend_t wasmfs_create_working_dir(backend_t root_backend) { + return is_node() ? wasmfs_create_node_backend(".") + : root_backend; + } + + int main(int argc, char** argv) { + return 0; + } + ''') + self.run_process([EMCC, 'main.c', '-sWASMFS', '-sENVIRONMENT=web']) + def test_noderawfs_readfile_prerun(self): create_file('foo', 'bar') self.add_pre_run("console.log(FS.readFile('foo', { encoding: 'utf8' }));") From 3751c1b13f05b06b7559e86f953f301555f74fc2 Mon Sep 17 00:00:00 2001 From: Kleis Auke Wolthuizen Date: Tue, 24 Mar 2026 13:54:33 +0100 Subject: [PATCH 6/7] Revert debugging --- system/lib/wasmfs/backends/node_backend.cpp | 2 +- test/other/test_std_filesystem.cpp | 2 +- test/test_other.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/system/lib/wasmfs/backends/node_backend.cpp b/system/lib/wasmfs/backends/node_backend.cpp index 73776adb434f4..24e78bc63d6a7 100644 --- a/system/lib/wasmfs/backends/node_backend.cpp +++ b/system/lib/wasmfs/backends/node_backend.cpp @@ -191,7 +191,7 @@ class NodeDirectory : public Directory { if (_wasmfs_node_get_mode(childPath.c_str(), &mode)) { return nullptr; } - // Allow reading from character device files (e.g. `/dev/random`, + // Allow reading from character device files too (e.g. `/dev/random`, // `/dev/urandom`) if (S_ISREG(mode) || S_ISCHR(mode)) { return std::make_shared(mode, getBackend(), childPath); diff --git a/test/other/test_std_filesystem.cpp b/test/other/test_std_filesystem.cpp index 894a129bdf5f7..41cc2275d08d4 100644 --- a/test/other/test_std_filesystem.cpp +++ b/test/other/test_std_filesystem.cpp @@ -13,7 +13,7 @@ int main() { std::cout << "remove(): " << fs::remove(tmp / foo) << '\n'; // success std::cout << "remove(): " << fs::remove(tmp / foo) << '\n'; // fail - std::filesystem::create_directories(tmp / "abcdef" / "example"); + std::filesystem::create_directories(tmp / "abcdef/example"); const std::uintmax_t n{fs::remove_all(tmp / "abcdef")}; std::cout << "remove_all(): " << n << " files or directories\n"; } diff --git a/test/test_other.py b/test/test_other.py index fea596148b975..5c58c4afe378e 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -13852,9 +13852,9 @@ def test_fs_icase(self): @crossplatform @with_all_fs def test_std_filesystem(self): - # build with `-fexceptions` to catch `filesystem_error`. Otherwise, the follwing exception may occur: - # "filesystem_error was thrown in -fno-exceptions mode" - self.do_other_test('test_std_filesystem.cpp', cflags=['-fexceptions']) + if (WINDOWS or MACOS) and self.get_setting('NODERAWFS') and self.get_setting('WASMFS'): + self.skipTest('fails with ENOTEMPTY (Directory not empty) during fs::remove_all') + self.do_other_test('test_std_filesystem.cpp') @crossplatform @with_all_fs From 1c4adc86ae40564b6364df0636f465801822a0c4 Mon Sep 17 00:00:00 2001 From: Kleis Auke Wolthuizen Date: Tue, 24 Mar 2026 20:10:45 +0100 Subject: [PATCH 7/7] Simplify --- system/lib/wasmfs/backend.h | 4 +++- system/lib/wasmfs/backends/ignore_case_backend.cpp | 4 ---- system/lib/wasmfs/backends/memory_backend.cpp | 3 --- system/lib/wasmfs/backends/opfs_backend.cpp | 4 ---- system/lib/wasmfs/js_impl_backend.h | 3 --- system/lib/wasmfs/proxied_async_js_impl_backend.h | 4 ---- 6 files changed, 3 insertions(+), 19 deletions(-) diff --git a/system/lib/wasmfs/backend.h b/system/lib/wasmfs/backend.h index 6b96189464f5e..18a9c305d322d 100644 --- a/system/lib/wasmfs/backend.h +++ b/system/lib/wasmfs/backend.h @@ -21,7 +21,9 @@ class Backend { virtual std::shared_ptr createDirectory(mode_t mode) = 0; virtual std::shared_ptr createSymlink(std::string target) = 0; - virtual std::string getRootPath(std::string_view path) = 0; + virtual std::string getRootPath(std::string_view path) { + return path.front() == '/' ? "/" : ""; + } virtual ~Backend() = default; }; diff --git a/system/lib/wasmfs/backends/ignore_case_backend.cpp b/system/lib/wasmfs/backends/ignore_case_backend.cpp index 992f9f4788d06..ba1f1f4e0b72a 100644 --- a/system/lib/wasmfs/backends/ignore_case_backend.cpp +++ b/system/lib/wasmfs/backends/ignore_case_backend.cpp @@ -225,10 +225,6 @@ class IgnoreCaseBackend : public Backend { std::shared_ptr createSymlink(std::string target) override { return virtualize(backend->createSymlink(target), this); } - - std::string getRootPath(std::string_view path) override { - return path.front() == '/' ? "/" : ""; - } }; // Create an ignore case backend by supplying another backend. diff --git a/system/lib/wasmfs/backends/memory_backend.cpp b/system/lib/wasmfs/backends/memory_backend.cpp index 975d0ccf8cf3f..6752b3bbbb5bd 100644 --- a/system/lib/wasmfs/backends/memory_backend.cpp +++ b/system/lib/wasmfs/backends/memory_backend.cpp @@ -104,9 +104,6 @@ class MemoryBackend : public Backend { std::shared_ptr createSymlink(std::string target) override { return std::make_shared(target, this); } - std::string getRootPath(std::string_view path) override { - return path.front() == '/' ? "/" : ""; - } }; backend_t createMemoryBackend() { diff --git a/system/lib/wasmfs/backends/opfs_backend.cpp b/system/lib/wasmfs/backends/opfs_backend.cpp index 81287d49003d6..f5a014f5d83b5 100644 --- a/system/lib/wasmfs/backends/opfs_backend.cpp +++ b/system/lib/wasmfs/backends/opfs_backend.cpp @@ -393,10 +393,6 @@ class OPFSBackend : public Backend { // Symlinks not supported. return nullptr; } - - std::string getRootPath(std::string_view path) override { - return path.front() == '/' ? "/" : ""; - } }; } // anonymous namespace diff --git a/system/lib/wasmfs/js_impl_backend.h b/system/lib/wasmfs/js_impl_backend.h index a2b5cd425faa8..38d4b59c61bdc 100644 --- a/system/lib/wasmfs/js_impl_backend.h +++ b/system/lib/wasmfs/js_impl_backend.h @@ -120,9 +120,6 @@ class JSImplBackend : public Backend { std::shared_ptr createSymlink(std::string target) override { return std::make_shared(target, this); } - std::string getRootPath(std::string_view path) override { - return path.front() == '/' ? "/" : ""; - } }; extern "C" { diff --git a/system/lib/wasmfs/proxied_async_js_impl_backend.h b/system/lib/wasmfs/proxied_async_js_impl_backend.h index aab1b95ebaffc..e91ec561ef5ff 100644 --- a/system/lib/wasmfs/proxied_async_js_impl_backend.h +++ b/system/lib/wasmfs/proxied_async_js_impl_backend.h @@ -161,10 +161,6 @@ class ProxiedAsyncJSBackend : public Backend { std::shared_ptr createSymlink(std::string target) override { return std::make_shared(target, this); } - - std::string getRootPath(std::string_view path) override { - return path.front() == '/' ? "/" : ""; - } }; } // namespace wasmfs