@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
@@ -0,0 +1,428 @@
1
+ /**
2
+ * Proc backend.
3
+ *
4
+ * Standalone VirtualFileSystem that handles /proc paths.
5
+ * Receives relative paths (e.g. "self/fd" not "/proc/self/fd").
6
+ * Designed to be mounted at /proc via MountTable.
7
+ */
8
+ import { KernelError } from "./types.js";
9
+ const S_IFREG = 0o100000;
10
+ const S_IFDIR = 0o040000;
11
+ const S_IFLNK = 0o120000;
12
+ const PROC_INO_BASE = 0xfffe_0000;
13
+ const PROC_PID_ENTRIES = [
14
+ { name: "fd", isDirectory: true },
15
+ { name: "cwd", isDirectory: false, isSymbolicLink: true },
16
+ { name: "exe", isDirectory: false, isSymbolicLink: true },
17
+ { name: "environ", isDirectory: false },
18
+ ];
19
+ const PROC_ROOT_ENTRIES = [
20
+ { name: "self", isDirectory: false, isSymbolicLink: true },
21
+ { name: "sys", isDirectory: true },
22
+ { name: "mounts", isDirectory: false },
23
+ ];
24
+ const PROC_SYS_ENTRIES = [
25
+ { name: "kernel", isDirectory: true },
26
+ ];
27
+ const PROC_SYS_KERNEL_ENTRIES = [
28
+ { name: "hostname", isDirectory: false },
29
+ ];
30
+ function procIno(seed) {
31
+ let hash = 0;
32
+ for (let i = 0; i < seed.length; i++) {
33
+ hash = ((hash * 33) ^ seed.charCodeAt(i)) >>> 0;
34
+ }
35
+ return PROC_INO_BASE + (hash & 0xffff);
36
+ }
37
+ function dirStat(seed) {
38
+ const now = Date.now();
39
+ return {
40
+ mode: S_IFDIR | 0o555,
41
+ size: 0,
42
+ isDirectory: true,
43
+ isSymbolicLink: false,
44
+ atimeMs: now,
45
+ mtimeMs: now,
46
+ ctimeMs: now,
47
+ birthtimeMs: now,
48
+ ino: procIno(seed),
49
+ nlink: 2,
50
+ uid: 0,
51
+ gid: 0,
52
+ };
53
+ }
54
+ function fileStat(seed, size) {
55
+ const now = Date.now();
56
+ return {
57
+ mode: S_IFREG | 0o444,
58
+ size,
59
+ isDirectory: false,
60
+ isSymbolicLink: false,
61
+ atimeMs: now,
62
+ mtimeMs: now,
63
+ ctimeMs: now,
64
+ birthtimeMs: now,
65
+ ino: procIno(seed),
66
+ nlink: 1,
67
+ uid: 0,
68
+ gid: 0,
69
+ };
70
+ }
71
+ function linkStat(seed, target) {
72
+ const now = Date.now();
73
+ return {
74
+ mode: S_IFLNK | 0o777,
75
+ size: target.length,
76
+ isDirectory: false,
77
+ isSymbolicLink: true,
78
+ atimeMs: now,
79
+ mtimeMs: now,
80
+ ctimeMs: now,
81
+ birthtimeMs: now,
82
+ ino: procIno(seed),
83
+ nlink: 1,
84
+ uid: 0,
85
+ gid: 0,
86
+ };
87
+ }
88
+ function encodeText(content) {
89
+ return new TextEncoder().encode(content);
90
+ }
91
+ function encodeEnviron(env) {
92
+ const entries = Object.entries(env);
93
+ if (entries.length === 0)
94
+ return new Uint8Array(0);
95
+ return encodeText(`${entries.map(([key, value]) => `${key}=${value}`).join("\0")}\0`);
96
+ }
97
+ function resolveExecPath(command) {
98
+ if (!command)
99
+ return "";
100
+ return command.startsWith("/") ? command : `/bin/${command}`;
101
+ }
102
+ function notFound(path) {
103
+ throw new KernelError("ENOENT", `no such proc entry: ${path}`);
104
+ }
105
+ function rejectWrite(path) {
106
+ throw new KernelError("EPERM", `cannot modify /proc/${path}`);
107
+ }
108
+ /**
109
+ * Resolve /proc/self references to the given PID.
110
+ * Paths are relative (no /proc prefix).
111
+ */
112
+ export function resolveProcSelfPath(path, pid) {
113
+ if (path === "self")
114
+ return `${pid}`;
115
+ if (path.startsWith("self/"))
116
+ return `${pid}${path.slice(4)}`;
117
+ return path;
118
+ }
119
+ /**
120
+ * Parse a relative proc path into PID + tail components.
121
+ * "1/fd/0" -> { pid: 1, tail: ["fd", "0"] }
122
+ */
123
+ function parsePidPath(path) {
124
+ const parts = path.split("/");
125
+ const pid = Number(parts[0]);
126
+ if (!Number.isInteger(pid) || pid < 0)
127
+ return null;
128
+ return { pid, tail: parts.slice(1) };
129
+ }
130
+ /**
131
+ * Format mount entries in Linux /proc/mounts format.
132
+ */
133
+ function formatMounts(mounts) {
134
+ return mounts
135
+ .map((m) => {
136
+ const fsType = m.path === "/" ? "rootfs" : "mount";
137
+ const opts = m.readOnly ? "ro" : "rw";
138
+ return `${fsType} ${m.path} ${fsType} ${opts} 0 0`;
139
+ })
140
+ .join("\n")
141
+ .concat("\n");
142
+ }
143
+ /**
144
+ * Create a standalone proc backend VFS.
145
+ * All paths are relative to /proc (e.g. "self/fd", "1/environ", "mounts").
146
+ * Mount at /proc via MountTable.
147
+ */
148
+ export function createProcBackend(options) {
149
+ const kernelHostname = encodeText(`${options.hostname ?? "sandbox"}\n`);
150
+ const getProcess = (pid) => {
151
+ const entry = options.processTable.get(pid);
152
+ if (!entry)
153
+ throw new KernelError("ENOENT", `no such process ${pid}`);
154
+ return entry;
155
+ };
156
+ const listPids = () => Array.from(options.processTable.listProcesses().keys()).sort((a, b) => a - b);
157
+ const listOpenFds = (pid) => {
158
+ const table = options.fdTableManager.get(pid);
159
+ if (!table)
160
+ return [];
161
+ const fds = [];
162
+ for (const entry of table)
163
+ fds.push(entry.fd);
164
+ return fds.sort((a, b) => a - b);
165
+ };
166
+ const getFdEntry = (pid, fd) => {
167
+ const table = options.fdTableManager.get(pid);
168
+ const entry = table?.get(fd);
169
+ if (!entry)
170
+ throw new KernelError("ENOENT", `no such fd ${fd} for process ${pid}`);
171
+ return entry;
172
+ };
173
+ const getLinkTarget = (pid, tail) => {
174
+ if (tail.length === 1 && tail[0] === "cwd")
175
+ return getProcess(pid).cwd;
176
+ if (tail.length === 1 && tail[0] === "exe")
177
+ return resolveExecPath(getProcess(pid).command);
178
+ if (tail.length === 2 && tail[0] === "fd") {
179
+ const fd = Number(tail[1]);
180
+ if (!Number.isInteger(fd) || fd < 0)
181
+ throw new KernelError("ENOENT", `invalid fd ${tail[1]}`);
182
+ return getFdEntry(pid, fd).description.path;
183
+ }
184
+ throw new KernelError("ENOENT", `unsupported proc link ${tail.join("/")}`);
185
+ };
186
+ const getProcFile = (pid, tail) => {
187
+ if (tail.length === 1 && tail[0] === "cwd")
188
+ return encodeText(getProcess(pid).cwd);
189
+ if (tail.length === 1 && tail[0] === "exe")
190
+ return encodeText(resolveExecPath(getProcess(pid).command));
191
+ if (tail.length === 1 && tail[0] === "environ")
192
+ return encodeEnviron(getProcess(pid).env);
193
+ if (tail.length === 2 && tail[0] === "fd")
194
+ return encodeText(getLinkTarget(pid, tail));
195
+ throw new KernelError("ENOENT", `unsupported proc file ${tail.join("/")}`);
196
+ };
197
+ const getMountsContent = () => {
198
+ if (!options.mountTable) {
199
+ return encodeText("rootfs / rootfs rw 0 0\n");
200
+ }
201
+ return encodeText(formatMounts(options.mountTable.getMounts()));
202
+ };
203
+ const getProcStat = (path, followSymlinks) => {
204
+ // Root /proc directory
205
+ if (path === "")
206
+ return dirStat("proc");
207
+ // /proc/self symlink
208
+ if (path === "self") {
209
+ return followSymlinks
210
+ ? dirStat("proc-self")
211
+ : linkStat("proc-self-link", "self");
212
+ }
213
+ // /proc/mounts
214
+ if (path === "mounts") {
215
+ const content = getMountsContent();
216
+ return fileStat("proc:mounts", content.length);
217
+ }
218
+ // /proc/sys tree
219
+ if (path === "sys")
220
+ return dirStat("proc:sys");
221
+ if (path === "sys/kernel")
222
+ return dirStat("proc:sys:kernel");
223
+ if (path === "sys/kernel/hostname") {
224
+ return fileStat("proc:sys:kernel:hostname", kernelHostname.length);
225
+ }
226
+ // /proc/[pid]/...
227
+ const parsed = parsePidPath(path);
228
+ if (!parsed)
229
+ notFound(path);
230
+ const { pid, tail } = parsed;
231
+ getProcess(pid);
232
+ if (tail.length === 0)
233
+ return dirStat(`proc:${pid}`);
234
+ if (tail.length === 1 && tail[0] === "fd")
235
+ return dirStat(`proc:${pid}:fd`);
236
+ if (tail.length === 1 && tail[0] === "environ") {
237
+ return fileStat(`proc:${pid}:environ`, encodeEnviron(getProcess(pid).env).length);
238
+ }
239
+ if ((tail.length === 1 && (tail[0] === "cwd" || tail[0] === "exe")) ||
240
+ (tail.length === 2 && tail[0] === "fd")) {
241
+ const target = getLinkTarget(pid, tail);
242
+ if (!followSymlinks)
243
+ return linkStat(`proc:${pid}:${tail.join(":")}`, target);
244
+ // For symlinks when following, return file stat for the target
245
+ return linkStat(`proc:${pid}:${tail.join(":")}`, target);
246
+ }
247
+ notFound(path);
248
+ };
249
+ const backend = {
250
+ async readFile(path) {
251
+ // Directories
252
+ if (path === "" ||
253
+ path === "self" ||
254
+ path === "sys" ||
255
+ path === "sys/kernel") {
256
+ throw new KernelError("EISDIR", `illegal operation on a directory, read '/proc/${path}'`);
257
+ }
258
+ // /proc/mounts
259
+ if (path === "mounts")
260
+ return getMountsContent();
261
+ // /proc/sys/kernel/hostname
262
+ if (path === "sys/kernel/hostname")
263
+ return kernelHostname;
264
+ // /proc/[pid]/...
265
+ const parsed = parsePidPath(path);
266
+ if (!parsed)
267
+ notFound(path);
268
+ const { pid, tail } = parsed;
269
+ if (tail.length === 0 || (tail.length === 1 && tail[0] === "fd")) {
270
+ throw new KernelError("EISDIR", `illegal operation on a directory, read '/proc/${path}'`);
271
+ }
272
+ return getProcFile(pid, tail);
273
+ },
274
+ async pread(path, offset, length) {
275
+ const content = await this.readFile(path);
276
+ if (offset >= content.length)
277
+ return new Uint8Array(0);
278
+ return content.slice(offset, offset + length);
279
+ },
280
+ async readTextFile(path) {
281
+ const content = await this.readFile(path);
282
+ return new TextDecoder().decode(content);
283
+ },
284
+ async readDir(path) {
285
+ return (await this.readDirWithTypes(path)).map((entry) => entry.name);
286
+ },
287
+ async readDirWithTypes(path) {
288
+ if (path === "") {
289
+ return [
290
+ ...PROC_ROOT_ENTRIES,
291
+ ...listPids().map((pid) => ({
292
+ name: String(pid),
293
+ isDirectory: true,
294
+ })),
295
+ ];
296
+ }
297
+ if (path === "sys")
298
+ return PROC_SYS_ENTRIES;
299
+ if (path === "sys/kernel")
300
+ return PROC_SYS_KERNEL_ENTRIES;
301
+ if (path === "self") {
302
+ throw new KernelError("ENOENT", `no such file or directory: /proc/${path}`);
303
+ }
304
+ const parsed = parsePidPath(path);
305
+ if (!parsed)
306
+ throw new KernelError("ENOENT", `no such file or directory: /proc/${path}`);
307
+ const { pid, tail } = parsed;
308
+ getProcess(pid);
309
+ if (tail.length === 0)
310
+ return PROC_PID_ENTRIES;
311
+ if (tail.length === 1 && tail[0] === "fd") {
312
+ return listOpenFds(pid).map((fd) => ({
313
+ name: String(fd),
314
+ isDirectory: false,
315
+ isSymbolicLink: true,
316
+ }));
317
+ }
318
+ throw new KernelError("ENOTDIR", `not a directory: /proc/${path}`);
319
+ },
320
+ async writeFile(path, _content) {
321
+ rejectWrite(path);
322
+ },
323
+ async createDir(path) {
324
+ rejectWrite(path);
325
+ },
326
+ async mkdir(path, _options) {
327
+ rejectWrite(path);
328
+ },
329
+ async exists(path) {
330
+ if (path === "" || path === "self" || path === "mounts")
331
+ return true;
332
+ if (path === "sys" ||
333
+ path === "sys/kernel" ||
334
+ path === "sys/kernel/hostname") {
335
+ return true;
336
+ }
337
+ const parsed = parsePidPath(path);
338
+ if (!parsed)
339
+ return false;
340
+ const { pid, tail } = parsed;
341
+ if (!options.processTable.get(pid))
342
+ return false;
343
+ if (tail.length === 0 || (tail.length === 1 && tail[0] === "fd"))
344
+ return true;
345
+ if (tail.length === 1 &&
346
+ (tail[0] === "cwd" || tail[0] === "exe" || tail[0] === "environ"))
347
+ return true;
348
+ if (tail.length === 2 && tail[0] === "fd") {
349
+ const fd = Number(tail[1]);
350
+ return (Number.isInteger(fd) &&
351
+ fd >= 0 &&
352
+ options.fdTableManager.get(pid)?.get(fd) !== undefined);
353
+ }
354
+ return false;
355
+ },
356
+ async stat(path) {
357
+ return getProcStat(path, true);
358
+ },
359
+ async removeFile(path) {
360
+ rejectWrite(path);
361
+ },
362
+ async removeDir(path) {
363
+ rejectWrite(path);
364
+ },
365
+ async rename(_oldPath, _newPath) {
366
+ throw new KernelError("EPERM", "cannot rename in /proc");
367
+ },
368
+ async realpath(path) {
369
+ if (path === "" || path === "mounts")
370
+ return path;
371
+ if (path === "self")
372
+ return path;
373
+ if (path === "sys" ||
374
+ path === "sys/kernel" ||
375
+ path === "sys/kernel/hostname") {
376
+ return path;
377
+ }
378
+ const parsed = parsePidPath(path);
379
+ if (!parsed)
380
+ notFound(path);
381
+ const { pid, tail } = parsed;
382
+ getProcess(pid);
383
+ if (tail.length === 0 || (tail.length === 1 && tail[0] === "fd"))
384
+ return path;
385
+ if (tail.length === 1 && tail[0] === "environ")
386
+ return path;
387
+ if ((tail.length === 1 && (tail[0] === "cwd" || tail[0] === "exe")) ||
388
+ (tail.length === 2 && tail[0] === "fd")) {
389
+ return getLinkTarget(pid, tail);
390
+ }
391
+ notFound(path);
392
+ },
393
+ async symlink(_target, _linkPath) {
394
+ throw new KernelError("EPERM", "cannot create symlink in /proc");
395
+ },
396
+ async readlink(path) {
397
+ if (path === "self")
398
+ return "self";
399
+ const parsed = parsePidPath(path);
400
+ if (!parsed)
401
+ throw new KernelError("EINVAL", `invalid argument: /proc/${path}`);
402
+ const { pid, tail } = parsed;
403
+ return getLinkTarget(pid, tail);
404
+ },
405
+ async lstat(path) {
406
+ return getProcStat(path, false);
407
+ },
408
+ async link(_oldPath, _newPath) {
409
+ throw new KernelError("EPERM", "cannot link in /proc");
410
+ },
411
+ async chmod(path, _mode) {
412
+ rejectWrite(path);
413
+ },
414
+ async chown(path, _uid, _gid) {
415
+ rejectWrite(path);
416
+ },
417
+ async utimes(path, _atime, _mtime) {
418
+ rejectWrite(path);
419
+ },
420
+ async truncate(path, _length) {
421
+ rejectWrite(path);
422
+ },
423
+ async pwrite(path, _offset, _data) {
424
+ rejectWrite(path);
425
+ },
426
+ };
427
+ return backend;
428
+ }
@@ -183,6 +183,7 @@ export function createProcessScopedFileSystem(vfs, pid) {
183
183
  utimes: (path, atime, mtime) => vfs.utimes(resolveProcSelfPath(path, pid), atime, mtime),
184
184
  truncate: (path, length) => vfs.truncate(resolveProcSelfPath(path, pid), length),
185
185
  pread: (path, offset, length) => vfs.pread(resolveProcSelfPath(path, pid), offset, length),
186
+ pwrite: (path, offset, data) => vfs.pwrite(resolveProcSelfPath(path, pid), offset, data),
186
187
  };
187
188
  }
188
189
  export function createProcLayer(vfs, options) {
@@ -496,6 +497,11 @@ export function createProcLayer(vfs, options) {
496
497
  rejectMutation(normalized);
497
498
  return vfs.truncate(clonePathArg(path, normalized), length);
498
499
  },
500
+ async pwrite(path, offset, data) {
501
+ const normalized = normalizePath(path);
502
+ rejectMutation(normalized);
503
+ return vfs.pwrite(clonePathArg(path, normalized), offset, data);
504
+ },
499
505
  };
500
506
  return wrapped;
501
507
  }
@@ -5,7 +5,7 @@
5
5
  * parent-child relationships, waitpid, and signal routing. A WasmVM
6
6
  * shell can waitpid on a Node child process.
7
7
  */
8
- import type { DriverProcess, ProcessContext, ProcessEntry, ProcessInfo, SignalHandler, ProcessSignalState } from "./types.js";
8
+ import type { DriverProcess, ProcessContext, ProcessEntry, ProcessInfo, SignalHandler, ProcessSignalState, KernelLogger } from "./types.js";
9
9
  export declare class ProcessTable {
10
10
  private entries;
11
11
  private nextPid;
@@ -13,10 +13,12 @@ export declare class ProcessTable {
13
13
  private zombieTimers;
14
14
  /** Pending alarm timers per PID: { timer, scheduledAt (ms epoch) }. */
15
15
  private alarmTimers;
16
+ private log;
16
17
  /** Called when a process exits, before waiters are notified. */
17
18
  onProcessExit: ((pid: number) => void) | null;
18
19
  /** Called when a zombie process is reaped (removed from the table). */
19
20
  onProcessReap: ((pid: number) => void) | null;
21
+ constructor(logger?: KernelLogger);
20
22
  /** Atomically allocate the next PID. */
21
23
  allocatePid(): number;
22
24
  /** Register a process with a pre-allocated PID. */
@@ -5,7 +5,7 @@
5
5
  * parent-child relationships, waitpid, and signal routing. A WasmVM
6
6
  * shell can waitpid on a Node child process.
7
7
  */
8
- import { KernelError, SIGCHLD, SIGALRM, SIGCONT, SIGSTOP, SIGTSTP, SIGKILL, WNOHANG, SA_RESETHAND, SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK } from "./types.js";
8
+ import { KernelError, SIGCHLD, SIGALRM, SIGCONT, SIGSTOP, SIGTSTP, SIGKILL, SIGWINCH, WNOHANG, SA_RESETHAND, SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK, noopKernelLogger } from "./types.js";
9
9
  import { WaitQueue } from "./wait.js";
10
10
  import { encodeExitStatus, encodeSignalStatus } from "./wstatus.js";
11
11
  const ZOMBIE_TTL_MS = 60_000;
@@ -16,10 +16,14 @@ export class ProcessTable {
16
16
  zombieTimers = new Map();
17
17
  /** Pending alarm timers per PID: { timer, scheduledAt (ms epoch) }. */
18
18
  alarmTimers = new Map();
19
+ log;
19
20
  /** Called when a process exits, before waiters are notified. */
20
21
  onProcessExit = null;
21
22
  /** Called when a zombie process is reaped (removed from the table). */
22
23
  onProcessReap = null;
24
+ constructor(logger) {
25
+ this.log = logger ?? noopKernelLogger;
26
+ }
23
27
  /** Atomically allocate the next PID. */
24
28
  allocatePid() {
25
29
  return this.nextPid++;
@@ -43,6 +47,7 @@ export class ProcessTable {
43
47
  exitCode: null,
44
48
  exitReason: null,
45
49
  termSignal: 0,
50
+ startTime: Date.now(),
46
51
  exitTime: null,
47
52
  env: { ...ctx.env },
48
53
  cwd: ctx.cwd,
@@ -61,6 +66,7 @@ export class ProcessTable {
61
66
  driverProcess,
62
67
  };
63
68
  this.entries.set(pid, entry);
69
+ this.log.debug({ pid, ppid: ctx.ppid, pgid, sid, driver, command, args }, "process registered");
64
70
  // Wire up exit callback to mark process as exited
65
71
  driverProcess.onExit = (code) => {
66
72
  this.markExited(pid, code);
@@ -90,6 +96,11 @@ export class ProcessTable {
90
96
  return;
91
97
  if (entry.status === "exited")
92
98
  return;
99
+ this.log.debug({
100
+ pid, exitCode, command: entry.command,
101
+ termSignal: entry.termSignal,
102
+ reason: entry.termSignal > 0 ? "signal" : "normal",
103
+ }, "process exited");
93
104
  entry.status = "exited";
94
105
  entry.exitCode = exitCode;
95
106
  entry.exitReason = entry.termSignal > 0 ? "signal" : "normal";
@@ -165,6 +176,7 @@ export class ProcessTable {
165
176
  if (signal < 0 || signal > 64) {
166
177
  throw new KernelError("EINVAL", `invalid signal ${signal}`);
167
178
  }
179
+ this.log.debug({ pid, signal }, "kill");
168
180
  if (pid < 0) {
169
181
  // Process group kill
170
182
  const pgid = -pid;
@@ -200,6 +212,7 @@ export class ProcessTable {
200
212
  */
201
213
  deliverSignal(entry, signal) {
202
214
  const { signalState } = entry;
215
+ this.log.trace({ pid: entry.pid, signal, command: entry.command }, "deliver signal");
203
216
  // SIGKILL and SIGSTOP always use default action — cannot be caught/blocked/ignored
204
217
  if (signal === SIGKILL || signal === SIGSTOP) {
205
218
  this.applyDefaultAction(entry, signal);
@@ -282,18 +295,21 @@ export class ProcessTable {
282
295
  /** Apply the kernel default action for a signal. */
283
296
  applyDefaultAction(entry, signal) {
284
297
  if (signal === SIGTSTP || signal === SIGSTOP) {
298
+ this.log.debug({ pid: entry.pid, signal, action: "stop" }, "signal default action");
285
299
  this.stop(entry.pid);
286
300
  entry.driverProcess.kill(signal);
287
301
  }
288
302
  else if (signal === SIGCONT) {
303
+ this.log.debug({ pid: entry.pid, signal, action: "continue" }, "signal default action");
289
304
  this.cont(entry.pid);
290
305
  entry.driverProcess.kill(signal);
291
306
  }
292
- else if (signal === SIGCHLD) {
293
- // Default SIGCHLD action: ignore (don't terminate)
307
+ else if (signal === SIGCHLD || signal === SIGWINCH) {
308
+ // Default action: ignore (POSIX — SIGCHLD and SIGWINCH don't terminate)
294
309
  return;
295
310
  }
296
311
  else {
312
+ this.log.debug({ pid: entry.pid, signal, action: "terminate", command: entry.command }, "signal default action");
297
313
  entry.termSignal = signal;
298
314
  entry.driverProcess.kill(signal);
299
315
  }
@@ -535,8 +551,12 @@ export class ProcessTable {
535
551
  sid: entry.sid,
536
552
  driver: entry.driver,
537
553
  command: entry.command,
554
+ args: entry.args,
555
+ cwd: entry.cwd,
538
556
  status: entry.status,
539
557
  exitCode: entry.exitCode,
558
+ startTime: entry.startTime,
559
+ exitTime: entry.exitTime,
540
560
  });
541
561
  }
542
562
  return result;
@@ -6,7 +6,7 @@
6
6
  * Writing to slave → readable from master (output direction).
7
7
  * Follows the same FileDescription/refCount pattern as PipeManager.
8
8
  */
9
- import type { FileDescription, Termios } from "./types.js";
9
+ import type { FileDescription, Termios, KernelLogger } from "./types.js";
10
10
  import { FILETYPE_CHARACTER_DEVICE } from "./types.js";
11
11
  import type { ProcessFDTable } from "./fd-table.js";
12
12
  export interface LineDisciplineConfig {
@@ -37,7 +37,8 @@ export declare class PtyManager {
37
37
  private onSignal;
38
38
  private nextPtyId;
39
39
  private nextPtyDescId;
40
- constructor(onSignal?: (pgid: number, signal: number, excludeLeaders: boolean) => number);
40
+ private log;
41
+ constructor(onSignal?: (pgid: number, signal: number, excludeLeaders: boolean) => number, logger?: KernelLogger);
41
42
  /**
42
43
  * Allocate a PTY pair. Returns two FileDescriptions:
43
44
  * one for the master and one for the slave.
@@ -6,7 +6,7 @@
6
6
  * Writing to slave → readable from master (output direction).
7
7
  * Follows the same FileDescription/refCount pattern as PipeManager.
8
8
  */
9
- import { FILETYPE_CHARACTER_DEVICE, O_RDWR, KernelError, defaultTermios, } from "./types.js";
9
+ import { FILETYPE_CHARACTER_DEVICE, O_RDWR, KernelError, defaultTermios, noopKernelLogger, } from "./types.js";
10
10
  /** Maximum buffered bytes per PTY direction before writes are rejected (EAGAIN). */
11
11
  export const MAX_PTY_BUFFER_BYTES = 65_536; // 64 KB
12
12
  /** Maximum canonical-mode line buffer size (POSIX MAX_CANON). */
@@ -23,8 +23,10 @@ export class PtyManager {
23
23
  onSignal;
24
24
  nextPtyId = 0;
25
25
  nextPtyDescId = 200_000; // High range to avoid FD/pipe ID collisions
26
- constructor(onSignal) {
26
+ log;
27
+ constructor(onSignal, logger) {
27
28
  this.onSignal = onSignal ?? null;
29
+ this.log = logger ?? noopKernelLogger;
28
30
  }
29
31
  /**
30
32
  * Allocate a PTY pair. Returns two FileDescriptions:
@@ -65,6 +67,7 @@ export class PtyManager {
65
67
  this.ptys.set(id, state);
66
68
  this.descToPty.set(masterDesc.id, { ptyId: id, end: "master" });
67
69
  this.descToPty.set(slaveDesc.id, { ptyId: id, end: "slave" });
70
+ this.log.debug({ ptyId: id, path, masterDescId: masterDesc.id, slaveDescId: slaveDesc.id }, "PTY created");
68
71
  return {
69
72
  master: { description: masterDesc, filetype: FILETYPE_CHARACTER_DEVICE },
70
73
  slave: { description: slaveDesc, filetype: FILETYPE_CHARACTER_DEVICE },
@@ -167,8 +170,10 @@ export class PtyManager {
167
170
  return;
168
171
  if (ref.end === "master") {
169
172
  state.closed.master = true;
173
+ this.log.debug({ ptyId: ref.ptyId, fgPgid: state.foregroundPgid }, "PTY master closed");
170
174
  // SIGHUP: when master closes, send SIGHUP to foreground process group
171
175
  if (state.foregroundPgid > 0 && this.onSignal) {
176
+ this.log.debug({ ptyId: ref.ptyId, pgid: state.foregroundPgid, signal: 1 }, "PTY SIGHUP delivery");
172
177
  try {
173
178
  this.onSignal(state.foregroundPgid, 1 /* SIGHUP */, false);
174
179
  }
@@ -244,6 +249,7 @@ export class PtyManager {
244
249
  const state = this.ptys.get(ptyId);
245
250
  if (!state)
246
251
  throw new KernelError("EBADF", "PTY not found");
252
+ this.log.trace({ ptyId, pgid, prev: state.foregroundPgid }, "PTY set foreground pgid");
247
253
  state.foregroundPgid = pgid;
248
254
  }
249
255
  /** Set the session leader pgid for SIGINT interception on this PTY. */
@@ -252,6 +258,7 @@ export class PtyManager {
252
258
  const state = this.ptys.get(ptyId);
253
259
  if (!state)
254
260
  throw new KernelError("EBADF", "PTY not found");
261
+ this.log.trace({ ptyId, pgid }, "PTY set session leader");
255
262
  state.sessionLeaderPgid = pgid;
256
263
  }
257
264
  /** Get terminal attributes for the PTY containing this description. */
@@ -276,6 +283,7 @@ export class PtyManager {
276
283
  const state = this.ptys.get(ptyId);
277
284
  if (!state)
278
285
  throw new KernelError("EBADF", "PTY not found");
286
+ this.log.trace({ ptyId, termios }, "PTY setTermios");
279
287
  if (termios.icrnl !== undefined)
280
288
  state.termios.icrnl = termios.icrnl;
281
289
  if (termios.opost !== undefined)
@@ -360,6 +368,7 @@ export class PtyManager {
360
368
  if (termios.isig) {
361
369
  const signal = this.signalForByte(state, byte);
362
370
  if (signal !== null) {
371
+ this.log.debug({ ptyId: state.id, signal, fgPgid: state.foregroundPgid, sessionLeader: state.sessionLeaderPgid }, "PTY signal char detected");
363
372
  if (termios.icanon)
364
373
  state.lineBuffer.length = 0;
365
374
  // Session-leader SIGINT interception: echo ^C, protect
@@ -383,6 +392,7 @@ export class PtyManager {
383
392
  // Signal delivery failure must not break line discipline
384
393
  }
385
394
  }
395
+ this.log.debug({ ptyId: state.id, childrenKilled, pgid: state.foregroundPgid }, "PTY session-leader SIGINT interception");
386
396
  // No children running → shell is at the prompt blocking on
387
397
  // fdRead. Inject a newline to unblock it and trigger a
388
398
  // fresh prompt.
@@ -401,6 +411,7 @@ export class PtyManager {
401
411
  }
402
412
  // Normal signal delivery (non-SIGINT or non-session-leader)
403
413
  if (state.foregroundPgid > 0) {
414
+ this.log.debug({ ptyId: state.id, signal, pgid: state.foregroundPgid }, "PTY signal delivery to foreground group");
404
415
  try {
405
416
  this.onSignal?.(state.foregroundPgid, signal, false);
406
417
  }