diff --git a/src/lib/libfs.js b/src/lib/libfs.js index 7c2aa82c2d04f..2c898f94af863 100644 --- a/src/lib/libfs.js +++ b/src/lib/libfs.js @@ -855,6 +855,25 @@ FS.staticInit();`; #endif return parent.node_ops.symlink(parent, newname, oldpath); }, + link(oldpath, newpath, flags) { + var lookup = FS.lookupPath(newpath, { parent: true }); + var parent = lookup.node; + if (!parent) { + throw new FS.ErrnoError({{{ cDefs.ENOENT }}}); + } + var newname = PATH.basename(newpath); + var errCode = FS.mayCreate(parent, newname); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + // Hardlinks are only supported by filesystem backends that provide a + // `link` node op (e.g. NODERAWFS backed by the host). NODEFS omits it: + // a host hardlink cannot be confined to the mount root. + if (!parent.node_ops.link) { + throw new FS.ErrnoError({{{ cDefs.EMLINK }}}); + } + return parent.node_ops.link(parent, newname, oldpath, flags); + }, rename(old_path, new_path) { var old_dirname = PATH.dirname(old_path); var new_dirname = PATH.dirname(new_path); @@ -1119,13 +1138,12 @@ FS.staticInit();`; } FS.doTruncate(stream, stream.node, len); }, - utime(path, atime, mtime) { - var lookup = FS.lookupPath(path, { follow: true }); - var node = lookup.node; - var setattr = FS.checkOpExists(node.node_ops.setattr, {{{ cDefs.EPERM }}}); - setattr(node, { + utime(path, atime, mtime, dontFollow) { + var lookup = FS.lookupPath(path, { follow: !dontFollow }); + FS.doSetAttr(null, lookup.node, { atime: atime, - mtime: mtime + mtime: mtime, + dontFollow }); }, open(path, flags, mode = 0o666) { diff --git a/src/lib/libnodefs.js b/src/lib/libnodefs.js index d3ba827f5a436..68301bcf73496 100644 --- a/src/lib/libnodefs.js +++ b/src/lib/libnodefs.js @@ -189,7 +189,11 @@ addToLibrary({ if (attr.mode != null && attr.dontFollow) { throw new FS.ErrnoError({{{ cDefs.ENOSYS }}}); } - NODEFS.setattr(path, node, attr, fs.chmodSync, fs.utimesSync, fs.truncateSync, fs.lstatSync); + // `dontFollow` (AT_SYMLINK_NOFOLLOW): use lutimes so the symlink's own + // timestamps are set without the host resolving it, which would + // otherwise escape the NODEFS mount root. + var utimes = attr.dontFollow ? fs.lutimesSync : fs.utimesSync; + NODEFS.setattr(path, node, attr, fs.chmodSync, utimes, fs.truncateSync, fs.lstatSync); }, lookup(parent, name) { var path = PATH.join2(NODEFS.realPath(parent), name); diff --git a/src/lib/libnoderawfs.js b/src/lib/libnoderawfs.js index 57387ad233674..ecb4a1704eac2 100644 --- a/src/lib/libnoderawfs.js +++ b/src/lib/libnoderawfs.js @@ -83,6 +83,14 @@ addToLibrary({ }, mkdir(...args) { fs.mkdirSync(...args); }, symlink(...args) { fs.symlinkSync(...args); }, + link(oldpath, newpath, flags) { + // AT_SYMLINK_FOLLOW (0x400): dereference oldpath if it is a symlink, + // since node's link(2) links to the symlink itself by default. + if (flags & 0x400) { + oldpath = fs.realpathSync(oldpath); + } + fs.linkSync(oldpath, newpath); + }, rename(...args) { fs.renameSync(...args); }, rmdir(...args) { fs.rmdirSync(...args); }, readdir(...args) { return ['.', '..'].concat(fs.readdirSync(...args)); }, @@ -99,6 +107,12 @@ addToLibrary({ }, fstat(fd) { var stream = FS.getStreamChecked(fd); + // Virtual streams (pipes, sockets) have no backing node fd; defer to their + // own getattr rather than node's fs.fstatSync. + var getattr = stream.stream_ops?.getattr ?? stream.node.node_ops?.getattr; + if (getattr) { + return getattr(stream.stream_ops?.getattr ? stream : stream.node); + } return fs.fstatSync(stream.nfd); }, statfs(path) { @@ -152,16 +166,20 @@ addToLibrary({ var stream = FS.getStreamChecked(fd); fs.ftruncateSync(stream.nfd, len); }, - utime(path, atime, mtime) { + utime(path, atime, mtime, dontFollow) { // null here for atime or mtime means UTIME_OMIT was passed. Since node // doesn't support this concept we need to first find the existing // timestamps in order to preserve them. if ((atime === null) || (mtime === null)) { - var st = fs.statSync(path); + var st = dontFollow ? fs.lstatSync(path) : fs.statSync(path); atime ||= st.atimeMs; mtime ||= st.mtimeMs; } - fs.utimesSync(path, atime/1000, mtime/1000); + if (dontFollow) { + fs.lutimesSync(path, atime/1000, mtime/1000); + } else { + fs.utimesSync(path, atime/1000, mtime/1000); + } }, open(path, flags, mode) { flags = FS_modeStringToFlags(flags); diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index d3a5b64b62605..538f80c9b7c2b 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -241,6 +241,7 @@ sigs = { __syscall_epoll_ctl__sig: 'iiiip', __syscall_epoll_pwait__sig: 'iipiipp', __syscall_faccessat__sig: 'iipii', + __syscall_fadvise64__sig: 'iijji', __syscall_fallocate__sig: 'iiijj', __syscall_fchdir__sig: 'ii', __syscall_fchmod__sig: 'iii', @@ -258,6 +259,7 @@ sigs = { __syscall_getsockname__sig: 'iippiii', __syscall_getsockopt__sig: 'iiiippi', __syscall_ioctl__sig: 'iiip', + __syscall_linkat__sig: 'iipipi', __syscall_listen__sig: 'iiiiiii', __syscall_lstat64__sig: 'ipp', __syscall_mkdirat__sig: 'iipi', diff --git a/src/lib/libsyscall.js b/src/lib/libsyscall.js index db0dc0bfcd0d0..603fdae2a50f3 100644 --- a/src/lib/libsyscall.js +++ b/src/lib/libsyscall.js @@ -961,6 +961,14 @@ var SyscallsLibrary = { FS.symlink(target, linkpath); return 0; }, + __syscall_linkat: (olddirfd, oldpath, newdirfd, newpath, flags) => { + oldpath = SYSCALLS.getStr(oldpath); + newpath = SYSCALLS.getStr(newpath); + oldpath = SYSCALLS.calculateAt(olddirfd, oldpath); + newpath = SYSCALLS.calculateAt(newdirfd, newpath); + FS.link(oldpath, newpath, flags); + return 0; + }, __syscall_readlinkat__deps: ['$lengthBytesUTF8', '$stringToUTF8'], __syscall_readlinkat: (dirfd, path, buf, bufsize) => { path = SYSCALLS.getStr(path); @@ -1009,10 +1017,8 @@ var SyscallsLibrary = { }, __syscall_utimensat__deps: ['$readI53FromI64'], __syscall_utimensat: (dirfd, path, times, flags) => { + var nofollow = flags & {{{ cDefs.AT_SYMLINK_NOFOLLOW }}}; path = SYSCALLS.getStr(path); -#if ASSERTIONS - assert(!flags); -#endif path = SYSCALLS.calculateAt(dirfd, path, true); var now = Date.now(), atime, mtime; if (!times) { @@ -1042,7 +1048,7 @@ var SyscallsLibrary = { // null here means UTIME_OMIT was passed. If both were set to UTIME_OMIT then // we can skip the call completely. if ((mtime ?? atime) !== null) { - FS.utime(path, atime, mtime); + FS.utime(path, atime, mtime, nofollow); } return 0; }, @@ -1055,6 +1061,11 @@ var SyscallsLibrary = { if (offset < 0 || len < 0) { return -{{{ cDefs.EINVAL }}} } + // fallocate is only meaningful on regular files; a pipe/socket is a seek + // error (ESPIPE), matching Linux. + if (!SYSCALLS.getStreamFromFD(fd).seekable) { + return -{{{ cDefs.ESPIPE }}}; + } // We only support mode == 0, which means we can implement fallocate // in terms of ftruncate. var oldSize = FS.fstat(fd).size; @@ -1064,6 +1075,15 @@ var SyscallsLibrary = { } return 0; }, + __syscall_fadvise64__i53abi: true, + __syscall_fadvise64: (fd, offset, len, advice) => { + // Advisory only, so a no-op, but a pipe/socket fd is still a seek error + // (ESPIPE), matching Linux. + if (!SYSCALLS.getStreamFromFD(fd).seekable) { + return -{{{ cDefs.ESPIPE }}}; + } + return 0; + }, __syscall_dup3: (fd, newfd, flags) => { if (fd === newfd) return -{{{ cDefs.EINVAL }}}; if (flags & ~{{{ cDefs.O_CLOEXEC }}}) return -{{{ cDefs.EINVAL }}}; diff --git a/system/lib/libc/emscripten_syscall_stubs.c b/system/lib/libc/emscripten_syscall_stubs.c index 1341cbf2256fd..e0e1f2f5158bf 100644 --- a/system/lib/libc/emscripten_syscall_stubs.c +++ b/system/lib/libc/emscripten_syscall_stubs.c @@ -96,10 +96,6 @@ weak pid_t __syscall_getppid() { return g_ppid; } -weak int __syscall_linkat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath, int flags) { - return -EMLINK; // no hardlinks for us -} - weak int __syscall_getgroups32(int count, gid_t list[]) { if (count < 1) { return -EINVAL; @@ -170,11 +166,6 @@ weak int __syscall_madvise(void *addr, size_t length, int advice) { return 0; } -weak int __syscall_fadvise64(int fd, off_t offset, off_t length, int advice) { - // this is purely advisory, so we don't warn - return 0; -} - weak int __syscall_mlock(const void *addr, size_t len) { REPORT(mlock); return 0; diff --git a/system/lib/wasmfs/syscalls.cpp b/system/lib/wasmfs/syscalls.cpp index 3b70211d152de..757a2d7f21c46 100644 --- a/system/lib/wasmfs/syscalls.cpp +++ b/system/lib/wasmfs/syscalls.cpp @@ -1855,6 +1855,15 @@ int __syscall_fadvise64(int fd, off_t offset, off_t len, int advice) { return 0; } +int __syscall_linkat(int olddirfd, + const char* oldpath, + int newdirfd, + const char* newpath, + int flags) { + // Hardlinks are not supported in WASMFS. + return -EMLINK; +} + // epoll is implemented in the legacy (non-WASMFS) JS syscall layer only. int __syscall_epoll_create1(int flags) { return -ENOSYS; } diff --git a/test/codesize/test_codesize_file_preload.expected.js b/test/codesize/test_codesize_file_preload.expected.js index 744e5a7203ba3..d6a8f15aea8cb 100644 --- a/test/codesize/test_codesize_file_preload.expected.js +++ b/test/codesize/test_codesize_file_preload.expected.js @@ -2053,6 +2053,27 @@ var FS = { } return parent.node_ops.symlink(parent, newname, oldpath); }, + link(oldpath, newpath, flags) { + var lookup = FS.lookupPath(newpath, { + parent: true + }); + var parent = lookup.node; + if (!parent) { + throw new FS.ErrnoError(44); + } + var newname = PATH.basename(newpath); + var errCode = FS.mayCreate(parent, newname); + if (errCode) { + throw new FS.ErrnoError(errCode); + } + // Hardlinks are only supported by filesystem backends that provide a + // `link` node op (e.g. NODERAWFS backed by the host). NODEFS omits it: + // a host hardlink cannot be confined to the mount root. + if (!parent.node_ops.link) { + throw new FS.ErrnoError(34); + } + return parent.node_ops.link(parent, newname, oldpath, flags); + }, rename(old_path, new_path) { var old_dirname = PATH.dirname(old_path); var new_dirname = PATH.dirname(new_path); @@ -2310,15 +2331,14 @@ var FS = { } FS.doTruncate(stream, stream.node, len); }, - utime(path, atime, mtime) { + utime(path, atime, mtime, dontFollow) { var lookup = FS.lookupPath(path, { - follow: true + follow: !dontFollow }); - var node = lookup.node; - var setattr = FS.checkOpExists(node.node_ops.setattr, 63); - setattr(node, { + FS.doSetAttr(null, lookup.node, { atime, - mtime + mtime, + dontFollow }); }, open(path, flags, mode = 438) { diff --git a/test/codesize/test_codesize_hello_O0.json b/test/codesize/test_codesize_hello_O0.json index a0dedcb9dc755..7a6749f3c294e 100644 --- a/test/codesize/test_codesize_hello_O0.json +++ b/test/codesize/test_codesize_hello_O0.json @@ -1,10 +1,10 @@ { - "a.out.js": 23671, - "a.out.js.gz": 8602, + "a.out.js": 23679, + "a.out.js.gz": 8604, "a.out.nodebug.wasm": 15115, "a.out.nodebug.wasm.gz": 7464, - "total": 38786, - "total_gz": 16066, + "total": 38794, + "total_gz": 16068, "sent": [ "fd_write" ], diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 947626e9b936b..1dca8f07ac0cb 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 268621, - "a.out.nodebug.wasm": 587978, - "total": 856599, + "a.out.js": 269157, + "a.out.nodebug.wasm": 588047, + "total": 857204, "sent": [ "IMG_Init", "IMG_Load", @@ -226,6 +226,7 @@ "__syscall_epoll_ctl", "__syscall_epoll_pwait", "__syscall_faccessat", + "__syscall_fadvise64", "__syscall_fallocate", "__syscall_fchdir", "__syscall_fchmod", @@ -243,6 +244,7 @@ "__syscall_getsockname", "__syscall_getsockopt", "__syscall_ioctl", + "__syscall_linkat", "__syscall_listen", "__syscall_lstat64", "__syscall_mkdirat", @@ -1751,6 +1753,7 @@ "env.__syscall_epoll_ctl", "env.__syscall_epoll_pwait", "env.__syscall_faccessat", + "env.__syscall_fadvise64", "env.__syscall_fallocate", "env.__syscall_fchdir", "env.__syscall_fchmod", @@ -1768,6 +1771,7 @@ "env.__syscall_getsockname", "env.__syscall_getsockopt", "env.__syscall_ioctl", + "env.__syscall_linkat", "env.__syscall_listen", "env.__syscall_lstat64", "env.__syscall_mkdirat", @@ -2205,7 +2209,6 @@ "__subvti3", "__synccall", "__syscall_acct", - "__syscall_fadvise64", "__syscall_getegid32", "__syscall_geteuid32", "__syscall_getgid32", @@ -2219,7 +2222,6 @@ "__syscall_getrusage", "__syscall_getsid", "__syscall_getuid32", - "__syscall_linkat", "__syscall_madvise", "__syscall_mincore", "__syscall_mlock", @@ -4090,14 +4092,12 @@ "$__subvti3", "$__synccall", "$__syscall_acct", - "$__syscall_fadvise64", "$__syscall_getgroups32", "$__syscall_getpid", "$__syscall_getppid", "$__syscall_getresuid32", "$__syscall_getrusage", "$__syscall_getsid", - "$__syscall_linkat", "$__syscall_madvise", "$__syscall_mincore", "$__syscall_mmap2", @@ -4926,6 +4926,7 @@ "$pop_arg_long_double", "$popen", "$posix_close", + "$posix_fadvise", "$posix_fallocate", "$posix_getdents", "$posix_openpt", diff --git a/test/codesize/test_codesize_minimal_O0.expected.js b/test/codesize/test_codesize_minimal_O0.expected.js index 31fc7a1c39f49..e11b174c400ee 100644 --- a/test/codesize/test_codesize_minimal_O0.expected.js +++ b/test/codesize/test_codesize_minimal_O0.expected.js @@ -1173,6 +1173,7 @@ missingLibrarySymbols.forEach(missingLibrarySymbol) 'FS_mkdir', 'FS_mkdev', 'FS_symlink', + 'FS_link', 'FS_rename', 'FS_rmdir', 'FS_readdir', diff --git a/test/codesize/test_codesize_minimal_O0.json b/test/codesize/test_codesize_minimal_O0.json index 1e370c03018c1..a48ad7acf9a1b 100644 --- a/test/codesize/test_codesize_minimal_O0.json +++ b/test/codesize/test_codesize_minimal_O0.json @@ -1,10 +1,10 @@ { - "a.out.js": 18849, - "a.out.js.gz": 6766, + "a.out.js": 18857, + "a.out.js.gz": 6768, "a.out.nodebug.wasm": 1015, "a.out.nodebug.wasm.gz": 602, - "total": 19864, - "total_gz": 7368, + "total": 19872, + "total_gz": 7370, "sent": [], "imports": [], "exports": [ diff --git a/test/codesize/test_unoptimized_code_size.json b/test/codesize/test_unoptimized_code_size.json index 7ea0feadc9484..117fdc26c92d8 100644 --- a/test/codesize/test_unoptimized_code_size.json +++ b/test/codesize/test_unoptimized_code_size.json @@ -1,16 +1,16 @@ { - "hello_world.js": 56352, - "hello_world.js.gz": 17727, + "hello_world.js": 56365, + "hello_world.js.gz": 17729, "hello_world.wasm": 15115, "hello_world.wasm.gz": 7464, "no_asserts.js": 25511, "no_asserts.js.gz": 8679, "no_asserts.wasm": 12229, "no_asserts.wasm.gz": 6004, - "strict.js": 53454, - "strict.js.gz": 16711, + "strict.js": 53467, + "strict.js.gz": 16713, "strict.wasm": 15115, "strict.wasm.gz": 7461, - "total": 177776, - "total_gz": 64046 + "total": 177802, + "total_gz": 64050 } diff --git a/test/fs/test_fadvise_fallocate.c b/test/fs/test_fadvise_fallocate.c new file mode 100644 index 0000000000000..49b2f29a00a5b --- /dev/null +++ b/test/fs/test_fadvise_fallocate.c @@ -0,0 +1,39 @@ +/* + * Copyright 2024 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. + */ + +#include +#include +#include +#include +#include +#include + +int main() { + // Regular file: fallocate extends it and fadvise is a successful no-op. + int fd = open("file", O_RDWR | O_CREAT | O_TRUNC, 0644); + assert(fd >= 0); + assert(posix_fallocate(fd, 0, 100) == 0); + struct stat st; + assert(fstat(fd, &st) == 0); + assert(st.st_size == 100); + assert(posix_fadvise(fd, 0, 0, POSIX_FADV_NORMAL) == 0); + close(fd); + + // Pipe: non-seekable, so both report ESPIPE (matching Linux). Its fstat also + // reports a FIFO even under NODERAWFS, where the stream has no host fd. + int fds[2]; + assert(pipe(fds) == 0); + assert(fstat(fds[0], &st) == 0); + assert(S_ISFIFO(st.st_mode)); + assert(posix_fallocate(fds[1], 0, 10) == ESPIPE); + assert(posix_fadvise(fds[0], 0, 0, POSIX_FADV_NORMAL) == ESPIPE); + close(fds[0]); + close(fds[1]); + + printf("done\n"); + return 0; +} diff --git a/test/fs/test_link.c b/test/fs/test_link.c new file mode 100644 index 0000000000000..234493b9ce8b8 --- /dev/null +++ b/test/fs/test_link.c @@ -0,0 +1,69 @@ +/* + * Copyright 2024 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. + */ + +#include +#include +#include +#include +#include +#include +#include + +static void create_file(const char* path, const char* data) { + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + assert(fd >= 0); + assert(write(fd, data, strlen(data)) == (ssize_t)strlen(data)); + close(fd); +} + +int main() { + create_file("foo", "hello"); + +#if defined(MEMFS) || defined(NODEFS) + // MEMFS and NODEFS provide no `link` node op, so hardlinks report EMLINK. + // NODEFS intentionally omits it: unlike NODERAWFS it confines access to a + // mount root, but a real host hardlink cannot be guaranteed to stay within + // that root (host symlinks in intermediate path components escape it). + assert(link("foo", "bar") == -1); + assert(errno == EMLINK); +#else + // NODERAWFS: real hardlinks backed by the host filesystem. + struct stat st; + assert(link("foo", "bar") == 0); + assert(stat("foo", &st) == 0); + assert(st.st_nlink == 2); + assert(stat("bar", &st) == 0); + assert(st.st_nlink == 2); + + char buf[7] = {0}; + int fd = open("bar", O_RDONLY); + assert(fd >= 0); + assert(read(fd, buf, 5) == 5); + assert(strcmp(buf, "hello") == 0); + close(fd); + + // Appending through one link is visible through the other: same inode. + fd = open("foo", O_WRONLY | O_APPEND); + assert(fd >= 0); + assert(write(fd, "!", 1) == 1); + close(fd); + assert(stat("bar", &st) == 0); + assert(st.st_size == 6); + + // linkat with AT_SYMLINK_FOLLOW dereferences a symlink to its target, so the + // new link shares foo's inode (nlink becomes 3) rather than the symlink's. + assert(symlink("foo", "sym") == 0); + assert(linkat(AT_FDCWD, "sym", AT_FDCWD, "baz", AT_SYMLINK_FOLLOW) == 0); + assert(lstat("baz", &st) == 0); + assert(!S_ISLNK(st.st_mode)); + assert(st.st_nlink == 3); + assert(st.st_size == 6); +#endif + + printf("done\n"); + return 0; +} diff --git a/test/fs/test_utimensat_nofollow.c b/test/fs/test_utimensat_nofollow.c new file mode 100644 index 0000000000000..f058850886c3f --- /dev/null +++ b/test/fs/test_utimensat_nofollow.c @@ -0,0 +1,38 @@ +/* + * Copyright 2024 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. + */ + +#include +#include +#include +#include +#include + +int main() { + int fd = open("target", O_WRONLY | O_CREAT | O_TRUNC, 0644); + assert(fd >= 0); + close(fd); + assert(symlink("target", "link") == 0); + + struct timespec times[2]; + times[0].tv_sec = 111111111; // atime + times[0].tv_nsec = 0; + times[1].tv_sec = 222222222; // mtime + times[1].tv_nsec = 0; + + // AT_SYMLINK_NOFOLLOW sets the symlink's own timestamps rather than the + // target's. + assert(utimensat(AT_FDCWD, "link", times, AT_SYMLINK_NOFOLLOW) == 0); + + struct stat ls, ts; + assert(lstat("link", &ls) == 0); // the symlink itself + assert(stat("target", &ts) == 0); // the target it points at + assert(ls.st_mtim.tv_sec == 222222222); + assert(ts.st_mtim.tv_sec != 222222222); // target left untouched + + printf("done\n"); + return 0; +} diff --git a/test/test_core.py b/test/test_core.py index 21b70ff71c42b..fe18964eaae1d 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -5973,6 +5973,20 @@ def test_fs_access_mode(self): def test_fs_emptyPath(self): self.do_runf_out_file('fs/test_emptyPath.c') + @no_windows('no symlink support on windows') + @also_with_nodefs_both + def test_fs_link(self): + self.do_runf('fs/test_link.c', 'done\n') + + @no_windows('no symlink support on windows') + @also_with_nodefs_both + def test_fs_utimensat_nofollow(self): + self.do_runf('fs/test_utimensat_nofollow.c', 'done\n') + + @also_with_nodefs_both + def test_fs_fadvise_fallocate(self): + self.do_runf('fs/test_fadvise_fallocate.c', 'done\n') + @no_windows('https://github.com/emscripten-core/emscripten/issues/8882') @crossplatform @also_with_nodefs_both