@volter/browser-wali 0.1.166 → 0.1.168

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +9 -0
  3. package/dist/browser-wali-command.d.ts +7 -0
  4. package/dist/browser-wali-command.d.ts.map +1 -1
  5. package/dist/browser-wali-command.js +70 -18
  6. package/dist/browser-wali-command.js.map +1 -1
  7. package/dist/bulk-memory.d.ts +4 -2
  8. package/dist/bulk-memory.d.ts.map +1 -1
  9. package/dist/bulk-memory.js +32 -9
  10. package/dist/bulk-memory.js.map +1 -1
  11. package/dist/capabilities.d.ts +1 -1
  12. package/dist/capabilities.d.ts.map +1 -1
  13. package/dist/capabilities.js +6 -2
  14. package/dist/capabilities.js.map +1 -1
  15. package/dist/child-processes.d.ts +2 -0
  16. package/dist/child-processes.d.ts.map +1 -1
  17. package/dist/child-processes.js +9 -1
  18. package/dist/child-processes.js.map +1 -1
  19. package/dist/clocks.d.ts +3 -0
  20. package/dist/clocks.d.ts.map +1 -0
  21. package/dist/clocks.js +44 -0
  22. package/dist/clocks.js.map +1 -0
  23. package/dist/coherent-file-locks.d.ts.map +1 -1
  24. package/dist/coherent-file-locks.js +2 -1
  25. package/dist/coherent-file-locks.js.map +1 -1
  26. package/dist/credentials.d.ts +4 -0
  27. package/dist/credentials.d.ts.map +1 -0
  28. package/dist/credentials.js +46 -0
  29. package/dist/credentials.js.map +1 -0
  30. package/dist/descriptors.d.ts +14 -0
  31. package/dist/descriptors.d.ts.map +1 -1
  32. package/dist/descriptors.js +45 -5
  33. package/dist/descriptors.js.map +1 -1
  34. package/dist/environment.d.ts +4 -0
  35. package/dist/environment.d.ts.map +1 -0
  36. package/dist/environment.js +23 -0
  37. package/dist/environment.js.map +1 -0
  38. package/dist/epoll-ready.d.ts +27 -0
  39. package/dist/epoll-ready.d.ts.map +1 -0
  40. package/dist/epoll-ready.js +88 -0
  41. package/dist/epoll-ready.js.map +1 -0
  42. package/dist/events.d.ts +1 -0
  43. package/dist/events.d.ts.map +1 -1
  44. package/dist/events.js +55 -24
  45. package/dist/events.js.map +1 -1
  46. package/dist/filesystem-host-session.d.ts +65 -0
  47. package/dist/filesystem-host-session.d.ts.map +1 -0
  48. package/dist/filesystem-host-session.js +951 -0
  49. package/dist/filesystem-host-session.js.map +1 -0
  50. package/dist/filesystem-host.d.ts +13 -81
  51. package/dist/filesystem-host.d.ts.map +1 -1
  52. package/dist/filesystem-host.js +14 -882
  53. package/dist/filesystem-host.js.map +1 -1
  54. package/dist/filesystem-protocol.d.ts +3 -1
  55. package/dist/filesystem-protocol.d.ts.map +1 -1
  56. package/dist/filesystem-remote.d.ts +8 -3
  57. package/dist/filesystem-remote.d.ts.map +1 -1
  58. package/dist/filesystem-remote.js +43 -20
  59. package/dist/filesystem-remote.js.map +1 -1
  60. package/dist/filesystem-service-client.d.ts +4 -0
  61. package/dist/filesystem-service-client.d.ts.map +1 -0
  62. package/dist/filesystem-service-client.js +16 -0
  63. package/dist/filesystem-service-client.js.map +1 -0
  64. package/dist/filesystem.d.ts +26 -4
  65. package/dist/filesystem.d.ts.map +1 -1
  66. package/dist/filesystem.js +633 -175
  67. package/dist/filesystem.js.map +1 -1
  68. package/dist/fork-memory.d.ts +23 -0
  69. package/dist/fork-memory.d.ts.map +1 -0
  70. package/dist/fork-memory.js +93 -0
  71. package/dist/fork-memory.js.map +1 -0
  72. package/dist/fork-worker.js +32 -11
  73. package/dist/fork-worker.js.map +1 -1
  74. package/dist/jumps.d.ts +7 -0
  75. package/dist/jumps.d.ts.map +1 -1
  76. package/dist/jumps.js +19 -1
  77. package/dist/jumps.js.map +1 -1
  78. package/dist/kernel-filesystem-worker.d.ts +2 -0
  79. package/dist/kernel-filesystem-worker.d.ts.map +1 -0
  80. package/dist/kernel-filesystem-worker.js +28 -0
  81. package/dist/kernel-filesystem-worker.js.map +1 -0
  82. package/dist/kernel-filesystem.d.ts +12 -0
  83. package/dist/kernel-filesystem.d.ts.map +1 -0
  84. package/dist/kernel-filesystem.js +45 -0
  85. package/dist/kernel-filesystem.js.map +1 -0
  86. package/dist/kernel-network-worker.d.ts +2 -0
  87. package/dist/kernel-network-worker.d.ts.map +1 -0
  88. package/dist/kernel-network-worker.js +14 -0
  89. package/dist/kernel-network-worker.js.map +1 -0
  90. package/dist/kernel-network.d.ts +14 -0
  91. package/dist/kernel-network.d.ts.map +1 -0
  92. package/dist/kernel-network.js +50 -0
  93. package/dist/kernel-network.js.map +1 -0
  94. package/dist/kernel-process-client.d.ts +6 -1
  95. package/dist/kernel-process-client.d.ts.map +1 -1
  96. package/dist/kernel-process-client.js +10 -1
  97. package/dist/kernel-process-client.js.map +1 -1
  98. package/dist/module-layout.d.ts +8 -0
  99. package/dist/module-layout.d.ts.map +1 -1
  100. package/dist/module-layout.js +263 -2
  101. package/dist/module-layout.js.map +1 -1
  102. package/dist/network-host.d.ts +6 -0
  103. package/dist/network-host.d.ts.map +1 -0
  104. package/dist/network-host.js +1582 -0
  105. package/dist/network-host.js.map +1 -0
  106. package/dist/network-protocol.d.ts +9 -2
  107. package/dist/network-protocol.d.ts.map +1 -1
  108. package/dist/network-protocol.js +3 -0
  109. package/dist/network-protocol.js.map +1 -1
  110. package/dist/network-service-client.d.ts +4 -0
  111. package/dist/network-service-client.d.ts.map +1 -0
  112. package/dist/network-service-client.js +16 -0
  113. package/dist/network-service-client.js.map +1 -0
  114. package/dist/network-worker.js +5 -1150
  115. package/dist/network-worker.js.map +1 -1
  116. package/dist/network.d.ts +13 -1
  117. package/dist/network.d.ts.map +1 -1
  118. package/dist/network.js +175 -48
  119. package/dist/network.js.map +1 -1
  120. package/dist/paths.d.ts +11 -0
  121. package/dist/paths.d.ts.map +1 -0
  122. package/dist/paths.js +97 -0
  123. package/dist/paths.js.map +1 -0
  124. package/dist/process-bridge.d.ts +12 -4
  125. package/dist/process-bridge.d.ts.map +1 -1
  126. package/dist/process-bridge.js +53 -17
  127. package/dist/process-bridge.js.map +1 -1
  128. package/dist/program.d.ts.map +1 -1
  129. package/dist/program.js +8 -0
  130. package/dist/program.js.map +1 -1
  131. package/dist/runtime.d.ts.map +1 -1
  132. package/dist/runtime.js +150 -92
  133. package/dist/runtime.js.map +1 -1
  134. package/dist/shared-buffer-pool.d.ts +18 -5
  135. package/dist/shared-buffer-pool.d.ts.map +1 -1
  136. package/dist/shared-buffer-pool.js +86 -14
  137. package/dist/shared-buffer-pool.js.map +1 -1
  138. package/dist/shared-file-layer.d.ts +3 -1
  139. package/dist/shared-file-layer.d.ts.map +1 -1
  140. package/dist/shared-file-layer.js +150 -35
  141. package/dist/shared-file-layer.js.map +1 -1
  142. package/dist/shared-pipes.d.ts +3 -1
  143. package/dist/shared-pipes.d.ts.map +1 -1
  144. package/dist/shared-pipes.js +105 -33
  145. package/dist/shared-pipes.js.map +1 -1
  146. package/dist/signal-interruption.d.ts +9 -0
  147. package/dist/signal-interruption.d.ts.map +1 -0
  148. package/dist/signal-interruption.js +6 -0
  149. package/dist/signal-interruption.js.map +1 -0
  150. package/dist/signals.d.ts +17 -6
  151. package/dist/signals.d.ts.map +1 -1
  152. package/dist/signals.js +101 -22
  153. package/dist/signals.js.map +1 -1
  154. package/dist/snapshot-filesystem.d.ts +12 -1
  155. package/dist/snapshot-filesystem.d.ts.map +1 -1
  156. package/dist/snapshot-filesystem.js +241 -73
  157. package/dist/snapshot-filesystem.js.map +1 -1
  158. package/dist/socket-flags.d.ts +3 -0
  159. package/dist/socket-flags.d.ts.map +1 -0
  160. package/dist/socket-flags.js +9 -0
  161. package/dist/socket-flags.js.map +1 -0
  162. package/dist/special-files.d.ts +6 -0
  163. package/dist/special-files.d.ts.map +1 -0
  164. package/dist/special-files.js +56 -0
  165. package/dist/special-files.js.map +1 -0
  166. package/dist/stdin.d.ts.map +1 -1
  167. package/dist/stdin.js +13 -5
  168. package/dist/stdin.js.map +1 -1
  169. package/dist/symlink.d.ts +2 -1
  170. package/dist/symlink.d.ts.map +1 -1
  171. package/dist/symlink.js +25 -93
  172. package/dist/symlink.js.map +1 -1
  173. package/dist/system.d.ts +2 -4
  174. package/dist/system.d.ts.map +1 -1
  175. package/dist/system.js +46 -29
  176. package/dist/system.js.map +1 -1
  177. package/dist/thread-coordinator-worker.js +58 -24
  178. package/dist/thread-coordinator-worker.js.map +1 -1
  179. package/dist/thread-lifecycle.d.ts +9 -0
  180. package/dist/thread-lifecycle.d.ts.map +1 -0
  181. package/dist/thread-lifecycle.js +30 -0
  182. package/dist/thread-lifecycle.js.map +1 -0
  183. package/dist/thread-worker.js +125 -44
  184. package/dist/thread-worker.js.map +1 -1
  185. package/dist/threads.d.ts +6 -0
  186. package/dist/threads.d.ts.map +1 -1
  187. package/dist/threads.js +46 -6
  188. package/dist/threads.js.map +1 -1
  189. package/dist/tty-job-control.d.ts +13 -0
  190. package/dist/tty-job-control.d.ts.map +1 -0
  191. package/dist/tty-job-control.js +56 -0
  192. package/dist/tty-job-control.js.map +1 -0
  193. package/dist/tty-settings.d.ts +3 -0
  194. package/dist/tty-settings.d.ts.map +1 -0
  195. package/dist/tty-settings.js +25 -0
  196. package/dist/tty-settings.js.map +1 -0
  197. package/dist/types.d.ts +26 -0
  198. package/dist/types.d.ts.map +1 -1
  199. package/dist/wait-state.d.ts +8 -0
  200. package/dist/wait-state.d.ts.map +1 -1
  201. package/dist/wait-state.js +15 -0
  202. package/dist/wait-state.js.map +1 -1
  203. package/dist/worker-program-protocol.d.ts +22 -2
  204. package/dist/worker-program-protocol.d.ts.map +1 -1
  205. package/dist/worker-program-worker.js +31 -9
  206. package/dist/worker-program-worker.js.map +1 -1
  207. package/dist/worker-program.d.ts +12 -2
  208. package/dist/worker-program.d.ts.map +1 -1
  209. package/dist/worker-program.js +214 -36
  210. package/dist/worker-program.js.map +1 -1
  211. package/dist/write-vector.d.ts +3 -0
  212. package/dist/write-vector.d.ts.map +1 -0
  213. package/dist/write-vector.js +35 -0
  214. package/dist/write-vector.js.map +1 -0
  215. package/package.json +1 -1
@@ -1,8 +1,16 @@
1
+ // File-call rules translated from The Emscripten Authors' WasmFS syscalls.cpp
2
+ // (Copyright 2021, MIT), 862dc0d48acc4134a0088d27ac8f5dd18baba1cd; see ../LICENSE.
3
+ // Storage, shared descriptors and process state remain owned by this kernel.
4
+ import { createWaliDevice, waliDeviceStat } from "./special-files.js";
1
5
  import { realtimeMilliseconds } from "./monotonic-clock.js";
6
+ import { gatherWaliWriteVector } from "./write-vector.js";
7
+ import { setBrowserFileTimes } from "@volter/browser-runtime/browser-file-times.js";
2
8
  import { WaliDescriptionRegistry, WaliProcessDescriptorTable } from "./descriptors.js";
9
+ import { WaliSharedPipeHost } from "./shared-pipes.js";
3
10
  import { createProcessFilesystemMutationGuard } from "@volter/browser-runtime/browser-process-filesystem-policy.js";
4
11
  import { BufferedWaliStdin } from "./stdin.js";
5
12
  import { readWaliSymlink, resolveWaliSymlinkPath, writeWaliSymlink } from "./symlink.js";
13
+ import { absoluteWaliPath, normalizeWaliPath } from "./paths.js";
6
14
  import { PR_WALI, PR_WALI_TREE_ADOPT, PR_WALI_TREE_CONFIRM, PR_WALI_TREE_DECLINE, PR_WALI_TREE_RELEASE, PRIVATE_TREE_INODES, PRIVATE_TREE_WINDOW_BASE, PRIVATE_TREE_WINDOW_SIZE, decodePrivateTree, encodePrivateTree, inPrivateTreeWindow, } from "./private-tree.js";
7
15
  let reportedDroppedEnvironment = false;
8
16
  const AT_FDCWD = -100;
@@ -15,6 +23,10 @@ const O_TRUNC = 0x200;
15
23
  const O_APPEND = 0x400;
16
24
  const O_NONBLOCK = 0x800;
17
25
  const O_DIRECTORY = 0x1_0000;
26
+ const O_NOFOLLOW = 0x2_0000;
27
+ const AT_EMPTY_PATH = 0x1000;
28
+ const AT_NO_AUTOMOUNT = 0x800;
29
+ const AT_EACCESS = 0x200;
18
30
  const SEEK_SET = 0;
19
31
  const SEEK_CUR = 1;
20
32
  const SEEK_END = 2;
@@ -53,9 +65,9 @@ export class WaliFileDescriptorTable {
53
65
  files;
54
66
  specialDescriptors;
55
67
  stdioAliases;
56
- descriptorStatusFlags = new Map();
57
68
  stdin;
58
69
  nextDescription = 1;
70
+ fileCreationMask;
59
71
  cwd;
60
72
  guestFilesystem;
61
73
  assertWritable;
@@ -64,8 +76,9 @@ export class WaliFileDescriptorTable {
64
76
  privateTreeDeclined = false;
65
77
  constructor(options) {
66
78
  this.options = options;
79
+ this.fileCreationMask = (options.fileCreationMask ?? 0o022) & 0o777;
67
80
  this.descriptors = options.descriptors ?? new WaliProcessDescriptorTable(new WaliDescriptionRegistry());
68
- this.files = this.descriptors.view("file");
81
+ this.files = this.descriptors.view("file", (file) => file.inode?.close());
69
82
  this.specialDescriptors = this.descriptors.view("special", (value) => value.close?.());
70
83
  this.stdioAliases = this.descriptors.view("stdio");
71
84
  this.cwd = normalizePath("/", options.cwd);
@@ -93,6 +106,9 @@ export class WaliFileDescriptorTable {
93
106
  ...(fs.truncateSync ? { truncateSync: (path, length) => fs.truncateSync(this.assertWritable(path), length) } : {}),
94
107
  mkdirSync: (path, settings) => fs.mkdirSync(this.assertWritable(path), settings),
95
108
  unlinkSync: (path) => fs.unlinkSync(this.assertWritable(path)),
109
+ // A writable alias must not let the guest mutate an immutable source.
110
+ ...(fs.linkSync ? { linkSync: (source, destination) => fs.linkSync(this.assertWritable(source), this.assertWritable(destination)) } : {}),
111
+ ...(fs.openFileSync ? { openFileSync: fs.openFileSync.bind(fs) } : {}),
96
112
  rmdirSync: (path) => fs.rmdirSync(this.assertWritable(path)),
97
113
  renameSync: (from, to) => fs.renameSync(this.assertWritable(from), this.assertWritable(to)),
98
114
  ...(fs.chmodSync ? { chmodSync: (path, mode) => fs.chmodSync(this.assertWritable(path), mode) } : {}),
@@ -101,7 +117,7 @@ export class WaliFileDescriptorTable {
101
117
  ...(fs.restoreNodeSync ? { restoreNodeSync: (path, node) => fs.restoreNodeSync(this.assertWritable(path), node) } : {}),
102
118
  } : fs;
103
119
  this.stdin = "read" in options.stdin ? options.stdin : new BufferedWaliStdin(options.stdin);
104
- if (!options.descriptors) {
120
+ if (!options.descriptors || options.initializeStdio) {
105
121
  for (const fd of [0, 1, 2])
106
122
  this.specialDescriptors.set(fd, { stdio: fd });
107
123
  }
@@ -117,6 +133,7 @@ export class WaliFileDescriptorTable {
117
133
  SYS_open: (path, flags, mode) => this.open(this.readCString(Number(path)), Number(flags), Number(mode)),
118
134
  SYS_openat: (dirfd, path, flags, mode) => this.openAt(Number(dirfd), this.readCString(Number(path)), Number(flags), Number(mode)),
119
135
  SYS_close: (fd) => this.close(Number(fd)),
136
+ SYS_ioctl: (fd, request, argument) => this.ioctl(Number(fd), Number(request), Number(argument)),
120
137
  SYS_read: (fd, pointer, length) => this.read(Number(fd), Number(pointer), Number(length)),
121
138
  SYS_pread64: (fd, pointer, length, offset) => this.pread(Number(fd), Number(pointer), Number(length), Number(offset)),
122
139
  SYS_write: (fd, pointer, length) => this.write(Number(fd), Number(pointer), Number(length)),
@@ -128,13 +145,19 @@ export class WaliFileDescriptorTable {
128
145
  SYS_pwritev: (fd, iov, count, offset, high) => this.pwritev(Number(fd), Number(iov), Number(count), vectorOffset(offset, high)),
129
146
  SYS_lseek: (fd, offset, whence) => this.lseek(Number(fd), Number(offset), Number(whence)),
130
147
  SYS_ftruncate: (fd, length) => this.ftruncate(Number(fd), Number(length)),
148
+ SYS_truncate: (path, length) => this.truncate(this.readCString(Number(path)), Number(length)),
131
149
  SYS_fstat: (fd, stat) => this.fstat(Number(fd), Number(stat)),
132
150
  SYS_stat: (path, stat) => this.stat(this.readCString(Number(path)), Number(stat), false),
133
151
  SYS_lstat: (path, stat) => this.stat(this.readCString(Number(path)), Number(stat), true),
134
152
  SYS_newfstatat: (dirfd, path, stat, flags) => this.statAt(Number(dirfd), this.readCString(Number(path)), Number(stat), Number(flags)),
135
- SYS_access: (path) => this.access(this.readCString(Number(path))),
136
- SYS_faccessat: (dirfd, path) => this.accessAt(Number(dirfd), this.readCString(Number(path))),
137
- SYS_faccessat2: (dirfd, path) => this.accessAt(Number(dirfd), this.readCString(Number(path))),
153
+ SYS_access: (path, mode) => this.accessAt(AT_FDCWD, this.readCString(Number(path)), Number(mode), 0),
154
+ SYS_faccessat: (dirfd, path, mode) => this.accessAt(Number(dirfd), this.readCString(Number(path)), Number(mode), 0),
155
+ SYS_faccessat2: (dirfd, path, mode, flags) => this.accessAt(Number(dirfd), this.readCString(Number(path)), Number(mode), Number(flags)),
156
+ SYS_umask: (mask) => {
157
+ const previous = this.fileCreationMask;
158
+ this.fileCreationMask = Number(mask) & 0o777;
159
+ return BigInt(previous);
160
+ },
138
161
  SYS_chmod: (path, mode) => this.chmod(this.readCString(Number(path)), Number(mode)),
139
162
  SYS_fchmod: (fd, mode) => this.fchmod(Number(fd), Number(mode)),
140
163
  SYS_fchmodat: (dirfd, path, mode, flags) => this.chmodAt(Number(dirfd), this.readCString(Number(path)), Number(mode), Number(flags)),
@@ -149,10 +172,13 @@ export class WaliFileDescriptorTable {
149
172
  SYS_inotify_init1: () => this.inotifyInit(),
150
173
  SYS_inotify_add_watch: (fd, path) => this.inotifyAddWatch(Number(fd), this.readCString(Number(path))),
151
174
  SYS_inotify_rm_watch: (fd) => this.specialDescriptors.has(Number(fd)) ? 0n : -9n,
152
- SYS_rmdir: (path) => this.mutate(() => this.fs().rmdirSync(this.resolveFilesystemPath(this.resolve(this.readCString(Number(path))), false))),
153
- SYS_unlink: (path) => this.mutate(() => this.fs().unlinkSync(this.resolveFilesystemPath(this.resolve(this.readCString(Number(path))), false))),
175
+ SYS_rmdir: (path) => this.unlinkAt(AT_FDCWD, this.readCString(Number(path)), AT_REMOVEDIR),
176
+ SYS_unlink: (path) => this.unlinkAt(AT_FDCWD, this.readCString(Number(path)), 0),
154
177
  SYS_unlinkat: (dirfd, path, flags) => this.unlinkAt(Number(dirfd), this.readCString(Number(path)), Number(flags)),
178
+ SYS_link: (source, destination) => this.linkAt(AT_FDCWD, this.readCString(Number(source)), AT_FDCWD, this.readCString(Number(destination)), 0),
179
+ SYS_linkat: (sourceDirfd, source, destinationDirfd, destination, flags) => this.linkAt(Number(sourceDirfd), this.readCString(Number(source)), Number(destinationDirfd), this.readCString(Number(destination)), Number(flags)),
155
180
  SYS_rename: (from, to) => this.rename(this.readCString(Number(from)), this.readCString(Number(to))),
181
+ SYS_renameat: (fromDirfd, from, toDirfd, to) => this.rename(this.readCString(Number(from)), this.readCString(Number(to)), Number(fromDirfd), Number(toDirfd)),
156
182
  SYS_utimensat: (dirfd, path, times, flags) => this.utimensat(Number(dirfd), Number(path), Number(times), Number(flags)),
157
183
  SYS_symlink: (target, link) => this.symlink(this.readCString(Number(target)), this.readCString(Number(link)), AT_FDCWD),
158
184
  SYS_symlinkat: (target, dirfd, link) => this.symlink(this.readCString(Number(target)), this.readCString(Number(link)), Number(dirfd)),
@@ -190,15 +216,17 @@ export class WaliFileDescriptorTable {
190
216
  this.options.filesystem.mkdirSync("/tmp", { recursive: true });
191
217
  }
192
218
  catch { /* already present */ }
193
- // The env file is one `name=value` per line, so it cannot carry a value
194
- // with a line break or a name that is not an identifier. A shell's own
219
+ // Legacy artifacts read one `name=value` per line. The owned linker now
220
+ // supplies direct environment imports; rebuilt programs never use this
221
+ // compatibility path. Old artifacts still cannot carry a line break.
222
+ // A shell's own
195
223
  // parameters (`?`, a newline-valued IFS) reach here through every lane's
196
224
  // spawn; refusing the whole spawn for them is what killed a script's
197
225
  // `spawn("ffmpeg")`. They are dropped and named once.
198
226
  const dropped = [];
199
227
  const lines = [];
200
228
  for (const [name, value] of Object.entries(env)) {
201
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name) || /[\u0000\r\n]/u.test(value)) {
229
+ if (/[=\u0000\r\n]/u.test(name) || /[\u0000\r\n]/u.test(value)) {
202
230
  dropped.push(name);
203
231
  continue;
204
232
  }
@@ -226,7 +254,8 @@ export class WaliFileDescriptorTable {
226
254
  // The range, not the whole file sliced: a mapping is read in pieces, and
227
255
  // each piece copied the file entire (2026-09-27, Blender reading a `.blend`).
228
256
  const fs = this.fs();
229
- return fs.readRangeSync ? fs.readRangeSync(file.path, offset, length) : fs.readFileSync(file.path).slice(offset, offset + length);
257
+ return file.inode ? file.inode.read(offset, length).slice()
258
+ : fs.readRangeSync ? fs.readRangeSync(file.path, offset, length) : fs.readFileSync(file.path).slice(offset, offset + length);
230
259
  }
231
260
  mappingPath(fd) {
232
261
  const file = this.files.get(fd);
@@ -270,7 +299,7 @@ export class WaliFileDescriptorTable {
270
299
  return this.specialDescriptors.get(fd)?.browserProcessPipe;
271
300
  }
272
301
  copyForFork(memory) {
273
- return new WaliFileDescriptorTable({ ...this.options, cwd: this.cwd, memory, descriptors: this.descriptors.copy() });
302
+ return new WaliFileDescriptorTable({ ...this.options, initializeStdio: false, fileCreationMask: this.fileCreationMask, cwd: this.cwd, memory, descriptors: this.descriptors.copy() });
274
303
  }
275
304
  copyForExec(memory, stdio) {
276
305
  const descriptors = this.descriptors.copy();
@@ -282,17 +311,45 @@ export class WaliFileDescriptorTable {
282
311
  });
283
312
  }
284
313
  descriptors.exec();
285
- return new WaliFileDescriptorTable({ ...this.options, cwd: this.cwd, memory, descriptors });
314
+ return new WaliFileDescriptorTable({ ...this.options, initializeStdio: false, fileCreationMask: this.fileCreationMask, cwd: this.cwd, memory, descriptors });
286
315
  }
287
316
  currentWorkingDirectory() { return this.cwd; }
288
- ioctl(fd, request) {
317
+ creationMask() { return this.fileCreationMask; }
318
+ // Translated from Emscripten libsyscall.js and wasmfs/syscalls.cpp (MIT),
319
+ // 862dc0d48acc4134a0088d27ac8f5dd18baba1cd. The kernel owns the description;
320
+ // terminal settings are dispatched separately to the attached tty.
321
+ ioctl(fd, request, argument) {
289
322
  if (!this.hasFd(fd))
290
- return;
291
- // FIONBIO/FIOCLEX/FIONCLEX only mutate descriptor flags. Browser pipes
292
- // are already nonblocking and close-on-exec is handled by the spawn ABI.
293
- if (request === 0x5421 || request === 0x5451 || request === 0x5450)
323
+ return -9n;
324
+ const operation = request >>> 0;
325
+ if (operation === 0x5451 || operation === 0x5450) {
326
+ return this.fcntl(fd, 2, operation === 0x5451 ? 1 : 0);
327
+ }
328
+ if (operation !== 0x5421 && operation !== 0x541b)
329
+ return -25n;
330
+ const file = this.files.get(fd);
331
+ if (operation === 0x541b && (!file || file.directory))
332
+ return -25n;
333
+ const memory = this.options.memory();
334
+ if (!Number.isSafeInteger(argument) || argument <= 0 || argument + 4 > memory.buffer.byteLength)
335
+ return -14n;
336
+ if (operation === 0x5421) {
337
+ const nonblocking = this.memoryView().getInt32(argument, true) !== 0;
338
+ const flags = this.fcntl(fd, 3, 0);
339
+ if (flags < 0n)
340
+ return flags;
341
+ return this.fcntl(fd, 4, nonblocking ? Number(flags) | O_NONBLOCK : Number(flags) & ~O_NONBLOCK);
342
+ }
343
+ if (!file)
344
+ return -25n;
345
+ try {
346
+ const remaining = Math.max(0, (file.inode?.stat() ?? this.fs().statSync(file.path)).size - file.position);
347
+ this.memoryView().setInt32(argument, remaining, true);
294
348
  return 0n;
295
- return;
349
+ }
350
+ catch (error) {
351
+ return negativeErrno(error);
352
+ }
296
353
  }
297
354
  pollDescriptor(fd, events) {
298
355
  if (fd < 0 || !this.hasFd(fd))
@@ -302,9 +359,12 @@ export class WaliFileDescriptorTable {
302
359
  return special.poll?.(events) ?? 0;
303
360
  const stdio = this.stdioFd(fd);
304
361
  let returned = 0;
305
- if ((events & 0x1) && stdio === 0 && this.stdin.poll())
362
+ const terminal = this.terminalFd(fd);
363
+ const input = terminal ? this.options.terminalStdin : this.stdin;
364
+ const mode = this.statusFlags(fd) & O_ACCMODE;
365
+ if ((events & 0x1) && stdio === 0 && (!terminal || mode !== O_WRONLY) && input?.poll())
306
366
  returned |= 0x1;
307
- if ((events & 0x4) && stdio !== 0)
367
+ if ((events & 0x4) && (stdio !== 0 || (terminal && mode !== 0)))
308
368
  returned |= 0x4;
309
369
  return returned;
310
370
  }
@@ -359,20 +419,22 @@ export class WaliFileDescriptorTable {
359
419
  }
360
420
  return text;
361
421
  }
362
- resolve(path) { return normalizePath(this.cwd, path); }
422
+ resolve(path) { return absoluteWaliPath(this.cwd, path); }
363
423
  resolveAt(dirfd, path) {
364
424
  if (path.startsWith("/"))
365
- return normalizePath("/", path);
425
+ return path;
366
426
  if (dirfd === AT_FDCWD)
367
427
  return this.resolve(path);
368
428
  const directory = this.files.get(dirfd);
369
- if (!directory?.directory)
429
+ if (!directory)
430
+ throw errno(this.hasFd(dirfd) ? "ENOTDIR" : "EBADF");
431
+ if (!directory.directory)
370
432
  throw errno("ENOTDIR");
371
- return normalizePath(directory.path, path);
433
+ return absoluteWaliPath(directory.path, path);
372
434
  }
373
435
  open(path, flags, mode) {
374
436
  try {
375
- return BigInt(this.openResolved(this.resolveFilesystemPath(this.resolve(path)), flags, mode));
437
+ return BigInt(this.openPath(this.resolve(path), flags, mode));
376
438
  }
377
439
  catch (error) {
378
440
  return negativeErrno(error);
@@ -380,45 +442,65 @@ export class WaliFileDescriptorTable {
380
442
  }
381
443
  openAt(dirfd, path, flags, mode) {
382
444
  try {
383
- return BigInt(this.openResolved(this.resolveFilesystemPath(this.resolveAt(dirfd, path)), flags, mode));
445
+ return BigInt(this.openPath(this.resolveAt(dirfd, path), flags, mode));
384
446
  }
385
447
  catch (error) {
386
448
  return negativeErrno(error);
387
449
  }
388
450
  }
451
+ openPath(path, flags, mode) {
452
+ if ((flags & O_ACCMODE) === O_ACCMODE)
453
+ throw errno("EINVAL");
454
+ // Linux >=6.4 rejects this pair instead of creating a regular file and
455
+ // then failing O_DIRECTORY (the old behaviour still in pinned WasmFS).
456
+ if ((flags & O_CREAT) && (flags & O_DIRECTORY))
457
+ throw errno("EINVAL");
458
+ const exclusive = Boolean((flags & O_CREAT) && (flags & O_EXCL));
459
+ const resolved = this.resolveFilesystemPath(path, !(exclusive || (flags & O_NOFOLLOW)));
460
+ const target = exclusive || (flags & O_NOFOLLOW) ? this.virtualLink(resolved) ?? this.readSymlink(resolved) : undefined;
461
+ const entry = exclusive || (flags & O_NOFOLLOW) ? statIfPresent(this.fs(), resolved, true) : undefined;
462
+ const link = target !== undefined || entry?.isSymbolicLink?.();
463
+ // Linux O_EXCL checks the link itself, even when its target is absent.
464
+ if (exclusive && (link || entry !== undefined
465
+ || ["/dev/null", "/dev/random", "/dev/urandom"].includes(resolved)))
466
+ throw errno("EEXIST");
467
+ if ((flags & O_NOFOLLOW) && link)
468
+ throw errno("ELOOP");
469
+ checkFileName(resolved);
470
+ const descriptor = this.procDescriptor(resolved);
471
+ if (descriptor !== undefined)
472
+ return this.reopenDescriptor(descriptor, flags, mode);
473
+ return this.openResolved(resolved, flags, mode);
474
+ }
389
475
  openResolved(path, flags, mode) {
390
476
  // O_CLOEXEC is recorded here for every kind of descriptor open makes: musl
391
477
  // no longer follows it with F_SETFD (musl 0003), which was the only thing
392
478
  // marking these special files (review of dfd87b6).
393
- if (path === "/dev/null") {
394
- return this.markCloseOnExecNumber(this.allocateSpecial({
395
- read: () => new Uint8Array(),
396
- write: (value) => BigInt(value.byteLength),
397
- poll: (events) => ((events & 1) ? 1 : 0) | ((events & 4) ? 4 : 0),
398
- fcntl: (command) => command === 3 ? BigInt(flags) : [1, 2, 4].includes(command) ? 0n : -22n,
399
- }), flags);
400
- }
401
- if (path === "/dev/urandom" || path === "/dev/random") {
402
- return this.markCloseOnExecNumber(this.allocateSpecial({
403
- read: (length) => {
404
- if (!Number.isSafeInteger(length) || length < 0)
405
- return -22n;
406
- const value = new Uint8Array(length);
407
- for (let offset = 0; offset < value.byteLength; offset += 65_536) {
408
- crypto.getRandomValues(value.subarray(offset, Math.min(offset + 65_536, value.byteLength)));
409
- }
410
- return value;
411
- },
412
- write: (value) => BigInt(value.byteLength),
413
- poll: (events) => ((events & 1) ? 1 : 0) | ((events & 4) ? 4 : 0),
414
- fcntl: (command) => command === 3 ? BigInt(flags) : [1, 2, 4].includes(command) ? 0n : -22n,
415
- }), flags);
479
+ if (path === "/dev/tty") {
480
+ if (flags & O_DIRECTORY)
481
+ throw errno("ENOTDIR");
482
+ const fd = this.allocateFd();
483
+ this.specialDescriptors.set(fd, { stdio: 0, terminal: true, stat: () => waliDeviceStat(path) });
484
+ Atomics.store(this.descriptors.registry.pipeMetadata, this.descriptors.entries[fd].description * 3 + 1, flags & ~O_CLOEXEC_FLAG);
485
+ // The calling realm validates controlling-session membership through
486
+ // ttyopen before returning this fd. The filesystem owner owns the fd;
487
+ // terminal input/output stay on the caller's attached transport.
488
+ return this.markCloseOnExecNumber(fd, flags);
489
+ }
490
+ const device = createWaliDevice(path, flags);
491
+ if (device) {
492
+ if (flags & O_DIRECTORY)
493
+ throw errno("ENOTDIR");
494
+ const fd = this.allocateFd();
495
+ this.specialDescriptors.set(fd, device);
496
+ return this.markCloseOnExecNumber(fd, flags);
416
497
  }
498
+ const fd = this.allocateFd();
417
499
  const fs = this.fs();
418
500
  // One lookup where the store answers ENOENT from statSync (a snapshot store's); exists-then-stat resolved it twice.
419
- let found = fs.linkTargetSync ? statIfPresent(fs, path) : fs.existsSync(path) ? fs.statSync(path) : undefined;
501
+ let found = this.virtualStat(path, false) ?? (fs.linkTargetSync ? statIfPresent(fs, path) : fs.existsSync(path) ? fs.statSync(path) : undefined);
420
502
  const exists = found !== undefined;
421
- if ((flags & O_ACCMODE) !== 0 || (!exists && (flags & O_CREAT)))
503
+ if ((flags & O_ACCMODE) !== 0 || (flags & O_TRUNC) || (!exists && (flags & O_CREAT)))
422
504
  this.assertWritable(path);
423
505
  if (!exists && !(flags & O_CREAT))
424
506
  throw errno("ENOENT");
@@ -426,30 +508,41 @@ export class WaliFileDescriptorTable {
426
508
  throw errno("EEXIST");
427
509
  if (!exists) {
428
510
  fs.writeFileSync(path, new Uint8Array());
429
- fs.chmodSync?.(path, mode & 0o7777);
511
+ fs.chmodSync?.(path, mode & 0o7777 & ~this.fileCreationMask);
430
512
  this.options.fileLocks?.created?.(path);
431
513
  found = fs.statSync(path);
432
514
  }
433
515
  const stat = found;
434
516
  if ((flags & O_DIRECTORY) && !stat.isDirectory())
435
517
  throw errno("ENOTDIR");
436
- if (stat.isDirectory() && (flags & O_ACCMODE) !== 0)
518
+ if (stat.isDirectory() && ((flags & O_ACCMODE) !== 0 || (flags & O_CREAT) || (flags & O_TRUNC)))
437
519
  throw errno("EISDIR");
438
- const truncated = (flags & O_TRUNC) !== 0 && (flags & O_ACCMODE) !== 0 && stat.isFile();
520
+ // doOpen does not recheck the mode of the file it just created: creating
521
+ // mode 0400 with O_RDWR still returns a writable open description.
522
+ if (exists) {
523
+ if ((flags & O_ACCMODE) !== O_WRONLY)
524
+ checkFilePermission(stat, 4);
525
+ if ((flags & O_ACCMODE) !== 0 || (flags & O_TRUNC))
526
+ checkFilePermission(stat, 2);
527
+ }
528
+ const truncated = (flags & O_TRUNC) !== 0 && stat.isFile();
439
529
  if (truncated) {
440
530
  if (fs.truncateSync)
441
531
  fs.truncateSync(path, 0);
442
532
  else
443
533
  fs.writeFileSync(path, new Uint8Array());
444
534
  }
445
- const fd = this.allocateFd();
446
535
  const directory = stat.isDirectory();
536
+ const virtualDirectory = ["/proc", "/proc/self", "/proc/self/fd"].includes(path);
537
+ const entries = directory && !virtualDirectory ? this.directoryEntries(path) : undefined;
447
538
  this.files.set(fd, {
448
539
  path,
540
+ ...(!directory && fs.openFileSync ? { inode: fs.openFileSync(path) } : {}),
449
541
  flags,
450
- // The stat above is the file's size unless this open emptied it.
451
- position: flags & O_APPEND && !truncated && stat.isFile() ? stat.size : 0,
542
+ // O_APPEND chooses EOF on each write; open itself starts at zero.
543
+ position: 0,
452
544
  directory,
545
+ ...(entries ? { entries } : {}),
453
546
  description: this.nextDescription++,
454
547
  });
455
548
  this.descriptors.entries[fd].flags = flags & 0x80000 ? 1 : 0;
@@ -471,8 +564,8 @@ export class WaliFileDescriptorTable {
471
564
  *
472
565
  * Scanning costs one probe per open descriptor, and a process has a handful.
473
566
  */
474
- allocateFd(minimum = 3) {
475
- const maximum = this.options.descriptorMaximum ?? 65_535;
567
+ allocateFd(minimum = 0) {
568
+ const maximum = Math.min(this.options.descriptorMaximum ?? 65_535, this.descriptors.descriptorLimit - 1);
476
569
  for (let fd = Math.max(minimum, 0); fd <= maximum; fd += 1) {
477
570
  // A private tree's numbers are its libc's, held or given back (ADR-0047).
478
571
  if (inPrivateTreeWindow(fd)) {
@@ -489,7 +582,6 @@ export class WaliFileDescriptorTable {
489
582
  }
490
583
  close(fd) {
491
584
  if (this.stdioAliases.delete(fd)) {
492
- this.descriptorStatusFlags.delete(fd);
493
585
  this.setSharedStdioAlias(fd, undefined);
494
586
  return 0n;
495
587
  }
@@ -526,9 +618,14 @@ export class WaliFileDescriptorTable {
526
618
  if (length < 0)
527
619
  throw errno("EINVAL");
528
620
  if (this.stdioFd(fd) === 0) {
529
- if ((this.statusFlags(fd) & O_NONBLOCK) && !this.stdin.poll())
621
+ if (this.terminalFd(fd) && (this.statusFlags(fd) & O_ACCMODE) === O_WRONLY)
622
+ return -9n;
623
+ const input = this.terminalFd(fd) ? this.options.terminalStdin : this.stdin;
624
+ if (!input)
625
+ return -95n; // No independent terminal transport was supplied.
626
+ if ((this.statusFlags(fd) & O_NONBLOCK) && !input.poll())
530
627
  return -11n;
531
- const chunk = this.stdin.read(length);
628
+ const chunk = input.read(length);
532
629
  this.copyIn(pointer, chunk);
533
630
  return BigInt(chunk.byteLength);
534
631
  }
@@ -552,7 +649,7 @@ export class WaliFileDescriptorTable {
552
649
  throw errno("EBADF");
553
650
  const fs = this.fs();
554
651
  // A view where the filesystem offers one: copied into the program's memory at once, so one copy, not two.
555
- const chunk = fs.viewRangeSync
652
+ const chunk = file.inode ? file.inode.read(file.position, length) : fs.viewRangeSync
556
653
  ? fs.viewRangeSync(file.path, file.position, length)
557
654
  : fs.readRangeSync
558
655
  ? fs.readRangeSync(file.path, file.position, length)
@@ -566,13 +663,20 @@ export class WaliFileDescriptorTable {
566
663
  }
567
664
  }
568
665
  write(fd, pointer, length) {
666
+ return this.writeBytes(fd, pointer, length);
667
+ }
668
+ /** One vector is one destination write and one shared-offset advance. */
669
+ writeBytes(fd, pointer, length, gathered) {
569
670
  try {
570
671
  const stdio = this.stdioFd(fd);
672
+ if (this.terminalFd(fd))
673
+ return (this.statusFlags(fd) & O_ACCMODE) === 0 ? -9n
674
+ : this.options.emitTerminal?.(gathered ?? this.copyOut(pointer, length)) ?? -95n;
571
675
  if (stdio === 1 || stdio === 2)
572
- return this.options.emit(stdio, this.copyOut(pointer, length));
676
+ return this.options.emit(stdio, gathered ?? this.copyOut(pointer, length));
573
677
  const special = this.specialDescriptors.get(fd);
574
678
  if (special)
575
- return special.write?.(this.copyOut(pointer, length)) ?? -9n;
679
+ return special.write?.(gathered ?? this.copyOut(pointer, length)) ?? -9n;
576
680
  const file = this.files.get(fd);
577
681
  if (!file)
578
682
  throw errno("EBADF");
@@ -581,32 +685,46 @@ export class WaliFileDescriptorTable {
581
685
  if ((file.flags & O_ACCMODE) === 0)
582
686
  throw errno("EBADF");
583
687
  const fs = this.fs();
688
+ if (file.inode) {
689
+ this.assertWritable(file.path);
690
+ if (file.flags & O_APPEND)
691
+ file.position = file.inode.stat().size;
692
+ if (!gathered)
693
+ assertRange(this.options.memory(), pointer, length);
694
+ file.inode.write(file.position, gathered ?? this.memoryBytes().subarray(pointer, pointer + length));
695
+ file.position += length;
696
+ return BigInt(length);
697
+ }
584
698
  if (fs.writeRangeSync) {
585
699
  if (file.flags & O_APPEND)
586
700
  file.position = fs.statSync(file.path).size;
587
701
  // A view, not a copy: the file copies what it keeps (2026-09-27, one copy of every write fewer).
588
- assertRange(this.options.memory(), pointer, length);
589
- fs.writeRangeSync(file.path, file.position, this.memoryBytes().subarray(pointer, pointer + length));
702
+ if (!gathered)
703
+ assertRange(this.options.memory(), pointer, length);
704
+ fs.writeRangeSync(file.path, file.position, gathered ?? this.memoryBytes().subarray(pointer, pointer + length));
590
705
  file.position += length;
591
706
  return BigInt(length);
592
707
  }
593
- const chunk = this.copyOut(pointer, length);
708
+ const value = gathered ?? this.copyOut(pointer, length);
594
709
  const current = fs.readFileSync(file.path);
595
710
  if (file.flags & O_APPEND)
596
711
  file.position = current.byteLength;
597
- const size = Math.max(current.byteLength, file.position + chunk.byteLength);
712
+ const size = Math.max(current.byteLength, file.position + value.byteLength);
598
713
  const next = new Uint8Array(size);
599
714
  next.set(current);
600
- next.set(chunk, file.position);
715
+ next.set(value, file.position);
601
716
  fs.writeFileSync(file.path, next);
602
- file.position += chunk.byteLength;
603
- return BigInt(chunk.byteLength);
717
+ file.position += value.byteLength;
718
+ return BigInt(value.byteLength);
604
719
  }
605
720
  catch (error) {
606
721
  return negativeErrno(error);
607
722
  }
608
723
  }
609
724
  pread(fd, pointer, length, offset) {
725
+ const special = this.specialDescriptors.get(fd);
726
+ if (special)
727
+ return !special.seek ? -29n : !Number.isSafeInteger(offset) || offset < 0 ? -22n : this.read(fd, pointer, length);
610
728
  const file = this.files.get(fd);
611
729
  if (!file)
612
730
  return -9n;
@@ -622,6 +740,9 @@ export class WaliFileDescriptorTable {
622
740
  }
623
741
  }
624
742
  pwrite(fd, pointer, length, offset) {
743
+ const special = this.specialDescriptors.get(fd);
744
+ if (special)
745
+ return !special.seek ? -29n : !Number.isSafeInteger(offset) || offset < 0 ? -22n : this.write(fd, pointer, length);
625
746
  const file = this.files.get(fd);
626
747
  if (!file)
627
748
  return -9n;
@@ -641,6 +762,9 @@ export class WaliFileDescriptorTable {
641
762
  }
642
763
  /** pread over a vector: the same answers, each buffer in turn at the offset, the position unmoved. */
643
764
  preadv(fd, pointer, count, offset) {
765
+ const special = this.specialDescriptors.get(fd);
766
+ if (special)
767
+ return !special.seek ? -29n : !Number.isSafeInteger(offset) || offset < 0 ? -22n : this.readv(fd, pointer, count);
644
768
  const file = this.files.get(fd);
645
769
  if (!file)
646
770
  return -9n;
@@ -657,6 +781,9 @@ export class WaliFileDescriptorTable {
657
781
  }
658
782
  /** pwrite over a vector, as pwrite: at the offset whatever O_APPEND says, the position unmoved. */
659
783
  pwritev(fd, pointer, count, offset) {
784
+ const special = this.specialDescriptors.get(fd);
785
+ if (special)
786
+ return !special.seek ? -29n : !Number.isSafeInteger(offset) || offset < 0 ? -22n : this.writev(fd, pointer, count);
660
787
  const file = this.files.get(fd);
661
788
  if (!file)
662
789
  return -9n;
@@ -689,29 +816,45 @@ export class WaliFileDescriptorTable {
689
816
  return total;
690
817
  }
691
818
  writev(fd, pointer, count) {
692
- let total = 0n;
693
- for (let index = 0; index < count; index += 1) {
694
- const offset = pointer + index * 8;
695
- assertRange(this.options.memory(), offset, 8);
696
- const result = this.write(fd, this.memoryView().getUint32(offset, true), this.memoryView().getUint32(offset + 4, true));
697
- if (result < 0n)
698
- return total > 0n ? total : result;
699
- total += result;
700
- }
701
- return total;
819
+ const value = gatherWaliWriteVector(this.options.memory(), pointer, count);
820
+ return typeof value === "bigint" ? value : this.writeBytes(fd, 0, value.byteLength, value);
821
+ }
822
+ // OpenFileState::create caches dirents in Emscripten file_table.cpp/h
823
+ // (Copyright 2021, MIT/NCSA), pinned at 862dc0d4. Each process retains the
824
+ // same description on dup/fork; kernel storage supplies the entry metadata.
825
+ directoryEntries(path) {
826
+ const fs = this.fs();
827
+ const entries = [];
828
+ const virtual = path === "/proc" ? ["self"] : path === "/proc/self" ? ["fd"]
829
+ : path === "/proc/self/fd" ? this.descriptors.entries.flatMap((entry, fd) => entry ? [String(fd)] : []) : undefined;
830
+ for (const name of [".", "..", ...(virtual ?? fs.readdirSync(path))]) {
831
+ const entryPath = name === "." ? path : name === ".."
832
+ ? normalizePath(path, "..") : normalizePath(path, name);
833
+ const stat = this.virtualStat(entryPath, true) ?? statIfPresent(fs, entryPath, true);
834
+ if (!stat)
835
+ continue; // A backend entry removed during enumeration.
836
+ const link = this.virtualLink(entryPath) ?? this.readSymlink(entryPath);
837
+ const type = link !== undefined || stat.isSymbolicLink?.() ? 10
838
+ : (stat.mode ?? 0) & 0o170000 ? ((stat.mode & 0o170000) >>> 12)
839
+ : stat.isDirectory() ? 4 : 8;
840
+ entries.push({ name, inode: stat.ino ?? 1, type });
841
+ }
842
+ return entries;
702
843
  }
703
844
  getdents64(fd, pointer, count) {
704
845
  try {
846
+ if (!Number.isSafeInteger(count) || count < 24)
847
+ throw errno("EINVAL");
705
848
  const directory = this.files.get(fd);
706
849
  if (!directory)
707
850
  throw errno("EBADF");
708
851
  if (!directory.directory)
709
852
  throw errno("ENOTDIR");
710
- const entries = [".", "..", ...this.fs().readdirSync(directory.path)];
853
+ const entries = directory.entries ??= this.directoryEntries(directory.path);
711
854
  let written = 0;
712
855
  while (directory.position < entries.length) {
713
- const name = entries[directory.position];
714
- const nameBytes = new TextEncoder().encode(name);
856
+ const entry = entries[directory.position];
857
+ const nameBytes = new TextEncoder().encode(entry.name);
715
858
  const recordLength = alignTo(19 + nameBytes.byteLength + 1, 8);
716
859
  if (recordLength > count - written) {
717
860
  if (written === 0)
@@ -720,13 +863,10 @@ export class WaliFileDescriptorTable {
720
863
  }
721
864
  const record = pointer + written;
722
865
  assertRange(this.options.memory(), record, recordLength);
723
- const entryPath = name === "." ? directory.path : name === ".."
724
- ? normalizePath(directory.path, "..") : normalizePath(directory.path, name);
725
- const stat = this.fs().statSync(entryPath);
726
- this.memoryView().setBigUint64(record, BigInt(directory.position + 1), true);
866
+ this.memoryView().setBigUint64(record, BigInt(entry.inode), true);
727
867
  this.memoryView().setBigInt64(record + 8, BigInt(directory.position + 1), true);
728
868
  this.memoryView().setUint16(record + 16, recordLength, true);
729
- this.memoryView().setUint8(record + 18, stat.isDirectory() ? 4 : 8);
869
+ this.memoryView().setUint8(record + 18, entry.type);
730
870
  this.memoryBytes().fill(0, record + 19, record + recordLength);
731
871
  this.memoryBytes().set(nameBytes, record + 19);
732
872
  directory.position += 1;
@@ -740,13 +880,14 @@ export class WaliFileDescriptorTable {
740
880
  }
741
881
  lseek(fd, offset, whence) {
742
882
  try {
883
+ const special = this.specialDescriptors.get(fd);
884
+ if (special)
885
+ return special.seek?.(offset, whence) ?? -29n;
743
886
  const file = this.files.get(fd);
744
887
  if (!file)
745
888
  throw errno("EBADF");
746
- if (file.directory)
747
- throw errno("EINVAL");
748
889
  const base = whence === SEEK_SET ? 0 : whence === SEEK_CUR ? file.position
749
- : whence === SEEK_END ? this.fs().statSync(file.path).size : undefined;
890
+ : whence === SEEK_END && !file.directory ? (file.inode?.stat() ?? this.fs().statSync(file.path)).size : undefined;
750
891
  // An offset or a position past 2^53 - 1 is not one a double holds (musl 0005's lseek says the same).
751
892
  if (base === undefined || !Number.isSafeInteger(offset) || base + offset < 0 || !Number.isSafeInteger(base + offset))
752
893
  throw errno("EINVAL");
@@ -759,6 +900,9 @@ export class WaliFileDescriptorTable {
759
900
  }
760
901
  ftruncate(fd, length) {
761
902
  try {
903
+ const special = this.specialDescriptors.get(fd);
904
+ if (special)
905
+ return special.truncate?.(length) ?? -22n;
762
906
  const file = this.files.get(fd);
763
907
  if (!file)
764
908
  throw errno("EBADF");
@@ -769,6 +913,12 @@ export class WaliFileDescriptorTable {
769
913
  if (!Number.isSafeInteger(length) || length < 0)
770
914
  throw errno("EINVAL");
771
915
  const fs = this.fs();
916
+ if (file.inode) {
917
+ this.assertWritable(file.path);
918
+ if (file.inode.stat().size !== length)
919
+ file.inode.truncate(length);
920
+ return 0n;
921
+ }
772
922
  if (fs.truncateSync) {
773
923
  if (fs.statSync(file.path).size !== length)
774
924
  fs.truncateSync(file.path, length);
@@ -786,9 +936,44 @@ export class WaliFileDescriptorTable {
786
936
  return negativeErrno(error);
787
937
  }
788
938
  }
939
+ truncate(path, length) {
940
+ try {
941
+ const resolved = this.resolveFilesystemPath(this.resolve(path));
942
+ if (waliDeviceStat(resolved))
943
+ throw errno("EINVAL");
944
+ const fs = this.fs();
945
+ const stat = fs.statSync(resolved);
946
+ if (!stat.isFile())
947
+ throw errno("EISDIR");
948
+ checkFilePermission(stat, 2);
949
+ if (!Number.isSafeInteger(length) || length < 0)
950
+ throw errno("EINVAL");
951
+ if (fs.truncateSync)
952
+ fs.truncateSync(resolved, length);
953
+ else {
954
+ const resized = new Uint8Array(length);
955
+ resized.set(fs.readFileSync(resolved).subarray(0, length));
956
+ fs.writeFileSync(resolved, resized);
957
+ }
958
+ return 0n;
959
+ }
960
+ catch (error) {
961
+ return negativeErrno(error);
962
+ }
963
+ }
789
964
  stat(path, pointer, lstat) {
790
965
  try {
791
966
  const resolved = this.resolveFilesystemPath(this.resolve(path), !lstat);
967
+ const virtual = this.virtualStat(resolved, lstat);
968
+ if (virtual) {
969
+ writeStat(this.memoryView(), pointer, virtual);
970
+ return 0n;
971
+ }
972
+ const device = waliDeviceStat(resolved);
973
+ if (device) {
974
+ writeStat(this.memoryView(), pointer, device);
975
+ return 0n;
976
+ }
792
977
  const fs = this.fs();
793
978
  // A snapshot store's statSync answers ENOENT itself: asking existsSync first resolved the path twice.
794
979
  if (!fs.linkTargetSync && !fs.existsSync(resolved))
@@ -796,7 +981,7 @@ export class WaliFileDescriptorTable {
796
981
  const target = lstat ? this.readSymlink(resolved) : undefined;
797
982
  writeStat(this.memoryView(), pointer, target === undefined
798
983
  ? (lstat && fs.lstatSync ? fs.lstatSync(resolved) : fs.statSync(resolved))
799
- : syntheticSymlinkStat(target));
984
+ : { ...fs.statSync(resolved), ...syntheticSymlinkStat(target) });
800
985
  return 0n;
801
986
  }
802
987
  catch (error) {
@@ -805,15 +990,32 @@ export class WaliFileDescriptorTable {
805
990
  }
806
991
  statAt(dirfd, path, pointer, flags) {
807
992
  try {
808
- if (flags & ~AT_SYMLINK_NOFOLLOW)
993
+ if (flags & ~(AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH | AT_NO_AUTOMOUNT))
809
994
  throw errno("EINVAL");
995
+ // getFileAt with AT_EMPTY_PATH addresses the open description directly,
996
+ // including a non-directory fd; AT_FDCWD means the current directory.
997
+ if (!path && (flags & AT_EMPTY_PATH)) {
998
+ return dirfd === AT_FDCWD ? this.stat(this.cwd, pointer, false) : this.fstat(dirfd, pointer);
999
+ }
810
1000
  const noFollow = Boolean(flags & AT_SYMLINK_NOFOLLOW);
811
1001
  const resolved = this.resolveFilesystemPath(this.resolveAt(dirfd, path), !noFollow);
1002
+ const virtual = this.virtualStat(resolved, noFollow);
1003
+ if (virtual) {
1004
+ writeStat(this.memoryView(), pointer, virtual);
1005
+ return 0n;
1006
+ }
1007
+ const device = waliDeviceStat(resolved);
1008
+ if (device) {
1009
+ writeStat(this.memoryView(), pointer, device);
1010
+ return 0n;
1011
+ }
812
1012
  const fs = this.fs();
813
1013
  if (!fs.linkTargetSync && !fs.existsSync(resolved))
814
1014
  throw errno("ENOENT");
815
1015
  const target = noFollow ? this.readSymlink(resolved) : undefined;
816
- writeStat(this.memoryView(), pointer, target === undefined ? fs.statSync(resolved) : syntheticSymlinkStat(target));
1016
+ writeStat(this.memoryView(), pointer, target === undefined
1017
+ ? (noFollow && fs.lstatSync ? fs.lstatSync(resolved) : fs.statSync(resolved))
1018
+ : { ...fs.statSync(resolved), ...syntheticSymlinkStat(target) });
817
1019
  return 0n;
818
1020
  }
819
1021
  catch (error) {
@@ -826,27 +1028,38 @@ export class WaliFileDescriptorTable {
826
1028
  writeStat(this.memoryView(), pointer, syntheticTerminalStat());
827
1029
  return 0n;
828
1030
  }
1031
+ const special = this.specialDescriptors.get(fd);
1032
+ const stat = special?.stat?.();
1033
+ if (stat) {
1034
+ writeStat(this.memoryView(), pointer, stat);
1035
+ return 0n;
1036
+ }
829
1037
  const file = this.files.get(fd);
830
1038
  if (!file)
831
1039
  throw errno("EBADF");
832
- writeStat(this.memoryView(), pointer, this.fs().statSync(file.path));
1040
+ writeStat(this.memoryView(), pointer, this.virtualStat(file.path, false) ?? (file.inode?.stat() ?? this.fs().statSync(file.path)));
833
1041
  return 0n;
834
1042
  }
835
1043
  catch (error) {
836
1044
  return negativeErrno(error);
837
1045
  }
838
1046
  }
839
- access(path) {
1047
+ accessAt(dirfd, path, mode, flags) {
840
1048
  try {
841
- return this.fs().existsSync(this.resolveFilesystemPath(this.resolve(path))) ? 0n : -2n;
842
- }
843
- catch (error) {
844
- return negativeErrno(error);
845
- }
846
- }
847
- accessAt(dirfd, path) {
848
- try {
849
- return this.fs().existsSync(this.resolveFilesystemPath(this.resolveAt(dirfd, path))) ? 0n : -2n;
1049
+ if (mode & ~7 || flags & ~(AT_EACCESS | AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH))
1050
+ throw errno("EINVAL");
1051
+ const noFollow = Boolean(flags & AT_SYMLINK_NOFOLLOW);
1052
+ const resolved = !path && (flags & AT_EMPTY_PATH)
1053
+ ? dirfd === AT_FDCWD ? this.cwd : this.files.get(dirfd)?.path
1054
+ : this.resolveFilesystemPath(this.resolveAt(dirfd, path), !noFollow);
1055
+ if (resolved === undefined)
1056
+ throw errno("EBADF");
1057
+ const fs = this.fs();
1058
+ const target = noFollow ? this.readSymlink(resolved) : undefined;
1059
+ const stat = this.virtualStat(resolved, noFollow) ?? waliDeviceStat(resolved) ?? (target !== undefined ? syntheticSymlinkStat(target)
1060
+ : noFollow && fs.lstatSync ? fs.lstatSync(resolved) : fs.statSync(resolved));
1061
+ checkFilePermission(stat, mode);
1062
+ return 0n;
850
1063
  }
851
1064
  catch (error) {
852
1065
  return negativeErrno(error);
@@ -885,7 +1098,12 @@ export class WaliFileDescriptorTable {
885
1098
  if (!file)
886
1099
  return -9n;
887
1100
  try {
888
- this.fs().chmodSync?.(file.path, mode & 0o7777);
1101
+ if (file.inode) {
1102
+ this.assertWritable(file.path);
1103
+ file.inode.chmod(mode & 0o7777);
1104
+ }
1105
+ else
1106
+ this.fs().chmodSync?.(file.path, mode & 0o7777);
889
1107
  return 0n;
890
1108
  }
891
1109
  catch (error) {
@@ -914,24 +1132,34 @@ export class WaliFileDescriptorTable {
914
1132
  return 0n;
915
1133
  }
916
1134
  getcwd(pointer, size) {
917
- const encoded = new TextEncoder().encode(`${this.cwd}\0`);
918
- if (encoded.byteLength > size)
919
- return -34n;
920
- this.copyIn(pointer, encoded);
921
- return BigInt(encoded.byteLength);
1135
+ try {
1136
+ if (!pointer && size > 0)
1137
+ throw errno("EFAULT");
1138
+ if (size === 0)
1139
+ throw errno("EINVAL");
1140
+ if (this.cwd !== "/" && !this.fs().existsSync(this.cwd))
1141
+ throw errno("ENOENT");
1142
+ const encoded = new TextEncoder().encode(`${this.cwd}\0`);
1143
+ if (encoded.byteLength > size)
1144
+ throw errno("ERANGE");
1145
+ this.copyIn(pointer, encoded);
1146
+ return BigInt(encoded.byteLength);
1147
+ }
1148
+ catch (error) {
1149
+ return negativeErrno(error);
1150
+ }
922
1151
  }
923
1152
  mkdir(path, mode) {
924
- return this.mutate(() => {
925
- const resolved = this.resolveFilesystemPath(this.resolve(path), false);
926
- this.fs().mkdirSync(resolved);
927
- this.fs().chmodSync?.(resolved, mode & 0o7777);
928
- });
1153
+ return this.mkdirAt(AT_FDCWD, path, mode);
929
1154
  }
930
1155
  mkdirAt(dirfd, path, mode) {
931
1156
  try {
932
- const resolved = this.resolveFilesystemPath(this.resolveAt(dirfd, path), false);
1157
+ const resolved = this.resolveFilesystemPath(this.resolveAt(dirfd, path), false, true);
1158
+ checkFileName(resolved);
1159
+ if (statIfPresent(this.fs(), resolved, true))
1160
+ throw errno("EEXIST");
933
1161
  this.fs().mkdirSync(resolved);
934
- this.fs().chmodSync?.(resolved, mode & 0o7777);
1162
+ this.fs().chmodSync?.(resolved, mode & 0o7777 & ~this.fileCreationMask);
935
1163
  return 0n;
936
1164
  }
937
1165
  catch (error) {
@@ -976,21 +1204,70 @@ export class WaliFileDescriptorTable {
976
1204
  try {
977
1205
  if (flags & ~AT_REMOVEDIR)
978
1206
  throw errno("EINVAL");
1207
+ const removeDirectory = Boolean(flags & AT_REMOVEDIR);
1208
+ const leaf = path.replace(/\/+$/u, "").split("/").at(-1);
1209
+ if (removeDirectory && leaf === ".")
1210
+ throw errno("EINVAL");
979
1211
  const resolved = this.resolveFilesystemPath(this.resolveAt(dirfd, path), false);
980
- if (flags & AT_REMOVEDIR)
981
- this.fs().rmdirSync(resolved);
1212
+ const fs = this.fs();
1213
+ const stat = fs.lstatSync?.(resolved) ?? fs.statSync(resolved);
1214
+ if (resolved === "/")
1215
+ throw errno("EBUSY");
1216
+ if (stat.isDirectory()) {
1217
+ if (!removeDirectory)
1218
+ throw errno("EISDIR");
1219
+ if (fs.readdirSync(resolved).length > 0)
1220
+ throw errno("ENOTEMPTY");
1221
+ }
1222
+ else if (removeDirectory)
1223
+ throw errno("ENOTDIR");
1224
+ checkFilePermission(fs.statSync(resolved.slice(0, resolved.lastIndexOf("/")) || "/"), 2);
1225
+ if (removeDirectory)
1226
+ fs.rmdirSync(resolved);
982
1227
  else
983
- this.fs().unlinkSync(resolved);
1228
+ fs.unlinkSync(resolved);
984
1229
  return 0n;
985
1230
  }
986
1231
  catch (error) {
987
1232
  return negativeErrno(error);
988
1233
  }
989
1234
  }
990
- rename(from, to) {
1235
+ linkAt(sourceDirfd, sourcePath, destinationDirfd, destinationPath, flags) {
1236
+ return this.mutate(() => {
1237
+ const follow = 0x400; // Linux AT_SYMLINK_FOLLOW
1238
+ if (flags & ~(follow | AT_EMPTY_PATH))
1239
+ throw errno("EINVAL");
1240
+ // AT_EMPTY_PATH needs the capability model and an inode handle for an
1241
+ // unlinked source. Do not substitute the descriptor's old pathname.
1242
+ if (!sourcePath && (flags & AT_EMPTY_PATH))
1243
+ throw errno("EPERM");
1244
+ if (!sourcePath || !destinationPath)
1245
+ throw errno("ENOENT");
1246
+ const source = this.resolveFilesystemPath(this.resolveAt(sourceDirfd, sourcePath), Boolean(flags & follow));
1247
+ const destination = this.resolveFilesystemPath(this.resolveAt(destinationDirfd, destinationPath), false);
1248
+ checkFileName(destination);
1249
+ if (this.virtualStat(source, true) || waliDeviceStat(source))
1250
+ throw errno("EXDEV");
1251
+ const fs = this.fs();
1252
+ const stat = fs.lstatSync?.(source) ?? fs.statSync(source);
1253
+ if (stat.isDirectory())
1254
+ throw errno("EPERM");
1255
+ if (this.virtualStat(destination, true) || waliDeviceStat(destination) || fs.existsSync(destination))
1256
+ throw errno("EEXIST");
1257
+ const directory = fs.statSync(destination.slice(0, destination.lastIndexOf("/")) || "/");
1258
+ if (!directory.isDirectory())
1259
+ throw errno("ENOTDIR");
1260
+ checkFilePermission(directory, 3);
1261
+ if (!fs.linkSync)
1262
+ throw errno("ENOSYS");
1263
+ fs.linkSync(source, destination);
1264
+ this.options.fileLocks?.created?.(destination);
1265
+ });
1266
+ }
1267
+ rename(from, to, fromDirfd = AT_FDCWD, toDirfd = AT_FDCWD) {
991
1268
  return this.mutate(() => {
992
- const source = this.resolveFilesystemPath(this.resolve(from), false);
993
- const destination = this.resolveFilesystemPath(this.resolve(to), false);
1269
+ const source = this.resolveFilesystemPath(this.resolveAt(fromDirfd, from), false);
1270
+ const destination = this.resolveFilesystemPath(this.resolveAt(toDirfd, to), false);
994
1271
  // Moving a directory the held tree is under moves the tree: the libc's to release first (ADR-0047).
995
1272
  if (this.privateTree && (underRoot(this.privateTree.root, source) || underRoot(this.privateTree.root, destination)))
996
1273
  throw errno("EREMOTE");
@@ -1003,11 +1280,14 @@ export class WaliFileDescriptorTable {
1003
1280
  if (fs.existsSync(source)) {
1004
1281
  if (source === destination)
1005
1282
  return;
1006
- const directory = fs.statSync(source).isDirectory();
1283
+ if (source === "/" || destination === "/")
1284
+ throw errno("EBUSY");
1285
+ checkFileName(destination);
1286
+ const directory = (fs.lstatSync?.(source) ?? fs.statSync(source)).isDirectory();
1007
1287
  if (directory && destination.startsWith(`${source}/`))
1008
1288
  throw errno("EINVAL");
1009
1289
  if (fs.existsSync(destination)) {
1010
- const into = fs.statSync(destination).isDirectory();
1290
+ const into = (fs.lstatSync?.(destination) ?? fs.statSync(destination)).isDirectory();
1011
1291
  if (directory && !into)
1012
1292
  throw errno("ENOTDIR");
1013
1293
  if (!directory && into)
@@ -1051,8 +1331,8 @@ export class WaliFileDescriptorTable {
1051
1331
  };
1052
1332
  const atimeMs = readTime(0, stat.atimeMs ?? stat.mtimeMs ?? now);
1053
1333
  const mtimeMs = readTime(1, stat.mtimeMs ?? now);
1054
- if (filesystem.utimesSync)
1055
- filesystem.utimesSync(path, new Date(atimeMs), new Date(mtimeMs));
1334
+ if (filesystem.utimesSync || filesystem.utimesMsSync)
1335
+ setBrowserFileTimes(filesystem, path, atimeMs, mtimeMs);
1056
1336
  else if (stat.isFile())
1057
1337
  filesystem.writeFileSync(path, filesystem.readFileSync(path));
1058
1338
  return 0n;
@@ -1063,12 +1343,12 @@ export class WaliFileDescriptorTable {
1063
1343
  }
1064
1344
  readlink(path, pointer, size, dirfd) {
1065
1345
  try {
1066
- if (!Number.isSafeInteger(size) || size < 0)
1346
+ if (!Number.isSafeInteger(size) || size <= 0)
1067
1347
  throw errno("EINVAL");
1068
- const resolved = this.resolveAt(dirfd, path);
1348
+ let resolved = this.resolveAt(dirfd, path);
1069
1349
  if (this.privateTree && underRoot(resolved, this.privateTree.root))
1070
1350
  throw errno("EREMOTE");
1071
- let target;
1351
+ let target = this.virtualLink(resolved);
1072
1352
  if (resolved === "/proc/self/exe")
1073
1353
  target = this.options.executablePath ?? "/usr/bin/wali-program";
1074
1354
  const descriptorMatch = /^\/proc\/self\/fd\/(\d+)$/u.exec(resolved);
@@ -1077,12 +1357,14 @@ export class WaliFileDescriptorTable {
1077
1357
  if (descriptor)
1078
1358
  target = descriptor.path;
1079
1359
  }
1360
+ if (target === undefined)
1361
+ resolved = this.resolveFilesystemPath(resolved, false);
1080
1362
  target ??= this.readSymlink(resolved);
1081
1363
  // musl realpath(3) probes every path component with readlink(2).
1082
1364
  // Existing non-symlinks must report EINVAL; ENOENT means the component
1083
1365
  // itself is missing and makes canonicalization fail.
1084
1366
  if (!target)
1085
- return this.fs().existsSync(resolved) ? -22n : -2n;
1367
+ return this.virtualStat(resolved, false) || waliDeviceStat(resolved) || this.fs().existsSync(resolved) ? -22n : -2n;
1086
1368
  const encoded = new TextEncoder().encode(target).subarray(0, size);
1087
1369
  this.copyIn(pointer, encoded);
1088
1370
  return BigInt(encoded.byteLength);
@@ -1094,6 +1376,7 @@ export class WaliFileDescriptorTable {
1094
1376
  symlink(target, link, dirfd) {
1095
1377
  try {
1096
1378
  const resolved = this.resolveFilesystemPath(this.resolveAt(dirfd, link), false);
1379
+ checkFileName(resolved);
1097
1380
  writeWaliSymlink(this.fs(), target, resolved);
1098
1381
  return 0n;
1099
1382
  }
@@ -1101,8 +1384,21 @@ export class WaliFileDescriptorTable {
1101
1384
  return negativeErrno(error);
1102
1385
  }
1103
1386
  }
1104
- resolveFilesystemPath(path, followFinal = true) {
1105
- const resolved = resolveWaliSymlinkPath(this.fs(), path, followFinal);
1387
+ resolveFilesystemPath(path, followFinal = true, allowMissingDirectory = false) {
1388
+ const resolved = resolveWaliSymlinkPath(this.fs(), path, followFinal, allowMissingDirectory, candidate => {
1389
+ const target = this.virtualLink(candidate, false);
1390
+ if (target !== undefined)
1391
+ return { directory: false, target };
1392
+ if (["/proc", "/proc/self", "/proc/self/fd"].includes(candidate))
1393
+ return { directory: true };
1394
+ const fd = this.procDescriptor(candidate);
1395
+ if (fd === undefined || !this.hasFd(fd))
1396
+ return;
1397
+ const file = this.files.get(fd);
1398
+ // Directory magic links must support descendants (fd/N/name). Files
1399
+ // remain magic leaves so reopen applies the caller's requested flags.
1400
+ return file?.directory ? { directory: false, target: file.path } : { directory: false };
1401
+ });
1106
1402
  // A held private tree is the libc's: a path that reaches it here (a symlink
1107
1403
  // elsewhere, a directory descriptor) is EREMOTE, and the libc releases the
1108
1404
  // tree and asks again (ADR-0047 §3).
@@ -1110,11 +1406,136 @@ export class WaliFileDescriptorTable {
1110
1406
  throw errno("EREMOTE");
1111
1407
  return resolved;
1112
1408
  }
1409
+ procDescriptor(path) {
1410
+ const match = /^\/proc\/self\/fd\/(\d+)$/u.exec(path);
1411
+ return match ? Number(match[1]) : undefined;
1412
+ }
1413
+ virtualLink(path, descriptors = true) {
1414
+ if (path === "/dev/fd")
1415
+ return "/proc/self/fd";
1416
+ if (path === "/dev/stdin")
1417
+ return "/proc/self/fd/0";
1418
+ if (path === "/dev/stdout")
1419
+ return "/proc/self/fd/1";
1420
+ if (path === "/dev/stderr")
1421
+ return "/proc/self/fd/2";
1422
+ if (!descriptors)
1423
+ return;
1424
+ const fd = this.procDescriptor(path);
1425
+ if (fd === undefined)
1426
+ return;
1427
+ const description = this.descriptors.get(fd);
1428
+ if (!description)
1429
+ throw errno("ENOENT");
1430
+ const file = this.files.get(fd);
1431
+ if (file)
1432
+ return file.path;
1433
+ if (description.kind === "pipe")
1434
+ return `pipe:[${description.value.pipe + 1}]`;
1435
+ const special = this.specialDescriptors.get(fd);
1436
+ const rdev = special?.stat?.().rdev;
1437
+ if (rdev !== undefined)
1438
+ return rdev === 0x103 ? "/dev/null" : rdev === 0x108 ? "/dev/random"
1439
+ : rdev === 0x109 ? "/dev/urandom" : "/dev/tty";
1440
+ if (special?.stdio !== undefined)
1441
+ return "/dev/tty";
1442
+ return `anon_inode:[${description.kind}]`;
1443
+ }
1444
+ virtualStat(path, noFollow) {
1445
+ if (noFollow) {
1446
+ const target = this.virtualLink(path);
1447
+ if (target !== undefined)
1448
+ return syntheticSymlinkStat(target);
1449
+ }
1450
+ if (["/proc", "/proc/self", "/proc/self/fd"].includes(path))
1451
+ return {
1452
+ isFile: () => false, isDirectory: () => true, size: 0, mode: 0o040555,
1453
+ };
1454
+ const fd = this.procDescriptor(path);
1455
+ if (fd === undefined)
1456
+ return;
1457
+ const description = this.descriptors.get(fd);
1458
+ if (!description)
1459
+ throw errno("ENOENT");
1460
+ const file = this.files.get(fd);
1461
+ if (file)
1462
+ return (file.inode?.stat() ?? this.fs().statSync(file.path));
1463
+ if (description.kind === "pipe")
1464
+ return {
1465
+ isFile: () => false, isDirectory: () => false, size: 0, mode: 0o010666,
1466
+ ino: description.value.pipe + 1,
1467
+ };
1468
+ return this.specialDescriptors.get(fd)?.stat?.() ?? syntheticTerminalStat();
1469
+ }
1470
+ reopenDescriptor(fd, flags, mode) {
1471
+ const description = this.descriptors.get(fd);
1472
+ if (!description)
1473
+ throw errno("ENOENT");
1474
+ const stat = this.virtualStat(`/proc/self/fd/${fd}`, false);
1475
+ if ((flags & O_DIRECTORY) && !stat.isDirectory())
1476
+ throw errno("ENOTDIR");
1477
+ if ((flags & O_ACCMODE) !== O_WRONLY)
1478
+ checkFilePermission(stat, 4);
1479
+ if ((flags & O_ACCMODE) !== 0)
1480
+ checkFilePermission(stat, 2);
1481
+ const file = this.files.get(fd);
1482
+ if (file) {
1483
+ if (!file.inode)
1484
+ return this.openResolved(file.path, flags & ~(O_CREAT | O_EXCL), mode);
1485
+ if ((flags & O_ACCMODE) !== 0 || (flags & O_TRUNC))
1486
+ this.assertWritable(file.path);
1487
+ const next = this.allocateFd();
1488
+ const inode = file.inode.retain();
1489
+ try {
1490
+ if (flags & O_TRUNC)
1491
+ inode.truncate(0);
1492
+ this.files.set(next, { path: file.path, inode, flags, position: 0, directory: false,
1493
+ description: this.nextDescription++ });
1494
+ return this.markCloseOnExecNumber(next, flags);
1495
+ }
1496
+ catch (error) {
1497
+ inode.close();
1498
+ throw error;
1499
+ }
1500
+ }
1501
+ if (description.kind === "pipe" && this.options.descriptorState) {
1502
+ const host = new WaliSharedPipeHost(this.options.memory, { ...this.options.descriptorState,
1503
+ table: this.descriptors.sharedEntries.buffer }, this.descriptors);
1504
+ const result = host.reopen(fd, flags);
1505
+ if (result < 0n)
1506
+ throw errno(result === -24n ? "EMFILE" : "EOPNOTSUPP");
1507
+ return Number(result);
1508
+ }
1509
+ const target = this.virtualLink(`/proc/self/fd/${fd}`);
1510
+ if (target === "/dev/tty" && this.specialDescriptors.get(fd)?.terminal)
1511
+ return this.openResolved(target, flags, mode);
1512
+ const device = target && createWaliDevice(target, flags);
1513
+ if (device) {
1514
+ const next = this.allocateFd();
1515
+ this.specialDescriptors.set(next, device);
1516
+ return this.markCloseOnExecNumber(next, flags);
1517
+ }
1518
+ const special = this.specialDescriptors.get(fd);
1519
+ if (special?.stdio !== undefined) {
1520
+ // Host streams currently expose one direction rather than an inode
1521
+ // that can be reopened in either direction. Do not return a descriptor
1522
+ // whose access mode promises I/O its transport cannot perform.
1523
+ if ((flags & O_ACCMODE) !== (special.stdio === 0 ? 0 : O_WRONLY))
1524
+ throw errno("EOPNOTSUPP");
1525
+ const next = this.allocateFd();
1526
+ this.specialDescriptors.set(next, { stdio: special.stdio });
1527
+ Atomics.store(this.descriptors.registry.pipeMetadata, this.descriptors.entries[next].description * 3 + 1, flags & ~O_CLOEXEC_FLAG);
1528
+ return this.markCloseOnExecNumber(next, flags);
1529
+ }
1530
+ throw errno("EOPNOTSUPP");
1531
+ }
1113
1532
  readSymlink(path) {
1114
1533
  return readWaliSymlink(this.fs(), path);
1115
1534
  }
1116
- dup(fd, target, minimum = 3) {
1117
- const maximum = this.options.descriptorMaximum ?? 65_535;
1535
+ dup(fd, target, minimum = 0) {
1536
+ if (target === fd && this.hasFd(fd))
1537
+ return BigInt(fd);
1538
+ const maximum = Math.min(this.options.descriptorMaximum ?? 65_535, this.descriptors.descriptorLimit - 1);
1118
1539
  const stdio = this.stdioFd(fd);
1119
1540
  if (stdio !== undefined) {
1120
1541
  const next = target ?? this.allocateFd(minimum);
@@ -1122,8 +1543,8 @@ export class WaliFileDescriptorTable {
1122
1543
  return -9n;
1123
1544
  if (next === fd)
1124
1545
  return BigInt(next);
1546
+ this.close(next);
1125
1547
  this.descriptors.set(next, this.descriptors.get(fd));
1126
- this.descriptorStatusFlags.set(next, this.statusFlags(fd));
1127
1548
  this.setSharedStdioAlias(next, stdio);
1128
1549
  return BigInt(next);
1129
1550
  }
@@ -1147,7 +1568,7 @@ export class WaliFileDescriptorTable {
1147
1568
  // wasmfs/syscalls.cpp __syscall_fcntl64 F_DUPFD: every open file,
1148
1569
  // including special files, shares its description through the table.
1149
1570
  // Our kernel retains process ownership and FD_CLOEXEC semantics.
1150
- if (argument < 0 || argument > (this.options.descriptorMaximum ?? 65_535))
1571
+ if (argument < 0 || argument >= this.descriptors.descriptorLimit || argument > (this.options.descriptorMaximum ?? 65_535))
1151
1572
  return -22n;
1152
1573
  if (special?.duplicate)
1153
1574
  return special.duplicate(argument, command === 1030);
@@ -1176,13 +1597,18 @@ export class WaliFileDescriptorTable {
1176
1597
  if (command === 3)
1177
1598
  return BigInt(this.statusFlags(fd));
1178
1599
  if (command === 4) {
1600
+ // libsyscall F_SETFL: change only mutable status bits. Access mode,
1601
+ // creation flags and descriptor CLOEXEC are not replaced by the argument.
1602
+ const mutable = O_APPEND | O_NONBLOCK | 0x2000 /* O_ASYNC */ | 0x4000 /* O_DIRECT */ | 0x40000 /* O_NOATIME */;
1179
1603
  const stdio = this.stdioFd(fd);
1180
- if (stdio !== undefined)
1181
- this.descriptorStatusFlags.set(fd, argument);
1604
+ if (stdio !== undefined) {
1605
+ const id = this.descriptors.entries[fd].description;
1606
+ Atomics.store(this.descriptors.registry.pipeMetadata, id * 3 + 1, (this.statusFlags(fd) & ~mutable) | (argument & mutable));
1607
+ }
1182
1608
  else {
1183
1609
  const file = this.files.get(fd);
1184
1610
  if (file)
1185
- file.flags = (file.flags & O_ACCMODE) | (argument & ~O_ACCMODE);
1611
+ file.flags = (file.flags & ~mutable) | (argument & mutable);
1186
1612
  }
1187
1613
  return 0n;
1188
1614
  }
@@ -1241,7 +1667,7 @@ export class WaliFileDescriptorTable {
1241
1667
  const offset = safeOffset(view.getBigInt64(pointer + 8, true));
1242
1668
  const length = safeOffset(view.getBigInt64(pointer + 16, true));
1243
1669
  const base = whence === SEEK_SET ? 0 : whence === SEEK_CUR ? file.position
1244
- : whence === SEEK_END ? this.fs().statSync(file.path).size : undefined;
1670
+ : whence === SEEK_END ? (file.inode?.stat() ?? this.fs().statSync(file.path)).size : undefined;
1245
1671
  if (base === undefined)
1246
1672
  throw errno("EINVAL");
1247
1673
  let start = base + offset;
@@ -1347,6 +1773,9 @@ export class WaliFileDescriptorTable {
1347
1773
  return BigInt(scan());
1348
1774
  }
1349
1775
  hasFd(fd) {
1776
+ if (this.options.descriptorState && !this.options.descriptors)
1777
+ return Number.isInteger(fd) && fd >= 0 && fd < 65_536
1778
+ && Atomics.load(new Int32Array(this.options.descriptorState.table), fd) !== 0;
1350
1779
  return this.descriptors.get(fd) !== undefined;
1351
1780
  }
1352
1781
  /** Descriptor aliases retain the identity of their terminal stream. */
@@ -1355,24 +1784,46 @@ export class WaliFileDescriptorTable {
1355
1784
  const state = this.options.descriptorState;
1356
1785
  if (fd < 0 || fd >= 65_536)
1357
1786
  return;
1358
- const id = Atomics.load(new Int32Array(state.table), fd);
1359
- const stream = Atomics.load(new Int32Array(state.descriptions), id * 3 + 2) - 1;
1787
+ const id = Atomics.load(this.options.descriptors ? this.descriptors.sharedEntries : new Int32Array(state.table), fd);
1788
+ const encoded = Atomics.load(new Int32Array(state.descriptions), id * 3 + 2);
1789
+ if (encoded === 4)
1790
+ return 0;
1791
+ const stream = encoded - 1;
1360
1792
  return stream >= 0 && stream <= 2 ? stream : undefined;
1361
1793
  }
1362
1794
  return this.specialDescriptors.get(fd)?.stdio;
1363
1795
  }
1796
+ terminalFd(fd) {
1797
+ const state = this.options.descriptorState;
1798
+ if (!state)
1799
+ return this.specialDescriptors.get(fd)?.terminal === true;
1800
+ if (fd < 0 || fd >= 65_536)
1801
+ return false;
1802
+ const id = Atomics.load(this.options.descriptors ? this.descriptors.sharedEntries : new Int32Array(state.table), fd);
1803
+ return Atomics.load(new Int32Array(state.descriptions), id * 3 + 2) === 4;
1804
+ }
1805
+ terminalAvailable(pointer) {
1806
+ if (!this.options.terminalStdin)
1807
+ return -95n;
1808
+ if (pointer <= 0 || pointer + 4 > this.options.memory().buffer.byteLength)
1809
+ return -14n;
1810
+ this.memoryView().setInt32(pointer, this.options.terminalStdin.available?.() ?? 0, true);
1811
+ return 0n;
1812
+ }
1364
1813
  setSharedStdioAlias(fd, target) {
1365
1814
  if (fd < 0 || fd >= (this.options.stdioAliasRegistry?.length ?? 0))
1366
1815
  return;
1367
1816
  Atomics.store(this.options.stdioAliasRegistry, fd, target ?? -1);
1368
1817
  }
1369
1818
  statusFlags(fd) {
1370
- const stored = this.descriptorStatusFlags.get(fd);
1371
- if (stored !== undefined)
1372
- return stored;
1373
1819
  const stdio = this.stdioFd(fd);
1374
- if (stdio !== undefined)
1375
- return stdio === 0 ? 0 : O_WRONLY;
1820
+ if (stdio !== undefined) {
1821
+ const state = this.options.descriptorState;
1822
+ const table = state && !this.options.descriptors ? new Int32Array(state.table) : this.descriptors.sharedEntries;
1823
+ const metadata = state ? new Int32Array(state.descriptions) : this.descriptors.registry.pipeMetadata;
1824
+ const id = Atomics.load(table, fd);
1825
+ return Atomics.load(metadata, id * 3 + 1) | (stdio === 0 || this.terminalFd(fd) ? 0 : O_WRONLY);
1826
+ }
1376
1827
  return this.files.get(fd)?.flags ?? O_RDWR;
1377
1828
  }
1378
1829
  mutate(operation) {
@@ -1409,7 +1860,7 @@ export class WaliFileDescriptorTable {
1409
1860
  else
1410
1861
  fs.writeFileSync(node.path, node.content);
1411
1862
  fs.chmodSync?.(node.path, node.mode & 0o7777);
1412
- fs.utimesSync?.(node.path, new Date(node.atimeMs), new Date(node.mtimeMs));
1863
+ setBrowserFileTimes(fs, node.path, node.atimeMs, node.mtimeMs);
1413
1864
  }
1414
1865
  }
1415
1866
  this.privateTree = undefined;
@@ -1546,7 +1997,7 @@ export class WaliFileDescriptorTable {
1546
1997
  else
1547
1998
  fs.writeFileSync(node.path, node.content);
1548
1999
  fs.chmodSync?.(node.path, node.mode & 0o7777);
1549
- fs.utimesSync?.(node.path, new Date(node.atimeMs), new Date(node.mtimeMs));
2000
+ setBrowserFileTimes(fs, node.path, node.atimeMs, node.mtimeMs);
1550
2001
  }
1551
2002
  }
1552
2003
  for (const descriptor of descriptors) {
@@ -1555,7 +2006,10 @@ export class WaliFileDescriptorTable {
1555
2006
  if (descriptor.group === descriptor.fd) {
1556
2007
  this.files.set(descriptor.fd, {
1557
2008
  path: descriptor.path, flags: descriptor.flags, position: descriptor.position,
1558
- directory: descriptor.directory, description: this.nextDescription++,
2009
+ ...(!descriptor.directory && fs.openFileSync ? { inode: fs.openFileSync(descriptor.path) } : {}),
2010
+ directory: descriptor.directory,
2011
+ ...(descriptor.directory ? { entries: this.directoryEntries(descriptor.path) } : {}),
2012
+ description: this.nextDescription++,
1559
2013
  });
1560
2014
  }
1561
2015
  else {
@@ -1570,6 +2024,19 @@ export class WaliFileDescriptorTable {
1570
2024
  return 0n;
1571
2025
  }
1572
2026
  }
2027
+ /** WasmFS's single-user mode checks; identity/credential policy is a separate seam. */
2028
+ function checkFilePermission(stat, requested) {
2029
+ const mode = stat.mode ?? (stat.isDirectory() ? 0o777 : 0o666);
2030
+ if (((requested & 4) && !(mode & 0o444))
2031
+ || ((requested & 2) && !(mode & 0o222))
2032
+ || ((requested & 1) && !(mode & 0o111)))
2033
+ throw errno("EACCES");
2034
+ }
2035
+ function checkFileName(path) {
2036
+ // WASMFS_NAME_MAX is Linux's 255-byte component limit, not a work budget.
2037
+ if (new TextEncoder().encode(path.slice(path.lastIndexOf("/") + 1)).byteLength > 255)
2038
+ throw errno("ENAMETOOLONG");
2039
+ }
1573
2040
  /** preadv's offset: one 64-bit argument, or two 32-bit halves from a libc that splits it (musl's own preadv.c). */
1574
2041
  function vectorOffset(offset, high) {
1575
2042
  if (high === undefined)
@@ -1585,19 +2052,11 @@ function normalizePath(cwd, path) {
1585
2052
  // Most paths a program names are already normal: itself, without the split.
1586
2053
  if (isNormalPath(path))
1587
2054
  return path;
1588
- const components = [];
1589
2055
  const value = path.startsWith("/") ? path : `${cwd}/${path}`;
1590
- for (const component of value.replaceAll("\\", "/").split("/")) {
1591
- if (!component || component === ".")
1592
- continue;
1593
- if (component === "..")
1594
- components.pop();
1595
- else
1596
- components.push(component);
1597
- }
1598
- return `/${components.join("/")}`;
2056
+ const normalized = normalizeWaliPath(value);
2057
+ return normalized === "/" ? "/" : normalized.replace(/\/$/u, "");
1599
2058
  }
1600
- /** Absolute, no empty, `.` or `..` component, no trailing slash, and no backslash (which `normalizePath` turns into one). */
2059
+ /** Absolute with no empty/dot component or trailing slash; backslash is a literal character. */
1601
2060
  function isNormalPath(path) {
1602
2061
  if (path.charCodeAt(0) !== 47)
1603
2062
  return false;
@@ -1606,8 +2065,6 @@ function isNormalPath(path) {
1606
2065
  let start = 1;
1607
2066
  for (let index = 1; index <= path.length; index += 1) {
1608
2067
  const code = index < path.length ? path.charCodeAt(index) : 47;
1609
- if (code === 92)
1610
- return false;
1611
2068
  if (code !== 47)
1612
2069
  continue;
1613
2070
  const length = index - start;
@@ -1637,9 +2094,9 @@ function assertRange(memory, pointer, length) {
1637
2094
  throw new WebAssembly.RuntimeError("WALI filesystem memory access is out of bounds.");
1638
2095
  }
1639
2096
  /** The stat of `path`, or undefined when the store says ENOENT; any other failure is thrown. */
1640
- function statIfPresent(fs, path) {
2097
+ function statIfPresent(fs, path, noFollow = false) {
1641
2098
  try {
1642
- return fs.statSync(path);
2099
+ return noFollow && fs.lstatSync ? fs.lstatSync(path) : fs.statSync(path);
1643
2100
  }
1644
2101
  catch (error) {
1645
2102
  if (/\bENOENT\b/u.test(error instanceof Error ? error.message : String(error)))
@@ -1657,8 +2114,9 @@ function writeStat(view, pointer, stat) {
1657
2114
  setUint64(view, pointer, stat.dev ?? 1);
1658
2115
  setUint64(view, pointer + 8, stat.ino ?? 1);
1659
2116
  setUint64(view, pointer + 16, stat.nlink ?? 1);
1660
- const kind = stat.isSymbolicLink?.() ? S_IFLNK : stat.isDirectory() ? S_IFDIR : S_IFREG;
1661
- view.setUint32(pointer + 24, kind | (stat.mode ?? (stat.isDirectory() ? 0o755 : 0o644)), true);
2117
+ // A device supplies S_IFCHR; ORing S_IFREG into it made it S_IFLNK.
2118
+ const kind = (stat.mode ?? 0) & 0o170000 || (stat.isSymbolicLink?.() ? S_IFLNK : stat.isDirectory() ? S_IFDIR : S_IFREG);
2119
+ view.setUint32(pointer + 24, kind | ((stat.mode ?? (stat.isDirectory() ? 0o755 : 0o644)) & 0o7777), true);
1662
2120
  view.setUint32(pointer + 28, stat.uid ?? 1000, true);
1663
2121
  view.setUint32(pointer + 32, stat.gid ?? 1000, true);
1664
2122
  view.setUint32(pointer + 36, 0, true);
@@ -1732,10 +2190,10 @@ function errno(errnoName) {
1732
2190
  return error;
1733
2191
  }
1734
2192
  const ERRNO_NUMBERS = {
1735
- EPERM: 1n, ENOENT: 2n, EIO: 5n, EBADF: 9n, EAGAIN: 11n, ENOMEM: 12n, EACCES: 13n,
2193
+ EPERM: 1n, ENOENT: 2n, EINTR: 4n, EIO: 5n, ENXIO: 6n, EBADF: 9n, EAGAIN: 11n, ENOMEM: 12n, EACCES: 13n,
1736
2194
  EFAULT: 14n, EBUSY: 16n, EEXIST: 17n, EXDEV: 18n, ENOTDIR: 20n, EISDIR: 21n,
1737
2195
  EINVAL: 22n, EMFILE: 24n, ENOSPC: 28n, ESPIPE: 29n, EROFS: 30n, ERANGE: 34n,
1738
- ENOSYS: 38n, ENOTEMPTY: 39n, ELOOP: 40n, ENAMETOOLONG: 36n, EREMOTE: 66n,
2196
+ ENOSYS: 38n, ENOTEMPTY: 39n, ELOOP: 40n, ENAMETOOLONG: 36n, EREMOTE: 66n, EOPNOTSUPP: 95n,
1739
2197
  };
1740
2198
  function negativeErrno(error) {
1741
2199
  // The errno's own name first, and the message parsed only for an error that has none.