WasmFS Node backend: host open() errors are used as file descriptors, and close() errors are returned with the wrong sign
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 70/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- javascript, node.js, wasm
- Área
- backend
Línea de trabajo
Start with src/lib/libwasmfs_node.js and system/lib/wasmfs/backends/node_backend.cpp, then check the error handling assertion in system/lib/wasmfs/syscalls.cpp. Reproduce the supplied open and close cases, and run wasmfs.test_fcntl_open_nodefs, wasmfs.test_fcntl_open_rawfs, and other.test_wasmfs_getdents_node. Done means host open and close errors propagate with the expected sign without bogus descriptors or assertions.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
The Node backend's JS helpers report errors as positive errno values (wasmfsTry). Two callers in node_backend.cpp expect something else:
NodeState::opentreats any non-negative result of_wasmfs_node_openas a host file descriptor. A failed host open returns the errno, which is positive, so it is taken for a descriptor andopen()succeeds.EACCES(2) becomes fd 2, Node's stderr.ENOENT(44) becomes fd 44, usually unopened.NodeState::closereturns_wasmfs_node_close's positive errno as is.__wasi_fd_closenegates it and asserts the result is>= 0.
Version of emscripten/emsdk:
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 6.0.9-git
clang version 22.1.8
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: /nix/store/a0h9ym1ai9yawkmymx2wp0vnh64hsv0r-clang-22.1.8/bin
This is the 6.0.9 release tag as packaged by nixpkgs. nixpkgs builds it against its own LLVM 22.1.8, so there is no commit hash. Node.js v24.20.0, x86_64 Linux.
Test case 1 (node_open_error.c): opening a root-owned, mode-0644 file for writing, as a normal user:
#include <emscripten/wasmfs.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void) {
/* Run as a non-root user: /etc/passwd is root-owned, mode 0644. */
wasmfs_create_directory("/etc", 0777, wasmfs_create_node_backend("/etc"));
errno = 0;
int fd = open("/etc/passwd", O_WRONLY);
printf("open(\"/etc/passwd\", O_WRONLY) = %d (%s)\n", fd,
fd < 0 ? strerror(errno) : "no error");
fflush(stdout);
if (fd >= 0) {
ssize_t n = write(fd, "written to /etc/passwd?\n", 24);
printf("write = %zd\n", n);
fflush(stdout);
errno = 0;
int r = close(fd);
printf("close = %d (%s)\n", r, r < 0 ? strerror(errno) : "no error");
}
return 0;
}
Failing command line in full (the same at -O2):
$ emcc -O0 -sWASMFS node_open_error.c -o node_open_error.js && id -u && node node_open_error.js 2> stderr.txt; echo "--- stderr:"; cat stderr.txt
1000
open("/etc/passwd", O_WRONLY) = 3 (no error)
write = 24
close = 0 (no error)
--- stderr:
written to /etc/passwd?
The host's EACCES (2) is taken for fd 2. open() succeeds, the write lands on stderr, and close() closes Node's own stderr. Expected: open("/etc/passwd", O_WRONLY) = -1 (Permission denied).
WasmFS's own permission check passes here: it accepts any write bit (0222) and ignores ownership. The host refuses because the caller doesn't own the file.
Test case 2 (node_close_errno.c): a host open that fails with ENOENT. The easiest way to get one is the stale path a rename leaves behind (#27824):
#include <emscripten/wasmfs.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv) {
/* argv[1]: an existing host directory, mounted at /host. */
wasmfs_create_directory("/host", 0777, wasmfs_create_node_backend(argv[1]));
close(open("/host/a", O_CREAT | O_WRONLY | O_TRUNC, 0644));
rename("/host/a", "/host/b");
/* The host open fails with ENOENT (see the rename issue). */
errno = 0;
int fd = open("/host/b", O_RDONLY);
printf("open = %d (%s)\n", fd, fd < 0 ? strerror(errno) : "no error");
char buf[8];
errno = 0;
ssize_t n = read(fd, buf, sizeof buf);
printf("read = %zd (%s)\n", n, n < 0 ? strerror(errno) : "no error");
fflush(stdout);
errno = 0;
int r = close(fd);
printf("close = %d (%s)\n", r, r < 0 ? strerror(errno) : "no error");
return 0;
}
$ emcc -O0 -sWASMFS node_close_errno.c -o node_close_errno.js && mkdir host && node node_close_errno.js "$PWD/host"
open = 3 (no error)
read = -1 (Bad file descriptor)
Aborted(Assertion failed: ret >= 0, at: /emsdk/emscripten/system/lib/wasmfs/syscalls.cpp,305,__wasi_fd_close)
node_close_errno.js:584
var e = new WebAssembly.RuntimeError(what);
^
RuntimeError: Aborted(Assertion failed: ret >= 0, at: /emsdk/emscripten/system/lib/wasmfs/syscalls.cpp,305,__wasi_fd_close)
at abort (node_close_errno.js:584:11)
at ___assert_fail (node_close_errno.js:904:7)
at wasm://wasm/00069496:wasm-function[374]:0x120a4
at wasm://wasm/00069496:wasm-function[37]:0xb7f
at wasm://wasm/00069496:wasm-function[34]:0xaf5
at node_close_errno.js:615:12
at callMain (node_close_errno.js:2051:15)
at run (node_close_errno.js:2097:22)
at node_close_errno.js:2137:25
Node.js v24.20.0
[exit status 1]
At -O2 the assertion is compiled out. __wasi_fd_close then returns -8 as a uint16_t __wasi_errno_t, so close() fails with errno 65528, which prints as "Unknown error":
$ emcc -O2 -sWASMFS node_close_errno.c -o node_close_errno.js && mkdir host && node node_close_errno.js "$PWD/host"
open = 3 (no error)
read = -1 (Bad file descriptor)
close = -1 (Unknown error)
Full compile and link output with -v appended (cache and scratch paths shortened)
/nix/store/l05jw62495v8gcd0f2r01klky8i5k52i-emscripten-llvm-6.0.9/bin/clang -target wasm32-unknown-emscripten -fignore-exceptions -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr --sysroot=$EM_CACHE/sysroot -resource-dir=/nix/store/l05jw62495v8gcd0f2r01klky8i5k52i-emscripten-llvm-6.0.9/lib/clang/22/ -idirafter$EM_CACHE/sysroot/include -iwithsysroot/include/c++/v1 -Xclang -iwithsysroot/include/fakesdl -Xclang -iwithsysroot/include/compat -v -O0 -c node_open_error.c -o /tmp/emscripten_temp_vt3o98bv/node_open_error.o
clang version 22.1.8
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: /nix/store/a0h9ym1ai9yawkmymx2wp0vnh64hsv0r-clang-22.1.8/bin
(in-process)
"/nix/store/a0h9ym1ai9yawkmymx2wp0vnh64hsv0r-clang-22.1.8/bin/clang-22" -cc1 -triple wasm32-unknown-emscripten -O0 -emit-obj -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name node_open_error.c -mrelocation-model static -mframe-pointer=none -ffp-contract=on -fno-rounding-math -mconstructor-aliases -target-cpu generic -fvisibility=hidden -debugger-tuning=gdb -fdebug-compilation-dir=. -v -fcoverage-compilation-dir=. -resource-dir /nix/store/l05jw62495v8gcd0f2r01klky8i5k52i-emscripten-llvm-6.0.9/lib/clang/22/ -idirafter $EM_CACHE/sysroot/include -iwithsysroot /include/c++/v1 -isysroot $EM_CACHE/sysroot -internal-isystem /nix/store/l05jw62495v8gcd0f2r01klky8i5k52i-emscripten-llvm-6.0.9/lib/clang/22/include -internal-isystem $EM_CACHE/sysroot/include/wasm32-emscripten -internal-isystem $EM_CACHE/sysroot/include -ferror-limit 19 -fgnuc-version=4.2.1 -fskip-odr-check-in-gmf -fignore-exceptions -iwithsysroot/include/fakesdl -iwithsysroot/include/compat -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr -o /tmp/emscripten_temp_vt3o98bv/node_open_error.o -x c node_open_error.c
clang -cc1 version 22.1.8 based upon LLVM 22.1.8 default target x86_64-unknown-linux-gnu
ignoring nonexistent directory "$EM_CACHE/sysroot/include/wasm32-emscripten"
ignoring duplicate directory "$EM_CACHE/sysroot/include"
#include "..." search starts here:
#include <...> search starts here:
$EM_CACHE/sysroot/include/c++/v1
$EM_CACHE/sysroot/include/fakesdl
$EM_CACHE/sysroot/include/compat
/nix/store/l05jw62495v8gcd0f2r01klky8i5k52i-emscripten-llvm-6.0.9/lib/clang/22/include
$EM_CACHE/sysroot/include
End of search list.
/nix/store/l05jw62495v8gcd0f2r01klky8i5k52i-emscripten-llvm-6.0.9/bin/clang --version
/nix/store/l05jw62495v8gcd0f2r01klky8i5k52i-emscripten-llvm-6.0.9/bin/wasm-ld -o node_open_error.wasm /tmp/tmpg_gwm8lolibemscripten_js_symbols.so -Bstatic --strip-debug --export=emscripten_stack_get_end --export=emscripten_stack_get_free --export=emscripten_stack_get_base --export=emscripten_stack_get_current --export=emscripten_stack_init --export=wasmfs_flush --export=_emscripten_stack_alloc --export=__wasm_call_ctors --export=_emscripten_stack_restore --export-if-defined=__start_em_asm --export-if-defined=__stop_em_asm --export-if-defined=__start_em_lib_deps --export-if-defined=__stop_em_lib_deps --export-if-defined=__start_em_js --export-if-defined=__stop_em_js --export-if-defined=main --export-if-defined=__main_argc_argv --export-if-defined=fflush --export-table -z stack-size=65536 --no-growable-memory --initial-heap=16777216 --no-entry --stack-first --table-base=1 /tmp/emscripten_temp_vt3o98bv/node_open_error.o -L$EM_CACHE/sysroot/lib/wasm32-emscripten -L/nix/store/03iyrnrsxnbc64y6r62irmlbnbap5ya2-emscripten-6.0.9/share/emscripten/src/lib -lGL-getprocaddr -lal -lhtml5 -lstubs-debug -lnoexit -lc-debug -ldlmalloc-debug -lclang_rt.builtins -lc++-debug-noexcept -lc++abi-debug-noexcept -lsockets -lwasmfs_no_fs -lwasmfs-debug -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr
/nix/store/l05jw62495v8gcd0f2r01klky8i5k52i-emscripten-llvm-6.0.9/bin/llvm-objcopy node_open_error.wasm node_open_error.wasm '--remove-section=llvm.func_attr.annotate.*' --remove-section=producers --remove-section=name '--remove-section=.debug*'
Where:
wasmfsTryreturnsERRNO_CODES[code], which is positive (libwasmfs_node.js#L19-L27)._wasmfs_node_openreturns either an fd or that errno (#L170-L173).NodeState::openchecksresult < 0(node_backend.cpp#L61, #L72).NodeState::closereturns the positive errno (#L93).__wasi_fd_closeasserts on it (syscalls.cpp#L304-L305).
Suggested fix: an fd and an errno can't share the positive range, so _wasmfs_node_open returns a negated errno. close negates its helper's result like the other callers do.
Reporting a failed open uncovers a second bug that the bogus descriptor hid. insertDataFile creates the file with 'ax' and closes it, then the open() that follows reopens it by path, where the new file's own mode can refuse it: open(path, O_CREAT | O_RDONLY, 0) gets EACCES, though POSIX lets the creating open through whatever the mode. Fixed alone, the sign change makes wasmfs.test_fcntl_open_nodefs, wasmfs.test_fcntl_open_rawfs and other.test_wasmfs_getdents_node fail on that. So the file is created with 'wx+', read-write, and that descriptor is kept for the first open. mknod() creates a file without opening it, so doOpen opens and closes a file it created when it returns no descriptor, which lets the kept one go (a no-op for backends that keep nothing). That also drops 'a', which would make every pwrite to the descriptor append on Linux.
--- a/src/lib/libwasmfs_node.js
+++ b/src/lib/libwasmfs_node.js
@@ -121,12 +121,16 @@
// implicitly return 0
},
- _wasmfs_node_insert_file__deps: ['$wasmfsTry'],
+ _wasmfs_node_insert_file__deps: ['$wasmfsNodeConvertNodeCode'],
_wasmfs_node_insert_file: (path_p, mode) => {
- return wasmfsTry(() => {
- fs.closeSync(fs.openSync(UTF8ToString(path_p), 'ax', mode));
- // implicitly return 0
- });
+ // A file descriptor open for reading and writing, whatever `mode` allows,
+ // or a negated errno.
+ try {
+ return fs.openSync(UTF8ToString(path_p), 'wx+', mode);
+ } catch (e) {
+ if (!e.code) throw e;
+ return -wasmfsNodeConvertNodeCode(e);
+ }
},
_wasmfs_node_insert_directory__deps: ['$wasmfsTry'],
@@ -167,9 +171,15 @@
return wasmfsTry(() => fs.ftruncateSync(fd, len));
},
- _wasmfs_node_open__deps: ['$wasmfsTry'],
+ _wasmfs_node_open__deps: ['$wasmfsNodeConvertNodeCode'],
_wasmfs_node_open: (path_p, mode_p) => {
- return wasmfsTry(() => fs.openSync(UTF8ToString(path_p), UTF8ToString(mode_p)));
+ // A file descriptor on success, a negated errno on failure.
+ try {
+ return fs.openSync(UTF8ToString(path_p), UTF8ToString(mode_p));
+ } catch (e) {
+ if (!e.code) throw e;
+ return -wasmfsNodeConvertNodeCode(e);
+ }
},
_wasmfs_node_rename__deps: ['$wasmfsTry'],
--- a/system/lib/wasmfs/backends/node_backend.cpp
+++ b/system/lib/wasmfs/backends/node_backend.cpp
@@ -21,6 +21,8 @@
size_t openCount = 0;
oflags_t openFlags = 0;
int fd = -1;
+ // The descriptor that created the file, until its first open takes it.
+ int created = -1;
public:
std::string path;
@@ -34,10 +36,29 @@
bool isOpen() { return openCount > 0; }
+ // Keep the read-write descriptor that created the file for the open that
+ // follows: the file's mode doesn't bind the open that creates it, but would
+ // refuse a second one.
+ void keepCreated(int createdFD) { created = createdFD; }
+
+ void closeCreated() {
+ if (created >= 0) {
+ (void)_wasmfs_node_close(created);
+ created = -1;
+ }
+ }
+
// Attempt to open the file with the given flags, returning 0 on success or an
// error code.
int open(oflags_t flags) {
int result = 0;
+ if (openCount == 0 && created >= 0) {
+ fd = created;
+ created = -1;
+ openFlags = O_RDWR;
+ ++openCount;
+ return 0;
+ }
if (openCount == 0) {
// No existing fd, so open a fresh one.
switch (flags) {
@@ -90,7 +111,7 @@
int close() {
int ret = 0;
if (--openCount == 0) {
- ret = _wasmfs_node_close(fd);
+ ret = -_wasmfs_node_close(fd);
*this = NodeState(path);
}
return ret;
@@ -104,6 +125,8 @@
NodeFile(mode_t mode, backend_t backend, std::string path)
: DataFile(mode, backend), state(path) {}
+ ~NodeFile() { state.closeCreated(); }
+
private:
off_t getSize() override {
// TODO: This should really be using a 64-bit file size type.
@@ -218,10 +241,13 @@
std::shared_ptr<DataFile> insertDataFile(const std::string& name,
mode_t mode) override {
auto childPath = getChildPath(name);
- if (_wasmfs_node_insert_file(childPath.c_str(), mode)) {
+ int fd = _wasmfs_node_insert_file(childPath.c_str(), mode);
+ if (fd < 0) {
return nullptr;
}
- return std::make_shared<NodeFile>(mode, getBackend(), childPath);
+ auto file = std::make_shared<NodeFile>(mode, getBackend(), childPath);
+ file->state.keepCreated(fd);
+ return file;
}
std::shared_ptr<Directory> insertDirectory(const std::string& name,
--- a/system/lib/wasmfs/backends/node_backend.h
+++ b/system/lib/wasmfs/backends/node_backend.h
@@ -19,8 +19,11 @@
int _wasmfs_node_stat_size(const char* path, uint32_t* size);
int _wasmfs_node_fstat_size(int fd, uint32_t* size);
-// Create a new file system entry and return 0 or an error code.
-int _wasmfs_node_insert_file(const char* path, mode_t mode);
+// Create a new file and return a file descriptor open for reading and
+// writing, or a negative error code.
+[[nodiscard]] int _wasmfs_node_insert_file(const char* path, mode_t mode);
+
+// Create a new directory and return 0 or an error code.
int _wasmfs_node_insert_directory(const char* path, mode_t mode);
// Unlink the given file and return 0 or an error code.
--- a/system/lib/wasmfs/syscalls.cpp
+++ b/system/lib/wasmfs/syscalls.cpp
@@ -499,6 +499,15 @@
}
// TODO: Check that the insert actually succeeds.
if (returnMode == OpenReturnMode::Nothing) {
+ // A backend may keep a new file open for the open that usually
+ // follows creation (the Node backend does, since the file's own mode
+ // may refuse a reopen). With none to follow, let it go.
+ if (auto dataFile = created->dynCast<DataFile>()) {
+ auto lockedFile = dataFile->locked();
+ if (lockedFile.open(O_RDONLY) == 0) {
+ (void)lockedFile.close();
+ }
+ }
return 0;
}
With this applied to 6.0.9, test case 1 prints:
open("/etc/passwd", O_WRONLY) = -1 (Permission denied)
--- stderr:
The header comment on _wasmfs_node_open in node_backend.h ("Open the file and return the underlying file descriptor") could say so too.
- Lenguaje dominante
- C++
- Estrellas
- 27.8k
- Forks
- 3.6k
- Merge medio
- 1 d 4 h
- PR fusionados (30 d)
- 179
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Sin plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de emscripten-core/emscripten
-
`other.test_cmake_cxx_import_std` fails on CMake 3.30–4.0.2 due to missing `CMAKE_EXPERIMENTAL_CXX_IMPORT_STD` UUIDsPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 85/100
emscripten-core/emscripten#28160 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
emscripten-core/emscripten#27994 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
-
LEGACY_GL_EMULATION: generated fixed-function fragment shaders use mediump — fp16 overflow where mediump is half precisionPosiblemente ocupada @TheTomBritton la tomó hace 12 días. AbiertoDesktop OpenGL emulation
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
emscripten-core/emscripten#27948 · 4 comentarios ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 64/100
emscripten-core/emscripten#27906 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
run_python, run_python_compiler could readlink -fPosiblemente ocupada @kapouer la tomó hace 127 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 74/100
emscripten-core/emscripten#27211 · 1 comentario ·
Los mantenedores suelen responder en 1 día
Todos los issues de emscripten-core/emscripten
Issues similares
-
bug libcudf
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 2 días
-
level/task module/cloud monitoring type/maintenance
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
LadybirdBrowser/ladybird#13809 ·
Los mantenedores suelen responder en 1 día
-
bug module:nodes tests
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
rocketride-org/rocketride-server#3020 ·
Los mantenedores suelen responder en 2 días
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
Los mantenedores suelen responder en 9 días