Skip to content

Commit 1ad7324

Browse files
committed
feat: US-115 - Harden SharedArrayBuffer deletion fallback
1 parent 56063d0 commit 1ad7324

3 files changed

Lines changed: 103 additions & 2 deletions

File tree

packages/secure-exec-core/isolate-runtime/src/inject/apply-timing-mitigation-freeze.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,50 @@ if (typeof __performance !== "undefined" && __performance !== null) {
3939
});
4040
}
4141

42-
if (!Reflect.deleteProperty(globalThis, "SharedArrayBuffer")) {
42+
/* Harden SharedArrayBuffer removal — neuter prototype so saved refs are useless,
43+
then lock the global property so sandbox code cannot restore it. */
44+
const __OrigSAB = globalThis.SharedArrayBuffer;
45+
if (typeof __OrigSAB === "function") {
46+
// Neuter the prototype so any previously-saved reference produces broken instances
47+
try {
48+
const proto = __OrigSAB.prototype;
49+
if (proto) {
50+
for (const key of [
51+
"byteLength",
52+
"slice",
53+
"grow",
54+
"maxByteLength",
55+
"growable",
56+
]) {
57+
try {
58+
Object.defineProperty(proto, key, {
59+
get() {
60+
throw new TypeError(
61+
"SharedArrayBuffer is not available in sandbox",
62+
);
63+
},
64+
configurable: false,
65+
});
66+
} catch {
67+
/* property may not exist or be non-configurable */
68+
}
69+
}
70+
}
71+
} catch {
72+
/* best-effort prototype neutering */
73+
}
74+
}
75+
76+
// Lock the global to undefined — configurable: false prevents re-definition
77+
try {
78+
Object.defineProperty(globalThis, "SharedArrayBuffer", {
79+
value: undefined,
80+
configurable: false,
81+
writable: false,
82+
enumerable: false,
83+
});
84+
} catch {
85+
// Fallback: delete then set
86+
Reflect.deleteProperty(globalThis, "SharedArrayBuffer");
4387
setGlobalValue("SharedArrayBuffer", undefined);
4488
}

packages/secure-exec-core/src/generated/isolate-runtime.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
export const ISOLATE_RUNTIME_SOURCES = {
44
"applyCustomGlobalPolicy": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-access.ts\n function hasOwnGlobal(name) {\n return Object.prototype.hasOwnProperty.call(globalThis, name);\n }\n function getGlobalValue(name) {\n return Reflect.get(globalThis, name);\n }\n\n // isolate-runtime/src/common/global-exposure.ts\n function defineRuntimeGlobalBinding(name, value, mutable) {\n Object.defineProperty(globalThis, name, {\n value,\n writable: mutable,\n configurable: mutable,\n enumerable: true\n });\n }\n function createRuntimeGlobalExposer(mutable) {\n return (name, value) => {\n defineRuntimeGlobalBinding(name, value, mutable);\n };\n }\n function getRuntimeExposeCustomGlobal() {\n if (typeof globalThis.__runtimeExposeCustomGlobal === \"function\") {\n return globalThis.__runtimeExposeCustomGlobal;\n }\n return createRuntimeGlobalExposer(false);\n }\n function getRuntimeExposeMutableGlobal() {\n if (typeof globalThis.__runtimeExposeMutableGlobal === \"function\") {\n return globalThis.__runtimeExposeMutableGlobal;\n }\n return createRuntimeGlobalExposer(true);\n }\n\n // isolate-runtime/src/inject/apply-custom-global-policy.ts\n var __runtimeExposeCustomGlobal = getRuntimeExposeCustomGlobal();\n var __runtimeExposeMutableGlobal = getRuntimeExposeMutableGlobal();\n var __globalPolicy = globalThis.__runtimeCustomGlobalPolicy ?? {};\n var __hardenedGlobals = Array.isArray(__globalPolicy.hardenedGlobals) ? __globalPolicy.hardenedGlobals : [];\n var __mutableGlobals = Array.isArray(__globalPolicy.mutableGlobals) ? __globalPolicy.mutableGlobals : [];\n for (const globalName of __hardenedGlobals) {\n if (hasOwnGlobal(globalName)) {\n __runtimeExposeCustomGlobal(globalName, getGlobalValue(globalName));\n }\n }\n for (const globalName of __mutableGlobals) {\n if (hasOwnGlobal(globalName)) {\n __runtimeExposeMutableGlobal(globalName, getGlobalValue(globalName));\n }\n }\n})();\n",
5-
"applyTimingMitigationFreeze": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-access.ts\n function setGlobalValue(name, value) {\n Reflect.set(globalThis, name, value);\n }\n\n // isolate-runtime/src/inject/apply-timing-mitigation-freeze.ts\n var __timingConfig = globalThis.__runtimeTimingMitigationConfig ?? {};\n var __frozenTimeMs = typeof __timingConfig.frozenTimeMs === \"number\" && Number.isFinite(__timingConfig.frozenTimeMs) ? __timingConfig.frozenTimeMs : Date.now();\n var __frozenDateNow = () => __frozenTimeMs;\n try {\n Object.defineProperty(Date, \"now\", {\n value: __frozenDateNow,\n configurable: true,\n writable: true\n });\n } catch {\n Date.now = __frozenDateNow;\n }\n var __frozenPerformanceNow = () => 0;\n var __performance = globalThis.performance;\n if (typeof __performance !== \"undefined\" && __performance !== null) {\n try {\n Object.defineProperty(__performance, \"now\", {\n value: __frozenPerformanceNow,\n configurable: true,\n writable: true\n });\n } catch {\n try {\n Object.assign(__performance, { now: __frozenPerformanceNow });\n } catch {\n }\n }\n } else {\n setGlobalValue(\"performance\", {\n now: __frozenPerformanceNow\n });\n }\n if (!Reflect.deleteProperty(globalThis, \"SharedArrayBuffer\")) {\n setGlobalValue(\"SharedArrayBuffer\", void 0);\n }\n})();\n",
5+
"applyTimingMitigationFreeze": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-access.ts\n function setGlobalValue(name, value) {\n Reflect.set(globalThis, name, value);\n }\n\n // isolate-runtime/src/inject/apply-timing-mitigation-freeze.ts\n var __timingConfig = globalThis.__runtimeTimingMitigationConfig ?? {};\n var __frozenTimeMs = typeof __timingConfig.frozenTimeMs === \"number\" && Number.isFinite(__timingConfig.frozenTimeMs) ? __timingConfig.frozenTimeMs : Date.now();\n var __frozenDateNow = () => __frozenTimeMs;\n try {\n Object.defineProperty(Date, \"now\", {\n value: __frozenDateNow,\n configurable: true,\n writable: true\n });\n } catch {\n Date.now = __frozenDateNow;\n }\n var __frozenPerformanceNow = () => 0;\n var __performance = globalThis.performance;\n if (typeof __performance !== \"undefined\" && __performance !== null) {\n try {\n Object.defineProperty(__performance, \"now\", {\n value: __frozenPerformanceNow,\n configurable: true,\n writable: true\n });\n } catch {\n try {\n Object.assign(__performance, { now: __frozenPerformanceNow });\n } catch {\n }\n }\n } else {\n setGlobalValue(\"performance\", {\n now: __frozenPerformanceNow\n });\n }\n var __OrigSAB = globalThis.SharedArrayBuffer;\n if (typeof __OrigSAB === \"function\") {\n try {\n const proto = __OrigSAB.prototype;\n if (proto) {\n for (const key of [\n \"byteLength\",\n \"slice\",\n \"grow\",\n \"maxByteLength\",\n \"growable\"\n ]) {\n try {\n Object.defineProperty(proto, key, {\n get() {\n throw new TypeError(\n \"SharedArrayBuffer is not available in sandbox\"\n );\n },\n configurable: false\n });\n } catch {\n }\n }\n }\n } catch {\n }\n }\n try {\n Object.defineProperty(globalThis, \"SharedArrayBuffer\", {\n value: void 0,\n configurable: false,\n writable: false,\n enumerable: false\n });\n } catch {\n Reflect.deleteProperty(globalThis, \"SharedArrayBuffer\");\n setGlobalValue(\"SharedArrayBuffer\", void 0);\n }\n})();\n",
66
"applyTimingMitigationOff": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-access.ts\n function setGlobalValue(name, value) {\n Reflect.set(globalThis, name, value);\n }\n\n // isolate-runtime/src/inject/apply-timing-mitigation-off.ts\n if (typeof globalThis.performance === \"undefined\" || globalThis.performance === null) {\n setGlobalValue(\"performance\", {\n now: () => Date.now()\n });\n }\n})();\n",
77
"bridgeAttach": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-exposure.ts\n function defineRuntimeGlobalBinding(name, value, mutable) {\n Object.defineProperty(globalThis, name, {\n value,\n writable: mutable,\n configurable: mutable,\n enumerable: true\n });\n }\n function createRuntimeGlobalExposer(mutable) {\n return (name, value) => {\n defineRuntimeGlobalBinding(name, value, mutable);\n };\n }\n function getRuntimeExposeCustomGlobal() {\n if (typeof globalThis.__runtimeExposeCustomGlobal === \"function\") {\n return globalThis.__runtimeExposeCustomGlobal;\n }\n return createRuntimeGlobalExposer(false);\n }\n\n // isolate-runtime/src/inject/bridge-attach.ts\n var __runtimeExposeCustomGlobal = getRuntimeExposeCustomGlobal();\n if (typeof globalThis.bridge !== \"undefined\") {\n __runtimeExposeCustomGlobal(\"bridge\", globalThis.bridge);\n }\n})();\n",
88
"bridgeInitialGlobals": "\"use strict\";\n(() => {\n // isolate-runtime/src/common/global-exposure.ts\n function defineRuntimeGlobalBinding(name, value, mutable) {\n Object.defineProperty(globalThis, name, {\n value,\n writable: mutable,\n configurable: mutable,\n enumerable: true\n });\n }\n function createRuntimeGlobalExposer(mutable) {\n return (name, value) => {\n defineRuntimeGlobalBinding(name, value, mutable);\n };\n }\n function getRuntimeExposeMutableGlobal() {\n if (typeof globalThis.__runtimeExposeMutableGlobal === \"function\") {\n return globalThis.__runtimeExposeMutableGlobal;\n }\n return createRuntimeGlobalExposer(true);\n }\n\n // isolate-runtime/src/inject/bridge-initial-globals.ts\n var __runtimeExposeMutableGlobal = getRuntimeExposeMutableGlobal();\n var __bridgeSetupConfig = globalThis.__runtimeBridgeSetupConfig ?? {};\n var __initialCwd = typeof __bridgeSetupConfig.initialCwd === \"string\" ? __bridgeSetupConfig.initialCwd : \"/\";\n var __jsonPayloadLimitBytes = typeof __bridgeSetupConfig.jsonPayloadLimitBytes === \"number\" && Number.isFinite(__bridgeSetupConfig.jsonPayloadLimitBytes) ? Math.max(0, Math.floor(__bridgeSetupConfig.jsonPayloadLimitBytes)) : 4 * 1024 * 1024;\n var __payloadLimitErrorCode = typeof __bridgeSetupConfig.payloadLimitErrorCode === \"string\" && __bridgeSetupConfig.payloadLimitErrorCode.length > 0 ? __bridgeSetupConfig.payloadLimitErrorCode : \"ERR_SANDBOX_PAYLOAD_TOO_LARGE\";\n function __scEncode(value, seen) {\n if (value === null) return null;\n if (value === void 0) return { t: \"undef\" };\n if (typeof value === \"boolean\") return value;\n if (typeof value === \"string\") return value;\n if (typeof value === \"bigint\") return { t: \"bigint\", v: String(value) };\n if (typeof value === \"number\") {\n if (Object.is(value, -0)) return { t: \"-0\" };\n if (Number.isNaN(value)) return { t: \"nan\" };\n if (value === Infinity) return { t: \"inf\" };\n if (value === -Infinity) return { t: \"-inf\" };\n return value;\n }\n const obj = value;\n if (seen.has(obj)) return { t: \"ref\", i: seen.get(obj) };\n const idx = seen.size;\n seen.set(obj, idx);\n if (value instanceof Date)\n return { t: \"date\", v: value.getTime() };\n if (value instanceof RegExp)\n return { t: \"regexp\", p: value.source, f: value.flags };\n if (value instanceof Map) {\n const entries = [];\n value.forEach((v, k) => {\n entries.push([__scEncode(k, seen), __scEncode(v, seen)]);\n });\n return { t: \"map\", v: entries };\n }\n if (value instanceof Set) {\n const elems = [];\n value.forEach((v) => {\n elems.push(__scEncode(v, seen));\n });\n return { t: \"set\", v: elems };\n }\n if (value instanceof ArrayBuffer) {\n return { t: \"ab\", v: Array.from(new Uint8Array(value)) };\n }\n if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {\n return {\n t: \"ta\",\n k: value.constructor.name,\n v: Array.from(\n new Uint8Array(value.buffer, value.byteOffset, value.byteLength)\n )\n };\n }\n if (Array.isArray(value)) {\n return {\n t: \"arr\",\n v: value.map((v) => __scEncode(v, seen))\n };\n }\n const result = {};\n for (const key of Object.keys(value)) {\n result[key] = __scEncode(\n value[key],\n seen\n );\n }\n return { t: \"obj\", v: result };\n }\n function __scDecode(tagged, refs) {\n if (tagged === null) return null;\n if (typeof tagged === \"boolean\" || typeof tagged === \"string\" || typeof tagged === \"number\")\n return tagged;\n const tag = tagged.t;\n if (tag === void 0) return tagged;\n switch (tag) {\n case \"undef\":\n return void 0;\n case \"nan\":\n return NaN;\n case \"inf\":\n return Infinity;\n case \"-inf\":\n return -Infinity;\n case \"-0\":\n return -0;\n case \"bigint\":\n return BigInt(tagged.v);\n case \"ref\":\n return refs[tagged.i];\n case \"date\": {\n const d = new Date(tagged.v);\n refs.push(d);\n return d;\n }\n case \"regexp\": {\n const r = new RegExp(\n tagged.p,\n tagged.f\n );\n refs.push(r);\n return r;\n }\n case \"map\": {\n const m = /* @__PURE__ */ new Map();\n refs.push(m);\n for (const [k, v] of tagged.v) {\n m.set(__scDecode(k, refs), __scDecode(v, refs));\n }\n return m;\n }\n case \"set\": {\n const s = /* @__PURE__ */ new Set();\n refs.push(s);\n for (const v of tagged.v) {\n s.add(__scDecode(v, refs));\n }\n return s;\n }\n case \"ab\": {\n const bytes = tagged.v;\n const ab = new ArrayBuffer(bytes.length);\n const u8 = new Uint8Array(ab);\n for (let i = 0; i < bytes.length; i++) u8[i] = bytes[i];\n refs.push(ab);\n return ab;\n }\n case \"ta\": {\n const { k, v: bytes } = tagged;\n const ctors = {\n Int8Array,\n Uint8Array,\n Uint8ClampedArray,\n Int16Array,\n Uint16Array,\n Int32Array,\n Uint32Array,\n Float32Array,\n Float64Array\n };\n const Ctor = ctors[k] ?? Uint8Array;\n const ab = new ArrayBuffer(bytes.length);\n const u8 = new Uint8Array(ab);\n for (let i = 0; i < bytes.length; i++) u8[i] = bytes[i];\n const ta = new Ctor(ab);\n refs.push(ta);\n return ta;\n }\n case \"arr\": {\n const arr = [];\n refs.push(arr);\n for (const v of tagged.v) {\n arr.push(__scDecode(v, refs));\n }\n return arr;\n }\n case \"obj\": {\n const obj = {};\n refs.push(obj);\n const entries = tagged.v;\n for (const key of Object.keys(entries)) {\n obj[key] = __scDecode(entries[key], refs);\n }\n return obj;\n }\n default:\n return tagged;\n }\n }\n __runtimeExposeMutableGlobal(\"_moduleCache\", {});\n globalThis._moduleCache = globalThis._moduleCache ?? {};\n var __moduleCache = globalThis._moduleCache;\n if (__moduleCache) {\n __moduleCache[\"v8\"] = {\n getHeapStatistics: function() {\n return {\n total_heap_size: 67108864,\n total_heap_size_executable: 1048576,\n total_physical_size: 67108864,\n total_available_size: 67108864,\n used_heap_size: 52428800,\n heap_size_limit: 134217728,\n malloced_memory: 8192,\n peak_malloced_memory: 16384,\n does_zap_garbage: 0,\n number_of_native_contexts: 1,\n number_of_detached_contexts: 0,\n external_memory: 0\n };\n },\n getHeapSpaceStatistics: function() {\n return [];\n },\n getHeapCodeStatistics: function() {\n return {};\n },\n setFlagsFromString: function() {\n },\n serialize: function(value) {\n return Buffer.from(\n JSON.stringify({ $v8sc: 1, d: __scEncode(value, /* @__PURE__ */ new Map()) })\n );\n },\n deserialize: function(buffer) {\n if (buffer.length > __jsonPayloadLimitBytes) {\n throw new Error(\n __payloadLimitErrorCode + \": v8.deserialize exceeds \" + String(__jsonPayloadLimitBytes) + \" bytes\"\n );\n }\n const text = buffer.toString();\n const envelope = JSON.parse(text);\n if (envelope !== null && typeof envelope === \"object\" && envelope.$v8sc === 1) {\n return __scDecode(envelope.d, []);\n }\n return envelope;\n },\n cachedDataVersionTag: function() {\n return 0;\n }\n };\n }\n __runtimeExposeMutableGlobal(\"_pendingModules\", {});\n __runtimeExposeMutableGlobal(\"_currentModule\", { dirname: __initialCwd });\n})();\n",

packages/secure-exec/tests/runtime-driver/node/index.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,63 @@ describe("NodeRuntime", () => {
11221122
});
11231123
});
11241124

1125+
it("SharedArrayBuffer global cannot be restored by sandbox code", async () => {
1126+
proc = createTestNodeRuntime();
1127+
const result = await proc.run(`
1128+
let restored = false;
1129+
try {
1130+
Object.defineProperty(globalThis, 'SharedArrayBuffer', {
1131+
value: function FakeSAB() {},
1132+
configurable: true,
1133+
});
1134+
restored = true;
1135+
} catch (e) {
1136+
restored = false;
1137+
}
1138+
// Also try direct assignment
1139+
globalThis.SharedArrayBuffer = function FakeSAB2() {};
1140+
module.exports = {
1141+
stillUndefined: typeof SharedArrayBuffer === 'undefined',
1142+
definePropertyFailed: !restored,
1143+
};
1144+
`);
1145+
expect(result.exports).toEqual({
1146+
stillUndefined: true,
1147+
definePropertyFailed: true,
1148+
});
1149+
});
1150+
1151+
it("saved SharedArrayBuffer reference is non-functional after freeze", async () => {
1152+
proc = createTestNodeRuntime();
1153+
const result = await proc.run(`
1154+
// Even if somehow a reference was obtained, the prototype is neutered
1155+
const desc = Object.getOwnPropertyDescriptor(globalThis, 'SharedArrayBuffer');
1156+
let protoNeutered = false;
1157+
try {
1158+
// SharedArrayBuffer.prototype should have been neutered before deletion;
1159+
// verify we can't construct anything useful
1160+
const sab = new ArrayBuffer(8);
1161+
// Attempt to access SharedArrayBuffer-specific props on a real SAB
1162+
// (they shouldn't exist on ArrayBuffer, this confirms SAB is gone)
1163+
protoNeutered = typeof sab.grow === 'undefined';
1164+
} catch {
1165+
protoNeutered = true;
1166+
}
1167+
module.exports = {
1168+
isUndefined: desc !== undefined && desc.value === undefined,
1169+
isNonConfigurable: desc !== undefined && desc.configurable === false,
1170+
isNonWritable: desc !== undefined && desc.writable === false,
1171+
protoNeutered,
1172+
};
1173+
`);
1174+
expect(result.exports).toEqual({
1175+
isUndefined: true,
1176+
isNonConfigurable: true,
1177+
isNonWritable: true,
1178+
protoNeutered: true,
1179+
});
1180+
});
1181+
11251182
it("restores advancing clocks when timing mitigation is off", async () => {
11261183
const capture = createConsoleCapture();
11271184
proc = createTestNodeRuntime({

0 commit comments

Comments
 (0)