@sysid/sandbox-runtime-improved 0.0.61-sysid.1 → 0.0.64-sysid.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.
- package/README.md +76 -15
- package/dist/cli.js +12 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/sandbox/credential-mask-files.d.ts +100 -15
- package/dist/sandbox/credential-mask-files.d.ts.map +1 -1
- package/dist/sandbox/credential-mask-files.js +172 -20
- package/dist/sandbox/credential-mask-files.js.map +1 -1
- package/dist/sandbox/http-proxy.d.ts +1 -1
- package/dist/sandbox/http-proxy.d.ts.map +1 -1
- package/dist/sandbox/http-proxy.js +20 -0
- package/dist/sandbox/http-proxy.js.map +1 -1
- package/dist/sandbox/linux-sandbox-utils.d.ts +5 -0
- package/dist/sandbox/linux-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/linux-sandbox-utils.js +29 -19
- package/dist/sandbox/linux-sandbox-utils.js.map +1 -1
- package/dist/sandbox/linux-violation-monitor.d.ts +48 -0
- package/dist/sandbox/linux-violation-monitor.d.ts.map +1 -0
- package/dist/sandbox/linux-violation-monitor.js +156 -0
- package/dist/sandbox/linux-violation-monitor.js.map +1 -0
- package/dist/sandbox/macos-sandbox-utils.js +3 -3
- package/dist/sandbox/macos-sandbox-utils.js.map +1 -1
- package/dist/sandbox/mitm-ca.d.ts +52 -1
- package/dist/sandbox/mitm-ca.d.ts.map +1 -1
- package/dist/sandbox/mitm-ca.js +143 -11
- package/dist/sandbox/mitm-ca.js.map +1 -1
- package/dist/sandbox/mitm-leaf.d.ts +1 -1
- package/dist/sandbox/mitm-leaf.d.ts.map +1 -1
- package/dist/sandbox/mitm-leaf.js +21 -32
- package/dist/sandbox/mitm-leaf.js.map +1 -1
- package/dist/sandbox/sandbox-config.d.ts +231 -40
- package/dist/sandbox/sandbox-config.d.ts.map +1 -1
- package/dist/sandbox/sandbox-config.js +161 -32
- package/dist/sandbox/sandbox-config.js.map +1 -1
- package/dist/sandbox/sandbox-manager.d.ts +1 -1
- package/dist/sandbox/sandbox-manager.d.ts.map +1 -1
- package/dist/sandbox/sandbox-manager.js +331 -252
- package/dist/sandbox/sandbox-manager.js.map +1 -1
- package/dist/sandbox/sandbox-utils.d.ts +13 -0
- package/dist/sandbox/sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/sandbox-utils.js +32 -7
- package/dist/sandbox/sandbox-utils.js.map +1 -1
- package/dist/sandbox/windows-sandbox-utils.d.ts +353 -297
- package/dist/sandbox/windows-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/windows-sandbox-utils.js +516 -396
- package/dist/sandbox/windows-sandbox-utils.js.map +1 -1
- package/dist/utils/shell-quote.d.ts +27 -0
- package/dist/utils/shell-quote.d.ts.map +1 -0
- package/dist/utils/shell-quote.js +47 -0
- package/dist/utils/shell-quote.js.map +1 -0
- package/package.json +1 -3
- package/vendor/seccomp/arm64/apply-seccomp +0 -0
- package/vendor/seccomp/x64/apply-seccomp +0 -0
|
@@ -4,7 +4,7 @@ import { createMuxProxyServer } from './mux-proxy.js';
|
|
|
4
4
|
import { listenInRange } from './listen-in-range.js';
|
|
5
5
|
import { SentinelRegistry } from './credential-sentinel.js';
|
|
6
6
|
import { MaskedFileStore, buildMaskedFileBinds, } from './credential-mask-files.js';
|
|
7
|
-
import { createMitmCA, disposeMitmCA } from './mitm-ca.js';
|
|
7
|
+
import { createMitmCA, CRL_PATH, disposeMitmCA, } from './mitm-ca.js';
|
|
8
8
|
import { logForDebugging } from '../utils/debug.js';
|
|
9
9
|
import { whichSync } from '../utils/which.js';
|
|
10
10
|
import { getPlatform, getWslVersion } from '../utils/platform.js';
|
|
@@ -12,12 +12,14 @@ import * as fs from 'fs';
|
|
|
12
12
|
import { randomBytes, X509Certificate } from 'node:crypto';
|
|
13
13
|
import { wrapCommandWithSandboxLinux, initializeLinuxNetworkBridge, checkLinuxDependencies, cleanupBwrapMountPoints, } from './linux-sandbox-utils.js';
|
|
14
14
|
import { wrapCommandWithSandboxMacOS, startMacOSSandboxLogMonitor, } from './macos-sandbox-utils.js';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { startLinuxSandboxViolationMonitor, } from './linux-violation-monitor.js';
|
|
16
|
+
import { checkWindowsDependencies, wrapCommandWithSandboxWindows, parseWindowsBinShell, expandWindowsFsPaths, stampWindowsAcl, restoreWindowsAcl, grantWindowsAcl, revokeWindowsAcl, getWindowsSandboxUserStatus, getWindowsSandboxCaCert, verifyWindowsWfpEgress, resolveSrtWin, DEFAULT_WINDOWS_PROXY_PORT_RANGE, } from './windows-sandbox-utils.js';
|
|
17
|
+
import { getDefaultWritePaths, containsGlobChars, removeTrailingGlobSuffix, expandGlobPattern, ensureSandboxTmpdir, } from './sandbox-utils.js';
|
|
17
18
|
import { SandboxViolationStore } from './sandbox-violation-store.js';
|
|
18
19
|
import { canonicalizeHost, isValidHost, redactUrl, resolveParentProxy, } from './parent-proxy.js';
|
|
19
20
|
import { matchesDomainPattern } from './domain-pattern.js';
|
|
20
21
|
import { EOL } from 'node:os';
|
|
22
|
+
import { dirname } from 'node:path';
|
|
21
23
|
// ============================================================================
|
|
22
24
|
// Private Module State
|
|
23
25
|
// ============================================================================
|
|
@@ -29,27 +31,36 @@ let managerContext;
|
|
|
29
31
|
let initializationPromise;
|
|
30
32
|
let cleanupRegistered = false;
|
|
31
33
|
let logMonitorShutdown;
|
|
34
|
+
let linuxMonitor;
|
|
32
35
|
let parentProxy;
|
|
33
36
|
let mitmCA;
|
|
34
37
|
// Per-session proxy auth token. Generated at proxy start, exported only into
|
|
35
38
|
// the sandbox child env, checked on every CONNECT/request — so a host process
|
|
36
39
|
// dialing 127.0.0.1:<proxyPort> can't reach the filter callback.
|
|
37
40
|
let proxyAuthToken;
|
|
38
|
-
// Windows: the resolved
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// which engages the per-exec dir/file fence — and running `acl
|
|
42
|
-
// restore` at reset()).
|
|
41
|
+
// Windows: the resolved access set that was actually applied at
|
|
42
|
+
// initialize(). `undefined` means no stamp/grant was applied
|
|
43
|
+
// (gates running `acl restore`/`acl revoke` at reset()).
|
|
43
44
|
let windowsFsStampedSet;
|
|
44
|
-
// The
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
let windowsFsStampedGroup;
|
|
45
|
+
// The sandbox user SID captured at initialize(). reset() uses this
|
|
46
|
+
// so a config change between init and reset can't strand ACEs
|
|
47
|
+
// under a different SID.
|
|
48
|
+
let windowsFsSbUserSid;
|
|
49
49
|
// The RAW config inputs that produced `windowsFsStampedSet`.
|
|
50
50
|
// updateConfig() compares these (not the resolved set) so it never
|
|
51
51
|
// re-expands globs — see `sameWindowsStampSet`.
|
|
52
52
|
let windowsFsRawInputs;
|
|
53
|
+
// `verifyWindowsWfpEgress()` is once per PROCESS (it spawns a
|
|
54
|
+
// CreateProcessWithLogonW runner; first call may create the sandbox
|
|
55
|
+
// user's profile). The WFP fence is install-scoped, not config- or
|
|
56
|
+
// session-scoped — reset() does NOT clear this, so updateConfig()'s
|
|
57
|
+
// reset+reinit and the test suite's per-test reset() don't re-verify.
|
|
58
|
+
let windowsWfpVerified = false;
|
|
59
|
+
// Resolved once at initialize() (`resolveSrtWin` stats the disk).
|
|
60
|
+
// Captured so wrapWithSandboxArgv/reset() don't re-resolve per call
|
|
61
|
+
// and so reset()'s revoke/restore addresses the SAME binary the
|
|
62
|
+
// grants/stamps were applied with even if `config` mutated between.
|
|
63
|
+
let srtWinSpawn;
|
|
53
64
|
const sandboxViolationStore = new SandboxViolationStore();
|
|
54
65
|
// Per-session sentinel↔real-value map for masked credentials. Lives only in
|
|
55
66
|
// process memory; never written to disk or logged. Cleared on reset().
|
|
@@ -288,88 +299,165 @@ async function initialize(runtimeConfig, sandboxAskCallback, enableLogMonitor =
|
|
|
288
299
|
logMonitorShutdown = startMacOSSandboxLogMonitor(sandboxViolationStore.addViolation.bind(sandboxViolationStore), config.ignoreViolations);
|
|
289
300
|
logForDebugging('Started macOS sandbox log monitor');
|
|
290
301
|
}
|
|
302
|
+
if (enableLogMonitor && getPlatform() === 'linux') {
|
|
303
|
+
linuxMonitor = startLinuxSandboxViolationMonitor(sandboxViolationStore.addViolation.bind(sandboxViolationStore), {
|
|
304
|
+
// apply-seccomp's observer reports every write-intent syscall
|
|
305
|
+
// (allowed or not). Only paths bwrap would actually refuse — outside
|
|
306
|
+
// allowWrite or inside a denyWrite carve-out — go to the store.
|
|
307
|
+
allowWritePaths: [
|
|
308
|
+
...getDefaultWritePaths(),
|
|
309
|
+
...config.filesystem.allowWrite,
|
|
310
|
+
],
|
|
311
|
+
denyWritePaths: config.filesystem.denyWrite,
|
|
312
|
+
ignoreViolations: config.ignoreViolations,
|
|
313
|
+
});
|
|
314
|
+
// Don't block initialization on listen() — wrap-time checks
|
|
315
|
+
// fs.existsSync(observeSocketPath) and degrades gracefully.
|
|
316
|
+
void linuxMonitor.ready;
|
|
317
|
+
logForDebugging('Started Linux seccomp violation monitor');
|
|
318
|
+
}
|
|
291
319
|
// Register cleanup handlers first time
|
|
292
320
|
registerCleanup();
|
|
293
|
-
// Windows:
|
|
294
|
-
// child can be spawned.
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
// partial — exit 2 means at least one input was skipped):
|
|
298
|
-
// fail-closed at session start.
|
|
321
|
+
// Windows: validate provisioning + filesystem config BEFORE any
|
|
322
|
+
// sandboxed child can be spawned. Doing this at initialize() (not
|
|
323
|
+
// wrap-time) means the host gets a single actionable error before
|
|
324
|
+
// any per-exec work happens, instead of exit-15 on every command.
|
|
299
325
|
if (getPlatform() === 'windows') {
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
326
|
+
// Resolve once (stats disk); captured module-level for wrap/reset.
|
|
327
|
+
srtWinSpawn = resolveSrtWin(runtimeConfig.windows?.srtWin);
|
|
328
|
+
const srtWin = srtWinSpawn;
|
|
329
|
+
const u = getWindowsSandboxUserStatus({ srtWin });
|
|
330
|
+
if (!u.provisioned || !u.credPresent) {
|
|
331
|
+
config = undefined;
|
|
332
|
+
throw new Error(`Windows sandbox user is not provisioned (user=` +
|
|
333
|
+
`${u.provisioned}, cred=${u.credPresent}). Run \`npx ` +
|
|
334
|
+
`sandbox-runtime windows-install\` (one UAC prompt) to ` +
|
|
335
|
+
`provision it.`);
|
|
336
|
+
}
|
|
337
|
+
// Behavioral proof the WFP egress fence is active for the
|
|
338
|
+
// sandbox user — BFE enumeration (`wfp status`) is admin-gated,
|
|
339
|
+
// so this is the non-elevated readiness check. Fails closed: a
|
|
340
|
+
// stale install (user provisioned but filters since removed)
|
|
341
|
+
// throws here instead of running every exec with full egress.
|
|
342
|
+
// After the user-status check so the not-provisioned message is
|
|
343
|
+
// the actionable one. Once per process — the fence is install-
|
|
344
|
+
// scoped, not session-scoped.
|
|
345
|
+
if (!windowsWfpVerified) {
|
|
346
|
+
try {
|
|
347
|
+
await verifyWindowsWfpEgress({
|
|
348
|
+
proxyPortRange: runtimeConfig.windows?.proxyPortRange,
|
|
349
|
+
srtWin,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch (e) {
|
|
308
353
|
config = undefined;
|
|
309
|
-
throw
|
|
310
|
-
`provisioned (user=${u.provisioned}, cred=${u.credPresent}). ` +
|
|
311
|
-
`Run \`npx sandbox-runtime windows-install\` (one UAC ` +
|
|
312
|
-
`prompt) to provision it.`);
|
|
354
|
+
throw e;
|
|
313
355
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
`this session's CA (thumb=${sessionThumb}). Run ` +
|
|
343
|
-
`\`srt-win user trust-ca ${mitmCA.certPath}\` to ` +
|
|
344
|
-
`update it.`);
|
|
345
|
-
}
|
|
356
|
+
windowsWfpVerified = true;
|
|
357
|
+
}
|
|
358
|
+
// schannel-level trust under the sandbox user is install-time
|
|
359
|
+
// (cert lifecycle = sandbox-user lifecycle), not per-session.
|
|
360
|
+
// System32 curl / IWR / .NET / default-backend git only trust
|
|
361
|
+
// what's in the sandbox user's `CurrentUser\Root` — which
|
|
362
|
+
// `srt-win exec` does not (and must not) write. Compare
|
|
363
|
+
// thumbprints so a stale install-time CA doesn't pass the gate
|
|
364
|
+
// while schannel rejects the session's proxy-minted leaves.
|
|
365
|
+
if (runtimeConfig.network.tlsTerminate && mitmCA) {
|
|
366
|
+
const installed = getWindowsSandboxCaCert(u);
|
|
367
|
+
const sessionThumb = new X509Certificate(mitmCA.certPem).fingerprint
|
|
368
|
+
.replace(/:/g, '')
|
|
369
|
+
.toUpperCase();
|
|
370
|
+
if (!installed) {
|
|
371
|
+
config = undefined;
|
|
372
|
+
throw new Error(`tlsTerminate on Windows requires the sandbox to be ` +
|
|
373
|
+
`installed with this CA (thumb=${sessionThumb}): run ` +
|
|
374
|
+
`\`srt-win user trust-ca ${mitmCA.certPath}\`. Per-exec ` +
|
|
375
|
+
`installs into the sandbox user's Root store are not ` +
|
|
376
|
+
`supported.`);
|
|
377
|
+
}
|
|
378
|
+
if (installed.thumb !== sessionThumb) {
|
|
379
|
+
config = undefined;
|
|
380
|
+
throw new Error(`tlsTerminate on Windows: the sandbox's installed CA ` +
|
|
381
|
+
`(thumb=${installed.thumb}) doesn't match this ` +
|
|
382
|
+
`session's CA (thumb=${sessionThumb}). Run \`srt-win ` +
|
|
383
|
+
`user trust-ca ${mitmCA.certPath}\` to update it.`);
|
|
346
384
|
}
|
|
347
385
|
}
|
|
386
|
+
// Filesystem grants/denies — additive sandbox-user ACEs.
|
|
348
387
|
try {
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
388
|
+
const acc = computeWindowsFsAccessSet(runtimeConfig);
|
|
389
|
+
// The trust bundle the CA-trust env vars point at
|
|
390
|
+
// (NODE_EXTRA_CA_CERTS etc.) must be readable by the
|
|
391
|
+
// srt-sandbox child. It's written into the broker's %TEMP%,
|
|
392
|
+
// which the sandbox user has no inherent rights on, so it
|
|
393
|
+
// rides the same session-level `acl grant` read-set as the
|
|
394
|
+
// working tree. Granted on the mkdtemp DIR (not the file)
|
|
395
|
+
// so the (OI)(CI) ACE covers both the file open AND the
|
|
396
|
+
// parent-directory list that cmd's `type`/`FindFirstFile`
|
|
397
|
+
// does before opening. Mirrors the mac/linux
|
|
398
|
+
// `expandedAllowRead` push in wrapWithSandbox.
|
|
399
|
+
if (mitmCA) {
|
|
400
|
+
acc.grantRead.push(dirname(mitmCA.trustBundlePath));
|
|
401
|
+
}
|
|
402
|
+
// `u` was fetched once above for the provisioning gate; the
|
|
403
|
+
// same status carries the SID — don't re-spawn `srt-win user
|
|
404
|
+
// status` here.
|
|
405
|
+
if (!u.sid) {
|
|
406
|
+
throw new Error('sandbox user SID missing from `srt-win user status` ' +
|
|
407
|
+
'(provisioned but in an inconsistent state)');
|
|
408
|
+
}
|
|
409
|
+
const sb = u.sid;
|
|
410
|
+
// Record module-level state BEFORE the first acl call so the
|
|
411
|
+
// catch's best-effort revoke/restore can address whatever
|
|
412
|
+
// partially landed.
|
|
413
|
+
windowsFsSbUserSid = sb;
|
|
414
|
+
// Grant FIRST so the sandbox user has working-tree access by
|
|
415
|
+
// the time the deny stamp runs. The two are independent
|
|
416
|
+
// refcounted state-DB sets keyed on the same holder PID.
|
|
417
|
+
if (acc.grantRead.length > 0 || acc.grantWrite.length > 0) {
|
|
418
|
+
grantWindowsAcl({
|
|
419
|
+
sandboxUserSid: sb,
|
|
420
|
+
read: acc.grantRead,
|
|
421
|
+
write: acc.grantWrite,
|
|
422
|
+
srtWin,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
if (acc.denyRead.length > 0 || acc.denyWrite.length > 0) {
|
|
352
426
|
stampWindowsAcl({
|
|
353
|
-
|
|
354
|
-
denyRead:
|
|
355
|
-
denyWrite:
|
|
427
|
+
sandboxUserSid: sb,
|
|
428
|
+
denyRead: acc.denyRead,
|
|
429
|
+
denyWrite: acc.denyWrite,
|
|
430
|
+
srtWin,
|
|
356
431
|
});
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
432
|
+
}
|
|
433
|
+
// Only record when something was actually applied — gates
|
|
434
|
+
// running revoke/restore at reset(). Recorded AFTER success —
|
|
435
|
+
// the catch below clears `config`, and a non-undefined
|
|
436
|
+
// stampedSet would leave reset()/updateConfig() seeing state
|
|
437
|
+
// that never landed.
|
|
438
|
+
const anyApplied = acc.grantRead.length > 0 ||
|
|
439
|
+
acc.grantWrite.length > 0 ||
|
|
440
|
+
acc.denyRead.length > 0 ||
|
|
441
|
+
acc.denyWrite.length > 0;
|
|
442
|
+
if (anyApplied) {
|
|
443
|
+
windowsFsStampedSet = acc;
|
|
444
|
+
logForDebugging(`[Sandbox Windows] fs applied: ` +
|
|
445
|
+
`${acc.grantWrite.length} grantWrite, ` +
|
|
446
|
+
`${acc.grantRead.length} grantRead, ` +
|
|
447
|
+
`${acc.denyRead.length} denyRead, ` +
|
|
448
|
+
`${acc.denyWrite.length} denyWrite`);
|
|
365
449
|
}
|
|
366
450
|
windowsFsRawInputs = rawWindowsFsInputs(runtimeConfig);
|
|
367
451
|
}
|
|
368
452
|
catch (e) {
|
|
369
|
-
// Best-effort release of whatever WAS
|
|
370
|
-
// failure (exit-2 partial stamps the resolvable
|
|
371
|
-
// harmless if nothing was
|
|
372
|
-
|
|
453
|
+
// Best-effort release of whatever WAS applied before the
|
|
454
|
+
// failure (exit-2 partial stamps/grants the resolvable
|
|
455
|
+
// inputs; harmless if nothing was — no holds for this PID).
|
|
456
|
+
if (windowsFsSbUserSid) {
|
|
457
|
+
revokeWindowsAcl({ sandboxUserSid: windowsFsSbUserSid, srtWin });
|
|
458
|
+
restoreWindowsAcl({ sandboxUserSid: windowsFsSbUserSid, srtWin });
|
|
459
|
+
}
|
|
460
|
+
windowsFsSbUserSid = undefined;
|
|
373
461
|
config = undefined;
|
|
374
462
|
throw e;
|
|
375
463
|
}
|
|
@@ -402,6 +490,18 @@ async function initialize(runtimeConfig, sandboxAskCallback, enableLogMonitor =
|
|
|
402
490
|
: undefined;
|
|
403
491
|
const httpProxyPort = config.network.httpProxyPort ?? muxPort;
|
|
404
492
|
const socksProxyPort = config.network.socksProxyPort ?? muxPort;
|
|
493
|
+
// Leaves are minted lazily per-CONNECT (after this point), so setting
|
|
494
|
+
// the CDP URL now means every leaf carries it. See MitmCA.crlUrl.
|
|
495
|
+
// Windows-only: on Linux the child runs under bwrap --unshare-net and
|
|
496
|
+
// reaches the proxy via a socat bridge on a fixed netns port, so a
|
|
497
|
+
// host-namespace mux port would be unreachable — worse than no CDP,
|
|
498
|
+
// since a Schannel-analog client (Java, OpenSSL with CRL_CHECK) then
|
|
499
|
+
// hard-fails "CRL fetch error" instead of soft-passing "no CDP". macOS
|
|
500
|
+
// has no in-tree Schannel-analog client. Also gated on `muxPort`: an
|
|
501
|
+
// external `network.httpProxyPort` doesn't answer /srt.crl.
|
|
502
|
+
if (mitmCA && muxPort !== undefined && getPlatform() === 'windows') {
|
|
503
|
+
mitmCA.crlUrl = `http://127.0.0.1:${muxPort}${CRL_PATH}`;
|
|
504
|
+
}
|
|
405
505
|
if (config.network.httpProxyPort !== undefined) {
|
|
406
506
|
logForDebugging(`Using external HTTP proxy on port ${httpProxyPort}`);
|
|
407
507
|
}
|
|
@@ -444,16 +544,6 @@ function isSupportedPlatform() {
|
|
|
444
544
|
}
|
|
445
545
|
return platform === 'macos' || platform === 'windows';
|
|
446
546
|
}
|
|
447
|
-
/**
|
|
448
|
-
* Resolve the Windows group reference from config. Used by both the
|
|
449
|
-
* dependency check and `wrapWithSandbox` so they agree.
|
|
450
|
-
*/
|
|
451
|
-
function getWindowsGroupRef() {
|
|
452
|
-
return {
|
|
453
|
-
groupName: config?.windows?.groupName ?? DEFAULT_WINDOWS_GROUP_NAME,
|
|
454
|
-
groupSid: config?.windows?.groupSid,
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
547
|
function isSandboxingEnabled() {
|
|
458
548
|
// Sandboxing is enabled if config has been set (via initialize())
|
|
459
549
|
return config !== undefined;
|
|
@@ -487,7 +577,18 @@ function checkDependencies(ripgrepConfig) {
|
|
|
487
577
|
warnings.push(...linuxDeps.warnings);
|
|
488
578
|
}
|
|
489
579
|
else if (platform === 'windows') {
|
|
490
|
-
|
|
580
|
+
let srtWin;
|
|
581
|
+
try {
|
|
582
|
+
srtWin = resolveSrtWin(config?.windows?.srtWin);
|
|
583
|
+
}
|
|
584
|
+
catch (e) {
|
|
585
|
+
errors.push(e.message);
|
|
586
|
+
return { errors, warnings };
|
|
587
|
+
}
|
|
588
|
+
const winDeps = checkWindowsDependencies({
|
|
589
|
+
sublayerGuid: config?.windows?.sublayerGuid ?? config?.windows?.wfpSublayerGuid,
|
|
590
|
+
srtWin,
|
|
591
|
+
});
|
|
491
592
|
errors.push(...winDeps.errors);
|
|
492
593
|
warnings.push(...winDeps.warnings);
|
|
493
594
|
}
|
|
@@ -539,10 +640,13 @@ function getCredentialRestrictions(credentials, allowedDomains) {
|
|
|
539
640
|
// Masked files: read the real bytes on the host, register a sentinel,
|
|
540
641
|
// write it to a fake file in the manager-owned temp dir. Missing/unreadable
|
|
541
642
|
// entries are skipped (same posture as an unset masked env var).
|
|
643
|
+
// degradeToDenyPaths carries paths whose extract pattern matched
|
|
644
|
+
// nothing with onExtractNoMatch: "deny" — merged into denyReadPaths
|
|
645
|
+
// below so both the read-deny config and the platform builders see them.
|
|
542
646
|
const files = credentials.files ?? [];
|
|
543
|
-
const maskedFileBinds = buildMaskedFileBinds(files, allowedDomains ?? [], sentinelRegistry, maskedFileStore);
|
|
647
|
+
const { binds: maskedFileBinds, degradeToDenyPaths } = buildMaskedFileBinds(files, allowedDomains ?? [], sentinelRegistry, maskedFileStore);
|
|
544
648
|
return {
|
|
545
|
-
denyReadPaths,
|
|
649
|
+
denyReadPaths: [...new Set([...denyReadPaths, ...degradeToDenyPaths])],
|
|
546
650
|
unsetEnvVars: [...new Set(unsetEnvVars)],
|
|
547
651
|
setEnvVars,
|
|
548
652
|
maskedFileBinds,
|
|
@@ -560,13 +664,6 @@ function getCredentialDenyReadPaths(credentials) {
|
|
|
560
664
|
const files = credentials?.files ?? [];
|
|
561
665
|
return [...new Set(files.filter(f => f.mode === 'deny').map(f => f.path))];
|
|
562
666
|
}
|
|
563
|
-
/** Order-insensitive string-set equality. */
|
|
564
|
-
function setEq(a, b) {
|
|
565
|
-
if (a.length !== b.length)
|
|
566
|
-
return false;
|
|
567
|
-
const bs = new Set(b);
|
|
568
|
-
return a.every(v => bs.has(v));
|
|
569
|
-
}
|
|
570
667
|
/**
|
|
571
668
|
* Union the explicit `filesystem.denyRead` with credential-derived
|
|
572
669
|
* deny paths. The single source of "what files does this config
|
|
@@ -650,78 +747,81 @@ function getFsWriteConfig() {
|
|
|
650
747
|
};
|
|
651
748
|
}
|
|
652
749
|
/**
|
|
653
|
-
* Build the Windows file-
|
|
654
|
-
*
|
|
655
|
-
* appearing after this returns is NOT
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
* `denyRead` ← `filesystem.denyRead` ∪ `credentials`-derived deny
|
|
660
|
-
* paths (via {@link getCredentialDenyReadPaths}).
|
|
661
|
-
* `denyWrite` ← `filesystem.denyWrite`.
|
|
750
|
+
* Build the Windows file-access set (deny stamps + sandbox-user
|
|
751
|
+
* grants) from `runtimeConfig`. Globs are expanded to concrete
|
|
752
|
+
* paths (point-in-time — a path appearing after this returns is NOT
|
|
753
|
+
* covered). Directory targets are accepted (the `(OI)(CI)` ACEs
|
|
754
|
+
* cover the subtree).
|
|
662
755
|
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
756
|
+
* The sandbox user has no inherent rights on real-user-owned files,
|
|
757
|
+
* so `allowWrite` (the working-tree roots) becomes a per-session
|
|
758
|
+
* `MODIFY_NO_FDC` ALLOW ACE for `<sb-SID>`, `allowRead` a
|
|
759
|
+
* `READ|EXECUTE` ALLOW ACE, and `denyRead`/`denyWrite` become an
|
|
760
|
+
* explicit DENY ACE for `<sb-SID>` on the target plus a
|
|
761
|
+
* `(OI)(CI) FILE_DELETE_CHILD` DENY on its parent.
|
|
669
762
|
*/
|
|
670
|
-
function
|
|
763
|
+
function computeWindowsFsAccessSet(c) {
|
|
671
764
|
const fs = c.filesystem;
|
|
672
765
|
// filesystem.disabled bypasses ALL filesystem rule generation —
|
|
673
766
|
// same as the macOS/Linux wrapWithSandbox path (readConfig /
|
|
674
767
|
// writeConfig left undefined). On Windows this means no ACL
|
|
675
|
-
// stamp; credential FILE denies are dropped along with the
|
|
676
|
-
// (credential ENV
|
|
677
|
-
//
|
|
678
|
-
//
|
|
768
|
+
// stamp/grant; credential FILE denies are dropped along with the
|
|
769
|
+
// rest (credential ENV: mode:'deny' is structural under the
|
|
770
|
+
// fresh srt-sandbox env; mode:'mask' sentinels are passed via
|
|
771
|
+
// the --env overlay).
|
|
679
772
|
if (fs?.disabled) {
|
|
680
|
-
return { denyRead: [], denyWrite: [] };
|
|
773
|
+
return { grantRead: [], grantWrite: [], denyRead: [], denyWrite: [] };
|
|
681
774
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
`on Windows. Remove the entries or narrow filesystem.denyRead to ` +
|
|
685
|
-
`exclude them.`);
|
|
686
|
-
}
|
|
687
|
-
if (fs?.allowWrite?.length) {
|
|
688
|
-
throw new Error(`filesystem.allowWrite is not supported on Windows — the Windows ` +
|
|
689
|
-
`sandbox is deny-listed only (the child writes wherever the host ` +
|
|
690
|
-
`user can, minus filesystem.denyWrite). Remove the allowWrite ` +
|
|
691
|
-
`entries.`);
|
|
692
|
-
}
|
|
693
|
-
const denyRead = expandWindowsFsDenyPaths([
|
|
775
|
+
const expand = expandWindowsFsPaths;
|
|
776
|
+
const denyRead = expand([
|
|
694
777
|
...new Set([
|
|
695
778
|
...(fs?.denyRead ?? []),
|
|
696
779
|
...getCredentialDenyReadPaths(c.credentials),
|
|
697
780
|
]),
|
|
698
781
|
]);
|
|
699
|
-
const denyWrite =
|
|
700
|
-
return {
|
|
782
|
+
const denyWrite = expand(fs?.denyWrite ?? []);
|
|
783
|
+
return {
|
|
784
|
+
// `allowRead` also serves as `allowWithinDeny`: a file under a
|
|
785
|
+
// denied dir gets an explicit ALLOW ACE for the sandbox user,
|
|
786
|
+
// and explicit DENY on the parent doesn't override it because
|
|
787
|
+
// the recompose chokepoint orders deny-before-allow per-path.
|
|
788
|
+
grantRead: expand(fs?.allowRead ?? []),
|
|
789
|
+
grantWrite: expand(fs?.allowWrite ?? []),
|
|
790
|
+
denyRead,
|
|
791
|
+
denyWrite,
|
|
792
|
+
};
|
|
701
793
|
}
|
|
702
794
|
/**
|
|
703
795
|
* Snapshot the raw config fields that feed
|
|
704
|
-
* {@link
|
|
796
|
+
* {@link computeWindowsFsAccessSet}. Used by updateConfig() to
|
|
705
797
|
* short-circuit the resolved-set diff (which re-runs glob
|
|
706
798
|
* expansion) when nothing relevant changed.
|
|
707
799
|
*/
|
|
708
800
|
function rawWindowsFsInputs(c) {
|
|
709
|
-
// Keyed exactly on what {@link
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
// (only mask injectHosts), so a network-only updateConfig
|
|
713
|
-
// hits the cache.
|
|
801
|
+
// Keyed exactly on what {@link computeWindowsFsAccessSet} reads.
|
|
802
|
+
// `network.allowedDomains` does NOT feed file-deny (only mask
|
|
803
|
+
// injectHosts), so a network-only updateConfig hits the cache.
|
|
714
804
|
return {
|
|
715
805
|
disabled: c.filesystem.disabled ?? false,
|
|
716
806
|
denyRead: [...c.filesystem.denyRead],
|
|
717
807
|
denyWrite: [...c.filesystem.denyWrite],
|
|
808
|
+
allowRead: [...(c.filesystem.allowRead ?? [])],
|
|
809
|
+
allowWrite: [...c.filesystem.allowWrite],
|
|
718
810
|
credFiles: getCredentialDenyReadPaths(c.credentials),
|
|
719
811
|
};
|
|
720
812
|
}
|
|
813
|
+
function setEq(a, b) {
|
|
814
|
+
if (a.length !== b.length)
|
|
815
|
+
return false;
|
|
816
|
+
const s = new Set(a);
|
|
817
|
+
return b.every(x => s.has(x));
|
|
818
|
+
}
|
|
721
819
|
function sameRawWindowsFsInputs(a, b) {
|
|
722
820
|
return (a.disabled === b.disabled &&
|
|
723
821
|
setEq(a.denyRead, b.denyRead) &&
|
|
724
822
|
setEq(a.denyWrite, b.denyWrite) &&
|
|
823
|
+
setEq(a.allowRead, b.allowRead) &&
|
|
824
|
+
setEq(a.allowWrite, b.allowWrite) &&
|
|
725
825
|
setEq(a.credFiles, b.credFiles));
|
|
726
826
|
}
|
|
727
827
|
/**
|
|
@@ -987,6 +1087,7 @@ async function wrapWithSandbox(command, binShell, customConfig, abortSignal) {
|
|
|
987
1087
|
seccompConfig: getSeccompConfig(),
|
|
988
1088
|
bwrapPath: config?.bwrapPath,
|
|
989
1089
|
socatPath: config?.socatPath,
|
|
1090
|
+
observeSocketPath: linuxMonitor?.observeSocketPath,
|
|
990
1091
|
abortSignal,
|
|
991
1092
|
});
|
|
992
1093
|
case 'windows':
|
|
@@ -1008,14 +1109,22 @@ async function wrapWithSandbox(command, binShell, customConfig, abortSignal) {
|
|
|
1008
1109
|
* `spawn(argv[0], argv.slice(1), {shell: false, env})`.
|
|
1009
1110
|
*
|
|
1010
1111
|
* On Windows this is the ONLY supported wrap method (see
|
|
1011
|
-
* {@link wrapWithSandbox}); `env`
|
|
1012
|
-
* sandboxed child
|
|
1013
|
-
*
|
|
1112
|
+
* {@link wrapWithSandbox}); `env` is the broker process's spawn env
|
|
1113
|
+
* — the sandboxed child gets a fresh `srt-sandbox` profile env with
|
|
1114
|
+
* only the `--env` overlay baked into `argv` (see
|
|
1115
|
+
* {@link wrapCommandWithSandboxWindows}). On
|
|
1014
1116
|
* macOS/Linux `argv` is `[binShell, '-c', <wrapWithSandbox result>]`
|
|
1015
1117
|
* (proxy env is baked into that command) and `env` is the unchanged
|
|
1016
1118
|
* `process.env`, so callers can spawn uniformly across platforms.
|
|
1119
|
+
*
|
|
1120
|
+
* @param cwd the working directory the caller will spawn the result
|
|
1121
|
+
* with. On Windows the child's cwd is whatever the caller passes
|
|
1122
|
+
* as the spawn `{cwd:}` option (there is no `--cwd` flag), and
|
|
1123
|
+
* the `safe.directory` git-config injection derives from this — so
|
|
1124
|
+
* pass the same value here as to `spawn({cwd})`. Defaults to
|
|
1125
|
+
* `process.cwd()`. Currently unused on macOS/Linux.
|
|
1017
1126
|
*/
|
|
1018
|
-
async function wrapWithSandboxArgv(command, binShell, customConfig, abortSignal) {
|
|
1127
|
+
async function wrapWithSandboxArgv(command, binShell, customConfig, abortSignal, cwd) {
|
|
1019
1128
|
const platform = getPlatform();
|
|
1020
1129
|
if (platform === 'windows') {
|
|
1021
1130
|
const hasNetworkConfig = customConfig?.network?.allowedDomains !== undefined ||
|
|
@@ -1026,44 +1135,34 @@ async function wrapWithSandboxArgv(command, binShell, customConfig, abortSignal)
|
|
|
1026
1135
|
const credentialRestrictions = getCredentialRestrictions(customConfig?.credentials ?? config?.credentials, customConfig?.network?.allowedDomains ?? config?.network?.allowedDomains);
|
|
1027
1136
|
// Per-exec FILE denies (customConfig only — the session-level
|
|
1028
1137
|
// config's denies were already stamped at initialize()).
|
|
1029
|
-
//
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1034
|
-
//
|
|
1035
|
-
// expand-and-drop-missing was for tolerant point-in-time
|
|
1036
|
-
// globs at init; per-exec is "deny THIS one command" and a
|
|
1037
|
-
// path that doesn't resolve is a bug the caller must see).
|
|
1138
|
+
// Paths go through `expandWindowsFsPaths` — the SAME
|
|
1139
|
+
// chokepoint the session-level set uses (point-in-time glob
|
|
1140
|
+
// expand, normalize, missing→drop) — so a per-exec entry
|
|
1141
|
+
// resolves identically to its session-level equivalent.
|
|
1142
|
+
// macOS/Linux per-exec already reuses session-level expansion;
|
|
1143
|
+
// Windows now matches.
|
|
1038
1144
|
//
|
|
1039
1145
|
// The dedup against `windowsFsStampedSet` is an OPTIMIZATION,
|
|
1040
1146
|
// not a correctness gate: re-stamping a session-held path
|
|
1041
|
-
// under the exec's distinct holder is refcount-safe but
|
|
1042
|
-
//
|
|
1043
|
-
// hardlink-alias guard lives in srt-win's `ensure_stamped`
|
|
1044
|
-
// (`refuse_escalation = true`), NOT here — canonical-path
|
|
1045
|
-
// identity and concurrent holders are only visible to Rust.
|
|
1147
|
+
// under the exec's distinct holder is refcount-safe but wastes
|
|
1148
|
+
// a SetSecurityInfo round-trip.
|
|
1046
1149
|
//
|
|
1047
1150
|
// filesystem.disabled bypasses ALL filesystem rule generation
|
|
1048
1151
|
// — including credential-derived file denies — same ordering
|
|
1049
|
-
// as session-level `
|
|
1050
|
-
//
|
|
1051
|
-
//
|
|
1052
|
-
//
|
|
1152
|
+
// as session-level `computeWindowsFsAccessSet` (credential
|
|
1153
|
+
// ENV: mode:'deny' is structural under the fresh srt-sandbox
|
|
1154
|
+
// env; mode:'mask' sentinels are passed via the --env
|
|
1155
|
+
// overlay).
|
|
1156
|
+
// Per-exec allowRead/allowWrite throw — `srt-win exec` only
|
|
1157
|
+
// exposes `--deny-*`; per-exec grants are not implemented.
|
|
1053
1158
|
const fsCfg = customConfig?.filesystem;
|
|
1054
1159
|
let perExecDenyRead = [];
|
|
1055
1160
|
let perExecDenyWrite = [];
|
|
1056
1161
|
if (!fsCfg?.disabled) {
|
|
1057
|
-
if (fsCfg?.allowRead?.length) {
|
|
1058
|
-
throw new Error(`Per-exec filesystem.allowRead
|
|
1059
|
-
`
|
|
1060
|
-
`
|
|
1061
|
-
}
|
|
1062
|
-
if (fsCfg?.allowWrite?.length) {
|
|
1063
|
-
throw new Error(`Per-exec filesystem.allowWrite is not supported on Windows — ` +
|
|
1064
|
-
`the Windows sandbox is deny-listed only (the child writes ` +
|
|
1065
|
-
`wherever the host user can, minus filesystem.denyWrite). ` +
|
|
1066
|
-
`Remove the allowWrite entries.`);
|
|
1162
|
+
if (fsCfg?.allowRead?.length || fsCfg?.allowWrite?.length) {
|
|
1163
|
+
throw new Error(`Per-exec filesystem.allowRead/allowWrite is not supported ` +
|
|
1164
|
+
`on Windows — \`srt-win exec\` only exposes per-exec ` +
|
|
1165
|
+
`denies. Set them at the session level (initialize()).`);
|
|
1067
1166
|
}
|
|
1068
1167
|
const rawRead = [
|
|
1069
1168
|
...(fsCfg?.denyRead ?? []),
|
|
@@ -1071,18 +1170,13 @@ async function wrapWithSandboxArgv(command, binShell, customConfig, abortSignal)
|
|
|
1071
1170
|
];
|
|
1072
1171
|
const rawWrite = fsCfg?.denyWrite ?? [];
|
|
1073
1172
|
// Skip on the dominant path (no per-exec fs or
|
|
1074
|
-
// credential-file deny)
|
|
1075
|
-
// `computeWindowsFsDenySet` (glob walk + statSync per
|
|
1076
|
-
// match) on every exec, including with
|
|
1077
|
-
// `customConfig === undefined`.
|
|
1173
|
+
// credential-file deny).
|
|
1078
1174
|
if (rawRead.length > 0 || rawWrite.length > 0) {
|
|
1079
1175
|
const sessRead = new Set(windowsFsStampedSet?.denyRead ?? []);
|
|
1080
1176
|
const sessWrite = new Set(windowsFsStampedSet?.denyWrite ?? []);
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
perExecDenyRead = norm(rawRead).filter(p => !sessRead.has(p));
|
|
1085
|
-
perExecDenyWrite = norm(rawWrite).filter(p => !sessRead.has(p) && !sessWrite.has(p));
|
|
1177
|
+
const expand = expandWindowsFsPaths;
|
|
1178
|
+
perExecDenyRead = expand(rawRead).filter(p => !sessRead.has(p));
|
|
1179
|
+
perExecDenyWrite = expand(rawWrite).filter(p => !sessRead.has(p) && !sessWrite.has(p));
|
|
1086
1180
|
}
|
|
1087
1181
|
}
|
|
1088
1182
|
// Per-exec deny rides on argv (`acl stamp` reads stdin, but
|
|
@@ -1090,37 +1184,31 @@ async function wrapWithSandboxArgv(command, binShell, customConfig, abortSignal)
|
|
|
1090
1184
|
// length check lives in `wrapCommandWithSandboxWindows`
|
|
1091
1185
|
// where the full argv (incl. shell + user command) is known.
|
|
1092
1186
|
//
|
|
1093
|
-
//
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
// sandbox's own proxy plumbing must survive a caller listing
|
|
1097
|
-
// e.g. HTTPS_PROXY as a denied credential). The `denyReadPaths`
|
|
1098
|
-
// half of the SESSION-level credentials is already unioned into
|
|
1099
|
-
// the stamp set at initialize() time via
|
|
1100
|
-
// `computeWindowsFsDenySet`.
|
|
1187
|
+
// The `denyReadPaths` half of the SESSION-level credentials
|
|
1188
|
+
// is already unioned into the stamp set at initialize() time
|
|
1189
|
+
// via `computeWindowsFsAccessSet`.
|
|
1101
1190
|
return wrapCommandWithSandboxWindows({
|
|
1102
1191
|
command,
|
|
1103
|
-
group: getWindowsGroupRef(),
|
|
1104
|
-
sublayerGuid: config?.windows?.wfpSublayerGuid,
|
|
1105
1192
|
httpProxyPort: hasNetworkConfig ? getProxyPort() : undefined,
|
|
1106
1193
|
socksProxyPort: hasNetworkConfig ? getSocksProxyPort() : undefined,
|
|
1107
1194
|
proxyAuthToken: hasNetworkConfig ? proxyAuthToken : undefined,
|
|
1108
|
-
|
|
1195
|
+
// mode:'deny' env vars are structurally absent (fresh
|
|
1196
|
+
// srt-sandbox profile env). mode:'mask' sentinels are
|
|
1197
|
+
// passed via the --env overlay so the sandboxed child sees
|
|
1198
|
+
// the sentinel value, same as macOS/Linux.
|
|
1109
1199
|
setEnvVars: credentialRestrictions.setEnvVars,
|
|
1110
|
-
// Engage the session-level fence only when this session
|
|
1111
|
-
// actually stamped — keeps `srt-win exec` standalone (no
|
|
1112
|
-
// state-DB dependency) when no file-deny is configured. The
|
|
1113
|
-
// per-exec deny below opens its own fence under the exec's
|
|
1114
|
-
// own PID regardless.
|
|
1115
|
-
holderPid: windowsFsStampedSet ? process.pid : undefined,
|
|
1116
1200
|
denyRead: perExecDenyRead,
|
|
1117
1201
|
denyWrite: perExecDenyWrite,
|
|
1118
|
-
//
|
|
1119
|
-
//
|
|
1120
|
-
//
|
|
1121
|
-
|
|
1202
|
+
// safe.directory: cwd + the resolved session-level write
|
|
1203
|
+
// grants — exactly the working-tree roots the sandbox user
|
|
1204
|
+
// has MODIFY on and where git will see real-user-owned files.
|
|
1205
|
+
cwd,
|
|
1206
|
+
allowWrite: windowsFsStampedSet?.grantWrite,
|
|
1122
1207
|
caCertPath: mitmCA?.trustBundlePath,
|
|
1123
1208
|
binShell: parseWindowsBinShell(binShell),
|
|
1209
|
+
srtWin: customConfig?.windows?.srtWin
|
|
1210
|
+
? resolveSrtWin(customConfig.windows.srtWin)
|
|
1211
|
+
: (srtWinSpawn ?? resolveSrtWin(config?.windows?.srtWin)),
|
|
1124
1212
|
});
|
|
1125
1213
|
}
|
|
1126
1214
|
// macOS/Linux: delegate to the existing string wrapper, then put
|
|
@@ -1140,7 +1228,7 @@ function getConfig() {
|
|
|
1140
1228
|
* Update the sandbox configuration in place.
|
|
1141
1229
|
*
|
|
1142
1230
|
* **Network/allowlist changes are a live swap**: the running
|
|
1143
|
-
*
|
|
1231
|
+
* mux proxy reads `config.network.allowedDomains` /
|
|
1144
1232
|
* `deniedDomains` per-request (via `filterNetworkRequest`), so
|
|
1145
1233
|
* reassigning `config` here takes effect on the next connection
|
|
1146
1234
|
* with no proxy rebind and no port change — on every platform,
|
|
@@ -1149,32 +1237,20 @@ function getConfig() {
|
|
|
1149
1237
|
*
|
|
1150
1238
|
* Filesystem changes (denyRead/denyWrite) are NOT applied live:
|
|
1151
1239
|
* macOS bakes them into the seatbelt profile at wrap time, and
|
|
1152
|
-
* Windows
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1155
|
-
* restrictions, `reset()` then `initialize()` with the new
|
|
1156
|
-
* config; on Windows, calling this with a config whose file-deny
|
|
1157
|
-
* inputs (`filesystem.denyRead`/`denyWrite`, `credentials.files`)
|
|
1158
|
-
* differ from those passed at `initialize()` logs a warning and
|
|
1159
|
-
* the stamped set stays as-is.
|
|
1240
|
+
* Linux/Windows bake them into the bwrap argv / DENY-ACE set at
|
|
1241
|
+
* wrap time. Call reset() + initialize() to apply a new
|
|
1242
|
+
* filesystem config.
|
|
1160
1243
|
*
|
|
1161
1244
|
* @param newConfig - The new configuration to use
|
|
1162
1245
|
*/
|
|
1163
1246
|
function updateConfig(newConfig) {
|
|
1164
|
-
if (getPlatform() === 'windows' &&
|
|
1165
|
-
config &&
|
|
1166
|
-
(newConfig.windows?.groupSid !== config.windows?.groupSid ||
|
|
1167
|
-
newConfig.windows?.groupName !== config.windows?.groupName)) {
|
|
1168
|
-
throw new Error('Changing the Windows sandbox group requires reset() and ' +
|
|
1169
|
-
're-initialize().');
|
|
1170
|
-
}
|
|
1171
1247
|
if (getPlatform() === 'windows' &&
|
|
1172
1248
|
config &&
|
|
1173
1249
|
!sameWindowsStampSet(newConfig)) {
|
|
1174
|
-
logForDebugging(`[Sandbox Windows] updateConfig: the resolved file-
|
|
1175
|
-
`(filesystem
|
|
1176
|
-
`
|
|
1177
|
-
`to apply. The previously-
|
|
1250
|
+
logForDebugging(`[Sandbox Windows] updateConfig: the resolved file-access set ` +
|
|
1251
|
+
`(filesystem.* ∪ credentials.files) changed but the ACL ` +
|
|
1252
|
+
`stamp/grant is session-wide — call reset() then initialize() ` +
|
|
1253
|
+
`to apply. The previously-applied set stays in effect.`, { level: 'warn' });
|
|
1178
1254
|
}
|
|
1179
1255
|
// Deep clone the config to avoid mutations. structuredClone cannot clone
|
|
1180
1256
|
// functions, so pull filterRequest out, clone the rest, and put it back —
|
|
@@ -1312,39 +1388,38 @@ function forceCloseHttpServer(server) {
|
|
|
1312
1388
|
});
|
|
1313
1389
|
}
|
|
1314
1390
|
async function reset() {
|
|
1315
|
-
// Windows: release this session's
|
|
1316
|
-
// — log anomalies
|
|
1317
|
-
//
|
|
1318
|
-
//
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
`${e.status}${tail}`, { level: 'warn' });
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
for (const e of r.parents ?? []) {
|
|
1337
|
-
if (!WINDOWS_ACL_PARENT_OK.has(e.status)) {
|
|
1338
|
-
logForDebugging(`[Sandbox Windows] file-deny restore: parent ` +
|
|
1339
|
-
`'${e.path}' ${e.status}` +
|
|
1340
|
-
(e.error ? `: ${e.error}` : ''), { level: 'warn' });
|
|
1341
|
-
}
|
|
1391
|
+
// Windows: release this session's sandbox-user ACEs. Best-effort
|
|
1392
|
+
// — log anomalies rather than throw, so teardown always
|
|
1393
|
+
// completes. Leftover ACEs are recoverable later via
|
|
1394
|
+
// `srt-win acl recover` (which sweeps by trustee SID).
|
|
1395
|
+
if (windowsFsStampedSet && windowsFsSbUserSid) {
|
|
1396
|
+
const sb = windowsFsSbUserSid;
|
|
1397
|
+
// Captured at initialize() — the SAME binary the grants/stamps
|
|
1398
|
+
// were applied with, immune to `config` mutation between.
|
|
1399
|
+
const srtWin = srtWinSpawn;
|
|
1400
|
+
// 'restored'/'alreadyOriginal' are the pre- same-user-removal
|
|
1401
|
+
// srt-win's success vocabulary; 'revoked'/'stillHeld' are the
|
|
1402
|
+
// post-. Either is non-anomalous.
|
|
1403
|
+
const ok = new Set(['revoked', 'stillHeld', 'restored', 'alreadyOriginal']);
|
|
1404
|
+
const log = (kind, e) => {
|
|
1405
|
+
if (!ok.has(e.status)) {
|
|
1406
|
+
logForDebugging(`[Sandbox Windows] ${kind}: '${e.path}' ${e.status} — ` +
|
|
1407
|
+
`ACE may be left in place; resolve and run ` +
|
|
1408
|
+
`\`srt-win acl recover\` to clear`, { level: 'warn' });
|
|
1342
1409
|
}
|
|
1410
|
+
};
|
|
1411
|
+
for (const e of revokeWindowsAcl({ sandboxUserSid: sb, srtWin }) ?? []) {
|
|
1412
|
+
log('grant revoke', e);
|
|
1413
|
+
}
|
|
1414
|
+
for (const e of restoreWindowsAcl({ sandboxUserSid: sb, srtWin }) ?? []) {
|
|
1415
|
+
log('deny restore', e);
|
|
1343
1416
|
}
|
|
1344
1417
|
}
|
|
1345
1418
|
windowsFsStampedSet = undefined;
|
|
1346
|
-
|
|
1419
|
+
windowsFsSbUserSid = undefined;
|
|
1347
1420
|
windowsFsRawInputs = undefined;
|
|
1421
|
+
srtWinSpawn = undefined;
|
|
1422
|
+
// windowsWfpVerified is NOT cleared — per-process, not per-session.
|
|
1348
1423
|
// Clean up any leftover bwrap mount points. Force past the
|
|
1349
1424
|
// active-sandbox counter — reset() means the session is over.
|
|
1350
1425
|
cleanupBwrapMountPoints({ force: true });
|
|
@@ -1353,6 +1428,10 @@ async function reset() {
|
|
|
1353
1428
|
logMonitorShutdown();
|
|
1354
1429
|
logMonitorShutdown = undefined;
|
|
1355
1430
|
}
|
|
1431
|
+
if (linuxMonitor) {
|
|
1432
|
+
linuxMonitor.stop();
|
|
1433
|
+
linuxMonitor = undefined;
|
|
1434
|
+
}
|
|
1356
1435
|
if (managerContext?.linuxBridge) {
|
|
1357
1436
|
const { httpSocketPath, socksSocketPath, httpBridgeProcess, socksBridgeProcess, } = managerContext.linuxBridge;
|
|
1358
1437
|
// Kill both bridges and wait for them to exit
|