Skip to content

Commit aefb630

Browse files
committed
NODEFS: hardlinks, utimensat nofollow, and fadvise/fallocate 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). * 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, and test_fs_fadvise_fallocate.
1 parent a41c221 commit aefb630

17 files changed

Lines changed: 294 additions & 48 deletions

src/lib/libfs.js

Lines changed: 24 additions & 5 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,13 @@ FS.staticInit();`;
11191138
}
11201139
FS.doTruncate(stream, stream.node, len);
11211140
},
1122-
utime(path, atime, mtime) {
1123-
var lookup = FS.lookupPath(path, { follow: true });
1141+
utime(path, atime, mtime, dontFollow) {
1142+
var lookup = FS.lookupPath(path, { follow: !dontFollow });
11241143
var node = lookup.node;
1125-
var setattr = FS.checkOpExists(node.node_ops.setattr, {{{ cDefs.EPERM }}});
1126-
setattr(node, {
1144+
FS.doSetAttr(null, node, {
11271145
atime: atime,
1128-
mtime: mtime
1146+
mtime: mtime,
1147+
dontFollow
11291148
});
11301149
},
11311150
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 & 5 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,15 @@ 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
});
23172338
var node = lookup.node;
2318-
var setattr = FS.checkOpExists(node.node_ops.setattr, 63);
2319-
setattr(node, {
2339+
FS.doSetAttr(null, node, {
23202340
atime,
2321-
mtime
2341+
mtime,
2342+
dontFollow
23222343
});
23232344
},
23242345
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)