@volter/browser-wali 0.1.4 → 0.1.6

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 (158) hide show
  1. package/README.md +73 -0
  2. package/dist/browser-wali-command.d.ts +3 -0
  3. package/dist/browser-wali-command.d.ts.map +1 -1
  4. package/dist/browser-wali-command.js +2 -1
  5. package/dist/browser-wali-command.js.map +1 -1
  6. package/dist/capabilities.d.ts +73 -1
  7. package/dist/capabilities.d.ts.map +1 -1
  8. package/dist/capabilities.js +86 -2
  9. package/dist/capabilities.js.map +1 -1
  10. package/dist/child-processes.d.ts +24 -0
  11. package/dist/child-processes.d.ts.map +1 -0
  12. package/dist/child-processes.js +134 -0
  13. package/dist/child-processes.js.map +1 -0
  14. package/dist/codec-protocol.d.ts +66 -0
  15. package/dist/codec-protocol.d.ts.map +1 -0
  16. package/dist/codec-protocol.js +59 -0
  17. package/dist/codec-protocol.js.map +1 -0
  18. package/dist/codec-remote.d.ts +33 -0
  19. package/dist/codec-remote.d.ts.map +1 -0
  20. package/dist/codec-remote.js +190 -0
  21. package/dist/codec-remote.js.map +1 -0
  22. package/dist/codec-worker.d.ts +2 -0
  23. package/dist/codec-worker.d.ts.map +1 -0
  24. package/dist/codec-worker.js +438 -0
  25. package/dist/codec-worker.js.map +1 -0
  26. package/dist/coherent-file-locks.d.ts.map +1 -1
  27. package/dist/coherent-file-locks.js +17 -5
  28. package/dist/coherent-file-locks.js.map +1 -1
  29. package/dist/descriptors.d.ts +49 -0
  30. package/dist/descriptors.d.ts.map +1 -0
  31. package/dist/descriptors.js +115 -0
  32. package/dist/descriptors.js.map +1 -0
  33. package/dist/events.d.ts.map +1 -1
  34. package/dist/events.js +2 -2
  35. package/dist/events.js.map +1 -1
  36. package/dist/filesystem-protocol.d.ts +28 -1
  37. package/dist/filesystem-protocol.d.ts.map +1 -1
  38. package/dist/filesystem-protocol.js +18 -0
  39. package/dist/filesystem-protocol.js.map +1 -1
  40. package/dist/filesystem-remote.d.ts +26 -2
  41. package/dist/filesystem-remote.d.ts.map +1 -1
  42. package/dist/filesystem-remote.js +132 -23
  43. package/dist/filesystem-remote.js.map +1 -1
  44. package/dist/filesystem-worker.js +274 -34
  45. package/dist/filesystem-worker.js.map +1 -1
  46. package/dist/filesystem.d.ts +30 -1
  47. package/dist/filesystem.d.ts.map +1 -1
  48. package/dist/filesystem.js +180 -50
  49. package/dist/filesystem.js.map +1 -1
  50. package/dist/fork-worker.d.ts +2 -0
  51. package/dist/fork-worker.d.ts.map +1 -0
  52. package/dist/fork-worker.js +52 -0
  53. package/dist/fork-worker.js.map +1 -0
  54. package/dist/host.d.ts +36 -0
  55. package/dist/host.d.ts.map +1 -0
  56. package/dist/host.js +153 -0
  57. package/dist/host.js.map +1 -0
  58. package/dist/loopback-host.d.ts +25 -1
  59. package/dist/loopback-host.d.ts.map +1 -1
  60. package/dist/loopback-host.js +447 -13
  61. package/dist/loopback-host.js.map +1 -1
  62. package/dist/memory.d.ts +65 -2
  63. package/dist/memory.d.ts.map +1 -1
  64. package/dist/memory.js +306 -49
  65. package/dist/memory.js.map +1 -1
  66. package/dist/module-layout.d.ts +2 -0
  67. package/dist/module-layout.d.ts.map +1 -1
  68. package/dist/module-layout.js +4 -2
  69. package/dist/module-layout.js.map +1 -1
  70. package/dist/monotonic-clock.d.ts +12 -0
  71. package/dist/monotonic-clock.d.ts.map +1 -0
  72. package/dist/monotonic-clock.js +14 -0
  73. package/dist/monotonic-clock.js.map +1 -0
  74. package/dist/network-protocol.d.ts +12 -1
  75. package/dist/network-protocol.d.ts.map +1 -1
  76. package/dist/network-protocol.js +6 -0
  77. package/dist/network-protocol.js.map +1 -1
  78. package/dist/network-worker.js +149 -20
  79. package/dist/network-worker.js.map +1 -1
  80. package/dist/network.d.ts +15 -3
  81. package/dist/network.d.ts.map +1 -1
  82. package/dist/network.js +74 -39
  83. package/dist/network.js.map +1 -1
  84. package/dist/process-adapter.d.ts +4 -1
  85. package/dist/process-adapter.d.ts.map +1 -1
  86. package/dist/process-adapter.js +3 -3
  87. package/dist/process-adapter.js.map +1 -1
  88. package/dist/process-bridge.d.ts +43 -0
  89. package/dist/process-bridge.d.ts.map +1 -1
  90. package/dist/process-bridge.js +30 -1
  91. package/dist/process-bridge.js.map +1 -1
  92. package/dist/program.d.ts +60 -1
  93. package/dist/program.d.ts.map +1 -1
  94. package/dist/program.js +92 -31
  95. package/dist/program.js.map +1 -1
  96. package/dist/runtime.d.ts.map +1 -1
  97. package/dist/runtime.js +491 -134
  98. package/dist/runtime.js.map +1 -1
  99. package/dist/select.d.ts +16 -0
  100. package/dist/select.d.ts.map +1 -0
  101. package/dist/select.js +100 -0
  102. package/dist/select.js.map +1 -0
  103. package/dist/shared-pipes.d.ts +17 -6
  104. package/dist/shared-pipes.d.ts.map +1 -1
  105. package/dist/shared-pipes.js +94 -70
  106. package/dist/shared-pipes.js.map +1 -1
  107. package/dist/signals.d.ts +48 -0
  108. package/dist/signals.d.ts.map +1 -0
  109. package/dist/signals.js +232 -0
  110. package/dist/signals.js.map +1 -0
  111. package/dist/snapshot-filesystem.d.ts +128 -2
  112. package/dist/snapshot-filesystem.d.ts.map +1 -1
  113. package/dist/snapshot-filesystem.js +481 -53
  114. package/dist/snapshot-filesystem.js.map +1 -1
  115. package/dist/spawn-policy.d.ts +5 -0
  116. package/dist/spawn-policy.d.ts.map +1 -0
  117. package/dist/spawn-policy.js +29 -0
  118. package/dist/spawn-policy.js.map +1 -0
  119. package/dist/stdin.d.ts +2 -0
  120. package/dist/stdin.d.ts.map +1 -1
  121. package/dist/stdin.js +4 -0
  122. package/dist/stdin.js.map +1 -1
  123. package/dist/symlink.d.ts +8 -0
  124. package/dist/symlink.d.ts.map +1 -1
  125. package/dist/symlink.js +13 -0
  126. package/dist/symlink.js.map +1 -1
  127. package/dist/system.d.ts +152 -1
  128. package/dist/system.d.ts.map +1 -1
  129. package/dist/system.js +226 -7
  130. package/dist/system.js.map +1 -1
  131. package/dist/thread-coordinator-worker.d.ts +1 -0
  132. package/dist/thread-coordinator-worker.d.ts.map +1 -1
  133. package/dist/thread-coordinator-worker.js +1 -0
  134. package/dist/thread-coordinator-worker.js.map +1 -1
  135. package/dist/thread-worker.js +241 -122
  136. package/dist/thread-worker.js.map +1 -1
  137. package/dist/threads.d.ts +12 -2
  138. package/dist/threads.d.ts.map +1 -1
  139. package/dist/threads.js +30 -10
  140. package/dist/threads.js.map +1 -1
  141. package/dist/types.d.ts +39 -0
  142. package/dist/types.d.ts.map +1 -1
  143. package/dist/websocket-peer.d.ts.map +1 -1
  144. package/dist/websocket-peer.js +4 -14
  145. package/dist/websocket-peer.js.map +1 -1
  146. package/dist/worker-program-protocol.d.ts +70 -3
  147. package/dist/worker-program-protocol.d.ts.map +1 -1
  148. package/dist/worker-program-worker.js +115 -23
  149. package/dist/worker-program-worker.js.map +1 -1
  150. package/dist/worker-program.d.ts +25 -1
  151. package/dist/worker-program.d.ts.map +1 -1
  152. package/dist/worker-program.js +470 -61
  153. package/dist/worker-program.js.map +1 -1
  154. package/dist/worker-standby.d.ts +3 -0
  155. package/dist/worker-standby.d.ts.map +1 -0
  156. package/dist/worker-standby.js +17 -0
  157. package/dist/worker-standby.js.map +1 -0
  158. package/package.json +21 -7
@@ -1,7 +1,10 @@
1
+ import { WaliProcessDescriptorTable } from "./descriptors.js";
2
+ import { type BrowserProcessFilesystemPolicy } from "@volter/browser-runtime/browser-process-filesystem-policy.js";
1
3
  import type { WaliFileSystem, WaliImportHandler, WaliStdin } from "./types.js";
2
4
  import type { BrowserProcessPipeHandle } from "./process-bridge.js";
3
5
  import type { BrowserFileLockRequest, BrowserFileLockResponse } from "@volter/browser-runtime/browser-file-locks.js";
4
6
  export interface WaliSpecialDescriptor {
7
+ stdio?: 0 | 1 | 2;
5
8
  read?(length: number): Uint8Array | bigint;
6
9
  write?(value: Uint8Array): bigint;
7
10
  poll?(events: number): number;
@@ -11,7 +14,10 @@ export interface WaliSpecialDescriptor {
11
14
  browserProcessPipe?: BrowserProcessPipeHandle;
12
15
  }
13
16
  export interface WaliFileDescriptorOptions {
17
+ descriptorState?: import("./shared-pipes.js").WaliSharedPipeState;
18
+ descriptors?: WaliProcessDescriptorTable;
14
19
  filesystem?: WaliFileSystem;
20
+ filesystemPolicy?: BrowserProcessFilesystemPolicy;
15
21
  cwd: string;
16
22
  stdin: Uint8Array | WaliStdin;
17
23
  executablePath?: string;
@@ -30,14 +36,16 @@ export interface WaliFileDescriptorOptions {
30
36
  export declare function createStdioAliasRegistry(): Int32Array;
31
37
  export declare class WaliFileDescriptorTable {
32
38
  private readonly options;
39
+ readonly descriptors: WaliProcessDescriptorTable;
33
40
  private readonly files;
34
41
  private readonly specialDescriptors;
35
42
  private readonly stdioAliases;
36
43
  private readonly descriptorStatusFlags;
37
44
  private readonly stdin;
38
- private nextFd;
39
45
  private nextDescription;
40
46
  private cwd;
47
+ private readonly guestFilesystem;
48
+ private readonly assertWritable;
41
49
  constructor(options: WaliFileDescriptorOptions);
42
50
  imports(): Record<string, WaliImportHandler>;
43
51
  writeEnvironmentFile(env: Readonly<Record<string, string>>, pid: number): string | undefined;
@@ -47,6 +55,8 @@ export declare class WaliFileDescriptorTable {
47
55
  writeForMapping(path: string, offset: number, value: Uint8Array): boolean;
48
56
  allocateSpecial(descriptor: WaliSpecialDescriptor, minimum?: number): number;
49
57
  exportBrowserProcessPipe(fd: number): BrowserProcessPipeHandle | undefined;
58
+ copyForFork(memory: () => WebAssembly.Memory): WaliFileDescriptorTable;
59
+ copyForExec(memory: () => WebAssembly.Memory, stdio?: number[]): WaliFileDescriptorTable;
50
60
  currentWorkingDirectory(): string;
51
61
  ioctl(fd: number, request: number): bigint | undefined;
52
62
  pollDescriptor(fd: number, events: number): number;
@@ -61,6 +71,22 @@ export declare class WaliFileDescriptorTable {
61
71
  private open;
62
72
  private openAt;
63
73
  private openResolved;
74
+ /**
75
+ * THE LOWEST DESCRIPTOR NOT CURRENTLY OPEN, which is what POSIX says `open`
76
+ * returns and `F_DUPFD` returns at or above its argument.
77
+ *
78
+ * This used to be a MONOTONIC CURSOR that never rewound, so a closed
79
+ * descriptor's NUMBER was gone for the life of the process and the table's
80
+ * maximum bounded a program's LIFETIME opens instead of its concurrent ones.
81
+ * MEASURED 2026-09-19 in Blender's WALI session, `descriptorMaximum: 9_999`:
82
+ * `os.stat` in a loop survived 40,000, `open(...).close()` in a loop raised
83
+ * `[Errno 24] No file descriptors available` after 9,176 with at most one
84
+ * file open at a time. The session's channel loop then went DEAF -- its
85
+ * `os.listdir` failed EMFILE forever -- and a call sat unanswered for 1,008
86
+ * seconds with the program alive and no error anywhere.
87
+ *
88
+ * Scanning costs one probe per open descriptor, and a process has a handful.
89
+ */
64
90
  private allocateFd;
65
91
  private close;
66
92
  private read;
@@ -102,6 +128,9 @@ export declare class WaliFileDescriptorTable {
102
128
  private readRecordLock;
103
129
  private writeRecordLock;
104
130
  private flock;
131
+ private readonly posixLockedPaths;
132
+ private readonly lockingOwners;
133
+ private heldLock;
105
134
  private lockOwner;
106
135
  private releasePosixLocks;
107
136
  private releaseLockOwner;
@@ -1 +1 @@
1
- {"version":3,"file":"filesystem.d.ts","sourceRoot":"","sources":["../src/filesystem.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,cAAc,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE7F,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,KAAK,EAIV,sBAAsB,EACtB,uBAAuB,EACxB,MAAM,+CAA+C,CAAC;AA4CvD,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IAC3C,KAAK,CAAC,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAAC;IAClC,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,KAAK,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAClD,SAAS,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1D,KAAK,CAAC,IAAI,IAAI,CAAC;IACf,kBAAkB,CAAC,EAAE,wBAAwB,CAAC;CAC/C;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,UAAU,GAAG,SAAS,CAAC;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,IAAI,WAAW,CAAC,MAAM,CAAC;IAC7B,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAAC;IAC5C,kBAAkB,CAAC,EAAE,UAAU,CAAC;IAChC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE;QACV,OAAO,CAAC,OAAO,EAAE,sBAAsB,GAAG,uBAAuB,CAAC;QAClE,OAAO,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;KAC9B,CAAC;IACF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,wBAAwB,IAAI,UAAU,CAOrD;AAED,qBAAa,uBAAuB;IAUtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IATpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA4C;IAC/E,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAgC;IAC7D,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAA6B;IACnE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAY;IAClC,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,GAAG,CAAS;gBAES,OAAO,EAAE,yBAAyB;IAK/D,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC;IAmF5C,oBAAoB,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAY5F,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAOlE,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAMlF,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAK3C,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO;IAgBzE,eAAe,CAAC,UAAU,EAAE,qBAAqB,EAAE,OAAO,SAAI,GAAG,MAAM;IAMvE,wBAAwB,CAAC,EAAE,EAAE,MAAM,GAAG,wBAAwB,GAAG,SAAS;IAI1E,uBAAuB,IAAI,MAAM;IAEjC,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAQtD,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAWlD,OAAO,CAAC,EAAE;IAKV,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,UAAU;IAElB,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,MAAM;IAKd,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,OAAO;IAEf,OAAO,CAAC,SAAS;IAQjB,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,YAAY;IAiDpB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,KAAK;IAwBb,OAAO,CAAC,IAAI;IA8BZ,OAAO,CAAC,KAAK;IAuBb,OAAO,CAAC,KAAK;IAUb,OAAO,CAAC,MAAM;IAYd,OAAO,CAAC,KAAK;IAab,OAAO,CAAC,MAAM;IAYd,OAAO,CAAC,UAAU;IAiClB,OAAO,CAAC,KAAK;IAab,OAAO,CAAC,SAAS;IAgBjB,OAAO,CAAC,IAAI;IAaZ,OAAO,CAAC,MAAM;IAad,OAAO,CAAC,KAAK;IAab,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,KAAK;IASb,OAAO,CAAC,OAAO;IAUf,OAAO,CAAC,MAAM;IAQd,OAAO,CAAC,KAAK;IASb,OAAO,CAAC,MAAM;IAQd,OAAO,CAAC,MAAM;IAOd,OAAO,CAAC,KAAK;IAQb,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,MAAM;IAQd,OAAO,CAAC,OAAO;IAMf,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,eAAe;IAKvB,OAAO,CAAC,QAAQ;IAUhB,OAAO,CAAC,MAAM;IAOd,OAAO,CAAC,SAAS;IA+BjB,OAAO,CAAC,QAAQ;IAsBhB,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,GAAG;IAoBX,OAAO,CAAC,KAAK;IA8Bb,OAAO,CAAC,UAAU;IA+BlB,OAAO,CAAC,cAAc;IAyBtB,OAAO,CAAC,eAAe;IAcvB,OAAO,CAAC,KAAK;IAmBb,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,IAAI;IAsBZ,OAAO,CAAC,KAAK;IAKb,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,mBAAmB;IAK3B,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,MAAM;CAGf"}
1
+ {"version":3,"file":"filesystem.d.ts","sourceRoot":"","sources":["../src/filesystem.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,8 @@
1
+ import { WaliDescriptionRegistry, WaliProcessDescriptorTable } from "./descriptors.js";
2
+ import { createProcessFilesystemMutationGuard } from "@volter/browser-runtime/browser-process-filesystem-policy.js";
1
3
  import { BufferedWaliStdin } from "./stdin.js";
2
4
  import { readWaliSymlink, resolveWaliSymlinkPath, writeWaliSymlink } from "./symlink.js";
5
+ let reportedDroppedEnvironment = false;
3
6
  const AT_FDCWD = -100;
4
7
  const O_ACCMODE = 3;
5
8
  const O_WRONLY = 1;
@@ -43,18 +46,56 @@ export function createStdioAliasRegistry() {
43
46
  }
44
47
  export class WaliFileDescriptorTable {
45
48
  options;
46
- files = new Map();
47
- specialDescriptors = new Map();
48
- stdioAliases = new Map();
49
+ descriptors;
50
+ files;
51
+ specialDescriptors;
52
+ stdioAliases;
49
53
  descriptorStatusFlags = new Map();
50
54
  stdin;
51
- nextFd = 3;
52
55
  nextDescription = 1;
53
56
  cwd;
57
+ guestFilesystem;
58
+ assertWritable;
54
59
  constructor(options) {
55
60
  this.options = options;
61
+ this.descriptors = options.descriptors ?? new WaliProcessDescriptorTable(new WaliDescriptionRegistry());
62
+ this.files = this.descriptors.view("file");
63
+ this.specialDescriptors = this.descriptors.view("special", (value) => value.close?.());
64
+ this.stdioAliases = this.descriptors.view("stdio");
56
65
  this.cwd = normalizePath("/", options.cwd);
66
+ const fs = options.filesystem;
67
+ this.assertWritable = options.filesystemPolicy ? createProcessFilesystemMutationGuard({
68
+ lstatSync: (path) => {
69
+ if (!fs)
70
+ throw errno("ENOSYS");
71
+ const stat = fs.lstatSync?.(path) ?? fs.statSync(path);
72
+ return { isSymbolicLink: () => stat.isSymbolicLink?.() === true || readWaliSymlink(fs, path) !== undefined };
73
+ },
74
+ }, options.filesystemPolicy) : (path) => path;
75
+ // Only guest operations use this view. Host imports and environment setup
76
+ // still own the raw tree; the guest cannot call snapshot replacement.
77
+ this.guestFilesystem = fs && options.filesystemPolicy ? {
78
+ existsSync: fs.existsSync.bind(fs), statSync: fs.statSync.bind(fs),
79
+ ...(fs.lstatSync ? { lstatSync: fs.lstatSync.bind(fs) } : {}),
80
+ readFileSync: fs.readFileSync.bind(fs), readFile: fs.readFile.bind(fs),
81
+ readdirSync: fs.readdirSync.bind(fs),
82
+ writeFileSync: (path, bytes) => fs.writeFileSync(this.assertWritable(path), bytes),
83
+ mkdirSync: (path, settings) => fs.mkdirSync(this.assertWritable(path), settings),
84
+ unlinkSync: (path) => fs.unlinkSync(this.assertWritable(path)),
85
+ rmdirSync: (path) => fs.rmdirSync(this.assertWritable(path)),
86
+ renameSync: (from, to) => fs.renameSync(this.assertWritable(from), this.assertWritable(to)),
87
+ ...(fs.chmodSync ? { chmodSync: (path, mode) => fs.chmodSync(this.assertWritable(path), mode) } : {}),
88
+ ...(fs.utimesSync ? { utimesSync: (path, atime, mtime) => fs.utimesSync(this.assertWritable(path), atime, mtime) } : {}),
89
+ } : fs;
57
90
  this.stdin = "read" in options.stdin ? options.stdin : new BufferedWaliStdin(options.stdin);
91
+ if (!options.descriptors) {
92
+ for (const fd of [0, 1, 2])
93
+ this.specialDescriptors.set(fd, { stdio: fd });
94
+ }
95
+ else if (this.descriptors.entries.length === 0 && !options.descriptorState) {
96
+ for (const fd of [0, 1, 2])
97
+ this.specialDescriptors.set(fd, { stdio: fd });
98
+ }
58
99
  }
59
100
  imports() {
60
101
  return {
@@ -115,17 +156,33 @@ export class WaliFileDescriptorTable {
115
156
  writeEnvironmentFile(env, pid) {
116
157
  if (!this.options.filesystem || Object.keys(env).length === 0)
117
158
  return;
118
- const path = `/tmp/.browser-runtime-wali-env-${pid}`;
159
+ // PIDs are local to each WALI worker; several native programs can have
160
+ // PID 2 while sharing the same host filesystem.
161
+ const path = `/tmp/.browser-runtime-wali-env-${pid}-${crypto.randomUUID()}`;
119
162
  try {
120
163
  this.options.filesystem.mkdirSync("/tmp", { recursive: true });
121
164
  }
122
165
  catch { /* already present */ }
123
- const lines = Object.entries(env).map(([name, value]) => {
124
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name) || /[\u0000\r\n]/u.test(value))
125
- throw new Error(`Invalid WALI environment entry: ${name}`);
126
- return `${name}=${value}`;
127
- });
166
+ // The env file is one `name=value` per line, so it cannot carry a value
167
+ // with a line break or a name that is not an identifier. A shell's own
168
+ // parameters (`?`, a newline-valued IFS) reach here through every lane's
169
+ // spawn; refusing the whole spawn for them is what killed a script's
170
+ // `spawn("ffmpeg")`. They are dropped and named once.
171
+ const dropped = [];
172
+ const lines = [];
173
+ for (const [name, value] of Object.entries(env)) {
174
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name) || /[\u0000\r\n]/u.test(value)) {
175
+ dropped.push(name);
176
+ continue;
177
+ }
178
+ lines.push(`${name}=${value}`);
179
+ }
180
+ if (dropped.length > 0 && !reportedDroppedEnvironment) {
181
+ reportedDroppedEnvironment = true;
182
+ console.warn(`[WALI] environment entries a process cannot receive were dropped: ${dropped.join(", ")}`);
183
+ }
128
184
  this.options.filesystem.writeFileSync(path, `${lines.join("\n")}\n`);
185
+ this.options.filesystem.chmodSync?.(path, 0o600);
129
186
  return path;
130
187
  }
131
188
  copyCString(pointer, value, limit) {
@@ -171,6 +228,21 @@ export class WaliFileDescriptorTable {
171
228
  exportBrowserProcessPipe(fd) {
172
229
  return this.specialDescriptors.get(fd)?.browserProcessPipe;
173
230
  }
231
+ copyForFork(memory) {
232
+ return new WaliFileDescriptorTable({ ...this.options, cwd: this.cwd, memory, descriptors: this.descriptors.copy() });
233
+ }
234
+ copyForExec(memory, stdio) {
235
+ const descriptors = this.descriptors.copy();
236
+ if (stdio) {
237
+ const streams = stdio.map(fd => this.descriptors.get(fd));
238
+ streams.forEach((description, fd) => {
239
+ if (description && stdio[fd] !== fd)
240
+ descriptors.set(fd, description);
241
+ });
242
+ }
243
+ descriptors.exec();
244
+ return new WaliFileDescriptorTable({ ...this.options, cwd: this.cwd, memory, descriptors });
245
+ }
174
246
  currentWorkingDirectory() { return this.cwd; }
175
247
  ioctl(fd, request) {
176
248
  if (!this.hasFd(fd))
@@ -185,7 +257,7 @@ export class WaliFileDescriptorTable {
185
257
  if (fd < 0 || !this.hasFd(fd))
186
258
  return 0x20;
187
259
  const special = this.specialDescriptors.get(fd);
188
- if (special)
260
+ if (special && special.stdio === undefined)
189
261
  return special.poll?.(events) ?? 0;
190
262
  const stdio = this.stdioFd(fd);
191
263
  let returned = 0;
@@ -196,9 +268,9 @@ export class WaliFileDescriptorTable {
196
268
  return returned;
197
269
  }
198
270
  fs() {
199
- if (!this.options.filesystem)
271
+ if (!this.guestFilesystem)
200
272
  throw errno("ENOSYS");
201
- return this.options.filesystem;
273
+ return this.guestFilesystem;
202
274
  }
203
275
  memoryBytes() { return new Uint8Array(this.options.memory().buffer); }
204
276
  memoryView() { return new DataView(this.options.memory().buffer); }
@@ -276,6 +348,8 @@ export class WaliFileDescriptorTable {
276
348
  }
277
349
  const fs = this.fs();
278
350
  const exists = fs.existsSync(path);
351
+ if ((flags & O_ACCMODE) !== 0 || (!exists && (flags & O_CREAT)))
352
+ this.assertWritable(path);
279
353
  if (!exists && !(flags & O_CREAT))
280
354
  throw errno("ENOENT");
281
355
  if (exists && (flags & O_CREAT) && (flags & O_EXCL))
@@ -301,20 +375,37 @@ export class WaliFileDescriptorTable {
301
375
  directory: stat.isDirectory(),
302
376
  description: this.nextDescription++,
303
377
  });
378
+ this.descriptors.entries[fd].flags = flags & 0x80000 ? 1 : 0;
304
379
  return fd;
305
380
  }
381
+ /**
382
+ * THE LOWEST DESCRIPTOR NOT CURRENTLY OPEN, which is what POSIX says `open`
383
+ * returns and `F_DUPFD` returns at or above its argument.
384
+ *
385
+ * This used to be a MONOTONIC CURSOR that never rewound, so a closed
386
+ * descriptor's NUMBER was gone for the life of the process and the table's
387
+ * maximum bounded a program's LIFETIME opens instead of its concurrent ones.
388
+ * MEASURED 2026-09-19 in Blender's WALI session, `descriptorMaximum: 9_999`:
389
+ * `os.stat` in a loop survived 40,000, `open(...).close()` in a loop raised
390
+ * `[Errno 24] No file descriptors available` after 9,176 with at most one
391
+ * file open at a time. The session's channel loop then went DEAF -- its
392
+ * `os.listdir` failed EMFILE forever -- and a call sat unanswered for 1,008
393
+ * seconds with the program alive and no error anywhere.
394
+ *
395
+ * Scanning costs one probe per open descriptor, and a process has a handful.
396
+ */
306
397
  allocateFd(minimum = 3) {
307
- this.nextFd = Math.max(this.nextFd, minimum);
308
- while (this.files.has(this.nextFd) || this.specialDescriptors.has(this.nextFd)
309
- || this.stdioFd(this.nextFd) !== undefined)
310
- this.nextFd += 1;
311
- if (this.nextFd > (this.options.descriptorMaximum ?? 65_535))
312
- throw errno("EMFILE");
313
- return this.nextFd++;
398
+ const maximum = this.options.descriptorMaximum ?? 65_535;
399
+ for (let fd = Math.max(minimum, 0); fd <= maximum; fd += 1) {
400
+ // `stdioFd` covers 0-2, this table's own aliases and the shared registry.
401
+ if (this.descriptors.get(fd) || this.files.has(fd) || this.specialDescriptors.has(fd)
402
+ || this.stdioFd(fd) !== undefined)
403
+ continue;
404
+ return fd;
405
+ }
406
+ throw errno("EMFILE");
314
407
  }
315
408
  close(fd) {
316
- if (fd >= 0 && fd <= 2)
317
- return 0n;
318
409
  if (this.stdioAliases.delete(fd)) {
319
410
  this.descriptorStatusFlags.delete(fd);
320
411
  this.setSharedStdioAlias(fd, undefined);
@@ -322,7 +413,6 @@ export class WaliFileDescriptorTable {
322
413
  }
323
414
  const special = this.specialDescriptors.get(fd);
324
415
  if (special) {
325
- special.close?.();
326
416
  this.specialDescriptors.delete(fd);
327
417
  return 0n;
328
418
  }
@@ -366,8 +456,10 @@ export class WaliFileDescriptorTable {
366
456
  throw errno("EISDIR");
367
457
  if ((file.flags & O_ACCMODE) === O_WRONLY)
368
458
  throw errno("EBADF");
369
- const content = this.fs().readFileSync(file.path);
370
- const chunk = content.subarray(file.position, file.position + length);
459
+ const fs = this.fs();
460
+ const chunk = fs.readRangeSync
461
+ ? fs.readRangeSync(file.path, file.position, length)
462
+ : fs.readFileSync(file.path).subarray(file.position, file.position + length);
371
463
  this.copyIn(pointer, chunk);
372
464
  file.position += chunk.byteLength;
373
465
  return BigInt(chunk.byteLength);
@@ -392,14 +484,22 @@ export class WaliFileDescriptorTable {
392
484
  throw errno("EISDIR");
393
485
  if ((file.flags & O_ACCMODE) === 0)
394
486
  throw errno("EBADF");
395
- const current = this.fs().readFileSync(file.path);
487
+ const fs = this.fs();
488
+ if (fs.writeRangeSync) {
489
+ if (file.flags & O_APPEND)
490
+ file.position = fs.statSync(file.path).size;
491
+ fs.writeRangeSync(file.path, file.position, chunk);
492
+ file.position += chunk.byteLength;
493
+ return BigInt(chunk.byteLength);
494
+ }
495
+ const current = fs.readFileSync(file.path);
396
496
  if (file.flags & O_APPEND)
397
497
  file.position = current.byteLength;
398
498
  const size = Math.max(current.byteLength, file.position + chunk.byteLength);
399
499
  const next = new Uint8Array(size);
400
500
  next.set(current);
401
501
  next.set(chunk, file.position);
402
- this.fs().writeFileSync(file.path, next);
502
+ fs.writeFileSync(file.path, next);
403
503
  file.position += chunk.byteLength;
404
504
  return BigInt(chunk.byteLength);
405
505
  }
@@ -843,35 +943,47 @@ export class WaliFileDescriptorTable {
843
943
  return -9n;
844
944
  if (next === fd)
845
945
  return BigInt(next);
846
- this.stdioAliases.set(next, stdio);
946
+ this.descriptors.set(next, this.descriptors.get(fd));
847
947
  this.descriptorStatusFlags.set(next, this.statusFlags(fd));
848
948
  this.setSharedStdioAlias(next, stdio);
849
949
  return BigInt(next);
850
950
  }
851
- const file = this.files.get(fd);
852
- if (!file)
951
+ if (!this.descriptors.get(fd))
853
952
  return -9n;
854
953
  const next = target ?? this.allocateFd(minimum);
855
954
  if (next < 0 || next > maximum)
856
955
  return -9n;
857
- this.files.set(next, { ...file });
956
+ if (next === fd)
957
+ return BigInt(next);
958
+ this.close(next);
959
+ this.descriptors.set(next, this.descriptors.get(fd));
858
960
  return BigInt(next);
859
961
  }
860
962
  fcntl(fd, command, argument) {
861
963
  if (!this.hasFd(fd))
862
964
  return -9n;
863
965
  const special = this.specialDescriptors.get(fd);
864
- if (special) {
966
+ if (special && special.stdio === undefined && command !== 1 && command !== 2) {
865
967
  if (command === 0 || command === 1030) {
866
968
  return special.duplicate?.(Math.max(argument, 3), command === 1030) ?? -22n;
867
969
  }
868
970
  return special.fcntl?.(command, argument) ?? -22n;
869
971
  }
870
972
  const file = this.files.get(fd);
871
- if (command === 0 || command === 1030)
872
- return this.dup(fd, undefined, Math.max(argument, 3));
873
- if (command === 1 || command === 2)
973
+ if (command === 0 || command === 1030) {
974
+ const result = this.dup(fd, undefined, Math.max(argument, 3));
975
+ if (result >= 0n && command === 1030)
976
+ this.descriptors.entries[Number(result)].flags = 1;
977
+ return result;
978
+ }
979
+ if (command === 1)
980
+ return BigInt(this.descriptors.entries[fd]?.flags ?? 0);
981
+ if (command === 2) {
982
+ const entry = this.descriptors.entries[fd];
983
+ if (entry)
984
+ entry.flags = argument & 1;
874
985
  return 0n;
986
+ }
875
987
  if (command === 3)
876
988
  return BigInt(this.statusFlags(fd));
877
989
  if (command === 4) {
@@ -920,10 +1032,13 @@ export class WaliFileDescriptorTable {
920
1032
  return 0n;
921
1033
  }
922
1034
  const blocking = command === F_SETLKW || command === F_OFD_SETLKW;
923
- return BigInt(client.request({
1035
+ const result = client.request({
924
1036
  operation: "set", owner, namespace, path: file.path, range: requested.range,
925
1037
  mode: requested.mode, pid: this.options.pid ?? 1, blocking,
926
- }).result);
1038
+ }).result;
1039
+ if (result >= 0)
1040
+ this.heldLock(namespace, owner, file.path);
1041
+ return BigInt(result);
927
1042
  }
928
1043
  catch (error) {
929
1044
  return negativeErrno(error);
@@ -980,10 +1095,24 @@ export class WaliFileDescriptorTable {
980
1095
  const mode = operation & LOCK_EX ? "write" : operation & LOCK_SH ? "read" : undefined;
981
1096
  if (!mode || operation & ~(LOCK_SH | LOCK_EX | LOCK_NB | LOCK_UN))
982
1097
  return -22n;
983
- return BigInt(client.request({
1098
+ const result = client.request({
984
1099
  operation: "set", owner, namespace: "flock", path: file.path, range: { start: 0 },
985
1100
  mode, pid: this.options.pid ?? 1, blocking: !(operation & LOCK_NB),
986
- }).result);
1101
+ }).result;
1102
+ if (result >= 0)
1103
+ this.heldLock("flock", owner, file.path);
1104
+ return BigInt(result);
1105
+ }
1106
+ // What this process has locked. A close releases only these: each release
1107
+ // is a synchronous request to the page's lock table, and a close of a file
1108
+ // never locked (git closes hundreds) paid three of them.
1109
+ posixLockedPaths = new Set();
1110
+ lockingOwners = new Set();
1111
+ heldLock(namespace, owner, path) {
1112
+ if (namespace === "posix")
1113
+ this.posixLockedPaths.add(path);
1114
+ else
1115
+ this.lockingOwners.add(owner);
987
1116
  }
988
1117
  lockOwner(namespace, file) {
989
1118
  const base = this.options.lockOwner;
@@ -993,14 +1122,14 @@ export class WaliFileDescriptorTable {
993
1122
  }
994
1123
  releasePosixLocks(path) {
995
1124
  const owner = this.options.lockOwner ? `${this.options.lockOwner}:posix` : undefined;
996
- if (!owner || !this.options.fileLocks)
1125
+ if (!owner || !this.options.fileLocks || !this.posixLockedPaths.delete(path))
997
1126
  return;
998
1127
  this.options.fileLocks.request({
999
1128
  operation: "unlock", owner, namespace: "posix", path, range: { start: 0 },
1000
1129
  });
1001
1130
  }
1002
1131
  releaseLockOwner(owner) {
1003
- if (owner && this.options.fileLocks)
1132
+ if (owner && this.options.fileLocks && this.lockingOwners.delete(owner))
1004
1133
  this.options.fileLocks.request({ operation: "release-owner", owner });
1005
1134
  }
1006
1135
  poll(pointer, count, timeout) {
@@ -1029,17 +1158,18 @@ export class WaliFileDescriptorTable {
1029
1158
  return BigInt(scan());
1030
1159
  }
1031
1160
  hasFd(fd) {
1032
- return (fd >= 0 && fd <= 2) || this.stdioAliases.has(fd)
1033
- || this.files.has(fd) || this.specialDescriptors.has(fd);
1161
+ return this.descriptors.get(fd) !== undefined;
1034
1162
  }
1035
1163
  stdioFd(fd) {
1036
- if (fd >= 0 && fd <= 2)
1037
- return fd;
1038
- const local = this.stdioAliases.get(fd);
1039
- if (local !== undefined)
1040
- return local;
1041
- const shared = this.options.stdioAliasRegistry?.[fd];
1042
- return shared === 0 || shared === 1 || shared === 2 ? shared : undefined;
1164
+ if (this.options.descriptorState) {
1165
+ const state = this.options.descriptorState;
1166
+ if (fd < 0 || fd >= 65_536)
1167
+ return;
1168
+ const id = Atomics.load(new Int32Array(state.table), fd);
1169
+ const stream = Atomics.load(new Int32Array(state.descriptions), id * 3 + 2) - 1;
1170
+ return stream >= 0 && stream <= 2 ? stream : undefined;
1171
+ }
1172
+ return this.specialDescriptors.get(fd)?.stdio;
1043
1173
  }
1044
1174
  setSharedStdioAlias(fd, target) {
1045
1175
  if (fd < 0 || fd >= (this.options.stdioAliasRegistry?.length ?? 0))