@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.
- package/dist/generated/isolate-runtime.d.ts +2 -2
- package/dist/generated/isolate-runtime.js +2 -2
- package/dist/index.d.ts +17 -4
- package/dist/index.js +10 -2
- package/dist/isolate-runtime/require-setup.js +1489 -239
- package/dist/isolate-runtime/setup-dynamic-import.js +31 -0
- package/dist/kernel/device-backend.d.ts +14 -0
- package/dist/kernel/device-backend.js +251 -0
- package/dist/kernel/device-layer.js +9 -0
- package/dist/kernel/file-lock.js +2 -3
- package/dist/kernel/index.d.ts +4 -4
- package/dist/kernel/index.js +3 -3
- package/dist/kernel/kernel.js +141 -122
- package/dist/kernel/mount-table.d.ts +75 -0
- package/dist/kernel/mount-table.js +353 -0
- package/dist/kernel/permissions.d.ts +9 -0
- package/dist/kernel/permissions.js +33 -1
- package/dist/kernel/proc-backend.d.ts +30 -0
- package/dist/kernel/proc-backend.js +428 -0
- package/dist/kernel/proc-layer.js +6 -0
- package/dist/kernel/process-table.d.ts +3 -1
- package/dist/kernel/process-table.js +23 -3
- package/dist/kernel/pty.d.ts +3 -2
- package/dist/kernel/pty.js +13 -2
- package/dist/kernel/socket-table.d.ts +7 -0
- package/dist/kernel/socket-table.js +99 -35
- package/dist/kernel/types.d.ts +45 -4
- package/dist/kernel/types.js +9 -0
- package/dist/kernel/vfs.d.ts +30 -2
- package/dist/kernel/vfs.js +19 -2
- package/dist/shared/api-types.d.ts +6 -0
- package/dist/shared/bridge-contract.d.ts +21 -3
- package/dist/shared/bridge-contract.js +2 -0
- package/dist/shared/console-formatter.js +8 -8
- package/dist/shared/global-exposure.js +95 -0
- package/dist/shared/in-memory-fs.d.ts +14 -59
- package/dist/shared/in-memory-fs.js +97 -597
- package/dist/shared/permissions.js +5 -0
- package/dist/test/block-store-conformance.d.ts +34 -0
- package/dist/test/block-store-conformance.js +251 -0
- package/dist/test/metadata-store-conformance.d.ts +37 -0
- package/dist/test/metadata-store-conformance.js +646 -0
- package/dist/test/vfs-conformance.d.ts +65 -0
- package/dist/test/vfs-conformance.js +842 -0
- package/dist/types.d.ts +1 -0
- package/dist/vfs/chunked-vfs.d.ts +66 -0
- package/dist/vfs/chunked-vfs.js +1290 -0
- package/dist/vfs/host-block-store.d.ts +19 -0
- package/dist/vfs/host-block-store.js +97 -0
- package/dist/vfs/memory-block-store.d.ts +16 -0
- package/dist/vfs/memory-block-store.js +45 -0
- package/dist/vfs/memory-metadata.d.ts +75 -0
- package/dist/vfs/memory-metadata.js +528 -0
- package/dist/vfs/sqlite-metadata.d.ts +91 -0
- package/dist/vfs/sqlite-metadata.js +582 -0
- package/dist/vfs/types.d.ts +210 -0
- package/dist/vfs/types.js +8 -0
- package/package.json +20 -1
- package/dist/kernel/inode-table.d.ts +0 -43
- 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
|
|
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;
|
package/dist/kernel/pty.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/dist/kernel/pty.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|