Skip to content

[NODEFS] hardlinks, utimensat nofollow, and fadvise/fallocate seek errors#27305

Merged
sbc100 merged 1 commit into
emscripten-core:mainfrom
guybedford:nodefs-fs-fixes
Jul 11, 2026
Merged

[NODEFS] hardlinks, utimensat nofollow, and fadvise/fallocate seek errors#27305
sbc100 merged 1 commit into
emscripten-core:mainfrom
guybedford:nodefs-fs-fixes

Conversation

@guybedford

@guybedford guybedford commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Adds a handful of filesystem fixes to the legacy (non-WASMFS) JS filesystem, mostly around the NODEFS/NODERAWFS host backends where these operations previously either errored unconditionally or asserted.

  • linkat(2): the weak stub always returned EMLINK, so hardlinks were impossible even on host-backed filesystems. A new FS.link core op looks up the target and delegates to a backend link node op when one exists, still returning EMLINK for backends without one. Only NODERAWFS provides it (via fs.linkSync). NODEFS deliberately does not: unlike NODERAWFS it confines the guest to a mount root, and a real host hardlink can't be guaranteed to stay within that root (host symlinks in intermediate path components are resolved by the OS and would escape it, with no portable atomic way to check), so NODEFS reports EMLINK like MEMFS. AT_SYMLINK_FOLLOW is honored.
  • utimensat(2): previously asserted !flags, so AT_SYMLINK_NOFOLLOW aborted under assertions. FS.utime gains a dontFollow argument threaded through doSetAttr. Both NODERAWFS and NODEFS use fs.lutimesSync for the nofollow case so a symlink's own timestamps are set without the host resolving the link — which also avoids escaping the NODEFS mount root that fs.utimesSync (follows symlinks) would allow.
  • posix_fadvise/posix_fallocate: return ESPIPE on a non-seekable fd (pipe/socket) to match Linux, rather than silently succeeding or truncating. __syscall_fadvise64 moves from a weak C stub into the JS syscall layer so it can inspect the stream.
  • NODERAWFS fstat: virtual streams (pipes, sockets) have no backing host fd, so defer to their own getattr instead of calling fs.fstatSync on an undefined nfd.

Tests in test/fs cover these across MEMFS/NODEFS/NODERAWFS (test_fs_link, test_fs_utimensat_nofollow, test_fs_fadvise_fallocate). Codesize expectations are updated for the new FS.link/FS_link symbol.

Made with AI assistance under my review

@guybedford guybedford force-pushed the nodefs-fs-fixes branch 3 times, most recently from 8a9e5da to f8a0538 Compare July 10, 2026 06:32
@sbc100 sbc100 changed the title NODEFS: hardlinks, utimensat nofollow, and fadvise/fallocate seek errors [NODEFS] hardlinks, utimensat nofollow, and fadvise/fallocate seek errors Jul 10, 2026
@guybedford guybedford force-pushed the nodefs-fs-fixes branch 2 times, most recently from c9be8ca to 39d23ee Compare July 10, 2026 23:24
@guybedford

Copy link
Copy Markdown
Collaborator Author

I added an addition here to properly support getuid and getgid on NODERAWFS only. For link and those they do slightly inflate the size for moving to JS stubs on those, but this needs to be weighed against the parity argument.

I'm open to removing / reconsidering any items here if we want to gate these as unsupported instead, but they are as far as I can tell some of the last parity gaps.

@guybedford guybedford changed the title [NODEFS] hardlinks, utimensat nofollow, and fadvise/fallocate seek errors [NODEFS] hardlinks, utimensat nofollow, uid/gid getters, and fadvise/fallocate seek errors Jul 10, 2026
Comment thread src/lib/libsyscall.js Outdated
@guybedford guybedford changed the title [NODEFS] hardlinks, utimensat nofollow, uid/gid getters, and fadvise/fallocate seek errors [NODEFS] hardlinks, utimensat nofollow, and fadvise/fallocate seek errors Jul 11, 2026
guybedford added a commit to guybedford/nix that referenced this pull request Jul 11, 2026
Adds wasm32-unknown-emscripten as a target for nix, plus a CI job that runs the
full test suite under Node.

Source changes to compile on emscripten (which behaves like a Linux/musl libc):

* ioctl: use the Linux ioctl module and a c_int ioctl_num_type on emscripten
  (src/sys/ioctl, src/sys/mod.rs).
* resource: enable getrlimit/setrlimit and the Resource enum via libc::rlimit.
* socket: include emscripten in the SockaddrStorage AF_UNIX (pathname/unnamed)
  decode arms.
* unistd: sethostname is unavailable on emscripten, so gate it out.

The suite spawns OS threads (signal and socket-peer tests), so std is rebuilt
with atomics via -Zbuild-std and linked -pthread with -sPROXY_TO_PTHREAD so the
main thread can block; JSPI (-sJSPI) provides the return-to-host suspension for
blocking reads/writes, and NODERAWFS/NODERAWSOCKETS back the filesystem and
sockets with node's. This needs nightly + rust-src and a JSPI-capable Node
(26+, or 22 with --experimental-wasm-jspi). No custom target spec is required:
nightly now emits the __main_argc_argv entry point (rust-lang/rust#158937).

Genuinely-unsupported operations are ignored with
#[cfg_attr(target_os = "emscripten", ignore)] rather than cfg'd out, covering
fork/wait, pty/termios, mkfifo, getpwnam/setgroups/acct, flock, if_nametoindex,
pipe2 FD_CLOEXEC, AF_UNIX SCM_RIGHTS/datagram, thread signal masking, and
setrlimit; a few tests are cfg'd out where the feature is entirely absent
(raw signal actions, socketpair) or the host differs (linkat symlink follow).

Temporary, until the dependencies land upstream:

* Cargo.toml patches libc to guybedford/libc#libc-0.2-emscripten for the
  emscripten externs (rust-lang/libc#5270).
* the CI job builds against the guybedford/emscripten `cf` fork, which carries
  the NODEFS/socket/blocking patches this target depends on
  (emscripten-core/emscripten#27305, #27306).

Suite result: 148 passed, 0 failed, 35 ignored under Node - green on Linux CI.

@sbc100 sbc100 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still lgtm, but I don't see the getuid stuff

Comment thread src/lib/libfs.js Outdated
guybedford added a commit to guybedford/nix that referenced this pull request Jul 11, 2026
Adds wasm32-unknown-emscripten as a target for nix, plus a CI job that runs the
full test suite under Node.

Source changes to compile on emscripten (which behaves like a Linux/musl libc):

* ioctl: use the Linux ioctl module and a c_int ioctl_num_type on emscripten
  (src/sys/ioctl, src/sys/mod.rs).
* resource: enable getrlimit/setrlimit and the Resource enum via libc::rlimit.
* socket: include emscripten in the SockaddrStorage AF_UNIX (pathname/unnamed)
  decode arms.
* unistd: sethostname is unavailable on emscripten, so gate it out.

The suite spawns OS threads (signal and socket-peer tests), so std is rebuilt
with atomics via -Zbuild-std and linked -pthread with -sPROXY_TO_PTHREAD so the
main thread can block; JSPI (-sJSPI) provides the return-to-host suspension for
blocking reads/writes, and NODERAWFS/NODERAWSOCKETS back the filesystem and
sockets with node's. This needs nightly + rust-src and a JSPI-capable Node
(26+, or 22 with --experimental-wasm-jspi). No custom target spec is required:
nightly now emits the __main_argc_argv entry point (rust-lang/rust#158937).

Genuinely-unsupported operations are ignored with
#[cfg_attr(target_os = "emscripten", ignore)] rather than cfg'd out, covering
fork/wait, pty/termios, mkfifo, getpwnam/setgroups/acct, flock, if_nametoindex,
pipe2 FD_CLOEXEC, AF_UNIX SCM_RIGHTS/datagram, thread signal masking, and
setrlimit; a few tests are cfg'd out where the feature is entirely absent
(raw signal actions, socketpair) or the host differs (linkat symlink follow).

Temporary, until the dependencies land upstream:

* Cargo.toml patches libc to guybedford/libc#libc-0.2-emscripten for the
  emscripten externs (rust-lang/libc#5270).
* the CI job builds against the guybedford/emscripten `cf` fork, which carries
  the NODEFS/socket/blocking patches this target depends on
  (emscripten-core/emscripten#27305, #27306).

Suite result: 148 passed, 0 failed, 35 ignored under Node - green on Linux CI.
@guybedford

Copy link
Copy Markdown
Collaborator Author

still lgtm, but I don't see the getuid stuff

Reposted this in #27319.

guybedford added a commit to guybedford/nix that referenced this pull request Jul 11, 2026
Adds wasm32-unknown-emscripten as a target for nix, plus a CI job that runs the
full test suite under Node.

Source changes to compile on emscripten (which behaves like a Linux/musl libc):

* ioctl: use the Linux ioctl module and a c_int ioctl_num_type on emscripten
  (src/sys/ioctl, src/sys/mod.rs).
* resource: enable getrlimit/setrlimit and the Resource enum via libc::rlimit.
* socket: include emscripten in the SockaddrStorage AF_UNIX (pathname/unnamed)
  decode arms.
* unistd: sethostname is unavailable on emscripten, so gate it out.

The suite spawns OS threads (signal and socket-peer tests), so std is rebuilt
with atomics via -Zbuild-std and linked -pthread with -sPROXY_TO_PTHREAD so the
main thread can block; JSPI (-sJSPI) provides the return-to-host suspension for
blocking reads/writes, and NODERAWFS/NODERAWSOCKETS back the filesystem and
sockets with node's. This needs nightly + rust-src and a JSPI-capable Node
(26+, or 22 with --experimental-wasm-jspi). No custom target spec is required:
nightly now emits the __main_argc_argv entry point (rust-lang/rust#158937).

Genuinely-unsupported operations are ignored with
#[cfg_attr(target_os = "emscripten", ignore)] rather than cfg'd out, covering
fork/wait, pty/termios, mkfifo, getpwnam/setgroups/acct, flock, if_nametoindex,
pipe2 FD_CLOEXEC, AF_UNIX SCM_RIGHTS/datagram, thread signal masking, and
setrlimit; a few tests are cfg'd out where the feature is entirely absent
(raw signal actions, socketpair) or the host differs (linkat symlink follow).

Temporary, until the dependencies land upstream:

* Cargo.toml patches libc to guybedford/libc#libc-0.2-emscripten for the
  emscripten externs (rust-lang/libc#5270).
* the CI job builds against the guybedford/emscripten `cf` fork, which carries
  the NODEFS/socket/blocking patches this target depends on
  (emscripten-core/emscripten#27305, #27306).

Suite result: 148 passed, 0 failed, 35 ignored under Node - green on Linux CI.
guybedford added a commit to guybedford/nix that referenced this pull request Jul 11, 2026
Adds wasm32-unknown-emscripten as a target for nix, plus a CI job that runs the
full test suite under Node.

Source changes to compile on emscripten (which behaves like a Linux/musl libc):

* ioctl: use the Linux ioctl module and a c_int ioctl_num_type on emscripten
  (src/sys/ioctl, src/sys/mod.rs).
* resource: enable getrlimit/setrlimit and the Resource enum via libc::rlimit.
* socket: include emscripten in the SockaddrStorage AF_UNIX (pathname/unnamed)
  decode arms.
* unistd: sethostname is unavailable on emscripten, so gate it out.

The suite spawns OS threads (signal and socket-peer tests), so std is rebuilt
with atomics via -Zbuild-std and linked -pthread with -sPROXY_TO_PTHREAD so the
main thread can block; JSPI (-sJSPI) provides the return-to-host suspension for
blocking reads/writes, and NODERAWFS/NODERAWSOCKETS back the filesystem and
sockets with node's. This needs nightly + rust-src and a JSPI-capable Node
(26+, or 22 with --experimental-wasm-jspi). No custom target spec is required:
nightly now emits the __main_argc_argv entry point (rust-lang/rust#158937).

Genuinely-unsupported operations are ignored with
fork/wait, pty/termios, mkfifo, getpwnam/setgroups/acct, flock, if_nametoindex,
pipe2 FD_CLOEXEC, AF_UNIX SCM_RIGHTS/datagram, thread signal masking, and
setrlimit; a few tests are cfg'd out where the feature is entirely absent
(raw signal actions, socketpair) or the host differs (linkat symlink follow).

Temporary, until the dependencies land upstream:

* Cargo.toml patches libc to guybedford/libc#libc-0.2-emscripten for the
  emscripten externs (rust-lang/libc#5270).
* the CI job builds against the guybedford/emscripten `cf` fork, which carries
  the NODEFS/socket/blocking patches this target depends on
  (emscripten-core/emscripten#27305, #27306).

Suite result: 148 passed, 0 failed, 35 ignored under Node - green on Linux CI.
guybedford added a commit to guybedford/nix that referenced this pull request Jul 11, 2026
Adds wasm32-unknown-emscripten as a target for nix, plus a CI job that runs the
full test suite under Node.

Source changes to compile on emscripten (which behaves like a Linux/musl libc):

* ioctl: use the Linux ioctl module and a c_int ioctl_num_type on emscripten
  (src/sys/ioctl, src/sys/mod.rs).
* resource: enable getrlimit/setrlimit and the Resource enum via libc::rlimit.
* socket: include emscripten in the SockaddrStorage AF_UNIX (pathname/unnamed)
  decode arms.
* unistd: sethostname is unavailable on emscripten, so gate it out.

The suite spawns OS threads (signal and socket-peer tests), so std is rebuilt
with atomics via -Zbuild-std and linked -pthread with -sPROXY_TO_PTHREAD so the
main thread can block; JSPI (-sJSPI) provides the return-to-host suspension for
blocking reads/writes, and NODERAWFS/NODERAWSOCKETS back the filesystem and
sockets with node's. This needs nightly + rust-src and a JSPI-capable Node
(26+, or 22 with --experimental-wasm-jspi). No custom target spec is required:
nightly now emits the __main_argc_argv entry point (rust-lang/rust#158937).

Unsupported operations are ignored with #[cfg_attr(target_os = "emscripten",
ignore)] rather than cfg'd out wherever they still compile, covering fork/wait,
pty/termios, mkfifo, getpwnam/setgroups/acct, flock, if_nametoindex, pipe2
FD_CLOEXEC, AF_UNIX SCM_RIGHTS/datagram, getsockname, socketpair, setrlimit, and
the linkat symlink-follow host check. The signal-delivery tests (signal, killpg,
thread mask) and the AF_ROUTE test are cfg'd out instead: compiling their
extern "C" handlers in perturbs the wasm indirect function table and aborts the
runtime.

Temporary, until the dependencies land upstream:

* the emscripten CI job injects a libc patch (guybedford/libc#libc-0.2-emscripten,
  rust-lang/libc#5270) via `cargo --config`, so other targets and CI jobs are
  unaffected.
* it also builds against the guybedford/emscripten `cf` fork, which carries the
  NODEFS/socket/blocking patches this target depends on
  (emscripten-core/emscripten#27305, #27306).

Suite result: 148 passed, 0 failed, 38 ignored under Node - green on Linux CI.
…allocate seek errors

Adds a set of filesystem fixes to the legacy (non-WASMFS) JS filesystem,
principally for the NODEFS/NODERAWFS host backends:

* linkat(2): replace the weak `-EMLINK` stub with a real implementation. A
  new `FS.link` core op looks up the target and delegates to a backend `link`
  node op when present, returning EMLINK for backends without one. Only
  NODERAWFS provides it (via `fs.linkSync`); NODEFS deliberately does not, since
  a real host hardlink cannot be confined to the mount root (host symlinks in
  intermediate path components would escape it), so it reports EMLINK like
  MEMFS. AT_SYMLINK_FOLLOW is honored.

* utimensat(2): support AT_SYMLINK_NOFOLLOW instead of asserting `!flags`.
  `FS.utime` gains a `dontFollow` argument threaded through `doSetAttr`.
  NODERAWFS and NODEFS use `fs.lutimesSync` so a symlink's own timestamps are
  set without the host resolving the link (which would otherwise escape the
  NODEFS mount root).

* getuid/geteuid/getgid/getegid: report the real host process credentials
  under NODERAWFS (via node's `process.get*id()`), rather than always 0.
  These move from weak C stubs to the JS syscall layer, since a weak C
  definition would otherwise win over the JS override. Other backends (and
  Windows, which has no uid/gid concept) continue to report 0. The set*
  variants remain EPERM: musl routes them through `__setxid`, which emscripten
  stubs out because it relies on dynamic (numeric) syscall dispatch.

* posix_fadvise/posix_fallocate: return ESPIPE on a non-seekable fd (pipe or
  socket), matching Linux. `__syscall_fadvise64` moves from a weak C stub to
  the JS syscall layer so it can inspect the stream.

* NODERAWFS fstat: defer virtual streams (pipes, sockets) that have no backing
  host fd to their own `getattr` rather than calling `fs.fstatSync` on an
  undefined nfd.

Adds test/fs coverage across MEMFS/NODEFS/NODERAWFS: test_fs_link,
test_fs_utimensat_nofollow, test_fs_getuid, and test_fs_fadvise_fallocate.
@guybedford

Copy link
Copy Markdown
Collaborator Author

@sbc100 ah it was still including the getgid functions incorrectly, I've fixed that now.

@sbc100 sbc100 enabled auto-merge (squash) July 11, 2026 02:28
@sbc100 sbc100 merged commit 16fa2c6 into emscripten-core:main Jul 11, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants