@secure-exec/core 0.2.1 → 0.3.0-rc.1

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 (248) hide show
  1. package/README.md +5 -5
  2. package/dist/binary.d.ts +4 -0
  3. package/dist/binary.js +25 -0
  4. package/dist/bytes.d.ts +2 -0
  5. package/dist/bytes.js +6 -0
  6. package/dist/callbacks.d.ts +41 -0
  7. package/dist/callbacks.js +94 -0
  8. package/dist/cargo.d.ts +2 -0
  9. package/dist/cargo.js +142 -0
  10. package/dist/correlation.d.ts +10 -0
  11. package/dist/correlation.js +49 -0
  12. package/dist/descriptors.d.ts +34 -0
  13. package/dist/descriptors.js +37 -0
  14. package/dist/event-buffer.d.ts +90 -0
  15. package/dist/event-buffer.js +313 -0
  16. package/dist/ext.d.ts +7 -0
  17. package/dist/ext.js +13 -0
  18. package/dist/filesystem.d.ts +41 -0
  19. package/dist/filesystem.js +70 -0
  20. package/dist/frame-payload-codec.d.ts +8 -0
  21. package/dist/frame-payload-codec.js +14 -0
  22. package/dist/frame-rpc.d.ts +38 -0
  23. package/dist/frame-rpc.js +73 -0
  24. package/dist/frame-stream.d.ts +27 -0
  25. package/dist/frame-stream.js +99 -0
  26. package/dist/framing.d.ts +7 -0
  27. package/dist/framing.js +22 -0
  28. package/dist/generated/AcpLimitsConfig.d.ts +4 -0
  29. package/dist/generated/AcpLimitsConfig.js +2 -0
  30. package/dist/generated/CreateVmConfig.d.ts +19 -0
  31. package/dist/generated/FsPermissionRule.d.ts +6 -0
  32. package/dist/generated/FsPermissionRuleSet.d.ts +6 -0
  33. package/dist/generated/FsPermissionRuleSet.js +1 -0
  34. package/dist/generated/FsPermissionScope.d.ts +3 -0
  35. package/dist/generated/FsPermissionScope.js +1 -0
  36. package/dist/generated/HttpLimitsConfig.d.ts +3 -0
  37. package/dist/generated/HttpLimitsConfig.js +2 -0
  38. package/dist/generated/JsModuleResolution.d.ts +1 -0
  39. package/dist/generated/JsModuleResolution.js +2 -0
  40. package/dist/generated/JsRuntimeConfig.d.ts +26 -0
  41. package/dist/generated/JsRuntimeConfig.js +1 -0
  42. package/dist/generated/JsRuntimeLimitsConfig.d.ts +7 -0
  43. package/dist/generated/JsRuntimeLimitsConfig.js +2 -0
  44. package/dist/generated/JsRuntimePlatform.d.ts +1 -0
  45. package/dist/generated/JsRuntimePlatform.js +2 -0
  46. package/dist/generated/MountPluginDescriptor.d.ts +4 -0
  47. package/dist/generated/MountPluginDescriptor.js +2 -0
  48. package/dist/generated/NativeRootFilesystemConfig.d.ts +5 -0
  49. package/dist/generated/NativeRootFilesystemConfig.js +1 -0
  50. package/dist/generated/PatternPermissionRule.d.ts +6 -0
  51. package/dist/generated/PatternPermissionRule.js +1 -0
  52. package/dist/generated/PatternPermissionRuleSet.d.ts +6 -0
  53. package/dist/generated/PatternPermissionRuleSet.js +1 -0
  54. package/dist/generated/PatternPermissionScope.d.ts +3 -0
  55. package/dist/generated/PatternPermissionScope.js +1 -0
  56. package/dist/generated/PermissionMode.d.ts +1 -0
  57. package/dist/generated/PermissionMode.js +2 -0
  58. package/dist/generated/PermissionsPolicy.d.ts +10 -0
  59. package/dist/generated/PermissionsPolicy.js +1 -0
  60. package/dist/generated/PluginLimitsConfig.d.ts +4 -0
  61. package/dist/generated/PluginLimitsConfig.js +2 -0
  62. package/dist/generated/PythonLimitsConfig.d.ts +5 -0
  63. package/dist/generated/PythonLimitsConfig.js +2 -0
  64. package/dist/generated/ResourceLimitsConfig.d.ts +22 -0
  65. package/dist/generated/ResourceLimitsConfig.js +2 -0
  66. package/dist/generated/RootFilesystemConfig.d.ts +9 -0
  67. package/dist/generated/RootFilesystemConfig.js +1 -0
  68. package/dist/generated/RootFilesystemEntry.d.ts +13 -0
  69. package/dist/generated/RootFilesystemEntry.js +1 -0
  70. package/dist/generated/RootFilesystemEntryEncoding.d.ts +1 -0
  71. package/dist/generated/RootFilesystemEntryEncoding.js +2 -0
  72. package/dist/generated/RootFilesystemEntryKind.d.ts +1 -0
  73. package/dist/generated/RootFilesystemEntryKind.js +2 -0
  74. package/dist/generated/RootFilesystemLowerDescriptor.d.ts +7 -0
  75. package/dist/generated/RootFilesystemLowerDescriptor.js +1 -0
  76. package/dist/generated/RootFilesystemMode.d.ts +1 -0
  77. package/dist/generated/RootFilesystemMode.js +2 -0
  78. package/dist/generated/ToolLimitsConfig.d.ts +10 -0
  79. package/dist/generated/ToolLimitsConfig.js +2 -0
  80. package/dist/generated/VmDnsConfig.d.ts +6 -0
  81. package/dist/generated/VmDnsConfig.js +2 -0
  82. package/dist/generated/VmLimitsConfig.d.ts +18 -0
  83. package/dist/generated/VmLimitsConfig.js +1 -0
  84. package/dist/generated/VmListenPolicyConfig.d.ts +5 -0
  85. package/dist/generated/VmListenPolicyConfig.js +2 -0
  86. package/dist/generated/WasmLimitsConfig.d.ts +5 -0
  87. package/dist/generated/WasmLimitsConfig.js +2 -0
  88. package/dist/generated-protocol.d.ts +1037 -0
  89. package/dist/generated-protocol.js +2887 -0
  90. package/dist/index.d.ts +24 -62
  91. package/dist/index.js +24 -53
  92. package/dist/json.d.ts +2 -0
  93. package/dist/json.js +20 -0
  94. package/dist/kernel-proxy.d.ts +149 -0
  95. package/dist/kernel-proxy.js +1733 -0
  96. package/dist/native-client.d.ts +41 -0
  97. package/dist/native-client.js +124 -0
  98. package/dist/node-runtime.d.ts +443 -0
  99. package/dist/node-runtime.js +569 -0
  100. package/dist/numbers.d.ts +1 -0
  101. package/dist/numbers.js +8 -0
  102. package/dist/ownership.d.ts +18 -0
  103. package/dist/ownership.js +77 -0
  104. package/dist/permissions.d.ts +29 -0
  105. package/dist/permissions.js +68 -0
  106. package/dist/process.d.ts +35 -0
  107. package/dist/process.js +125 -0
  108. package/dist/protocol-client.d.ts +46 -0
  109. package/dist/protocol-client.js +180 -0
  110. package/dist/protocol-frames.d.ts +68 -0
  111. package/dist/protocol-frames.js +139 -0
  112. package/dist/protocol-maps.d.ts +28 -0
  113. package/dist/protocol-maps.js +217 -0
  114. package/dist/protocol-schema.d.ts +10 -0
  115. package/dist/protocol-schema.js +11 -0
  116. package/dist/request-payloads.d.ts +137 -0
  117. package/dist/request-payloads.js +210 -0
  118. package/dist/response-payloads.d.ts +107 -0
  119. package/dist/response-payloads.js +161 -0
  120. package/dist/sidecar-client.d.ts +242 -0
  121. package/dist/sidecar-client.js +797 -0
  122. package/dist/state.d.ts +40 -0
  123. package/dist/state.js +44 -0
  124. package/dist/test-runtime.d.ts +526 -0
  125. package/dist/test-runtime.js +2119 -0
  126. package/dist/vm-config.d.ts +31 -0
  127. package/dist/vm-config.js +1 -0
  128. package/fixtures/alpine-defaults.json +520 -0
  129. package/fixtures/base-filesystem.json +528 -0
  130. package/package.json +193 -115
  131. package/LICENSE +0 -191
  132. package/dist/bridge-setup.d.ts +0 -6
  133. package/dist/bridge-setup.js +0 -9
  134. package/dist/esm-compiler.d.ts +0 -18
  135. package/dist/esm-compiler.js +0 -72
  136. package/dist/fs-helpers.d.ts +0 -23
  137. package/dist/fs-helpers.js +0 -41
  138. package/dist/generated/isolate-runtime.d.ts +0 -19
  139. package/dist/generated/isolate-runtime.js +0 -21
  140. package/dist/generated/polyfills.d.ts +0 -82
  141. package/dist/generated/polyfills.js +0 -82
  142. package/dist/isolate-runtime/apply-custom-global-policy.js +0 -53
  143. package/dist/isolate-runtime/apply-timing-mitigation-freeze.js +0 -130
  144. package/dist/isolate-runtime/apply-timing-mitigation-off.js +0 -14
  145. package/dist/isolate-runtime/bridge-attach.js +0 -29
  146. package/dist/isolate-runtime/bridge-initial-globals.js +0 -385
  147. package/dist/isolate-runtime/eval-script-result.js +0 -8
  148. package/dist/isolate-runtime/global-exposure-helpers.js +0 -36
  149. package/dist/isolate-runtime/init-commonjs-module-globals.js +0 -28
  150. package/dist/isolate-runtime/override-process-cwd.js +0 -8
  151. package/dist/isolate-runtime/override-process-env.js +0 -8
  152. package/dist/isolate-runtime/require-setup.js +0 -4153
  153. package/dist/isolate-runtime/set-commonjs-file-globals.js +0 -36
  154. package/dist/isolate-runtime/set-stdin-data.js +0 -10
  155. package/dist/isolate-runtime/setup-dynamic-import.js +0 -123
  156. package/dist/isolate-runtime/setup-fs-facade.js +0 -87
  157. package/dist/kernel/command-registry.d.ts +0 -44
  158. package/dist/kernel/command-registry.js +0 -114
  159. package/dist/kernel/device-backend.d.ts +0 -14
  160. package/dist/kernel/device-backend.js +0 -251
  161. package/dist/kernel/device-layer.d.ts +0 -12
  162. package/dist/kernel/device-layer.js +0 -271
  163. package/dist/kernel/dns-cache.d.ts +0 -29
  164. package/dist/kernel/dns-cache.js +0 -52
  165. package/dist/kernel/fd-table.d.ts +0 -84
  166. package/dist/kernel/fd-table.js +0 -278
  167. package/dist/kernel/file-lock.d.ts +0 -34
  168. package/dist/kernel/file-lock.js +0 -122
  169. package/dist/kernel/host-adapter.d.ts +0 -50
  170. package/dist/kernel/host-adapter.js +0 -8
  171. package/dist/kernel/index.d.ts +0 -36
  172. package/dist/kernel/index.js +0 -34
  173. package/dist/kernel/kernel.d.ts +0 -9
  174. package/dist/kernel/kernel.js +0 -1415
  175. package/dist/kernel/mount-table.d.ts +0 -75
  176. package/dist/kernel/mount-table.js +0 -353
  177. package/dist/kernel/permissions.d.ts +0 -36
  178. package/dist/kernel/permissions.js +0 -150
  179. package/dist/kernel/pipe-manager.d.ts +0 -64
  180. package/dist/kernel/pipe-manager.js +0 -267
  181. package/dist/kernel/proc-backend.d.ts +0 -30
  182. package/dist/kernel/proc-backend.js +0 -428
  183. package/dist/kernel/proc-layer.d.ts +0 -11
  184. package/dist/kernel/proc-layer.js +0 -507
  185. package/dist/kernel/process-table.d.ts +0 -126
  186. package/dist/kernel/process-table.js +0 -651
  187. package/dist/kernel/pty.d.ts +0 -109
  188. package/dist/kernel/pty.js +0 -552
  189. package/dist/kernel/socket-table.d.ts +0 -312
  190. package/dist/kernel/socket-table.js +0 -1188
  191. package/dist/kernel/timer-table.d.ts +0 -54
  192. package/dist/kernel/timer-table.js +0 -108
  193. package/dist/kernel/types.d.ts +0 -541
  194. package/dist/kernel/types.js +0 -98
  195. package/dist/kernel/user.d.ts +0 -29
  196. package/dist/kernel/user.js +0 -35
  197. package/dist/kernel/vfs.d.ts +0 -82
  198. package/dist/kernel/vfs.js +0 -25
  199. package/dist/kernel/wait.d.ts +0 -45
  200. package/dist/kernel/wait.js +0 -112
  201. package/dist/kernel/wstatus.d.ts +0 -21
  202. package/dist/kernel/wstatus.js +0 -33
  203. package/dist/module-resolver.d.ts +0 -29
  204. package/dist/module-resolver.js +0 -314
  205. package/dist/package-bundler.d.ts +0 -41
  206. package/dist/package-bundler.js +0 -497
  207. package/dist/runtime-driver.d.ts +0 -66
  208. package/dist/shared/api-types.d.ts +0 -83
  209. package/dist/shared/bridge-contract.d.ts +0 -772
  210. package/dist/shared/bridge-contract.js +0 -169
  211. package/dist/shared/console-formatter.d.ts +0 -22
  212. package/dist/shared/console-formatter.js +0 -161
  213. package/dist/shared/constants.d.ts +0 -3
  214. package/dist/shared/constants.js +0 -3
  215. package/dist/shared/errors.d.ts +0 -16
  216. package/dist/shared/errors.js +0 -21
  217. package/dist/shared/esm-utils.d.ts +0 -28
  218. package/dist/shared/esm-utils.js +0 -97
  219. package/dist/shared/global-exposure.d.ts +0 -38
  220. package/dist/shared/global-exposure.js +0 -876
  221. package/dist/shared/in-memory-fs.d.ts +0 -16
  222. package/dist/shared/in-memory-fs.js +0 -115
  223. package/dist/shared/permissions.d.ts +0 -36
  224. package/dist/shared/permissions.js +0 -314
  225. package/dist/shared/require-setup.d.ts +0 -6
  226. package/dist/shared/require-setup.js +0 -9
  227. package/dist/test/block-store-conformance.d.ts +0 -34
  228. package/dist/test/block-store-conformance.js +0 -251
  229. package/dist/test/metadata-store-conformance.d.ts +0 -37
  230. package/dist/test/metadata-store-conformance.js +0 -646
  231. package/dist/test/vfs-conformance.d.ts +0 -65
  232. package/dist/test/vfs-conformance.js +0 -842
  233. package/dist/types.d.ts +0 -98
  234. package/dist/types.js +0 -6
  235. package/dist/vfs/chunked-vfs.d.ts +0 -66
  236. package/dist/vfs/chunked-vfs.js +0 -1290
  237. package/dist/vfs/host-block-store.d.ts +0 -19
  238. package/dist/vfs/host-block-store.js +0 -97
  239. package/dist/vfs/memory-block-store.d.ts +0 -16
  240. package/dist/vfs/memory-block-store.js +0 -45
  241. package/dist/vfs/memory-metadata.d.ts +0 -75
  242. package/dist/vfs/memory-metadata.js +0 -528
  243. package/dist/vfs/sqlite-metadata.d.ts +0 -91
  244. package/dist/vfs/sqlite-metadata.js +0 -582
  245. package/dist/vfs/types.d.ts +0 -210
  246. package/dist/vfs/types.js +0 -8
  247. /package/dist/{runtime-driver.js → generated/CreateVmConfig.js} +0 -0
  248. /package/dist/{shared/api-types.js → generated/FsPermissionRule.js} +0 -0
@@ -1,1415 +0,0 @@
1
- /**
2
- * Kernel implementation.
3
- *
4
- * The kernel is the OS. It owns VFS, FD table, process table, device layer,
5
- * pipe manager, command registry, and permissions. Runtimes are execution
6
- * engines that make "syscalls" to the kernel.
7
- */
8
- import { createDeviceBackend } from "./device-backend.js";
9
- import { createProcBackend } from "./proc-backend.js";
10
- import { MountTable } from "./mount-table.js";
11
- import { FDTableManager } from "./fd-table.js";
12
- import { ProcessTable } from "./process-table.js";
13
- import { PipeManager } from "./pipe-manager.js";
14
- import { PtyManager } from "./pty.js";
15
- import { FileLockManager } from "./file-lock.js";
16
- import { CommandRegistry } from "./command-registry.js";
17
- import { wrapFileSystem, checkChildProcess } from "./permissions.js";
18
- import { UserManager } from "./user.js";
19
- import { SocketTable } from "./socket-table.js";
20
- import { TimerTable } from "./timer-table.js";
21
- import { FILETYPE_REGULAR_FILE, SEEK_SET, SEEK_CUR, SEEK_END, O_APPEND, O_CREAT, O_EXCL, O_TRUNC, SIGTERM, SIGPIPE, SIGWINCH, F_DUPFD, F_GETFD, F_SETFD, F_GETFL, F_DUPFD_CLOEXEC, FD_CLOEXEC, KernelError, noopKernelLogger, } from "./types.js";
22
- export function createKernel(options) {
23
- return new KernelImpl(options);
24
- }
25
- class KernelImpl {
26
- vfs;
27
- mountTable;
28
- fdTableManager = new FDTableManager();
29
- processTable;
30
- pipeManager = new PipeManager();
31
- ptyManager;
32
- fileLockManager = new FileLockManager();
33
- commandRegistry = new CommandRegistry();
34
- socketTable;
35
- timerTable;
36
- userManager;
37
- drivers = [];
38
- driverPids = new Map();
39
- permissions;
40
- maxProcesses;
41
- env;
42
- cwd;
43
- disposed = false;
44
- pendingBinEntries = [];
45
- posixDirsReady;
46
- log;
47
- constructor(options) {
48
- this.log = options.logger ?? noopKernelLogger;
49
- this.processTable = new ProcessTable(this.log.child({ component: "process" }));
50
- this.ptyManager = new PtyManager((pgid, signal, excludeLeaders) => {
51
- try {
52
- if (excludeLeaders) {
53
- return this.processTable.killGroupExcludeLeaders(pgid, signal);
54
- }
55
- this.processTable.kill(-pgid, signal);
56
- }
57
- catch { /* no-op if pgid gone */ }
58
- return 0;
59
- }, this.log.child({ component: "pty" }));
60
- // Build mount table: root FS → /dev → /proc → user mounts.
61
- const mt = new MountTable(options.filesystem);
62
- mt.mount("/dev", createDeviceBackend());
63
- mt.mount("/proc", createProcBackend({
64
- processTable: this.processTable,
65
- fdTableManager: this.fdTableManager,
66
- hostname: options.env?.HOSTNAME,
67
- mountTable: mt,
68
- }));
69
- // Mount user-supplied filesystems
70
- if (options.mounts) {
71
- for (const m of options.mounts) {
72
- mt.mount(m.path, m.fs, { readOnly: m.readOnly });
73
- }
74
- }
75
- this.mountTable = mt;
76
- // Apply permission wrapping on top of the mount table
77
- let fs = mt;
78
- if (options.permissions) {
79
- fs = wrapFileSystem(fs, options.permissions);
80
- }
81
- this.vfs = fs;
82
- this.permissions = options.permissions;
83
- this.maxProcesses = options.maxProcesses;
84
- this.env = { ...options.env };
85
- this.cwd = options.cwd ?? "/home/user";
86
- this.userManager = new UserManager();
87
- this.socketTable = new SocketTable({
88
- vfs: this.vfs,
89
- networkCheck: options.permissions?.network,
90
- hostAdapter: options.hostNetworkAdapter,
91
- getSignalState: (pid) => this.processTable.getSignalState(pid),
92
- processExists: (pid) => this.processTable.get(pid) !== undefined,
93
- });
94
- this.timerTable = new TimerTable();
95
- // Clean up FD table and sockets when a process exits
96
- this.processTable.onProcessExit = (pid) => {
97
- this.log.debug({ pid }, "process exit cleanup");
98
- this.cleanupProcessFDs(pid);
99
- this.socketTable.closeAllForProcess(pid);
100
- this.timerTable.clearAllForProcess(pid);
101
- };
102
- // Clean up driver PID ownership when zombie is reaped
103
- this.processTable.onProcessReap = (pid) => {
104
- const entry = this.processTable.get(pid);
105
- if (entry)
106
- this.driverPids.get(entry.driver)?.delete(pid);
107
- };
108
- // Deliver SIGPIPE default action: terminate writer with 128+SIGPIPE
109
- this.pipeManager.onBrokenPipe = (pid) => {
110
- try {
111
- this.processTable.kill(pid, SIGPIPE);
112
- }
113
- catch {
114
- // Process may already be exited
115
- }
116
- };
117
- // Create standard POSIX directory hierarchy so all programs see /tmp,
118
- // /usr, /etc, etc. — matching a real Linux root filesystem layout.
119
- this.posixDirsReady = this.initPosixDirs();
120
- }
121
- async initPosixDirs() {
122
- // /dev and /proc are auto-created by MountTable mounts — don't create them here.
123
- const dirs = [
124
- "/tmp",
125
- "/bin",
126
- "/lib",
127
- "/sbin",
128
- "/boot",
129
- "/etc",
130
- "/root",
131
- "/run",
132
- "/srv",
133
- "/sys",
134
- "/opt",
135
- "/mnt",
136
- "/media",
137
- "/home",
138
- "/usr",
139
- "/usr/bin",
140
- "/usr/games",
141
- "/usr/include",
142
- "/usr/lib",
143
- "/usr/libexec",
144
- "/usr/man",
145
- "/usr/sbin",
146
- "/usr/share",
147
- "/usr/share/man",
148
- "/var",
149
- "/var/cache",
150
- "/var/empty",
151
- "/var/lib",
152
- "/var/lock",
153
- "/var/log",
154
- "/var/run",
155
- "/var/spool",
156
- "/var/tmp",
157
- ];
158
- for (const dir of dirs) {
159
- try {
160
- await this.vfs.mkdir(dir, { recursive: true });
161
- }
162
- catch {
163
- // Directory may already exist
164
- }
165
- }
166
- // Standard utility that many scripts expect
167
- try {
168
- await this.vfs.writeFile("/usr/bin/env", new Uint8Array(1));
169
- }
170
- catch {
171
- // File may already exist
172
- }
173
- }
174
- // -----------------------------------------------------------------------
175
- // Kernel public API
176
- // -----------------------------------------------------------------------
177
- async mount(driver) {
178
- this.assertNotDisposed();
179
- await this.posixDirsReady;
180
- this.log.debug({ driver: driver.name, commands: driver.commands }, "mounting runtime driver");
181
- // Track PIDs owned by this driver
182
- if (!this.driverPids.has(driver.name)) {
183
- this.driverPids.set(driver.name, new Set());
184
- }
185
- // Initialize the driver with a scoped kernel interface
186
- await driver.init(this.createKernelInterface(driver.name));
187
- // Register commands
188
- this.commandRegistry.register(driver);
189
- this.drivers.push(driver);
190
- // Populate /bin stubs for shell PATH lookup
191
- await this.commandRegistry.populateBin(this.vfs);
192
- this.log.info({ driver: driver.name, commands: driver.commands }, "runtime driver mounted");
193
- }
194
- mountFs(path, fs, options) {
195
- this.assertNotDisposed();
196
- this.mountTable.mount(path, fs, options);
197
- }
198
- unmountFs(path) {
199
- this.assertNotDisposed();
200
- this.mountTable.unmount(path);
201
- }
202
- async dispose() {
203
- if (this.disposed)
204
- return;
205
- this.disposed = true;
206
- this.log.info({}, "kernel disposing");
207
- // Terminate all running processes
208
- await this.processTable.terminateAll();
209
- // Clean up all sockets
210
- this.socketTable.disposeAll();
211
- this.timerTable.disposeAll();
212
- // Dispose all drivers (reverse mount order)
213
- for (let i = this.drivers.length - 1; i >= 0; i--) {
214
- try {
215
- await this.drivers[i].dispose();
216
- }
217
- catch {
218
- // Best effort cleanup
219
- }
220
- }
221
- this.drivers.length = 0;
222
- }
223
- /**
224
- * Flush pending /bin stub entries created by on-demand command discovery.
225
- * Ensures VFS is consistent before shell PATH lookups.
226
- */
227
- async flushPendingBinEntries() {
228
- if (this.pendingBinEntries.length > 0) {
229
- await Promise.all(this.pendingBinEntries);
230
- this.pendingBinEntries.length = 0;
231
- }
232
- }
233
- async exec(command, options) {
234
- this.assertNotDisposed();
235
- this.log.debug({ command, timeout: options?.timeout, cwd: options?.cwd }, "exec start");
236
- // Flush pending /bin stubs before shell PATH lookup
237
- await this.flushPendingBinEntries();
238
- // Route through shell
239
- const shell = this.commandRegistry.resolve("sh");
240
- if (!shell) {
241
- throw new Error("No shell available. Mount a WasmVM runtime to enable exec().");
242
- }
243
- const proc = this.spawnInternal("sh", ["-c", command], options);
244
- // Write stdin if provided
245
- if (options?.stdin) {
246
- const data = typeof options.stdin === "string"
247
- ? new TextEncoder().encode(options.stdin)
248
- : options.stdin;
249
- proc.writeStdin(data);
250
- proc.closeStdin();
251
- }
252
- // Collect output
253
- const stdoutChunks = [];
254
- const stderrChunks = [];
255
- proc.onStdout = (data) => {
256
- stdoutChunks.push(data);
257
- options?.onStdout?.(data);
258
- };
259
- proc.onStderr = (data) => {
260
- stderrChunks.push(data);
261
- options?.onStderr?.(data);
262
- };
263
- // Wait with optional timeout
264
- let exitCode;
265
- if (options?.timeout) {
266
- let timer;
267
- try {
268
- exitCode = await Promise.race([
269
- proc.wait().then((code) => {
270
- clearTimeout(timer);
271
- return code;
272
- }),
273
- new Promise((_, reject) => {
274
- timer = setTimeout(() => {
275
- // Kill process and detach output callbacks
276
- this.log.warn({ command, timeout: options.timeout }, "exec timeout, sending SIGTERM");
277
- proc.onStdout = null;
278
- proc.onStderr = null;
279
- proc.kill(SIGTERM);
280
- reject(new KernelError("ETIMEDOUT", "exec timeout"));
281
- }, options.timeout);
282
- }),
283
- ]);
284
- }
285
- catch (err) {
286
- clearTimeout(timer);
287
- throw err;
288
- }
289
- }
290
- else {
291
- exitCode = await proc.wait();
292
- }
293
- return {
294
- exitCode,
295
- stdout: concatUint8(stdoutChunks),
296
- stderr: concatUint8(stderrChunks),
297
- };
298
- }
299
- spawn(command, args, options) {
300
- this.assertNotDisposed();
301
- return this.spawnManaged(command, args, options);
302
- }
303
- openShell(options) {
304
- this.assertNotDisposed();
305
- const command = options?.command ?? "sh";
306
- const args = options?.args ?? [];
307
- this.log.debug({ command, args, cols: options?.cols, rows: options?.rows, cwd: options?.cwd }, "openShell start");
308
- // Allocate a controller PID with an FD table to hold the PTY master
309
- const controllerPid = this.processTable.allocatePid();
310
- const controllerTable = this.fdTableManager.create(controllerPid);
311
- // Create PTY pair in the controller's FD table
312
- const { masterFd, slaveFd } = this.ptyManager.createPtyFDs(controllerTable);
313
- const masterDescId = controllerTable.get(masterFd).description.id;
314
- // Spawn shell with PTY slave as stdin/stdout/stderr
315
- // Propagate terminal dimensions as POSIX COLUMNS/LINES env vars
316
- const cols = options?.cols;
317
- const rows = options?.rows;
318
- const dimEnv = {};
319
- if (cols !== undefined)
320
- dimEnv.COLUMNS = String(cols);
321
- if (rows !== undefined)
322
- dimEnv.LINES = String(rows);
323
- const proc = this.spawnInternal(command, args, {
324
- env: { ...options?.env, ...dimEnv },
325
- cwd: options?.cwd,
326
- stdinFd: slaveFd,
327
- stdoutFd: slaveFd,
328
- stderrFd: slaveFd,
329
- }, controllerPid);
330
- // Shell becomes its own process group leader, set as PTY foreground
331
- this.processTable.setpgid(proc.pid, proc.pid);
332
- this.ptyManager.setForegroundPgid(masterDescId, proc.pid);
333
- this.ptyManager.setSessionLeader(masterDescId, proc.pid);
334
- this.log.debug({ shellPid: proc.pid, controllerPid, masterFd, masterDescId }, "openShell PTY attached");
335
- // Close controller's copy of slave FD (child inherited its own copy via fork).
336
- // Without this, slave refCount stays >0 after shell exits, preventing EOF on master.
337
- const slaveEntry = controllerTable.get(slaveFd);
338
- const slaveDescId = slaveEntry.description.id;
339
- controllerTable.close(slaveFd);
340
- if (slaveEntry.description.refCount <= 0) {
341
- this.ptyManager.close(slaveDescId);
342
- }
343
- // Start read pump: master reads → onData callback
344
- // Use object wrapper so TypeScript doesn't narrow to null in the async closure
345
- const pump = { onData: null, exited: false };
346
- const pumpPromise = (async () => {
347
- try {
348
- while (!pump.exited) {
349
- const data = await this.ptyManager.read(masterDescId, 4096);
350
- if (!data || data.length === 0)
351
- break;
352
- try {
353
- pump.onData?.(data);
354
- }
355
- catch (cbErr) {
356
- // Propagate callback errors — don't silently swallow
357
- console.error("openShell readPump: onData callback error:", cbErr);
358
- }
359
- }
360
- }
361
- catch (err) {
362
- // Master closed or PTY gone — expected when shell exits
363
- if (pump.exited)
364
- return;
365
- console.error("openShell readPump: PTY read error:", err);
366
- }
367
- })();
368
- // wait() resolves after both shell exit AND pump drain
369
- const waitPromise = proc.wait().then(async (exitCode) => {
370
- pump.exited = true;
371
- // Wait for pump to finish delivering remaining data
372
- await pumpPromise;
373
- // Clean up controller PID's FD table (incl. PTY master)
374
- this.cleanupProcessFDs(controllerPid);
375
- return exitCode;
376
- });
377
- return {
378
- pid: proc.pid,
379
- write: (data) => {
380
- const bytes = typeof data === "string"
381
- ? new TextEncoder().encode(data)
382
- : data;
383
- this.ptyManager.write(masterDescId, bytes);
384
- },
385
- get onData() { return pump.onData; },
386
- set onData(fn) { pump.onData = fn; },
387
- resize: (_cols, _rows) => {
388
- const fgPgid = this.ptyManager.getForegroundPgid(masterDescId);
389
- this.log.trace({ shellPid: proc.pid, cols: _cols, rows: _rows, fgPgid }, "PTY resize");
390
- if (fgPgid > 0) {
391
- try {
392
- this.processTable.kill(-fgPgid, SIGWINCH);
393
- }
394
- catch { /* pgid may be gone */ }
395
- }
396
- },
397
- kill: (signal) => {
398
- proc.kill(signal ?? SIGTERM);
399
- },
400
- wait: () => waitPromise,
401
- };
402
- }
403
- async connectTerminal(options) {
404
- this.assertNotDisposed();
405
- this.log.debug({ command: options?.command, cols: options?.cols, rows: options?.rows }, "connectTerminal start");
406
- const stdin = process.stdin;
407
- const stdout = process.stdout;
408
- const isTTY = stdin.isTTY;
409
- let onStdinData;
410
- let onResize;
411
- try {
412
- const shell = this.openShell(options);
413
- // Set raw mode so keypresses pass through directly
414
- if (isTTY)
415
- stdin.setRawMode(true);
416
- // Forward stdin to shell
417
- onStdinData = (data) => shell.write(data);
418
- stdin.on("data", onStdinData);
419
- stdin.resume();
420
- // Forward shell output to stdout or custom handler
421
- const outputHandler = options?.onData
422
- ?? ((data) => { stdout.write(data); });
423
- shell.onData = outputHandler;
424
- // Forward terminal resize → PTY SIGWINCH
425
- if (stdout.isTTY) {
426
- onResize = () => {
427
- shell.resize(stdout.columns, stdout.rows);
428
- };
429
- stdout.on("resize", onResize);
430
- }
431
- return await shell.wait();
432
- }
433
- finally {
434
- // Restore terminal — guard each cleanup since setup may have partially completed
435
- if (onStdinData)
436
- stdin.removeListener("data", onStdinData);
437
- stdin.pause();
438
- if (isTTY)
439
- stdin.setRawMode(false);
440
- if (onResize && stdout.isTTY)
441
- stdout.removeListener("resize", onResize);
442
- }
443
- }
444
- // Filesystem convenience wrappers
445
- readFile(path) { return this.vfs.readFile(path); }
446
- writeFile(path, content) { return this.vfs.writeFile(path, content); }
447
- mkdir(path) { return this.vfs.mkdir(path); }
448
- readdir(path) { return this.vfs.readDir(path); }
449
- stat(path) { return this.vfs.stat(path); }
450
- exists(path) { return this.vfs.exists(path); }
451
- removeFile(path) { return this.vfs.removeFile(path); }
452
- removeDir(path) { return this.vfs.removeDir(path); }
453
- rename(oldPath, newPath) { return this.vfs.rename(oldPath, newPath); }
454
- // Introspection
455
- get commands() {
456
- return this.commandRegistry.list();
457
- }
458
- get processes() {
459
- return this.processTable.listProcesses();
460
- }
461
- get zombieTimerCount() {
462
- return this.processTable.zombieTimerCount;
463
- }
464
- // -----------------------------------------------------------------------
465
- // Internal spawn
466
- // -----------------------------------------------------------------------
467
- spawnInternal(command, args, options, callerPid) {
468
- this.log.debug({ command, args, callerPid, cwd: options?.cwd }, "spawn start");
469
- let driver = this.commandRegistry.resolve(command);
470
- // On-demand discovery: ask mounted drivers to resolve unknown commands
471
- if (!driver) {
472
- const basename = command.includes("/")
473
- ? command.split("/").pop()
474
- : command;
475
- if (basename) {
476
- for (const d of this.drivers) {
477
- if (d.tryResolve?.(basename)) {
478
- this.commandRegistry.registerCommand(basename, d);
479
- // Store pending promise so exec() can flush before shell PATH lookup
480
- const p = this.commandRegistry.populateBinEntry(this.vfs, basename);
481
- this.pendingBinEntries.push(p);
482
- p.then(() => {
483
- const idx = this.pendingBinEntries.indexOf(p);
484
- if (idx >= 0)
485
- this.pendingBinEntries.splice(idx, 1);
486
- });
487
- driver = d;
488
- break;
489
- }
490
- }
491
- }
492
- }
493
- if (!driver) {
494
- this.log.warn({ command }, "command not found");
495
- throw new KernelError("ENOENT", `command not found: ${command}`);
496
- }
497
- // Check childProcess permission
498
- try {
499
- checkChildProcess(this.permissions, command, args, options?.cwd);
500
- }
501
- catch (err) {
502
- this.log.warn({ command, args }, "spawn permission denied");
503
- throw err;
504
- }
505
- // Enforce maxProcesses budget
506
- if (this.maxProcesses !== undefined && this.processTable.runningCount() >= this.maxProcesses) {
507
- this.log.warn({ command, running: this.processTable.runningCount(), max: this.maxProcesses }, "process limit reached");
508
- throw new KernelError("EAGAIN", "maximum process limit reached");
509
- }
510
- // Allocate PID atomically
511
- const pid = this.processTable.allocatePid();
512
- // Register PID ownership before driver.spawn() so the driver can use it
513
- this.driverPids.get(driver.name)?.add(pid);
514
- // Cross-runtime spawn: parent driver must also track child PID so
515
- // it can waitpid/kill/interact with the child process
516
- if (callerPid !== undefined) {
517
- for (const [name, pids] of this.driverPids) {
518
- if (name !== driver.name && pids.has(callerPid)) {
519
- pids.add(pid);
520
- break;
521
- }
522
- }
523
- }
524
- // Create FD table — wire pipe FDs when overrides are provided
525
- const table = this.createChildFDTable(pid, options, callerPid);
526
- // Check which stdio channels are piped (data flows through kernel, not callbacks)
527
- const stdoutPiped = this.isStdioPiped(table, 1);
528
- const stderrPiped = this.isStdioPiped(table, 2);
529
- // Buffer stdout/stderr — wired before spawn so nothing is lost
530
- const stdoutBuf = [];
531
- const stderrBuf = [];
532
- // Resolve output callbacks. Drivers invoke BOTH ctx.onStdout and
533
- // proc.onStdout per message, so the two must never point at the same
534
- // callback — otherwise the host sees every chunk twice.
535
- //
536
- // ctx callbacks — kernel-internal routing (pipes, parent forwarding)
537
- // + temporary buffer during spawn() to catch any
538
- // synchronous output (disabled right after spawn).
539
- // proc callbacks — user / host callback (options.onStdout) or buffer
540
- // for later replay. Set AFTER spawn returns.
541
- let ctxStdoutCb;
542
- let ctxStderrCb;
543
- if (stdoutPiped) {
544
- ctxStdoutCb = this.createPipedOutputCallback(table, 1, pid);
545
- }
546
- else if (!options?.onStdout && callerPid !== undefined) {
547
- const parent = this.processTable.get(callerPid);
548
- if (parent?.driverProcess.onStdout) {
549
- ctxStdoutCb = parent.driverProcess.onStdout;
550
- }
551
- }
552
- if (stderrPiped) {
553
- ctxStderrCb = this.createPipedOutputCallback(table, 2, pid);
554
- }
555
- else if (!options?.onStderr && callerPid !== undefined) {
556
- const parent = this.processTable.get(callerPid);
557
- if (parent?.driverProcess.onStderr) {
558
- ctxStderrCb = parent.driverProcess.onStderr;
559
- }
560
- }
561
- // Inherit env from parent process if spawned by another process, else use kernel defaults
562
- const parentEntry = callerPid ? this.processTable.get(callerPid) : undefined;
563
- const baseEnv = parentEntry?.env ?? this.env;
564
- // Detect PTY slave on stdio FDs
565
- const stdinIsTTY = this.isFdPtySlave(table, 0);
566
- const stdoutIsTTY = this.isFdPtySlave(table, 1);
567
- const stderrIsTTY = this.isFdPtySlave(table, 2);
568
- // Build process context with pre-wired callbacks.
569
- // When not piped/forwarded, ctx gets a temporary buffer so that any
570
- // data emitted synchronously during driver.spawn() is captured.
571
- const resolvedCwd = options?.cwd ?? this.cwd;
572
- const ctx = {
573
- pid,
574
- ppid: callerPid ?? 0,
575
- env: { ...baseEnv, ...options?.env, PWD: resolvedCwd },
576
- cwd: resolvedCwd,
577
- fds: { stdin: 0, stdout: 1, stderr: 2 },
578
- stdinIsTTY,
579
- stdoutIsTTY,
580
- stderrIsTTY,
581
- streamStdin: options?.streamStdin,
582
- onStdout: ctxStdoutCb ?? (stdoutPiped ? undefined : (data) => stdoutBuf.push(data)),
583
- onStderr: ctxStderrCb ?? (stderrPiped ? undefined : (data) => stderrBuf.push(data)),
584
- };
585
- // Spawn via driver
586
- const driverProcess = driver.spawn(command, args, ctx);
587
- this.log.debug({
588
- pid, command, driver: driver.name, callerPid,
589
- stdinIsTTY, stdoutIsTTY, stderrIsTTY,
590
- }, "process spawned");
591
- // After spawn, disable the temporary ctx buffer so that async output
592
- // flows only through proc.onStdout — prevents double-delivery.
593
- // Pipe/parent-forwarding callbacks stay active (they live in ctxStdoutCb).
594
- if (!stdoutPiped) {
595
- ctx.onStdout = ctxStdoutCb;
596
- }
597
- if (!stderrPiped) {
598
- ctx.onStderr = ctxStderrCb;
599
- }
600
- // User/host callback goes ONLY on driverProcess (never on ctx) to
601
- // avoid double-delivery — drivers invoke both ctx and proc callbacks.
602
- if (!stdoutPiped) {
603
- driverProcess.onStdout = options?.onStdout ?? ((data) => stdoutBuf.push(data));
604
- }
605
- if (!stderrPiped) {
606
- driverProcess.onStderr = options?.onStderr ?? ((data) => stderrBuf.push(data));
607
- }
608
- // Register in process table
609
- const entry = this.processTable.register(pid, driver.name, command, args, ctx, driverProcess);
610
- return {
611
- pid: entry.pid,
612
- driverProcess,
613
- wait: () => driverProcess.wait(),
614
- writeStdin: (data) => driverProcess.writeStdin(data),
615
- closeStdin: () => driverProcess.closeStdin(),
616
- kill: (signal) => driverProcess.kill(signal ?? 15),
617
- get onStdout() { return driverProcess.onStdout; },
618
- set onStdout(fn) {
619
- driverProcess.onStdout = fn;
620
- // Replay buffered data
621
- if (fn)
622
- for (const chunk of stdoutBuf)
623
- fn(chunk);
624
- stdoutBuf.length = 0;
625
- },
626
- get onStderr() { return driverProcess.onStderr; },
627
- set onStderr(fn) {
628
- driverProcess.onStderr = fn;
629
- if (fn)
630
- for (const chunk of stderrBuf)
631
- fn(chunk);
632
- stderrBuf.length = 0;
633
- },
634
- };
635
- }
636
- spawnManaged(command, args, options, callerPid) {
637
- const internal = this.spawnInternal(command, args, options, callerPid);
638
- let exitCode = null;
639
- // Note: options.onStdout/onStderr are already wired through ctx.onStdout
640
- // by spawnInternal. Do NOT also set them on driverProcess.onStdout here —
641
- // the driver calls both ctx.onStdout and proc.onStdout per message, so
642
- // setting both to the same callback would double-deliver output.
643
- internal.driverProcess.wait().then((code) => {
644
- exitCode = code;
645
- });
646
- return {
647
- pid: internal.pid,
648
- writeStdin: (data) => {
649
- const bytes = typeof data === "string"
650
- ? new TextEncoder().encode(data)
651
- : data;
652
- internal.writeStdin(bytes);
653
- },
654
- closeStdin: () => internal.closeStdin(),
655
- kill: (signal) => this.processTable.kill(internal.pid, signal ?? 15),
656
- wait: () => internal.driverProcess.wait(),
657
- get exitCode() { return exitCode; },
658
- };
659
- }
660
- // -----------------------------------------------------------------------
661
- // Kernel interface (exposed to drivers)
662
- // -----------------------------------------------------------------------
663
- createKernelInterface(driverName) {
664
- // Validate that the calling driver owns the target PID
665
- const assertOwns = (pid) => {
666
- if (this.driverPids.get(driverName)?.has(pid))
667
- return;
668
- // Check if any driver owns this PID — if not, the PID doesn't exist
669
- for (const pids of this.driverPids.values()) {
670
- if (pids.has(pid)) {
671
- throw new KernelError("EPERM", `driver "${driverName}" does not own PID ${pid}`);
672
- }
673
- }
674
- throw new KernelError("ESRCH", `no such process ${pid}`);
675
- };
676
- const kernelInterface = {
677
- vfs: this.vfs,
678
- // FD operations
679
- fdOpen: (pid, path, flags, mode) => {
680
- assertOwns(pid);
681
- // /dev/fd/N → dup(N): equivalent to open() on the underlying FD
682
- if (path.startsWith("/dev/fd/")) {
683
- const raw = path.slice(8);
684
- const n = parseInt(raw, 10);
685
- if (isNaN(n) || n < 0 || String(n) !== raw)
686
- throw new KernelError("EBADF", `bad file descriptor: ${path}`);
687
- const table = this.getTable(pid);
688
- const entry = table.get(n);
689
- if (!entry)
690
- throw new KernelError("EBADF", `bad file descriptor ${n}`);
691
- return table.dup(n);
692
- }
693
- const created = (flags & (O_CREAT | O_EXCL | O_TRUNC)) !== 0
694
- ? this.prepareOpenSync(path, flags)
695
- : false;
696
- const table = this.getTable(pid);
697
- const filetype = FILETYPE_REGULAR_FILE;
698
- const fd = table.open(path, flags, filetype);
699
- const fdEntry = table.get(fd);
700
- // Stash the effective mode for the first write that materializes a new file.
701
- if (created && (flags & O_CREAT)) {
702
- const entry = this.processTable.get(pid);
703
- const umask = entry?.umask ?? 0o022;
704
- const requestedMode = mode ?? 0o666;
705
- if (fdEntry) {
706
- fdEntry.description.creationMode = requestedMode & ~umask;
707
- }
708
- }
709
- return fd;
710
- },
711
- fdRead: async (pid, fd, length) => {
712
- assertOwns(pid);
713
- const table = this.getTable(pid);
714
- const entry = table.get(fd);
715
- if (!entry)
716
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
717
- // Pipe reads route through PipeManager
718
- if (this.pipeManager.isPipe(entry.description.id)) {
719
- const data = await this.pipeManager.read(entry.description.id, length);
720
- return data ?? new Uint8Array(0);
721
- }
722
- // PTY reads route through PtyManager
723
- if (this.ptyManager.isPty(entry.description.id)) {
724
- const data = await this.ptyManager.read(entry.description.id, length);
725
- return data ?? new Uint8Array(0);
726
- }
727
- // Positional read from VFS — avoids loading entire file
728
- const cursor = Number(entry.description.cursor);
729
- const slice = await this.preadDescription(entry.description, cursor, length);
730
- entry.description.cursor += BigInt(slice.length);
731
- return slice;
732
- },
733
- fdWrite: (pid, fd, data) => {
734
- assertOwns(pid);
735
- const table = this.getTable(pid);
736
- const entry = table.get(fd);
737
- if (!entry)
738
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
739
- if (this.pipeManager.isPipe(entry.description.id)) {
740
- return this.pipeManager.write(entry.description.id, data, pid);
741
- }
742
- if (this.ptyManager.isPty(entry.description.id)) {
743
- return this.ptyManager.write(entry.description.id, data);
744
- }
745
- // Write to VFS at cursor position (async — returns Promise)
746
- return this.vfsWrite(entry, data);
747
- },
748
- fdClose: (pid, fd) => {
749
- assertOwns(pid);
750
- const table = this.getTable(pid);
751
- const entry = table.get(fd);
752
- if (!entry)
753
- return;
754
- const descId = entry.description.id;
755
- const isPipe = this.pipeManager.isPipe(descId);
756
- const isPty = this.ptyManager.isPty(descId);
757
- // Close FD first (decrements refCount on shared FileDescription)
758
- table.close(fd);
759
- // Only signal pipe/pty/lock closure when last reference is dropped
760
- if (entry.description.refCount <= 0) {
761
- this.releaseDescriptionInode(entry.description);
762
- if (isPipe)
763
- this.pipeManager.close(descId);
764
- if (isPty)
765
- this.ptyManager.close(descId);
766
- this.fileLockManager.releaseByDescription(descId);
767
- }
768
- },
769
- fdSeek: async (pid, fd, offset, whence) => {
770
- assertOwns(pid);
771
- const table = this.getTable(pid);
772
- const entry = table.get(fd);
773
- if (!entry)
774
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
775
- // Pipes and PTYs are not seekable
776
- if (this.pipeManager.isPipe(entry.description.id) || this.ptyManager.isPty(entry.description.id)) {
777
- throw new KernelError("ESPIPE", "illegal seek");
778
- }
779
- let newCursor;
780
- switch (whence) {
781
- case SEEK_SET:
782
- newCursor = offset;
783
- break;
784
- case SEEK_CUR:
785
- newCursor = entry.description.cursor + offset;
786
- break;
787
- case SEEK_END: {
788
- newCursor = BigInt(await this.getDescriptionSize(entry.description)) + offset;
789
- break;
790
- }
791
- default:
792
- throw new KernelError("EINVAL", `invalid whence ${whence}`);
793
- }
794
- if (newCursor < 0n)
795
- throw new KernelError("EINVAL", "negative seek position");
796
- entry.description.cursor = newCursor;
797
- return newCursor;
798
- },
799
- fdPread: async (pid, fd, length, offset) => {
800
- assertOwns(pid);
801
- const table = this.getTable(pid);
802
- const entry = table.get(fd);
803
- if (!entry)
804
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
805
- // Pipes and PTYs are not seekable
806
- if (this.pipeManager.isPipe(entry.description.id) || this.ptyManager.isPty(entry.description.id)) {
807
- throw new KernelError("ESPIPE", "illegal seek");
808
- }
809
- // Read from VFS at given offset without moving cursor
810
- return this.preadDescription(entry.description, Number(offset), length);
811
- },
812
- fdPwrite: async (pid, fd, data, offset) => {
813
- assertOwns(pid);
814
- const table = this.getTable(pid);
815
- const entry = table.get(fd);
816
- if (!entry)
817
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
818
- // Pipes and PTYs are not seekable
819
- if (this.pipeManager.isPipe(entry.description.id) || this.ptyManager.isPty(entry.description.id)) {
820
- throw new KernelError("ESPIPE", "illegal seek");
821
- }
822
- // Delegate positional write to VFS.
823
- await this.pwriteDescription(entry.description, Number(offset), data);
824
- return data.length;
825
- },
826
- fdDup: (pid, fd) => {
827
- assertOwns(pid);
828
- return this.getTable(pid).dup(fd);
829
- },
830
- fdDup2: (pid, oldFd, newFd) => {
831
- assertOwns(pid);
832
- const table = this.getTable(pid);
833
- const targetEntry = table.get(newFd);
834
- const targetDesc = targetEntry?.description;
835
- const targetDescId = targetDesc?.id;
836
- table.dup2(oldFd, newFd);
837
- if (targetDesc && targetDesc.refCount <= 0) {
838
- this.releaseDescriptionInode(targetDesc);
839
- if (targetDescId !== undefined) {
840
- if (this.pipeManager.isPipe(targetDescId))
841
- this.pipeManager.close(targetDescId);
842
- if (this.ptyManager.isPty(targetDescId))
843
- this.ptyManager.close(targetDescId);
844
- this.fileLockManager.releaseByDescription(targetDescId);
845
- }
846
- }
847
- },
848
- fdDupMin: (pid, fd, minFd) => {
849
- assertOwns(pid);
850
- return this.getTable(pid).dupMinFd(fd, minFd);
851
- },
852
- fdStat: (pid, fd) => {
853
- assertOwns(pid);
854
- return this.getTable(pid).stat(fd);
855
- },
856
- fdPoll: (pid, fd) => {
857
- try {
858
- const table = this.getTable(pid);
859
- const entry = table.get(fd);
860
- if (!entry)
861
- return { readable: false, writable: false, hangup: false, invalid: true };
862
- const descId = entry.description.id;
863
- if (this.pipeManager.isPipe(descId)) {
864
- const ps = this.pipeManager.pollState(descId);
865
- return ps ? { ...ps, invalid: false } : { readable: false, writable: false, hangup: false, invalid: true };
866
- }
867
- // Regular files are always readable/writable
868
- return { readable: true, writable: true, hangup: false, invalid: false };
869
- }
870
- catch {
871
- return { readable: false, writable: false, hangup: false, invalid: true };
872
- }
873
- },
874
- fdPollWait: async (pid, fd, timeoutMs) => {
875
- assertOwns(pid);
876
- const table = this.getTable(pid);
877
- const entry = table.get(fd);
878
- if (!entry)
879
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
880
- const descId = entry.description.id;
881
- if (this.pipeManager.isPipe(descId)) {
882
- await this.pipeManager.waitForPoll(descId, timeoutMs);
883
- }
884
- },
885
- fdSetCloexec: (pid, fd, value) => {
886
- assertOwns(pid);
887
- const table = this.getTable(pid);
888
- const entry = table.get(fd);
889
- if (!entry)
890
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
891
- entry.cloexec = value;
892
- },
893
- fdGetCloexec: (pid, fd) => {
894
- assertOwns(pid);
895
- const table = this.getTable(pid);
896
- const entry = table.get(fd);
897
- if (!entry)
898
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
899
- return entry.cloexec;
900
- },
901
- fcntl: (pid, fd, cmd, arg) => {
902
- assertOwns(pid);
903
- const table = this.getTable(pid);
904
- const entry = table.get(fd);
905
- if (!entry)
906
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
907
- switch (cmd) {
908
- case F_DUPFD:
909
- return table.dupMinFd(fd, arg ?? 0);
910
- case F_DUPFD_CLOEXEC: {
911
- const newFd = table.dupMinFd(fd, arg ?? 0);
912
- table.get(newFd).cloexec = true;
913
- return newFd;
914
- }
915
- case F_GETFD:
916
- return entry.cloexec ? FD_CLOEXEC : 0;
917
- case F_SETFD:
918
- entry.cloexec = ((arg ?? 0) & FD_CLOEXEC) !== 0;
919
- return 0;
920
- case F_GETFL:
921
- return entry.description.flags;
922
- default:
923
- throw new KernelError("EINVAL", `unsupported fcntl command ${cmd}`);
924
- }
925
- },
926
- // Advisory file locking
927
- flock: async (pid, fd, operation) => {
928
- assertOwns(pid);
929
- const table = this.getTable(pid);
930
- const entry = table.get(fd);
931
- if (!entry)
932
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
933
- await this.fileLockManager.flock(entry.description.path, entry.description.id, operation);
934
- },
935
- // Process operations
936
- spawn: (command, args, ctx) => {
937
- if (ctx.ppid)
938
- assertOwns(ctx.ppid);
939
- return this.spawnManaged(command, args, {
940
- env: ctx.env,
941
- cwd: ctx.cwd,
942
- streamStdin: ctx.streamStdin,
943
- onStdout: ctx.onStdout,
944
- onStderr: ctx.onStderr,
945
- stdinFd: ctx.stdinFd,
946
- stdoutFd: ctx.stdoutFd,
947
- stderrFd: ctx.stderrFd,
948
- }, ctx.ppid);
949
- },
950
- waitpid: (pid, options) => {
951
- try {
952
- assertOwns(pid);
953
- }
954
- catch (e) {
955
- return Promise.reject(e);
956
- }
957
- return this.processTable.waitpid(pid, options);
958
- },
959
- kill: (pid, signal) => {
960
- // Negative PID = process group kill, handled by kernel directly
961
- if (pid >= 0)
962
- assertOwns(pid);
963
- this.log.debug({ pid, signal }, "signal delivery");
964
- this.processTable.kill(pid, signal);
965
- },
966
- getpid: (pid) => {
967
- assertOwns(pid);
968
- return pid;
969
- },
970
- getppid: (pid) => {
971
- assertOwns(pid);
972
- return this.processTable.getppid(pid);
973
- },
974
- // Process group / session
975
- setpgid: (pid, pgid) => {
976
- assertOwns(pid);
977
- this.processTable.setpgid(pid, pgid);
978
- },
979
- getpgid: (pid) => {
980
- assertOwns(pid);
981
- return this.processTable.getpgid(pid);
982
- },
983
- setsid: (pid) => {
984
- assertOwns(pid);
985
- return this.processTable.setsid(pid);
986
- },
987
- getsid: (pid) => {
988
- assertOwns(pid);
989
- return this.processTable.getsid(pid);
990
- },
991
- // Pipe operations
992
- pipe: (pid) => {
993
- assertOwns(pid);
994
- const table = this.getTable(pid);
995
- return this.pipeManager.createPipeFDs(table);
996
- },
997
- // PTY operations
998
- openpty: (pid) => {
999
- assertOwns(pid);
1000
- const table = this.getTable(pid);
1001
- return this.ptyManager.createPtyFDs(table);
1002
- },
1003
- isatty: (pid, fd) => {
1004
- assertOwns(pid);
1005
- const table = this.getTable(pid);
1006
- const entry = table.get(fd);
1007
- if (!entry)
1008
- return false;
1009
- return this.ptyManager.isSlave(entry.description.id);
1010
- },
1011
- ptySetDiscipline: (pid, fd, config) => {
1012
- assertOwns(pid);
1013
- const table = this.getTable(pid);
1014
- const entry = table.get(fd);
1015
- if (!entry)
1016
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
1017
- this.ptyManager.setDiscipline(entry.description.id, config);
1018
- },
1019
- ptySetForegroundPgid: (pid, fd, pgid) => {
1020
- assertOwns(pid);
1021
- const table = this.getTable(pid);
1022
- const entry = table.get(fd);
1023
- if (!entry)
1024
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
1025
- this.ptyManager.setForegroundPgid(entry.description.id, pgid);
1026
- },
1027
- // Termios operations
1028
- tcgetattr: (pid, fd) => {
1029
- assertOwns(pid);
1030
- const table = this.getTable(pid);
1031
- const entry = table.get(fd);
1032
- if (!entry)
1033
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
1034
- return this.ptyManager.getTermios(entry.description.id);
1035
- },
1036
- tcsetattr: (pid, fd, termios) => {
1037
- assertOwns(pid);
1038
- const table = this.getTable(pid);
1039
- const entry = table.get(fd);
1040
- if (!entry)
1041
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
1042
- this.ptyManager.setTermios(entry.description.id, termios);
1043
- },
1044
- tcsetpgrp: (pid, fd, pgid) => {
1045
- assertOwns(pid);
1046
- const table = this.getTable(pid);
1047
- const entry = table.get(fd);
1048
- if (!entry)
1049
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
1050
- // Validate target PGID refers to an existing process group
1051
- if (!this.processTable.hasProcessGroup(pgid)) {
1052
- throw new KernelError("ESRCH", `no such process group ${pgid}`);
1053
- }
1054
- this.ptyManager.setForegroundPgid(entry.description.id, pgid);
1055
- },
1056
- tcgetpgrp: (pid, fd) => {
1057
- assertOwns(pid);
1058
- const table = this.getTable(pid);
1059
- const entry = table.get(fd);
1060
- if (!entry)
1061
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
1062
- return this.ptyManager.getForegroundPgid(entry.description.id);
1063
- },
1064
- // /dev/fd operations
1065
- devFdReadDir: (pid) => {
1066
- assertOwns(pid);
1067
- const table = this.fdTableManager.get(pid);
1068
- if (!table)
1069
- return [];
1070
- const fds = [];
1071
- for (const entry of table)
1072
- fds.push(entry.fd);
1073
- return fds.sort((a, b) => a - b).map(String);
1074
- },
1075
- devFdStat: async (pid, fd) => {
1076
- assertOwns(pid);
1077
- const table = this.getTable(pid);
1078
- const entry = table.get(fd);
1079
- if (!entry)
1080
- throw new KernelError("EBADF", `bad file descriptor ${fd}`);
1081
- // Pipe/PTY FDs return a synthetic character device stat
1082
- if (this.pipeManager.isPipe(entry.description.id) || this.ptyManager.isPty(entry.description.id)) {
1083
- const now = Date.now();
1084
- return {
1085
- mode: 0o666,
1086
- size: 0,
1087
- isDirectory: false,
1088
- isSymbolicLink: false,
1089
- atimeMs: now,
1090
- mtimeMs: now,
1091
- ctimeMs: now,
1092
- birthtimeMs: now,
1093
- ino: entry.description.id,
1094
- nlink: 1,
1095
- uid: 0,
1096
- gid: 0,
1097
- };
1098
- }
1099
- // Regular file — stat the underlying path
1100
- return this.statDescription(entry.description);
1101
- },
1102
- // Environment
1103
- getenv: (pid) => {
1104
- assertOwns(pid);
1105
- const entry = this.processTable.get(pid);
1106
- return entry?.env ?? { ...this.env };
1107
- },
1108
- setenv: (pid, key, value) => {
1109
- assertOwns(pid);
1110
- const entry = this.processTable.get(pid);
1111
- if (!entry)
1112
- throw new KernelError("ESRCH", `no such process ${pid}`);
1113
- entry.env[key] = value;
1114
- },
1115
- unsetenv: (pid, key) => {
1116
- assertOwns(pid);
1117
- const entry = this.processTable.get(pid);
1118
- if (!entry)
1119
- throw new KernelError("ESRCH", `no such process ${pid}`);
1120
- delete entry.env[key];
1121
- },
1122
- getcwd: (pid) => {
1123
- assertOwns(pid);
1124
- const entry = this.processTable.get(pid);
1125
- return entry?.cwd ?? this.cwd;
1126
- },
1127
- // Working directory
1128
- chdir: async (pid, path) => {
1129
- assertOwns(pid);
1130
- const entry = this.processTable.get(pid);
1131
- if (!entry)
1132
- throw new KernelError("ESRCH", `no such process ${pid}`);
1133
- // Validate path exists and is a directory
1134
- let st;
1135
- try {
1136
- st = await this.vfs.stat(path);
1137
- }
1138
- catch {
1139
- throw new KernelError("ENOENT", `no such file or directory: ${path}`);
1140
- }
1141
- if (!st.isDirectory) {
1142
- throw new KernelError("ENOTDIR", `not a directory: ${path}`);
1143
- }
1144
- entry.cwd = path;
1145
- entry.env.PWD = path;
1146
- },
1147
- // Alarm (SIGALRM)
1148
- alarm: (pid, seconds) => {
1149
- assertOwns(pid);
1150
- return this.processTable.alarm(pid, seconds);
1151
- },
1152
- // File mode creation mask
1153
- umask: (pid, newMask) => {
1154
- assertOwns(pid);
1155
- const entry = this.processTable.get(pid);
1156
- if (!entry)
1157
- throw new KernelError("ESRCH", `no such process ${pid}`);
1158
- const old = entry.umask;
1159
- if (newMask !== undefined) {
1160
- entry.umask = newMask & 0o777;
1161
- }
1162
- return old;
1163
- },
1164
- // Directory creation with umask
1165
- mkdir: async (pid, path, mode) => {
1166
- assertOwns(pid);
1167
- const entry = this.processTable.get(pid);
1168
- const umask = entry?.umask ?? 0o022;
1169
- const requestedMode = mode ?? 0o777;
1170
- const effectiveMode = requestedMode & ~umask;
1171
- await this.vfs.mkdir(path);
1172
- await this.vfs.chmod(path, effectiveMode);
1173
- },
1174
- // Socket table (shared across runtimes)
1175
- socketTable: this.socketTable,
1176
- timerTable: this.timerTable,
1177
- // Process table (shared across runtimes)
1178
- processTable: this.processTable,
1179
- };
1180
- return kernelInterface;
1181
- }
1182
- /**
1183
- * Create FD table for a child process via fork + optional FD overrides.
1184
- *
1185
- * When callerPid exists, forks the parent's FD table so the child inherits
1186
- * all open FDs (shared cursors via refcounted FileDescription). Then applies
1187
- * stdinFd/stdoutFd/stderrFd overrides on top of the forked table.
1188
- */
1189
- createChildFDTable(childPid, options, callerPid) {
1190
- // Fork parent's FD table if parent exists
1191
- if (callerPid && this.fdTableManager.get(callerPid)) {
1192
- const table = this.fdTableManager.fork(callerPid, childPid);
1193
- // Apply FD overrides on top of the forked table
1194
- const hasFdOverrides = options?.stdinFd !== undefined ||
1195
- options?.stdoutFd !== undefined ||
1196
- options?.stderrFd !== undefined;
1197
- if (hasFdOverrides) {
1198
- const callerTable = this.fdTableManager.get(callerPid);
1199
- this.applyStdioOverride(table, callerTable, 0, options.stdinFd);
1200
- this.applyStdioOverride(table, callerTable, 1, options.stdoutFd);
1201
- this.applyStdioOverride(table, callerTable, 2, options.stderrFd);
1202
- }
1203
- // Close inherited pipe FDs above stdio that share a pipe with an
1204
- // overridden stdio FD — prevents pipe deadlocks (close-on-exec for
1205
- // counterpart pipe ends only, so tests that intentionally inherit pipe
1206
- // FDs without overrides are not affected).
1207
- if (hasFdOverrides) {
1208
- const overridePipeIds = new Set();
1209
- for (const fd of [0, 1, 2]) {
1210
- const e = table.get(fd);
1211
- if (e && this.pipeManager.isPipe(e.description.id)) {
1212
- const pipeId = this.pipeManager.pipeIdFor(e.description.id);
1213
- if (pipeId !== undefined)
1214
- overridePipeIds.add(pipeId);
1215
- }
1216
- }
1217
- if (overridePipeIds.size > 0) {
1218
- const toClose = [];
1219
- for (const entry of table) {
1220
- if (entry.fd > 2 && this.pipeManager.isPipe(entry.description.id)) {
1221
- const pid2 = this.pipeManager.pipeIdFor(entry.description.id);
1222
- if (pid2 !== undefined && overridePipeIds.has(pid2)) {
1223
- toClose.push(entry.fd);
1224
- }
1225
- }
1226
- }
1227
- for (const fd of toClose) {
1228
- table.close(fd);
1229
- }
1230
- }
1231
- }
1232
- return table;
1233
- }
1234
- return this.fdTableManager.create(childPid);
1235
- }
1236
- /** Close inherited stdio FD and install an override from the caller's table. */
1237
- applyStdioOverride(childTable, callerTable, targetFd, overrideFd) {
1238
- if (overrideFd === undefined)
1239
- return;
1240
- if (overrideFd === 0xFFFFFFFF)
1241
- return; // /dev/null sentinel — keep inherited
1242
- const entry = callerTable.get(overrideFd);
1243
- if (!entry)
1244
- return;
1245
- // Close the inherited FD and install the override
1246
- const existing = childTable.get(targetFd);
1247
- childTable.close(targetFd);
1248
- if (existing && existing.description.refCount <= 0) {
1249
- this.releaseDescriptionInode(existing.description);
1250
- const descId = existing.description.id;
1251
- if (this.pipeManager.isPipe(descId))
1252
- this.pipeManager.close(descId);
1253
- if (this.ptyManager.isPty(descId))
1254
- this.ptyManager.close(descId);
1255
- this.fileLockManager.releaseByDescription(descId);
1256
- }
1257
- childTable.openWith(entry.description, entry.filetype, targetFd);
1258
- }
1259
- /** Check if a stdio FD (0/1/2) in a process's table is a pipe or PTY. */
1260
- isStdioPiped(table, fd) {
1261
- const entry = table.get(fd);
1262
- if (!entry)
1263
- return false;
1264
- return this.pipeManager.isPipe(entry.description.id) || this.ptyManager.isPty(entry.description.id);
1265
- }
1266
- /** Check if an FD in the given table refers to a PTY slave (terminal). */
1267
- isFdPtySlave(table, fd) {
1268
- const entry = table.get(fd);
1269
- if (!entry)
1270
- return false;
1271
- return this.ptyManager.isSlave(entry.description.id);
1272
- }
1273
- /**
1274
- * Create a callback that forwards data through a piped stdio FD.
1275
- * Needed for drivers (like Node) that emit output via callbacks rather
1276
- * than kernel FD writes (like WasmVM does via WASI fd_write).
1277
- */
1278
- createPipedOutputCallback(table, fd, pid) {
1279
- const entry = table.get(fd);
1280
- if (!entry)
1281
- return undefined;
1282
- const descId = entry.description.id;
1283
- if (this.pipeManager.isPipe(descId)) {
1284
- return (data) => {
1285
- try {
1286
- this.pipeManager.write(descId, data, pid);
1287
- }
1288
- catch { /* pipe closed */ }
1289
- };
1290
- }
1291
- if (this.ptyManager.isPty(descId)) {
1292
- return (data) => {
1293
- try {
1294
- this.ptyManager.write(descId, data);
1295
- }
1296
- catch { /* pty closed */ }
1297
- };
1298
- }
1299
- return undefined;
1300
- }
1301
- /** Clean up all FDs for a process, closing pipe/PTY ends when last reference drops. */
1302
- cleanupProcessFDs(pid) {
1303
- const table = this.fdTableManager.get(pid);
1304
- if (!table)
1305
- return;
1306
- // Collect descriptions before closing so we can check refCounts after.
1307
- const descriptions = new Map();
1308
- const managedDescs = [];
1309
- for (const entry of table) {
1310
- descriptions.set(entry.description.id, entry.description);
1311
- const descId = entry.description.id;
1312
- if (this.pipeManager.isPipe(descId)) {
1313
- managedDescs.push({ id: descId, description: entry.description, type: "pipe" });
1314
- }
1315
- else if (this.ptyManager.isPty(descId)) {
1316
- managedDescs.push({ id: descId, description: entry.description, type: "pty" });
1317
- }
1318
- else if (this.fileLockManager.hasLock(descId)) {
1319
- managedDescs.push({ id: descId, description: entry.description, type: "lock" });
1320
- }
1321
- }
1322
- // Close all FDs and remove the table
1323
- this.fdTableManager.remove(pid);
1324
- // Flush buffered writes when the last shared reference closes.
1325
- for (const description of descriptions.values()) {
1326
- if (description.refCount <= 0) {
1327
- this.releaseDescriptionInode(description);
1328
- }
1329
- }
1330
- // Signal closure for managed descriptions whose last reference was dropped.
1331
- for (const { id, description, type } of managedDescs) {
1332
- if (description.refCount <= 0) {
1333
- if (type === "pipe")
1334
- this.pipeManager.close(id);
1335
- else if (type === "pty")
1336
- this.ptyManager.close(id);
1337
- else if (type === "lock")
1338
- this.fileLockManager.releaseByDescription(id);
1339
- }
1340
- }
1341
- }
1342
- async vfsWrite(entry, data) {
1343
- let content;
1344
- try {
1345
- content = await this.readDescriptionFile(entry.description);
1346
- }
1347
- catch {
1348
- content = new Uint8Array(0);
1349
- }
1350
- // O_APPEND: every write seeks to end of file first (POSIX)
1351
- const cursor = (entry.description.flags & O_APPEND)
1352
- ? content.length
1353
- : Number(entry.description.cursor);
1354
- const endPos = cursor + data.length;
1355
- const newContent = new Uint8Array(Math.max(content.length, endPos));
1356
- newContent.set(content);
1357
- newContent.set(data, cursor);
1358
- await this.writeDescriptionFile(entry.description, newContent);
1359
- // Apply creation mode once the descriptor's newly created file is materialized.
1360
- if (entry.description.creationMode !== undefined) {
1361
- await this.vfs.chmod(entry.description.path, entry.description.creationMode);
1362
- entry.description.creationMode = undefined;
1363
- }
1364
- entry.description.cursor = BigInt(endPos);
1365
- return data.length;
1366
- }
1367
- releaseDescriptionInode(description) {
1368
- // Flush buffered writes to durable storage when the last FD is closed.
1369
- void this.vfs.fsync?.(description.path);
1370
- }
1371
- async readDescriptionFile(description) {
1372
- return this.vfs.readFile(description.path);
1373
- }
1374
- async writeDescriptionFile(description, content) {
1375
- await this.vfs.writeFile(description.path, content);
1376
- }
1377
- prepareOpenSync(path, flags) {
1378
- const syncVfs = this.vfs;
1379
- return syncVfs.prepareOpenSync?.(path, flags) ?? false;
1380
- }
1381
- async preadDescription(description, offset, length) {
1382
- return this.vfs.pread(description.path, offset, length);
1383
- }
1384
- async pwriteDescription(description, offset, data) {
1385
- await this.vfs.pwrite(description.path, offset, data);
1386
- }
1387
- async getDescriptionSize(description) {
1388
- return (await this.statDescription(description)).size;
1389
- }
1390
- async statDescription(description) {
1391
- return this.vfs.stat(description.path);
1392
- }
1393
- getTable(pid) {
1394
- const table = this.fdTableManager.get(pid);
1395
- if (!table)
1396
- throw new KernelError("ESRCH", `no FD table for PID ${pid}`);
1397
- return table;
1398
- }
1399
- assertNotDisposed() {
1400
- if (this.disposed)
1401
- throw new Error("Kernel is disposed");
1402
- }
1403
- }
1404
- function concatUint8(chunks) {
1405
- if (chunks.length === 0)
1406
- return "";
1407
- const total = chunks.reduce((sum, c) => sum + c.length, 0);
1408
- const buf = new Uint8Array(total);
1409
- let offset = 0;
1410
- for (const chunk of chunks) {
1411
- buf.set(chunk, offset);
1412
- offset += chunk.length;
1413
- }
1414
- return new TextDecoder().decode(buf);
1415
- }