Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/pkg-pr-new.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,15 @@ jobs:
cache-dependency-path: pnpm-lock.yaml

- name: Install dependencies
run: pnpm install --frozen-lockfile
run: pnpm install --no-frozen-lockfile

- name: Build packages
run: pnpm turbo run build --filter='@secure-exec/typescript...'
run: pnpm turbo run build

- name: Build linux-x64 binary via Docker
run: |
cd crates/v8-runtime
docker build -f docker/Dockerfile.linux-x64-gnu -o type=local,dest=npm/linux-x64-gnu .

- name: Publish to pkg.pr.new
run: |
Expand All @@ -57,4 +62,5 @@ jobs:
"./packages/secure-exec-browser" \
"./packages/secure-exec-python" \
"./packages/secure-exec-typescript" \
"./packages/secure-exec-v8" \
--packageManager pnpm
2 changes: 1 addition & 1 deletion packages/secure-exec-browser/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@secure-exec/browser",
"version": "0.1.1-rc.2",
"version": "0.1.0",
"type": "module",
"license": "Apache-2.0",
"main": "./dist/index.js",
Expand Down
14 changes: 9 additions & 5 deletions packages/secure-exec-browser/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ function getUtf8ByteLength(text: string): number {
return encoder.encode(text).byteLength;
}

function getBase64EncodedByteLength(rawByteLength: number): number {
return Math.ceil(rawByteLength / 3) * 4;
}

function assertPayloadByteLength(
payloadLabel: string,
Expand Down Expand Up @@ -317,14 +320,15 @@ async function initRuntime(payload: BrowserWorkerInitPayload): Promise<void> {
const data = await fsOps.readFile(path);
assertPayloadByteLength(
`fs.readFileBinary ${path}`,
data.byteLength,
getBase64EncodedByteLength(data.byteLength),
base64TransferLimitBytes,
);
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
return btoa(String.fromCharCode(...data));
});
const writeFileBinaryRef = makeApplySyncPromise(async (path: string, binaryContent: Uint8Array) => {
assertPayloadByteLength(`fs.writeFileBinary ${path}`, binaryContent.byteLength, base64TransferLimitBytes);
return fsOps.writeFile(path, binaryContent);
const writeFileBinaryRef = makeApplySyncPromise(async (path: string, base64: string) => {
assertTextPayloadSize(`fs.writeFileBinary ${path}`, base64, base64TransferLimitBytes);
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
return fsOps.writeFile(path, bytes);
});
const readDirRef = makeApplySyncPromise(async (path: string) => {
const entries = await fsOps.readDirWithTypes(path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,17 +116,6 @@ declare global {
var __runtimeCommonJsFileConfig: RuntimeCommonJsFileConfig | undefined;
var __runtimeTimingMitigationConfig: RuntimeTimingMitigationConfig | undefined;
var __runtimeCustomGlobalPolicy: RuntimeCustomGlobalPolicy | undefined;
var __runtimeJsonPayloadLimitBytes: number | undefined;
var __runtimePayloadLimitErrorCode: string | undefined;
var __runtimeApplyConfig:
| ((config: {
timingMitigation?: string;
frozenTimeMs?: number;
payloadLimitBytes?: number;
payloadLimitErrorCode?: string;
}) => void)
| undefined;
var __runtimeResetProcessState: (() => void) | undefined;
var __runtimeProcessCwdOverride: unknown;
var __runtimeProcessEnvOverride: unknown;
var __runtimeStdinData: unknown;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { getRuntimeExposeMutableGlobal } from "../common/global-exposure";
import { setGlobalValue } from "../common/global-access";

const __runtimeExposeMutableGlobal = getRuntimeExposeMutableGlobal();

Expand All @@ -9,15 +8,12 @@ const __initialCwd =
typeof __bridgeSetupConfig.initialCwd === "string"
? __bridgeSetupConfig.initialCwd
: "/";

// Set payload limit defaults on globalThis — read at call time by v8.deserialize,
// overridable via __runtimeApplyConfig for context snapshot restore
globalThis.__runtimeJsonPayloadLimitBytes =
const __jsonPayloadLimitBytes =
typeof __bridgeSetupConfig.jsonPayloadLimitBytes === "number" &&
Number.isFinite(__bridgeSetupConfig.jsonPayloadLimitBytes)
? Math.max(0, Math.floor(__bridgeSetupConfig.jsonPayloadLimitBytes))
: 4 * 1024 * 1024;
globalThis.__runtimePayloadLimitErrorCode =
const __payloadLimitErrorCode =
typeof __bridgeSetupConfig.payloadLimitErrorCode === "string" &&
__bridgeSetupConfig.payloadLimitErrorCode.length > 0
? __bridgeSetupConfig.payloadLimitErrorCode
Expand Down Expand Up @@ -241,15 +237,12 @@ if (__moduleCache) {
);
},
deserialize: function (buffer: Buffer) {
// Read limits from globals at call time (not captured at setup) for snapshot compatibility
const limit = globalThis.__runtimeJsonPayloadLimitBytes ?? 4 * 1024 * 1024;
const errorCode = globalThis.__runtimePayloadLimitErrorCode ?? "ERR_SANDBOX_PAYLOAD_TOO_LARGE";
// Check raw buffer size BEFORE allocating the decoded string
if (buffer.length > limit) {
if (buffer.length > __jsonPayloadLimitBytes) {
throw new Error(
errorCode +
__payloadLimitErrorCode +
": v8.deserialize exceeds " +
String(limit) +
String(__jsonPayloadLimitBytes) +
" bytes",
);
}
Expand All @@ -276,176 +269,3 @@ if (__moduleCache) {

__runtimeExposeMutableGlobal("_pendingModules", {});
__runtimeExposeMutableGlobal("_currentModule", { dirname: __initialCwd });

// Post-restore config application — called after bridge IIFE to apply
// per-session config (timing mitigation, payload limits). Enables context
// snapshot reuse: the IIFE runs once at snapshot creation, this function
// applies session-specific config after restore.
globalThis.__runtimeApplyConfig = function (config: {
timingMitigation?: string;
frozenTimeMs?: number;
payloadLimitBytes?: number;
payloadLimitErrorCode?: string;
}) {
// Apply payload limits
if (
typeof config.payloadLimitBytes === "number" &&
Number.isFinite(config.payloadLimitBytes)
) {
globalThis.__runtimeJsonPayloadLimitBytes = Math.max(
0,
Math.floor(config.payloadLimitBytes),
);
}
if (
typeof config.payloadLimitErrorCode === "string" &&
config.payloadLimitErrorCode.length > 0
) {
globalThis.__runtimePayloadLimitErrorCode =
config.payloadLimitErrorCode;
}

// Apply timing mitigation freeze
if (config.timingMitigation === "freeze") {
const frozenTimeMs =
typeof config.frozenTimeMs === "number" &&
Number.isFinite(config.frozenTimeMs)
? config.frozenTimeMs
: Date.now();
const frozenDateNow = () => frozenTimeMs;

// Freeze Date.now
try {
Object.defineProperty(Date, "now", {
value: frozenDateNow,
configurable: false,
writable: false,
});
} catch {
Date.now = frozenDateNow;
}

// Patch Date constructor so new Date().getTime() returns degraded time
const OrigDate = Date;
const FrozenDate = function Date(
this: InstanceType<DateConstructor>,
...args: unknown[]
) {
if (new.target) {
if (args.length === 0) {
return new OrigDate(frozenTimeMs);
}
// @ts-expect-error — spread forwarding to variadic Date constructor
return new OrigDate(...args);
}
return OrigDate();
} as unknown as DateConstructor;
Object.defineProperty(FrozenDate, "prototype", {
value: OrigDate.prototype,
writable: false,
configurable: false,
});
FrozenDate.now = frozenDateNow;
FrozenDate.parse = OrigDate.parse;
FrozenDate.UTC = OrigDate.UTC;
Object.defineProperty(FrozenDate, "now", {
value: frozenDateNow,
configurable: false,
writable: false,
});
try {
Object.defineProperty(globalThis, "Date", {
value: FrozenDate,
configurable: false,
writable: false,
});
} catch {
(globalThis as Record<string, unknown>).Date = FrozenDate;
}

// Freeze performance.now
const frozenPerformanceNow = () => 0;
const origPerf = globalThis.performance;
const frozenPerf = Object.create(null) as Record<string, unknown>;
if (typeof origPerf !== "undefined" && origPerf !== null) {
const src = origPerf as unknown as Record<string, unknown>;
for (const key of Object.getOwnPropertyNames(
Object.getPrototypeOf(origPerf) ?? origPerf,
)) {
if (key !== "now") {
try {
const val = src[key];
if (typeof val === "function") {
frozenPerf[key] = val.bind(origPerf);
} else {
frozenPerf[key] = val;
}
} catch {
/* skip inaccessible properties */
}
}
}
}
Object.defineProperty(frozenPerf, "now", {
value: frozenPerformanceNow,
configurable: false,
writable: false,
});
Object.freeze(frozenPerf);
try {
Object.defineProperty(globalThis, "performance", {
value: frozenPerf,
configurable: false,
writable: false,
});
} catch {
(globalThis as Record<string, unknown>).performance = frozenPerf;
}

// Harden SharedArrayBuffer removal
const OrigSAB = globalThis.SharedArrayBuffer;
if (typeof OrigSAB === "function") {
try {
const proto = OrigSAB.prototype;
if (proto) {
for (const key of [
"byteLength",
"slice",
"grow",
"maxByteLength",
"growable",
]) {
try {
Object.defineProperty(proto, key, {
get() {
throw new TypeError(
"SharedArrayBuffer is not available in sandbox",
);
},
configurable: false,
});
} catch {
/* property may not exist or be non-configurable */
}
}
}
} catch {
/* best-effort prototype neutering */
}
}
try {
Object.defineProperty(globalThis, "SharedArrayBuffer", {
value: undefined,
configurable: false,
writable: false,
enumerable: false,
});
} catch {
Reflect.deleteProperty(globalThis, "SharedArrayBuffer");
setGlobalValue("SharedArrayBuffer", undefined);
}
}

// Clean up — one-shot function
delete globalThis.__runtimeApplyConfig;
};
Original file line number Diff line number Diff line change
Expand Up @@ -1289,7 +1289,26 @@
declare const _loadFileSync: { applySync(recv: undefined, args: [string]): string | null } | undefined;

function _resolveFrom(moduleName, fromDir) {
const resolved = _resolveModule(moduleName, fromDir);
const cacheKey = fromDir + '\0' + moduleName;
if (cacheKey in _resolveCache) {
const cached = _resolveCache[cacheKey];
if (cached === null) {
const err = new Error("Cannot find module '" + moduleName + "'");
err.code = 'MODULE_NOT_FOUND';
throw err;
}
return cached;
}
// Use synchronous resolution when available (always works, even inside
// applySync contexts like net socket data callbacks). Fall back to
// applySyncPromise for environments without the sync bridge.
let resolved;
if (typeof _resolveModuleSync !== 'undefined') {
resolved = _resolveModuleSync.applySync(undefined, [moduleName, fromDir]);
} else {
resolved = _resolveModule.applySyncPromise(undefined, [moduleName, fromDir]);
}
_resolveCache[cacheKey] = resolved;
if (resolved === null) {
const err = new Error("Cannot find module '" + moduleName + "'");
err.code = 'MODULE_NOT_FOUND';
Expand Down Expand Up @@ -1653,7 +1672,10 @@
}

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

Expand Down Expand Up @@ -1704,8 +1726,14 @@
return _pendingModules[cacheKey].exports;
}

// Load file content
const source = _loadFile(resolved);
// Load file content. Use synchronous loading when available (works
// inside applySync contexts). Fall back to applySyncPromise otherwise.
let source;
if (typeof _loadFileSync !== 'undefined') {
source = _loadFileSync.applySync(undefined, [resolved]);
} else {
source = _loadFile.applySyncPromise(undefined, [resolved]);
}
if (source === null) {
const err = new Error("Cannot find module '" + resolved + "'");
err.code = 'MODULE_NOT_FOUND';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ const __dynamicImportHandler = async function (
const allowRequireFallback =
request.endsWith(".cjs") || request.endsWith(".json");

const namespace = await globalThis._dynamicImport(request, referrer);
const namespace = await globalThis._dynamicImport.apply(
undefined,
[request, referrer],
{ result: { promise: true } },
);

if (namespace !== null) {
return namespace;
Expand Down
Loading
Loading