@secure-exec/core 0.2.0-rc.1 → 0.2.0

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 (60) hide show
  1. package/dist/generated/isolate-runtime.d.ts +2 -2
  2. package/dist/generated/isolate-runtime.js +2 -2
  3. package/dist/index.d.ts +17 -4
  4. package/dist/index.js +10 -2
  5. package/dist/isolate-runtime/require-setup.js +1489 -239
  6. package/dist/isolate-runtime/setup-dynamic-import.js +31 -0
  7. package/dist/kernel/device-backend.d.ts +14 -0
  8. package/dist/kernel/device-backend.js +251 -0
  9. package/dist/kernel/device-layer.js +9 -0
  10. package/dist/kernel/file-lock.js +2 -3
  11. package/dist/kernel/index.d.ts +4 -4
  12. package/dist/kernel/index.js +3 -3
  13. package/dist/kernel/kernel.js +141 -122
  14. package/dist/kernel/mount-table.d.ts +75 -0
  15. package/dist/kernel/mount-table.js +353 -0
  16. package/dist/kernel/permissions.d.ts +9 -0
  17. package/dist/kernel/permissions.js +33 -1
  18. package/dist/kernel/proc-backend.d.ts +30 -0
  19. package/dist/kernel/proc-backend.js +428 -0
  20. package/dist/kernel/proc-layer.js +6 -0
  21. package/dist/kernel/process-table.d.ts +3 -1
  22. package/dist/kernel/process-table.js +23 -3
  23. package/dist/kernel/pty.d.ts +3 -2
  24. package/dist/kernel/pty.js +13 -2
  25. package/dist/kernel/socket-table.d.ts +7 -0
  26. package/dist/kernel/socket-table.js +99 -35
  27. package/dist/kernel/types.d.ts +45 -4
  28. package/dist/kernel/types.js +9 -0
  29. package/dist/kernel/vfs.d.ts +30 -2
  30. package/dist/kernel/vfs.js +19 -2
  31. package/dist/shared/api-types.d.ts +6 -0
  32. package/dist/shared/bridge-contract.d.ts +21 -3
  33. package/dist/shared/bridge-contract.js +2 -0
  34. package/dist/shared/console-formatter.js +8 -8
  35. package/dist/shared/global-exposure.js +95 -0
  36. package/dist/shared/in-memory-fs.d.ts +14 -59
  37. package/dist/shared/in-memory-fs.js +97 -597
  38. package/dist/shared/permissions.js +5 -0
  39. package/dist/test/block-store-conformance.d.ts +34 -0
  40. package/dist/test/block-store-conformance.js +251 -0
  41. package/dist/test/metadata-store-conformance.d.ts +37 -0
  42. package/dist/test/metadata-store-conformance.js +646 -0
  43. package/dist/test/vfs-conformance.d.ts +65 -0
  44. package/dist/test/vfs-conformance.js +842 -0
  45. package/dist/types.d.ts +1 -0
  46. package/dist/vfs/chunked-vfs.d.ts +66 -0
  47. package/dist/vfs/chunked-vfs.js +1290 -0
  48. package/dist/vfs/host-block-store.d.ts +19 -0
  49. package/dist/vfs/host-block-store.js +97 -0
  50. package/dist/vfs/memory-block-store.d.ts +16 -0
  51. package/dist/vfs/memory-block-store.js +45 -0
  52. package/dist/vfs/memory-metadata.d.ts +75 -0
  53. package/dist/vfs/memory-metadata.js +528 -0
  54. package/dist/vfs/sqlite-metadata.d.ts +91 -0
  55. package/dist/vfs/sqlite-metadata.js +582 -0
  56. package/dist/vfs/types.d.ts +210 -0
  57. package/dist/vfs/types.js +8 -0
  58. package/package.json +20 -1
  59. package/dist/kernel/inode-table.d.ts +0 -43
  60. package/dist/kernel/inode-table.js +0 -85
@@ -5,8 +5,9 @@
5
5
  * pipe manager, command registry, and permissions. Runtimes are execution
6
6
  * engines that make "syscalls" to the kernel.
7
7
  */
8
- import { createDeviceLayer } from "./device-layer.js";
9
- import { createProcLayer } from "./proc-layer.js";
8
+ import { createDeviceBackend } from "./device-backend.js";
9
+ import { createProcBackend } from "./proc-backend.js";
10
+ import { MountTable } from "./mount-table.js";
10
11
  import { FDTableManager } from "./fd-table.js";
11
12
  import { ProcessTable } from "./process-table.js";
12
13
  import { PipeManager } from "./pipe-manager.js";
@@ -17,33 +18,21 @@ import { wrapFileSystem, checkChildProcess } from "./permissions.js";
17
18
  import { UserManager } from "./user.js";
18
19
  import { SocketTable } from "./socket-table.js";
19
20
  import { TimerTable } from "./timer-table.js";
20
- import { InodeTable } from "./inode-table.js";
21
- import { InMemoryFileSystem } from "../shared/in-memory-fs.js";
22
- 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, } from "./types.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";
23
22
  export function createKernel(options) {
24
23
  return new KernelImpl(options);
25
24
  }
26
25
  class KernelImpl {
27
26
  vfs;
28
- rawInMemoryFs;
27
+ mountTable;
29
28
  fdTableManager = new FDTableManager();
30
- processTable = new ProcessTable();
29
+ processTable;
31
30
  pipeManager = new PipeManager();
32
- ptyManager = new PtyManager((pgid, signal, excludeLeaders) => {
33
- try {
34
- if (excludeLeaders) {
35
- return this.processTable.killGroupExcludeLeaders(pgid, signal);
36
- }
37
- this.processTable.kill(-pgid, signal);
38
- }
39
- catch { /* no-op if pgid gone */ }
40
- return 0;
41
- });
31
+ ptyManager;
42
32
  fileLockManager = new FileLockManager();
43
33
  commandRegistry = new CommandRegistry();
44
34
  socketTable;
45
35
  timerTable;
46
- inodeTable;
47
36
  userManager;
48
37
  drivers = [];
49
38
  driverPids = new Map();
@@ -54,21 +43,38 @@ class KernelImpl {
54
43
  disposed = false;
55
44
  pendingBinEntries = [];
56
45
  posixDirsReady;
46
+ log;
57
47
  constructor(options) {
58
- this.inodeTable = new InodeTable();
59
- if (options.filesystem instanceof InMemoryFileSystem) {
60
- options.filesystem.setInodeTable(this.inodeTable);
61
- this.rawInMemoryFs = options.filesystem;
62
- }
63
- // Apply pseudo-filesystems before permissions so dynamic entries are
64
- // subject to the same policy as regular files.
65
- let fs = createDeviceLayer(options.filesystem);
66
- fs = createProcLayer(fs, {
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({
67
64
  processTable: this.processTable,
68
65
  fdTableManager: this.fdTableManager,
69
66
  hostname: options.env?.HOSTNAME,
70
- });
71
- // Apply permission wrapping
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;
72
78
  if (options.permissions) {
73
79
  fs = wrapFileSystem(fs, options.permissions);
74
80
  }
@@ -80,12 +86,15 @@ class KernelImpl {
80
86
  this.userManager = new UserManager();
81
87
  this.socketTable = new SocketTable({
82
88
  vfs: this.vfs,
89
+ networkCheck: options.permissions?.network,
83
90
  hostAdapter: options.hostNetworkAdapter,
84
91
  getSignalState: (pid) => this.processTable.getSignalState(pid),
92
+ processExists: (pid) => this.processTable.get(pid) !== undefined,
85
93
  });
86
94
  this.timerTable = new TimerTable();
87
95
  // Clean up FD table and sockets when a process exits
88
96
  this.processTable.onProcessExit = (pid) => {
97
+ this.log.debug({ pid }, "process exit cleanup");
89
98
  this.cleanupProcessFDs(pid);
90
99
  this.socketTable.closeAllForProcess(pid);
91
100
  this.timerTable.clearAllForProcess(pid);
@@ -110,6 +119,7 @@ class KernelImpl {
110
119
  this.posixDirsReady = this.initPosixDirs();
111
120
  }
112
121
  async initPosixDirs() {
122
+ // /dev and /proc are auto-created by MountTable mounts — don't create them here.
113
123
  const dirs = [
114
124
  "/tmp",
115
125
  "/bin",
@@ -121,7 +131,6 @@ class KernelImpl {
121
131
  "/run",
122
132
  "/srv",
123
133
  "/sys",
124
- "/proc",
125
134
  "/opt",
126
135
  "/mnt",
127
136
  "/media",
@@ -168,6 +177,7 @@ class KernelImpl {
168
177
  async mount(driver) {
169
178
  this.assertNotDisposed();
170
179
  await this.posixDirsReady;
180
+ this.log.debug({ driver: driver.name, commands: driver.commands }, "mounting runtime driver");
171
181
  // Track PIDs owned by this driver
172
182
  if (!this.driverPids.has(driver.name)) {
173
183
  this.driverPids.set(driver.name, new Set());
@@ -179,11 +189,21 @@ class KernelImpl {
179
189
  this.drivers.push(driver);
180
190
  // Populate /bin stubs for shell PATH lookup
181
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);
182
201
  }
183
202
  async dispose() {
184
203
  if (this.disposed)
185
204
  return;
186
205
  this.disposed = true;
206
+ this.log.info({}, "kernel disposing");
187
207
  // Terminate all running processes
188
208
  await this.processTable.terminateAll();
189
209
  // Clean up all sockets
@@ -212,6 +232,7 @@ class KernelImpl {
212
232
  }
213
233
  async exec(command, options) {
214
234
  this.assertNotDisposed();
235
+ this.log.debug({ command, timeout: options?.timeout, cwd: options?.cwd }, "exec start");
215
236
  // Flush pending /bin stubs before shell PATH lookup
216
237
  await this.flushPendingBinEntries();
217
238
  // Route through shell
@@ -252,6 +273,7 @@ class KernelImpl {
252
273
  new Promise((_, reject) => {
253
274
  timer = setTimeout(() => {
254
275
  // Kill process and detach output callbacks
276
+ this.log.warn({ command, timeout: options.timeout }, "exec timeout, sending SIGTERM");
255
277
  proc.onStdout = null;
256
278
  proc.onStderr = null;
257
279
  proc.kill(SIGTERM);
@@ -282,6 +304,7 @@ class KernelImpl {
282
304
  this.assertNotDisposed();
283
305
  const command = options?.command ?? "sh";
284
306
  const args = options?.args ?? [];
307
+ this.log.debug({ command, args, cols: options?.cols, rows: options?.rows, cwd: options?.cwd }, "openShell start");
285
308
  // Allocate a controller PID with an FD table to hold the PTY master
286
309
  const controllerPid = this.processTable.allocatePid();
287
310
  const controllerTable = this.fdTableManager.create(controllerPid);
@@ -289,8 +312,16 @@ class KernelImpl {
289
312
  const { masterFd, slaveFd } = this.ptyManager.createPtyFDs(controllerTable);
290
313
  const masterDescId = controllerTable.get(masterFd).description.id;
291
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);
292
323
  const proc = this.spawnInternal(command, args, {
293
- env: options?.env,
324
+ env: { ...options?.env, ...dimEnv },
294
325
  cwd: options?.cwd,
295
326
  stdinFd: slaveFd,
296
327
  stdoutFd: slaveFd,
@@ -300,6 +331,7 @@ class KernelImpl {
300
331
  this.processTable.setpgid(proc.pid, proc.pid);
301
332
  this.ptyManager.setForegroundPgid(masterDescId, proc.pid);
302
333
  this.ptyManager.setSessionLeader(masterDescId, proc.pid);
334
+ this.log.debug({ shellPid: proc.pid, controllerPid, masterFd, masterDescId }, "openShell PTY attached");
303
335
  // Close controller's copy of slave FD (child inherited its own copy via fork).
304
336
  // Without this, slave refCount stays >0 after shell exits, preventing EOF on master.
305
337
  const slaveEntry = controllerTable.get(slaveFd);
@@ -354,6 +386,7 @@ class KernelImpl {
354
386
  set onData(fn) { pump.onData = fn; },
355
387
  resize: (_cols, _rows) => {
356
388
  const fgPgid = this.ptyManager.getForegroundPgid(masterDescId);
389
+ this.log.trace({ shellPid: proc.pid, cols: _cols, rows: _rows, fgPgid }, "PTY resize");
357
390
  if (fgPgid > 0) {
358
391
  try {
359
392
  this.processTable.kill(-fgPgid, SIGWINCH);
@@ -369,6 +402,7 @@ class KernelImpl {
369
402
  }
370
403
  async connectTerminal(options) {
371
404
  this.assertNotDisposed();
405
+ this.log.debug({ command: options?.command, cols: options?.cols, rows: options?.rows }, "connectTerminal start");
372
406
  const stdin = process.stdin;
373
407
  const stdout = process.stdout;
374
408
  const isTTY = stdin.isTTY;
@@ -387,15 +421,12 @@ class KernelImpl {
387
421
  const outputHandler = options?.onData
388
422
  ?? ((data) => { stdout.write(data); });
389
423
  shell.onData = outputHandler;
390
- // Handle terminal resize
391
- onResize = () => {
392
- shell.resize(stdout.columns || 80, stdout.rows || 24);
393
- };
394
- if (stdout.isTTY)
395
- stdout.on("resize", onResize);
396
- // Set initial terminal size
424
+ // Forward terminal resize → PTY SIGWINCH
397
425
  if (stdout.isTTY) {
398
- shell.resize(stdout.columns || 80, stdout.rows || 24);
426
+ onResize = () => {
427
+ shell.resize(stdout.columns, stdout.rows);
428
+ };
429
+ stdout.on("resize", onResize);
399
430
  }
400
431
  return await shell.wait();
401
432
  }
@@ -417,6 +448,9 @@ class KernelImpl {
417
448
  readdir(path) { return this.vfs.readDir(path); }
418
449
  stat(path) { return this.vfs.stat(path); }
419
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); }
420
454
  // Introspection
421
455
  get commands() {
422
456
  return this.commandRegistry.list();
@@ -431,6 +465,7 @@ class KernelImpl {
431
465
  // Internal spawn
432
466
  // -----------------------------------------------------------------------
433
467
  spawnInternal(command, args, options, callerPid) {
468
+ this.log.debug({ command, args, callerPid, cwd: options?.cwd }, "spawn start");
434
469
  let driver = this.commandRegistry.resolve(command);
435
470
  // On-demand discovery: ask mounted drivers to resolve unknown commands
436
471
  if (!driver) {
@@ -456,12 +491,20 @@ class KernelImpl {
456
491
  }
457
492
  }
458
493
  if (!driver) {
494
+ this.log.warn({ command }, "command not found");
459
495
  throw new KernelError("ENOENT", `command not found: ${command}`);
460
496
  }
461
497
  // Check childProcess permission
462
- checkChildProcess(this.permissions, command, args, options?.cwd);
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
+ }
463
505
  // Enforce maxProcesses budget
464
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");
465
508
  throw new KernelError("EAGAIN", "maximum process limit reached");
466
509
  }
467
510
  // Allocate PID atomically
@@ -486,44 +529,34 @@ class KernelImpl {
486
529
  // Buffer stdout/stderr — wired before spawn so nothing is lost
487
530
  const stdoutBuf = [];
488
531
  const stderrBuf = [];
489
- // Resolve output callbacks: when a child inherits non-piped stdio from
490
- // a parent, forward output to the parent's DriverProcess callbacks so
491
- // cross-runtime child output reaches the top-level collector.
492
- // When piped, wire a callback that forwards through the pipe/PTY so
493
- // drivers that emit output via callbacks (Node) reach the PTY/pipe.
494
- let stdoutCb;
495
- let stderrCb;
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;
496
543
  if (stdoutPiped) {
497
- stdoutCb = this.createPipedOutputCallback(table, 1, pid);
544
+ ctxStdoutCb = this.createPipedOutputCallback(table, 1, pid);
498
545
  }
499
- else {
500
- if (options?.onStdout) {
501
- stdoutCb = options.onStdout;
546
+ else if (!options?.onStdout && callerPid !== undefined) {
547
+ const parent = this.processTable.get(callerPid);
548
+ if (parent?.driverProcess.onStdout) {
549
+ ctxStdoutCb = parent.driverProcess.onStdout;
502
550
  }
503
- else if (callerPid !== undefined) {
504
- const parent = this.processTable.get(callerPid);
505
- if (parent?.driverProcess.onStdout) {
506
- stdoutCb = parent.driverProcess.onStdout;
507
- }
508
- }
509
- if (!stdoutCb)
510
- stdoutCb = (data) => stdoutBuf.push(data);
511
551
  }
512
552
  if (stderrPiped) {
513
- stderrCb = this.createPipedOutputCallback(table, 2, pid);
553
+ ctxStderrCb = this.createPipedOutputCallback(table, 2, pid);
514
554
  }
515
- else {
516
- if (options?.onStderr) {
517
- stderrCb = options.onStderr;
555
+ else if (!options?.onStderr && callerPid !== undefined) {
556
+ const parent = this.processTable.get(callerPid);
557
+ if (parent?.driverProcess.onStderr) {
558
+ ctxStderrCb = parent.driverProcess.onStderr;
518
559
  }
519
- else if (callerPid !== undefined) {
520
- const parent = this.processTable.get(callerPid);
521
- if (parent?.driverProcess.onStderr) {
522
- stderrCb = parent.driverProcess.onStderr;
523
- }
524
- }
525
- if (!stderrCb)
526
- stderrCb = (data) => stderrBuf.push(data);
527
560
  }
528
561
  // Inherit env from parent process if spawned by another process, else use kernel defaults
529
562
  const parentEntry = callerPid ? this.processTable.get(callerPid) : undefined;
@@ -532,27 +565,45 @@ class KernelImpl {
532
565
  const stdinIsTTY = this.isFdPtySlave(table, 0);
533
566
  const stdoutIsTTY = this.isFdPtySlave(table, 1);
534
567
  const stderrIsTTY = this.isFdPtySlave(table, 2);
535
- // Build process context with pre-wired callbacks
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;
536
572
  const ctx = {
537
573
  pid,
538
574
  ppid: callerPid ?? 0,
539
- env: { ...baseEnv, ...options?.env },
540
- cwd: options?.cwd ?? this.cwd,
575
+ env: { ...baseEnv, ...options?.env, PWD: resolvedCwd },
576
+ cwd: resolvedCwd,
541
577
  fds: { stdin: 0, stdout: 1, stderr: 2 },
542
578
  stdinIsTTY,
543
579
  stdoutIsTTY,
544
580
  stderrIsTTY,
545
- onStdout: stdoutCb,
546
- onStderr: stderrCb,
581
+ streamStdin: options?.streamStdin,
582
+ onStdout: ctxStdoutCb ?? (stdoutPiped ? undefined : (data) => stdoutBuf.push(data)),
583
+ onStderr: ctxStderrCb ?? (stderrPiped ? undefined : (data) => stderrBuf.push(data)),
547
584
  };
548
585
  // Spawn via driver
549
586
  const driverProcess = driver.spawn(command, args, ctx);
550
- // Capture data emitted via DriverProcess callbacks after spawn returns.
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).
551
594
  if (!stdoutPiped) {
552
- driverProcess.onStdout = stdoutCb ?? ((data) => stdoutBuf.push(data));
595
+ ctx.onStdout = ctxStdoutCb;
553
596
  }
554
597
  if (!stderrPiped) {
555
- driverProcess.onStderr = stderrCb ?? ((data) => stderrBuf.push(data));
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));
556
607
  }
557
608
  // Register in process table
558
609
  const entry = this.processTable.register(pid, driver.name, command, args, ctx, driverProcess);
@@ -646,9 +697,6 @@ class KernelImpl {
646
697
  const filetype = FILETYPE_REGULAR_FILE;
647
698
  const fd = table.open(path, flags, filetype);
648
699
  const fdEntry = table.get(fd);
649
- if (fdEntry) {
650
- this.trackDescriptionInode(fdEntry.description);
651
- }
652
700
  // Stash the effective mode for the first write that materializes a new file.
653
701
  if (created && (flags & O_CREAT)) {
654
702
  const entry = this.processTable.get(pid);
@@ -771,14 +819,8 @@ class KernelImpl {
771
819
  if (this.pipeManager.isPipe(entry.description.id) || this.ptyManager.isPty(entry.description.id)) {
772
820
  throw new KernelError("ESPIPE", "illegal seek");
773
821
  }
774
- // Write at offset without moving cursor.
775
- const content = await this.readDescriptionFile(entry.description);
776
- const pos = Number(offset);
777
- const endPos = pos + data.length;
778
- const newContent = new Uint8Array(Math.max(content.length, endPos));
779
- newContent.set(content);
780
- newContent.set(data, pos);
781
- await this.writeDescriptionFile(entry.description, newContent);
822
+ // Delegate positional write to VFS.
823
+ await this.pwriteDescription(entry.description, Number(offset), data);
782
824
  return data.length;
783
825
  },
784
826
  fdDup: (pid, fd) => {
@@ -897,6 +939,7 @@ class KernelImpl {
897
939
  return this.spawnManaged(command, args, {
898
940
  env: ctx.env,
899
941
  cwd: ctx.cwd,
942
+ streamStdin: ctx.streamStdin,
900
943
  onStdout: ctx.onStdout,
901
944
  onStderr: ctx.onStderr,
902
945
  stdinFd: ctx.stdinFd,
@@ -917,6 +960,7 @@ class KernelImpl {
917
960
  // Negative PID = process group kill, handled by kernel directly
918
961
  if (pid >= 0)
919
962
  assertOwns(pid);
963
+ this.log.debug({ pid, signal }, "signal delivery");
920
964
  this.processTable.kill(pid, signal);
921
965
  },
922
966
  getpid: (pid) => {
@@ -1098,6 +1142,7 @@ class KernelImpl {
1098
1142
  throw new KernelError("ENOTDIR", `not a directory: ${path}`);
1099
1143
  }
1100
1144
  entry.cwd = path;
1145
+ entry.env.PWD = path;
1101
1146
  },
1102
1147
  // Alarm (SIGALRM)
1103
1148
  alarm: (pid, seconds) => {
@@ -1276,7 +1321,7 @@ class KernelImpl {
1276
1321
  }
1277
1322
  // Close all FDs and remove the table
1278
1323
  this.fdTableManager.remove(pid);
1279
- // Release inode-backed file data after the last shared reference closes.
1324
+ // Flush buffered writes when the last shared reference closes.
1280
1325
  for (const description of descriptions.values()) {
1281
1326
  if (description.refCount <= 0) {
1282
1327
  this.releaseDescriptionInode(description);
@@ -1319,56 +1364,30 @@ class KernelImpl {
1319
1364
  entry.description.cursor = BigInt(endPos);
1320
1365
  return data.length;
1321
1366
  }
1322
- trackDescriptionInode(description) {
1323
- if (!this.rawInMemoryFs || description.inode !== undefined)
1324
- return;
1325
- const ino = this.rawInMemoryFs.getInodeForPath(description.path);
1326
- if (ino === null)
1327
- return;
1328
- description.inode = ino;
1329
- this.inodeTable.incrementOpenRefs(ino);
1330
- }
1331
1367
  releaseDescriptionInode(description) {
1332
- if (description.inode === undefined)
1333
- return;
1334
- this.inodeTable.decrementOpenRefs(description.inode);
1335
- if (this.inodeTable.shouldDelete(description.inode)) {
1336
- this.rawInMemoryFs?.deleteInodeData(description.inode);
1337
- this.inodeTable.delete(description.inode);
1338
- }
1339
- description.inode = undefined;
1368
+ // Flush buffered writes to durable storage when the last FD is closed.
1369
+ void this.vfs.fsync?.(description.path);
1340
1370
  }
1341
1371
  async readDescriptionFile(description) {
1342
- if (description.inode !== undefined && this.rawInMemoryFs) {
1343
- return this.rawInMemoryFs.readFileByInode(description.inode);
1344
- }
1345
1372
  return this.vfs.readFile(description.path);
1346
1373
  }
1347
1374
  async writeDescriptionFile(description, content) {
1348
- if (description.inode !== undefined && this.rawInMemoryFs) {
1349
- this.rawInMemoryFs.writeFileByInode(description.inode, content);
1350
- return;
1351
- }
1352
1375
  await this.vfs.writeFile(description.path, content);
1353
- this.trackDescriptionInode(description);
1354
1376
  }
1355
1377
  prepareOpenSync(path, flags) {
1356
1378
  const syncVfs = this.vfs;
1357
1379
  return syncVfs.prepareOpenSync?.(path, flags) ?? false;
1358
1380
  }
1359
1381
  async preadDescription(description, offset, length) {
1360
- if (description.inode !== undefined && this.rawInMemoryFs) {
1361
- return this.rawInMemoryFs.preadByInode(description.inode, offset, length);
1362
- }
1363
1382
  return this.vfs.pread(description.path, offset, length);
1364
1383
  }
1384
+ async pwriteDescription(description, offset, data) {
1385
+ await this.vfs.pwrite(description.path, offset, data);
1386
+ }
1365
1387
  async getDescriptionSize(description) {
1366
1388
  return (await this.statDescription(description)).size;
1367
1389
  }
1368
1390
  async statDescription(description) {
1369
- if (description.inode !== undefined && this.rawInMemoryFs) {
1370
- return this.rawInMemoryFs.statByInode(description.inode);
1371
- }
1372
1391
  return this.vfs.stat(description.path);
1373
1392
  }
1374
1393
  getTable(pid) {
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Mount Table.
3
+ *
4
+ * Linux-style VFS mount table that routes paths to mounted filesystem backends
5
+ * by longest-prefix matching. Replaces the hardcoded layer composition
6
+ * (DeviceLayer wraps ProcLayer wraps base FS) with a unified routing table.
7
+ */
8
+ import type { VirtualDirEntry, VirtualDirStatEntry, VirtualFileSystem, VirtualStat } from "./vfs.js";
9
+ export interface MountOptions {
10
+ readOnly?: boolean;
11
+ }
12
+ export interface MountEntry {
13
+ path: string;
14
+ readOnly: boolean;
15
+ }
16
+ export declare class MountTable implements VirtualFileSystem {
17
+ /**
18
+ * Mounts sorted by path length descending so longest-prefix match is first hit.
19
+ */
20
+ private mounts;
21
+ constructor(rootFs: VirtualFileSystem);
22
+ /**
23
+ * Mount a filesystem at the given path.
24
+ * Auto-creates the mount point directory in the parent filesystem if needed.
25
+ */
26
+ mount(path: string, fs: VirtualFileSystem, options?: MountOptions): void;
27
+ /**
28
+ * Unmount the filesystem at the given path.
29
+ */
30
+ unmount(path: string): void;
31
+ /**
32
+ * List all current mounts.
33
+ */
34
+ getMounts(): ReadonlyArray<MountEntry>;
35
+ private resolve;
36
+ private assertWritable;
37
+ readFile(path: string): Promise<Uint8Array>;
38
+ readTextFile(path: string): Promise<string>;
39
+ readDir(path: string): Promise<string[]>;
40
+ readDirWithTypes(path: string): Promise<VirtualDirEntry[]>;
41
+ exists(path: string): Promise<boolean>;
42
+ stat(path: string): Promise<VirtualStat>;
43
+ lstat(path: string): Promise<VirtualStat>;
44
+ realpath(path: string): Promise<string>;
45
+ readlink(path: string): Promise<string>;
46
+ pread(path: string, offset: number, length: number): Promise<Uint8Array>;
47
+ pwrite(path: string, offset: number, data: Uint8Array): Promise<void>;
48
+ writeFile(path: string, content: string | Uint8Array): Promise<void>;
49
+ createDir(path: string): Promise<void>;
50
+ mkdir(path: string, options?: {
51
+ recursive?: boolean;
52
+ }): Promise<void>;
53
+ removeFile(path: string): Promise<void>;
54
+ removeDir(path: string): Promise<void>;
55
+ symlink(target: string, linkPath: string): Promise<void>;
56
+ link(oldPath: string, newPath: string): Promise<void>;
57
+ rename(oldPath: string, newPath: string): Promise<void>;
58
+ chmod(path: string, mode: number): Promise<void>;
59
+ chown(path: string, uid: number, gid: number): Promise<void>;
60
+ utimes(path: string, atime: number, mtime: number): Promise<void>;
61
+ truncate(path: string, length: number): Promise<void>;
62
+ fsync(path: string): Promise<void>;
63
+ copy(srcPath: string, dstPath: string): Promise<void>;
64
+ readDirStat(path: string): Promise<VirtualDirStatEntry[]>;
65
+ /**
66
+ * Get basenames of child mount points under a directory.
67
+ * For example, if mounts exist at /dev and /proc, calling with "/" returns ["dev", "proc"].
68
+ */
69
+ private getChildMountBasenames;
70
+ /**
71
+ * Synchronous open preparation (O_CREAT, O_EXCL, O_TRUNC).
72
+ * Delegates to the underlying backend's prepareOpenSync if it exists.
73
+ */
74
+ prepareOpenSync(path: string, flags: number): boolean;
75
+ }