From a6e22e44ff0b7fd0a71b88a38b3d233529779495 Mon Sep 17 00:00:00 2001 From: Kleis Auke Wolthuizen Date: Thu, 17 Jul 2025 21:21:52 +0200 Subject: [PATCH 1/6] [WasmFS] Fix absolute path access under NODERAWFS Resolves: #24830. --- system/lib/wasmfs/backend.h | 2 ++ system/lib/wasmfs/backends/node_backend.cpp | 5 +++++ system/lib/wasmfs/backends/noderawfs_root.cpp | 2 +- system/lib/wasmfs/paths.cpp | 15 ++++++++++++--- test/test_core.py | 7 +++++-- test/test_other.py | 18 +++++++++++------- 6 files changed, 36 insertions(+), 13 deletions(-) diff --git a/system/lib/wasmfs/backend.h b/system/lib/wasmfs/backend.h index d8e584c01136a..a028e41ad0993 100644 --- a/system/lib/wasmfs/backend.h +++ b/system/lib/wasmfs/backend.h @@ -21,6 +21,8 @@ class Backend { virtual std::shared_ptr createDirectory(mode_t mode) = 0; virtual std::shared_ptr createSymlink(std::string target) = 0; + virtual bool isVirtualized() { return true; } + virtual ~Backend() = default; }; diff --git a/system/lib/wasmfs/backends/node_backend.cpp b/system/lib/wasmfs/backends/node_backend.cpp index a3a7ad19b32f3..14a8176a2626b 100644 --- a/system/lib/wasmfs/backends/node_backend.cpp +++ b/system/lib/wasmfs/backends/node_backend.cpp @@ -180,6 +180,9 @@ class NodeDirectory : public Directory { private: std::string getChildPath(const std::string& name) { + if (state.path.empty()) { + return name; + } return state.path + '/' + name; } @@ -294,6 +297,8 @@ class NodeBackend : public Backend { std::shared_ptr createSymlink(std::string target) override { WASMFS_UNREACHABLE("TODO: implement NodeBackend::createSymlink"); } + + virtual bool isVirtualized() override { return false; } }; // TODO: symlink diff --git a/system/lib/wasmfs/backends/noderawfs_root.cpp b/system/lib/wasmfs/backends/noderawfs_root.cpp index 6ed7af81daf56..5dae16a4c9a1d 100644 --- a/system/lib/wasmfs/backends/noderawfs_root.cpp +++ b/system/lib/wasmfs/backends/noderawfs_root.cpp @@ -6,5 +6,5 @@ #include "emscripten/wasmfs.h" backend_t wasmfs_create_root_dir(void) { - return wasmfs_create_node_backend("."); + return wasmfs_create_node_backend(""); } diff --git a/system/lib/wasmfs/paths.cpp b/system/lib/wasmfs/paths.cpp index 3d1fbf4c081e8..b11879b5f878c 100644 --- a/system/lib/wasmfs/paths.cpp +++ b/system/lib/wasmfs/paths.cpp @@ -66,12 +66,21 @@ ParsedParent doParseParent(std::string_view path, size_t& recursions) { // Empty paths never exist. if (path.empty()) { - return {-ENOENT}; + return -ENOENT; + } + + auto root = wasmFS.getRootDirectory(); + + // If the root backend is not virtualized, we assume there is only a single + // backend, making parent traversal unnecessary. Simply return the current + // directory along with the full path. + if (!root->getBackend()->isVirtualized()) { + return {std::make_pair(std::move(curr), path)}; } // Handle absolute paths. if (path.front() == '/') { - curr = wasmFS.getRootDirectory(); + curr = root; path.remove_prefix(1); } @@ -84,7 +93,7 @@ ParsedParent doParseParent(std::string_view path, // contain a child segment for us to return. The root is its own parent, so we // can handle this by returning (root, "."). if (path.empty()) { - return {std::make_pair(std::move(curr), std::string_view("."))}; + return {std::make_pair(std::move(curr), ".")}; } while (true) { diff --git a/test/test_core.py b/test/test_core.py index c3f4e7fa1ed17..d330bbf2a5be9 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -6151,9 +6151,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 f5fab92327817..13f8f511547bc 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -9291,12 +9291,19 @@ 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')]) @@ -13254,11 +13261,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') @@ -13844,15 +13850,13 @@ 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') + 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 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 f04892c645351fe01e17ca6d66f0f1145a472dc0 Mon Sep 17 00:00:00 2001 From: Kleis Auke Wolthuizen Date: Thu, 2 Apr 2026 10:14:13 +0200 Subject: [PATCH 2/6] Add comments --- system/lib/wasmfs/backends/node_backend.cpp | 3 +++ system/lib/wasmfs/backends/noderawfs_root.cpp | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/system/lib/wasmfs/backends/node_backend.cpp b/system/lib/wasmfs/backends/node_backend.cpp index 14a8176a2626b..3cdc0777ae559 100644 --- a/system/lib/wasmfs/backends/node_backend.cpp +++ b/system/lib/wasmfs/backends/node_backend.cpp @@ -23,6 +23,9 @@ class NodeState { int fd = -1; public: + // Base path in the host filesystem for this backend. + // An empty string means "no base path": paths are used as-is without + // prefixing. std::string path; NodeState(std::string path) : path(path) {} diff --git a/system/lib/wasmfs/backends/noderawfs_root.cpp b/system/lib/wasmfs/backends/noderawfs_root.cpp index 5dae16a4c9a1d..e2cc7b9db3d10 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) { + // Use an empty string as the backend "path" to indicate that this backend + // is mounted at the real filesystem root. In this mode, paths are passed + // through verbatim (no "./" prefix or path rewriting), which is required + // for correct handling of absolute paths under NODERAWFS. return wasmfs_create_node_backend(""); } From cb9bb3ece93950ee59d46ace904a3eca263016aa Mon Sep 17 00:00:00 2001 From: Kleis Auke Wolthuizen Date: Thu, 2 Apr 2026 10:25:33 +0200 Subject: [PATCH 3/6] Rename `isVirtualized()` to `requiresPathTraversal()` --- system/lib/wasmfs/backend.h | 5 ++++- system/lib/wasmfs/backends/node_backend.cpp | 6 +++++- system/lib/wasmfs/paths.cpp | 8 ++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/system/lib/wasmfs/backend.h b/system/lib/wasmfs/backend.h index a028e41ad0993..ef13d6dcc6639 100644 --- a/system/lib/wasmfs/backend.h +++ b/system/lib/wasmfs/backend.h @@ -21,7 +21,10 @@ class Backend { virtual std::shared_ptr createDirectory(mode_t mode) = 0; virtual std::shared_ptr createSymlink(std::string target) = 0; - virtual bool isVirtualized() { return true; } + // Returns true if this backend requires WasmFS to traverse and resolve + // paths (e.g. because it is mounted under a prefix). Returns false if + // paths can be passed through directly to the backend. + virtual bool requiresPathTraversal() { return true; } virtual ~Backend() = default; }; diff --git a/system/lib/wasmfs/backends/node_backend.cpp b/system/lib/wasmfs/backends/node_backend.cpp index 3cdc0777ae559..fffaf13706e56 100644 --- a/system/lib/wasmfs/backends/node_backend.cpp +++ b/system/lib/wasmfs/backends/node_backend.cpp @@ -183,6 +183,8 @@ class NodeDirectory : public Directory { private: std::string getChildPath(const std::string& name) { + // If state.path is empty, this backend represents the real root and paths + // should be passed through unchanged. if (state.path.empty()) { return name; } @@ -301,7 +303,9 @@ class NodeBackend : public Backend { WASMFS_UNREACHABLE("TODO: implement NodeBackend::createSymlink"); } - virtual bool isVirtualized() override { return false; } + virtual bool requiresPathTraversal() override { + return !mountPath.empty(); + } }; // TODO: symlink diff --git a/system/lib/wasmfs/paths.cpp b/system/lib/wasmfs/paths.cpp index b11879b5f878c..d5c7ab9bac315 100644 --- a/system/lib/wasmfs/paths.cpp +++ b/system/lib/wasmfs/paths.cpp @@ -71,10 +71,10 @@ ParsedParent doParseParent(std::string_view path, auto root = wasmFS.getRootDirectory(); - // If the root backend is not virtualized, we assume there is only a single - // backend, making parent traversal unnecessary. Simply return the current - // directory along with the full path. - if (!root->getBackend()->isVirtualized()) { + // If the root backend does not require path traversal, it can operate on the + // full path directly. In that case, skip WasmFS path parsing and pass the + // path through unchanged. + if (!root->getBackend()->requiresPathTraversal()) { return {std::make_pair(std::move(curr), path)}; } From 11233e0995ecbcb774a896d58dc4a5f2dc377a2a Mon Sep 17 00:00:00 2001 From: Kleis Auke Wolthuizen Date: Thu, 2 Apr 2026 20:03:17 +0200 Subject: [PATCH 4/6] Cherry-pick commit f3b85c992943f0ef20765fbac3ff61b3b580135d --- test/test_other.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/test_other.py b/test/test_other.py index 13f8f511547bc..08450b3a7a7ed 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -13261,10 +13261,13 @@ 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): + if self.get_setting('NODERAWFS'): + self.skipTest('test expectations assumes /dev is virtualized') # Run only in WASMFS for now. self.set_setting('FORCE_FILESYSTEM') - self.do_run_in_out_file_test('wasmfs/wasmfs_getdents.c', cflags=['-sWASMFS']) + self.do_run_in_out_file_test('wasmfs/wasmfs_getdents.c') def test_wasmfs_jsfile(self): self.set_setting('WASMFS') From e21bc63820ff7f89947102450879e78301381fd3 Mon Sep 17 00:00:00 2001 From: Kleis Auke Wolthuizen Date: Fri, 3 Apr 2026 08:53:57 +0200 Subject: [PATCH 5/6] Simplify --- system/lib/wasmfs/backend.h | 11 +++++++---- system/lib/wasmfs/backends/node_backend.cpp | 4 +--- system/lib/wasmfs/backends/noderawfs_root.cpp | 6 ++---- system/lib/wasmfs/paths.cpp | 13 ++++++------- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/system/lib/wasmfs/backend.h b/system/lib/wasmfs/backend.h index ef13d6dcc6639..6f2ce1906eb6f 100644 --- a/system/lib/wasmfs/backend.h +++ b/system/lib/wasmfs/backend.h @@ -21,10 +21,13 @@ class Backend { virtual std::shared_ptr createDirectory(mode_t mode) = 0; virtual std::shared_ptr createSymlink(std::string target) = 0; - // Returns true if this backend requires WasmFS to traverse and resolve - // paths (e.g. because it is mounted under a prefix). Returns false if - // paths can be passed through directly to the backend. - virtual bool requiresPathTraversal() { return true; } + // Indicates whether this backend relies on WasmFS to resolve paths and + // traverse the directory hierarchy. + // - true (default): WasmFS performs path parsing, symlink resolution, + // and intermediate directory traversal for this backend. + // - false: the backend handles full paths itself (e.g. NODERAWFS), so + // WasmFS should pass paths as-is without interpreting them. + virtual bool requiresPathResolution() { return true; } virtual ~Backend() = default; }; diff --git a/system/lib/wasmfs/backends/node_backend.cpp b/system/lib/wasmfs/backends/node_backend.cpp index 0328e24be9132..b65b38e41689b 100644 --- a/system/lib/wasmfs/backends/node_backend.cpp +++ b/system/lib/wasmfs/backends/node_backend.cpp @@ -305,9 +305,7 @@ class NodeBackend : public Backend { WASMFS_UNREACHABLE("TODO: implement NodeBackend::createSymlink"); } - virtual bool requiresPathTraversal() override { - return !mountPath.empty(); - } + virtual bool requiresPathResolution() override { return false; } }; // TODO: symlink diff --git a/system/lib/wasmfs/backends/noderawfs_root.cpp b/system/lib/wasmfs/backends/noderawfs_root.cpp index e2cc7b9db3d10..e5ac7eca1dd9c 100644 --- a/system/lib/wasmfs/backends/noderawfs_root.cpp +++ b/system/lib/wasmfs/backends/noderawfs_root.cpp @@ -6,9 +6,7 @@ #include "emscripten/wasmfs.h" backend_t wasmfs_create_root_dir(void) { - // Use an empty string as the backend "path" to indicate that this backend - // is mounted at the real filesystem root. In this mode, paths are passed - // through verbatim (no "./" prefix or path rewriting), which is required - // for correct handling of absolute paths under NODERAWFS. + // Use an empty string as the backend "path" to indicate that paths are + // passed as-is (i.e. without the "./" prefix). return wasmfs_create_node_backend(""); } diff --git a/system/lib/wasmfs/paths.cpp b/system/lib/wasmfs/paths.cpp index d5c7ab9bac315..13072c833270a 100644 --- a/system/lib/wasmfs/paths.cpp +++ b/system/lib/wasmfs/paths.cpp @@ -69,18 +69,17 @@ ParsedParent doParseParent(std::string_view path, return -ENOENT; } - auto root = wasmFS.getRootDirectory(); - - // If the root backend does not require path traversal, it can operate on the - // full path directly. In that case, skip WasmFS path parsing and pass the - // path through unchanged. - if (!root->getBackend()->requiresPathTraversal()) { + // For backends that do not require path resolution, WasmFS must not + // interpret or traverse the path (e.g. via getChild). Once such a + // backend is reached, the remaining path is forwarded as a whole, + // and the backend is responsible for resolving it. + if (!curr->getBackend()->requiresPathResolution()) { return {std::make_pair(std::move(curr), path)}; } // Handle absolute paths. if (path.front() == '/') { - curr = root; + curr = wasmFS.getRootDirectory(); path.remove_prefix(1); } From fa12429f8fa5dfba6cbbfbfd03d8f1ab4711f003 Mon Sep 17 00:00:00 2001 From: Kleis Auke Wolthuizen Date: Fri, 3 Apr 2026 13:18:51 +0200 Subject: [PATCH 6/6] Fix NodeFS test failures after e21bc63 --- system/lib/wasmfs/backends/node_backend.cpp | 7 ++++++- system/lib/wasmfs/backends/noderawfs_root.cpp | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/system/lib/wasmfs/backends/node_backend.cpp b/system/lib/wasmfs/backends/node_backend.cpp index b65b38e41689b..39e85a7be7081 100644 --- a/system/lib/wasmfs/backends/node_backend.cpp +++ b/system/lib/wasmfs/backends/node_backend.cpp @@ -305,7 +305,12 @@ class NodeBackend : public Backend { WASMFS_UNREACHABLE("TODO: implement NodeBackend::createSymlink"); } - virtual bool requiresPathResolution() override { return false; } + virtual bool requiresPathResolution() override { + // This backend requires WasmFS to resolve paths only if it has a non-empty + // mountPath. If mountPath is empty (e.g. NODERAWFS), the backend handles + // full paths itself and WasmFS should pass paths through as-is. + return !mountPath.empty(); + } }; // TODO: symlink diff --git a/system/lib/wasmfs/backends/noderawfs_root.cpp b/system/lib/wasmfs/backends/noderawfs_root.cpp index e5ac7eca1dd9c..c06c7aa0da7e4 100644 --- a/system/lib/wasmfs/backends/noderawfs_root.cpp +++ b/system/lib/wasmfs/backends/noderawfs_root.cpp @@ -6,7 +6,7 @@ #include "emscripten/wasmfs.h" backend_t wasmfs_create_root_dir(void) { - // Use an empty string as the backend "path" to indicate that paths are + // Use an empty string as the backend "mountPath" to indicate that paths are // passed as-is (i.e. without the "./" prefix). return wasmfs_create_node_backend(""); }