Skip to content

Commit 82b1b44

Browse files
NathanFlurryclaude
andcommitted
feat: US-060 - Defer config-dependent bridge setup to post-restore init
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5f829a2 commit 82b1b44

4 files changed

Lines changed: 227 additions & 26 deletions

File tree

packages/secure-exec-core/isolate-runtime/src/common/runtime-globals.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,16 @@ declare global {
104104
var __runtimeCommonJsFileConfig: RuntimeCommonJsFileConfig | undefined;
105105
var __runtimeTimingMitigationConfig: RuntimeTimingMitigationConfig | undefined;
106106
var __runtimeCustomGlobalPolicy: RuntimeCustomGlobalPolicy | undefined;
107+
var __runtimeJsonPayloadLimitBytes: number | undefined;
108+
var __runtimePayloadLimitErrorCode: string | undefined;
109+
var __runtimeApplyConfig:
110+
| ((config: {
111+
timingMitigation?: string;
112+
frozenTimeMs?: number;
113+
payloadLimitBytes?: number;
114+
payloadLimitErrorCode?: string;
115+
}) => void)
116+
| undefined;
107117
var __runtimeProcessCwdOverride: unknown;
108118
var __runtimeProcessEnvOverride: unknown;
109119
var __runtimeStdinData: unknown;

packages/secure-exec-core/isolate-runtime/src/inject/bridge-initial-globals.ts

Lines changed: 185 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getRuntimeExposeMutableGlobal } from "../common/global-exposure";
2+
import { setGlobalValue } from "../common/global-access";
23

34
const __runtimeExposeMutableGlobal = getRuntimeExposeMutableGlobal();
45

@@ -8,12 +9,15 @@ const __initialCwd =
89
typeof __bridgeSetupConfig.initialCwd === "string"
910
? __bridgeSetupConfig.initialCwd
1011
: "/";
11-
const __jsonPayloadLimitBytes =
12+
13+
// Set payload limit defaults on globalThis — read at call time by v8.deserialize,
14+
// overridable via __runtimeApplyConfig for context snapshot restore
15+
globalThis.__runtimeJsonPayloadLimitBytes =
1216
typeof __bridgeSetupConfig.jsonPayloadLimitBytes === "number" &&
1317
Number.isFinite(__bridgeSetupConfig.jsonPayloadLimitBytes)
1418
? Math.max(0, Math.floor(__bridgeSetupConfig.jsonPayloadLimitBytes))
1519
: 4 * 1024 * 1024;
16-
const __payloadLimitErrorCode =
20+
globalThis.__runtimePayloadLimitErrorCode =
1721
typeof __bridgeSetupConfig.payloadLimitErrorCode === "string" &&
1822
__bridgeSetupConfig.payloadLimitErrorCode.length > 0
1923
? __bridgeSetupConfig.payloadLimitErrorCode
@@ -237,12 +241,15 @@ if (__moduleCache) {
237241
);
238242
},
239243
deserialize: function (buffer: Buffer) {
244+
// Read limits from globals at call time (not captured at setup) for snapshot compatibility
245+
const limit = globalThis.__runtimeJsonPayloadLimitBytes ?? 4 * 1024 * 1024;
246+
const errorCode = globalThis.__runtimePayloadLimitErrorCode ?? "ERR_SANDBOX_PAYLOAD_TOO_LARGE";
240247
// Check raw buffer size BEFORE allocating the decoded string
241-
if (buffer.length > __jsonPayloadLimitBytes) {
248+
if (buffer.length > limit) {
242249
throw new Error(
243-
__payloadLimitErrorCode +
250+
errorCode +
244251
": v8.deserialize exceeds " +
245-
String(__jsonPayloadLimitBytes) +
252+
String(limit) +
246253
" bytes",
247254
);
248255
}
@@ -269,3 +276,176 @@ if (__moduleCache) {
269276

270277
__runtimeExposeMutableGlobal("_pendingModules", {});
271278
__runtimeExposeMutableGlobal("_currentModule", { dirname: __initialCwd });
279+
280+
// Post-restore config application — called after bridge IIFE to apply
281+
// per-session config (timing mitigation, payload limits). Enables context
282+
// snapshot reuse: the IIFE runs once at snapshot creation, this function
283+
// applies session-specific config after restore.
284+
globalThis.__runtimeApplyConfig = function (config: {
285+
timingMitigation?: string;
286+
frozenTimeMs?: number;
287+
payloadLimitBytes?: number;
288+
payloadLimitErrorCode?: string;
289+
}) {
290+
// Apply payload limits
291+
if (
292+
typeof config.payloadLimitBytes === "number" &&
293+
Number.isFinite(config.payloadLimitBytes)
294+
) {
295+
globalThis.__runtimeJsonPayloadLimitBytes = Math.max(
296+
0,
297+
Math.floor(config.payloadLimitBytes),
298+
);
299+
}
300+
if (
301+
typeof config.payloadLimitErrorCode === "string" &&
302+
config.payloadLimitErrorCode.length > 0
303+
) {
304+
globalThis.__runtimePayloadLimitErrorCode =
305+
config.payloadLimitErrorCode;
306+
}
307+
308+
// Apply timing mitigation freeze
309+
if (config.timingMitigation === "freeze") {
310+
const frozenTimeMs =
311+
typeof config.frozenTimeMs === "number" &&
312+
Number.isFinite(config.frozenTimeMs)
313+
? config.frozenTimeMs
314+
: Date.now();
315+
const frozenDateNow = () => frozenTimeMs;
316+
317+
// Freeze Date.now
318+
try {
319+
Object.defineProperty(Date, "now", {
320+
value: frozenDateNow,
321+
configurable: false,
322+
writable: false,
323+
});
324+
} catch {
325+
Date.now = frozenDateNow;
326+
}
327+
328+
// Patch Date constructor so new Date().getTime() returns degraded time
329+
const OrigDate = Date;
330+
const FrozenDate = function Date(
331+
this: InstanceType<DateConstructor>,
332+
...args: unknown[]
333+
) {
334+
if (new.target) {
335+
if (args.length === 0) {
336+
return new OrigDate(frozenTimeMs);
337+
}
338+
// @ts-expect-error — spread forwarding to variadic Date constructor
339+
return new OrigDate(...args);
340+
}
341+
return OrigDate();
342+
} as unknown as DateConstructor;
343+
Object.defineProperty(FrozenDate, "prototype", {
344+
value: OrigDate.prototype,
345+
writable: false,
346+
configurable: false,
347+
});
348+
FrozenDate.now = frozenDateNow;
349+
FrozenDate.parse = OrigDate.parse;
350+
FrozenDate.UTC = OrigDate.UTC;
351+
Object.defineProperty(FrozenDate, "now", {
352+
value: frozenDateNow,
353+
configurable: false,
354+
writable: false,
355+
});
356+
try {
357+
Object.defineProperty(globalThis, "Date", {
358+
value: FrozenDate,
359+
configurable: false,
360+
writable: false,
361+
});
362+
} catch {
363+
(globalThis as Record<string, unknown>).Date = FrozenDate;
364+
}
365+
366+
// Freeze performance.now
367+
const frozenPerformanceNow = () => 0;
368+
const origPerf = globalThis.performance;
369+
const frozenPerf = Object.create(null) as Record<string, unknown>;
370+
if (typeof origPerf !== "undefined" && origPerf !== null) {
371+
const src = origPerf as unknown as Record<string, unknown>;
372+
for (const key of Object.getOwnPropertyNames(
373+
Object.getPrototypeOf(origPerf) ?? origPerf,
374+
)) {
375+
if (key !== "now") {
376+
try {
377+
const val = src[key];
378+
if (typeof val === "function") {
379+
frozenPerf[key] = val.bind(origPerf);
380+
} else {
381+
frozenPerf[key] = val;
382+
}
383+
} catch {
384+
/* skip inaccessible properties */
385+
}
386+
}
387+
}
388+
}
389+
Object.defineProperty(frozenPerf, "now", {
390+
value: frozenPerformanceNow,
391+
configurable: false,
392+
writable: false,
393+
});
394+
Object.freeze(frozenPerf);
395+
try {
396+
Object.defineProperty(globalThis, "performance", {
397+
value: frozenPerf,
398+
configurable: false,
399+
writable: false,
400+
});
401+
} catch {
402+
(globalThis as Record<string, unknown>).performance = frozenPerf;
403+
}
404+
405+
// Harden SharedArrayBuffer removal
406+
const OrigSAB = globalThis.SharedArrayBuffer;
407+
if (typeof OrigSAB === "function") {
408+
try {
409+
const proto = OrigSAB.prototype;
410+
if (proto) {
411+
for (const key of [
412+
"byteLength",
413+
"slice",
414+
"grow",
415+
"maxByteLength",
416+
"growable",
417+
]) {
418+
try {
419+
Object.defineProperty(proto, key, {
420+
get() {
421+
throw new TypeError(
422+
"SharedArrayBuffer is not available in sandbox",
423+
);
424+
},
425+
configurable: false,
426+
});
427+
} catch {
428+
/* property may not exist or be non-configurable */
429+
}
430+
}
431+
}
432+
} catch {
433+
/* best-effort prototype neutering */
434+
}
435+
}
436+
try {
437+
Object.defineProperty(globalThis, "SharedArrayBuffer", {
438+
value: undefined,
439+
configurable: false,
440+
writable: false,
441+
enumerable: false,
442+
});
443+
} catch {
444+
Reflect.deleteProperty(globalThis, "SharedArrayBuffer");
445+
setGlobalValue("SharedArrayBuffer", undefined);
446+
}
447+
}
448+
449+
// Clean up — one-shot function
450+
delete globalThis.__runtimeApplyConfig;
451+
};

0 commit comments

Comments
 (0)