Skip to content

Commit c462089

Browse files
authored
Merge pull request #20 from rivet-dev/nathan/v8-process-isolation-pt1
chore: V8 process isolation
2 parents 78e0a52 + 32e80b0 commit c462089

29 files changed

Lines changed: 2161 additions & 917 deletions

.github/workflows/pkg-pr-new.yaml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,15 @@ jobs:
4343
cache-dependency-path: pnpm-lock.yaml
4444

4545
- name: Install dependencies
46-
run: pnpm install --frozen-lockfile
46+
run: pnpm install --no-frozen-lockfile
4747

4848
- name: Build packages
49-
run: pnpm turbo run build --filter='@secure-exec/typescript...'
49+
run: pnpm turbo run build
50+
51+
- name: Build linux-x64 binary via Docker
52+
run: |
53+
cd crates/v8-runtime
54+
docker build -f docker/Dockerfile.linux-x64-gnu -o type=local,dest=npm/linux-x64-gnu .
5055
5156
- name: Publish to pkg.pr.new
5257
run: |
@@ -57,4 +62,5 @@ jobs:
5762
"./packages/secure-exec-browser" \
5863
"./packages/secure-exec-python" \
5964
"./packages/secure-exec-typescript" \
65+
"./packages/secure-exec-v8" \
6066
--packageManager pnpm

packages/secure-exec-browser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@secure-exec/browser",
3-
"version": "0.1.1-rc.2",
3+
"version": "0.1.0",
44
"type": "module",
55
"license": "Apache-2.0",
66
"main": "./dist/index.js",

packages/secure-exec-browser/src/worker.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ function getUtf8ByteLength(text: string): number {
6868
return encoder.encode(text).byteLength;
6969
}
7070

71+
function getBase64EncodedByteLength(rawByteLength: number): number {
72+
return Math.ceil(rawByteLength / 3) * 4;
73+
}
7174

7275
function assertPayloadByteLength(
7376
payloadLabel: string,
@@ -317,14 +320,15 @@ async function initRuntime(payload: BrowserWorkerInitPayload): Promise<void> {
317320
const data = await fsOps.readFile(path);
318321
assertPayloadByteLength(
319322
`fs.readFileBinary ${path}`,
320-
data.byteLength,
323+
getBase64EncodedByteLength(data.byteLength),
321324
base64TransferLimitBytes,
322325
);
323-
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
326+
return btoa(String.fromCharCode(...data));
324327
});
325-
const writeFileBinaryRef = makeApplySyncPromise(async (path: string, binaryContent: Uint8Array) => {
326-
assertPayloadByteLength(`fs.writeFileBinary ${path}`, binaryContent.byteLength, base64TransferLimitBytes);
327-
return fsOps.writeFile(path, binaryContent);
328+
const writeFileBinaryRef = makeApplySyncPromise(async (path: string, base64: string) => {
329+
assertTextPayloadSize(`fs.writeFileBinary ${path}`, base64, base64TransferLimitBytes);
330+
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
331+
return fsOps.writeFile(path, bytes);
328332
});
329333
const readDirRef = makeApplySyncPromise(async (path: string) => {
330334
const entries = await fsOps.readDirWithTypes(path);

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

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -116,17 +116,6 @@ declare global {
116116
var __runtimeCommonJsFileConfig: RuntimeCommonJsFileConfig | undefined;
117117
var __runtimeTimingMitigationConfig: RuntimeTimingMitigationConfig | undefined;
118118
var __runtimeCustomGlobalPolicy: RuntimeCustomGlobalPolicy | undefined;
119-
var __runtimeJsonPayloadLimitBytes: number | undefined;
120-
var __runtimePayloadLimitErrorCode: string | undefined;
121-
var __runtimeApplyConfig:
122-
| ((config: {
123-
timingMitigation?: string;
124-
frozenTimeMs?: number;
125-
payloadLimitBytes?: number;
126-
payloadLimitErrorCode?: string;
127-
}) => void)
128-
| undefined;
129-
var __runtimeResetProcessState: (() => void) | undefined;
130119
var __runtimeProcessCwdOverride: unknown;
131120
var __runtimeProcessEnvOverride: unknown;
132121
var __runtimeStdinData: unknown;

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

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

43
const __runtimeExposeMutableGlobal = getRuntimeExposeMutableGlobal();
54

@@ -9,15 +8,12 @@ const __initialCwd =
98
typeof __bridgeSetupConfig.initialCwd === "string"
109
? __bridgeSetupConfig.initialCwd
1110
: "/";
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 =
11+
const __jsonPayloadLimitBytes =
1612
typeof __bridgeSetupConfig.jsonPayloadLimitBytes === "number" &&
1713
Number.isFinite(__bridgeSetupConfig.jsonPayloadLimitBytes)
1814
? Math.max(0, Math.floor(__bridgeSetupConfig.jsonPayloadLimitBytes))
1915
: 4 * 1024 * 1024;
20-
globalThis.__runtimePayloadLimitErrorCode =
16+
const __payloadLimitErrorCode =
2117
typeof __bridgeSetupConfig.payloadLimitErrorCode === "string" &&
2218
__bridgeSetupConfig.payloadLimitErrorCode.length > 0
2319
? __bridgeSetupConfig.payloadLimitErrorCode
@@ -241,15 +237,12 @@ if (__moduleCache) {
241237
);
242238
},
243239
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";
247240
// Check raw buffer size BEFORE allocating the decoded string
248-
if (buffer.length > limit) {
241+
if (buffer.length > __jsonPayloadLimitBytes) {
249242
throw new Error(
250-
errorCode +
243+
__payloadLimitErrorCode +
251244
": v8.deserialize exceeds " +
252-
String(limit) +
245+
String(__jsonPayloadLimitBytes) +
253246
" bytes",
254247
);
255248
}
@@ -276,176 +269,3 @@ if (__moduleCache) {
276269

277270
__runtimeExposeMutableGlobal("_pendingModules", {});
278271
__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-
};

packages/secure-exec-core/isolate-runtime/src/inject/require-setup.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,7 +1289,26 @@
12891289
declare const _loadFileSync: { applySync(recv: undefined, args: [string]): string | null } | undefined;
12901290

12911291
function _resolveFrom(moduleName, fromDir) {
1292-
const resolved = _resolveModule(moduleName, fromDir);
1292+
const cacheKey = fromDir + '\0' + moduleName;
1293+
if (cacheKey in _resolveCache) {
1294+
const cached = _resolveCache[cacheKey];
1295+
if (cached === null) {
1296+
const err = new Error("Cannot find module '" + moduleName + "'");
1297+
err.code = 'MODULE_NOT_FOUND';
1298+
throw err;
1299+
}
1300+
return cached;
1301+
}
1302+
// Use synchronous resolution when available (always works, even inside
1303+
// applySync contexts like net socket data callbacks). Fall back to
1304+
// applySyncPromise for environments without the sync bridge.
1305+
let resolved;
1306+
if (typeof _resolveModuleSync !== 'undefined') {
1307+
resolved = _resolveModuleSync.applySync(undefined, [moduleName, fromDir]);
1308+
} else {
1309+
resolved = _resolveModule.applySyncPromise(undefined, [moduleName, fromDir]);
1310+
}
1311+
_resolveCache[cacheKey] = resolved;
12931312
if (resolved === null) {
12941313
const err = new Error("Cannot find module '" + moduleName + "'");
12951314
err.code = 'MODULE_NOT_FOUND';
@@ -1653,7 +1672,10 @@
16531672
}
16541673

16551674
// Try to load polyfill first (for built-in modules like path, events, etc.)
1656-
const polyfillCode = _loadPolyfill(name);
1675+
// Skip for relative/absolute paths — they're never polyfills and the
1676+
// applySyncPromise call can't run inside applySync contexts.
1677+
const isPath = name[0] === '.' || name[0] === '/';
1678+
const polyfillCode = isPath ? null : _loadPolyfill.applySyncPromise(undefined, [name]);
16571679
if (polyfillCode !== null) {
16581680
if (__internalModuleCache[name]) return __internalModuleCache[name];
16591681

@@ -1704,8 +1726,14 @@
17041726
return _pendingModules[cacheKey].exports;
17051727
}
17061728

1707-
// Load file content
1708-
const source = _loadFile(resolved);
1729+
// Load file content. Use synchronous loading when available (works
1730+
// inside applySync contexts). Fall back to applySyncPromise otherwise.
1731+
let source;
1732+
if (typeof _loadFileSync !== 'undefined') {
1733+
source = _loadFileSync.applySync(undefined, [resolved]);
1734+
} else {
1735+
source = _loadFile.applySyncPromise(undefined, [resolved]);
1736+
}
17091737
if (source === null) {
17101738
const err = new Error("Cannot find module '" + resolved + "'");
17111739
err.code = 'MODULE_NOT_FOUND';

packages/secure-exec-core/isolate-runtime/src/inject/setup-dynamic-import.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ const __dynamicImportHandler = async function (
2323
const allowRequireFallback =
2424
request.endsWith(".cjs") || request.endsWith(".json");
2525

26-
const namespace = await globalThis._dynamicImport(request, referrer);
26+
const namespace = await globalThis._dynamicImport.apply(
27+
undefined,
28+
[request, referrer],
29+
{ result: { promise: true } },
30+
);
2731

2832
if (namespace !== null) {
2933
return namespace;

0 commit comments

Comments
 (0)