Skip to content

Commit 16fa2c6

Browse files
authored
[NODEFS] hardlinks, utimensat nofollow, and fadvise/fallocate seek errors (emscripten-core#27305)
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_
1 parent 93ff3a9 commit 16fa2c6

17 files changed

Lines changed: 294 additions & 50 deletions

src/lib/libfs.js

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,25 @@ FS.staticInit();`;
855855
#endif
856856
return parent.node_ops.symlink(parent, newname, oldpath);
857857
},
858+
link(oldpath, newpath, flags) {
859+
var lookup = FS.lookupPath(newpath, { parent: true });
860+
var parent = lookup.node;
861+
if (!parent) {
862+
throw new FS.ErrnoError({{{ cDefs.ENOENT }}});
863+
}
864+
var newname = PATH.basename(newpath);
865+
var errCode = FS.mayCreate(parent, newname);
866+
if (errCode) {
867+
throw new FS.ErrnoError(errCode);
868+
}
869+
// Hardlinks are only supported by filesystem backends that provide a
870+
// `link` node op (e.g. NODERAWFS backed by the host). NODEFS omits it:
871+
// a host hardlink cannot be confined to the mount root.
872+
if (!parent.node_ops.link) {
873+
throw new FS.ErrnoError({{{ cDefs.EMLINK }}});
874+
}
875+
return parent.node_ops.link(parent, newname, oldpath, flags);
876+
},
858877
rename(old_path, new_path) {
859878
var old_dirname = PATH.dirname(old_path);
860879
var new_dirname = PATH.dirname(new_path);
@@ -1119,13 +1138,12 @@ FS.staticInit();`;
11191138
}
11201139
FS.doTruncate(stream, stream.node, len);
11211140
},
1122-
utime(path, atime, mtime) {
1123-
var lookup = FS.lookupPath(path, { follow: true });
1124-
var node = lookup.node;
1125-
var setattr = FS.checkOpExists(node.node_ops.setattr, {{{ cDefs.EPERM }}});
1126-
setattr(node, {
1141+
utime(path, atime, mtime, dontFollow) {
1142+
var lookup = FS.lookupPath(path, { follow: !dontFollow });
1143+
FS.doSetAttr(null, lookup.node, {
11271144
atime: atime,
1128-
mtime: mtime
1145+
mtime: mtime,
1146+
dontFollow
11291147
});
11301148
},
11311149
open(path, flags, mode = 0o666) {

src/lib/libnodefs.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,11 @@ addToLibrary({
189189
if (attr.mode != null && attr.dontFollow) {
190190
throw new FS.ErrnoError({{{ cDefs.ENOSYS }}});
191191
}
192-
NODEFS.setattr(path, node, attr, fs.chmodSync, fs.utimesSync, fs.truncateSync, fs.lstatSync);
192+
// `dontFollow` (AT_SYMLINK_NOFOLLOW): use lutimes so the symlink's own
193+
// timestamps are set without the host resolving it, which would
194+
// otherwise escape the NODEFS mount root.
195+
var utimes = attr.dontFollow ? fs.lutimesSync : fs.utimesSync;
196+
NODEFS.setattr(path, node, attr, fs.chmodSync, utimes, fs.truncateSync, fs.lstatSync);
193197
},
194198
lookup(parent, name) {
195199
var path = PATH.join2(NODEFS.realPath(parent), name);

src/lib/libnoderawfs.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ addToLibrary({
8383
},
8484
mkdir(...args) { fs.mkdirSync(...args); },
8585
symlink(...args) { fs.symlinkSync(...args); },
86+
link(oldpath, newpath, flags) {
87+
// AT_SYMLINK_FOLLOW (0x400): dereference oldpath if it is a symlink,
88+
// since node's link(2) links to the symlink itself by default.
89+
if (flags & 0x400) {
90+
oldpath = fs.realpathSync(oldpath);
91+
}
92+
fs.linkSync(oldpath, newpath);
93+
},
8694
rename(...args) { fs.renameSync(...args); },
8795
rmdir(...args) { fs.rmdirSync(...args); },
8896
readdir(...args) { return ['.', '..'].concat(fs.readdirSync(...args)); },
@@ -99,6 +107,12 @@ addToLibrary({
99107
},
100108
fstat(fd) {
101109
var stream = FS.getStreamChecked(fd);
110+
// Virtual streams (pipes, sockets) have no backing node fd; defer to their
111+
// own getattr rather than node's fs.fstatSync.
112+
var getattr = stream.stream_ops?.getattr ?? stream.node.node_ops?.getattr;
113+
if (getattr) {
114+
return getattr(stream.stream_ops?.getattr ? stream : stream.node);
115+
}
102116
return fs.fstatSync(stream.nfd);
103117
},
104118
statfs(path) {
@@ -152,16 +166,20 @@ addToLibrary({
152166
var stream = FS.getStreamChecked(fd);
153167
fs.ftruncateSync(stream.nfd, len);
154168
},
155-
utime(path, atime, mtime) {
169+
utime(path, atime, mtime, dontFollow) {
156170
// null here for atime or mtime means UTIME_OMIT was passed. Since node
157171
// doesn't support this concept we need to first find the existing
158172
// timestamps in order to preserve them.
159173
if ((atime === null) || (mtime === null)) {
160-
var st = fs.statSync(path);
174+
var st = dontFollow ? fs.lstatSync(path) : fs.statSync(path);
161175
atime ||= st.atimeMs;
162176
mtime ||= st.mtimeMs;
163177
}
164-
fs.utimesSync(path, atime/1000, mtime/1000);
178+
if (dontFollow) {
179+
fs.lutimesSync(path, atime/1000, mtime/1000);
180+
} else {
181+
fs.utimesSync(path, atime/1000, mtime/1000);
182+
}
165183
},
166184
open(path, flags, mode) {
167185
flags = FS_modeStringToFlags(flags);

src/lib/libsigs.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ sigs = {
241241
__syscall_epoll_ctl__sig: 'iiiip',
242242
__syscall_epoll_pwait__sig: 'iipiipp',
243243
__syscall_faccessat__sig: 'iipii',
244+
__syscall_fadvise64__sig: 'iijji',
244245
__syscall_fallocate__sig: 'iiijj',
245246
__syscall_fchdir__sig: 'ii',
246247
__syscall_fchmod__sig: 'iii',
@@ -258,6 +259,7 @@ sigs = {
258259
__syscall_getsockname__sig: 'iippiii',
259260
__syscall_getsockopt__sig: 'iiiippi',
260261
__syscall_ioctl__sig: 'iiip',
262+
__syscall_linkat__sig: 'iipipi',
261263
__syscall_listen__sig: 'iiiiiii',
262264
__syscall_lstat64__sig: 'ipp',
263265
__syscall_mkdirat__sig: 'iipi',

src/lib/libsyscall.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,14 @@ var SyscallsLibrary = {
961961
FS.symlink(target, linkpath);
962962
return 0;
963963
},
964+
__syscall_linkat: (olddirfd, oldpath, newdirfd, newpath, flags) => {
965+
oldpath = SYSCALLS.getStr(oldpath);
966+
newpath = SYSCALLS.getStr(newpath);
967+
oldpath = SYSCALLS.calculateAt(olddirfd, oldpath);
968+
newpath = SYSCALLS.calculateAt(newdirfd, newpath);
969+
FS.link(oldpath, newpath, flags);
970+
return 0;
971+
},
964972
__syscall_readlinkat__deps: ['$lengthBytesUTF8', '$stringToUTF8'],
965973
__syscall_readlinkat: (dirfd, path, buf, bufsize) => {
966974
path = SYSCALLS.getStr(path);
@@ -1009,10 +1017,8 @@ var SyscallsLibrary = {
10091017
},
10101018
__syscall_utimensat__deps: ['$readI53FromI64'],
10111019
__syscall_utimensat: (dirfd, path, times, flags) => {
1020+
var nofollow = flags & {{{ cDefs.AT_SYMLINK_NOFOLLOW }}};
10121021
path = SYSCALLS.getStr(path);
1013-
#if ASSERTIONS
1014-
assert(!flags);
1015-
#endif
10161022
path = SYSCALLS.calculateAt(dirfd, path, true);
10171023
var now = Date.now(), atime, mtime;
10181024
if (!times) {
@@ -1042,7 +1048,7 @@ var SyscallsLibrary = {
10421048
// null here means UTIME_OMIT was passed. If both were set to UTIME_OMIT then
10431049
// we can skip the call completely.
10441050
if ((mtime ?? atime) !== null) {
1045-
FS.utime(path, atime, mtime);
1051+
FS.utime(path, atime, mtime, nofollow);
10461052
}
10471053
return 0;
10481054
},
@@ -1055,6 +1061,11 @@ var SyscallsLibrary = {
10551061
if (offset < 0 || len < 0) {
10561062
return -{{{ cDefs.EINVAL }}}
10571063
}
1064+
// fallocate is only meaningful on regular files; a pipe/socket is a seek
1065+
// error (ESPIPE), matching Linux.
1066+
if (!SYSCALLS.getStreamFromFD(fd).seekable) {
1067+
return -{{{ cDefs.ESPIPE }}};
1068+
}
10581069
// We only support mode == 0, which means we can implement fallocate
10591070
// in terms of ftruncate.
10601071
var oldSize = FS.fstat(fd).size;
@@ -1064,6 +1075,15 @@ var SyscallsLibrary = {
10641075
}
10651076
return 0;
10661077
},
1078+
__syscall_fadvise64__i53abi: true,
1079+
__syscall_fadvise64: (fd, offset, len, advice) => {
1080+
// Advisory only, so a no-op, but a pipe/socket fd is still a seek error
1081+
// (ESPIPE), matching Linux.
1082+
if (!SYSCALLS.getStreamFromFD(fd).seekable) {
1083+
return -{{{ cDefs.ESPIPE }}};
1084+
}
1085+
return 0;
1086+
},
10671087
__syscall_dup3: (fd, newfd, flags) => {
10681088
if (fd === newfd) return -{{{ cDefs.EINVAL }}};
10691089
if (flags & ~{{{ cDefs.O_CLOEXEC }}}) return -{{{ cDefs.EINVAL }}};

system/lib/libc/emscripten_syscall_stubs.c

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,6 @@ weak pid_t __syscall_getppid() {
9696
return g_ppid;
9797
}
9898

99-
weak int __syscall_linkat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath, int flags) {
100-
return -EMLINK; // no hardlinks for us
101-
}
102-
10399
weak int __syscall_getgroups32(int count, gid_t list[]) {
104100
if (count < 1) {
105101
return -EINVAL;
@@ -170,11 +166,6 @@ weak int __syscall_madvise(void *addr, size_t length, int advice) {
170166
return 0;
171167
}
172168

173-
weak int __syscall_fadvise64(int fd, off_t offset, off_t length, int advice) {
174-
// this is purely advisory, so we don't warn
175-
return 0;
176-
}
177-
178169
weak int __syscall_mlock(const void *addr, size_t len) {
179170
REPORT(mlock);
180171
return 0;

system/lib/wasmfs/syscalls.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1855,6 +1855,15 @@ int __syscall_fadvise64(int fd, off_t offset, off_t len, int advice) {
18551855
return 0;
18561856
}
18571857

1858+
int __syscall_linkat(int olddirfd,
1859+
const char* oldpath,
1860+
int newdirfd,
1861+
const char* newpath,
1862+
int flags) {
1863+
// Hardlinks are not supported in WASMFS.
1864+
return -EMLINK;
1865+
}
1866+
18581867
// epoll is implemented in the legacy (non-WASMFS) JS syscall layer only.
18591868
int __syscall_epoll_create1(int flags) { return -ENOSYS; }
18601869

test/codesize/test_codesize_file_preload.expected.js

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2053,6 +2053,27 @@ var FS = {
20532053
}
20542054
return parent.node_ops.symlink(parent, newname, oldpath);
20552055
},
2056+
link(oldpath, newpath, flags) {
2057+
var lookup = FS.lookupPath(newpath, {
2058+
parent: true
2059+
});
2060+
var parent = lookup.node;
2061+
if (!parent) {
2062+
throw new FS.ErrnoError(44);
2063+
}
2064+
var newname = PATH.basename(newpath);
2065+
var errCode = FS.mayCreate(parent, newname);
2066+
if (errCode) {
2067+
throw new FS.ErrnoError(errCode);
2068+
}
2069+
// Hardlinks are only supported by filesystem backends that provide a
2070+
// `link` node op (e.g. NODERAWFS backed by the host). NODEFS omits it:
2071+
// a host hardlink cannot be confined to the mount root.
2072+
if (!parent.node_ops.link) {
2073+
throw new FS.ErrnoError(34);
2074+
}
2075+
return parent.node_ops.link(parent, newname, oldpath, flags);
2076+
},
20562077
rename(old_path, new_path) {
20572078
var old_dirname = PATH.dirname(old_path);
20582079
var new_dirname = PATH.dirname(new_path);
@@ -2310,15 +2331,14 @@ var FS = {
23102331
}
23112332
FS.doTruncate(stream, stream.node, len);
23122333
},
2313-
utime(path, atime, mtime) {
2334+
utime(path, atime, mtime, dontFollow) {
23142335
var lookup = FS.lookupPath(path, {
2315-
follow: true
2336+
follow: !dontFollow
23162337
});
2317-
var node = lookup.node;
2318-
var setattr = FS.checkOpExists(node.node_ops.setattr, 63);
2319-
setattr(node, {
2338+
FS.doSetAttr(null, lookup.node, {
23202339
atime,
2321-
mtime
2340+
mtime,
2341+
dontFollow
23222342
});
23232343
},
23242344
open(path, flags, mode = 438) {

test/codesize/test_codesize_hello_O0.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
2-
"a.out.js": 23671,
3-
"a.out.js.gz": 8602,
2+
"a.out.js": 23679,
3+
"a.out.js.gz": 8604,
44
"a.out.nodebug.wasm": 15115,
55
"a.out.nodebug.wasm.gz": 7464,
6-
"total": 38786,
7-
"total_gz": 16066,
6+
"total": 38794,
7+
"total_gz": 16068,
88
"sent": [
99
"fd_write"
1010
],

test/codesize/test_codesize_hello_dylink_all.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"a.out.js": 268621,
3-
"a.out.nodebug.wasm": 587978,
4-
"total": 856599,
2+
"a.out.js": 269157,
3+
"a.out.nodebug.wasm": 588047,
4+
"total": 857204,
55
"sent": [
66
"IMG_Init",
77
"IMG_Load",
@@ -226,6 +226,7 @@
226226
"__syscall_epoll_ctl",
227227
"__syscall_epoll_pwait",
228228
"__syscall_faccessat",
229+
"__syscall_fadvise64",
229230
"__syscall_fallocate",
230231
"__syscall_fchdir",
231232
"__syscall_fchmod",
@@ -243,6 +244,7 @@
243244
"__syscall_getsockname",
244245
"__syscall_getsockopt",
245246
"__syscall_ioctl",
247+
"__syscall_linkat",
246248
"__syscall_listen",
247249
"__syscall_lstat64",
248250
"__syscall_mkdirat",
@@ -1751,6 +1753,7 @@
17511753
"env.__syscall_epoll_ctl",
17521754
"env.__syscall_epoll_pwait",
17531755
"env.__syscall_faccessat",
1756+
"env.__syscall_fadvise64",
17541757
"env.__syscall_fallocate",
17551758
"env.__syscall_fchdir",
17561759
"env.__syscall_fchmod",
@@ -1768,6 +1771,7 @@
17681771
"env.__syscall_getsockname",
17691772
"env.__syscall_getsockopt",
17701773
"env.__syscall_ioctl",
1774+
"env.__syscall_linkat",
17711775
"env.__syscall_listen",
17721776
"env.__syscall_lstat64",
17731777
"env.__syscall_mkdirat",
@@ -2205,7 +2209,6 @@
22052209
"__subvti3",
22062210
"__synccall",
22072211
"__syscall_acct",
2208-
"__syscall_fadvise64",
22092212
"__syscall_getegid32",
22102213
"__syscall_geteuid32",
22112214
"__syscall_getgid32",
@@ -2219,7 +2222,6 @@
22192222
"__syscall_getrusage",
22202223
"__syscall_getsid",
22212224
"__syscall_getuid32",
2222-
"__syscall_linkat",
22232225
"__syscall_madvise",
22242226
"__syscall_mincore",
22252227
"__syscall_mlock",
@@ -4090,14 +4092,12 @@
40904092
"$__subvti3",
40914093
"$__synccall",
40924094
"$__syscall_acct",
4093-
"$__syscall_fadvise64",
40944095
"$__syscall_getgroups32",
40954096
"$__syscall_getpid",
40964097
"$__syscall_getppid",
40974098
"$__syscall_getresuid32",
40984099
"$__syscall_getrusage",
40994100
"$__syscall_getsid",
4100-
"$__syscall_linkat",
41014101
"$__syscall_madvise",
41024102
"$__syscall_mincore",
41034103
"$__syscall_mmap2",
@@ -4926,6 +4926,7 @@
49264926
"$pop_arg_long_double",
49274927
"$popen",
49284928
"$posix_close",
4929+
"$posix_fadvise",
49294930
"$posix_fallocate",
49304931
"$posix_getdents",
49314932
"$posix_openpt",

0 commit comments

Comments
 (0)