Skip to content

Commit e14d21c

Browse files
committed
feat: US-107 - Add concurrent host timer cap
1 parent e08f819 commit e14d21c

3 files changed

Lines changed: 63 additions & 2 deletions

File tree

packages/secure-exec-node/src/execution-driver.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { createCommandExecutorStub, createFsStub, createNetworkStub, filterEnv,
55
import { executeWithRuntime } from "./execution.js";
66
import type { NetworkAdapter, RuntimeDriver } from "@secure-exec/core";
77
import type { StdioHook, ExecOptions, ExecResult, RunResult, TimingMitigation } from "@secure-exec/core/internal/shared/api-types";
8-
import { type DriverDeps, type NodeExecutionDriverOptions, createBudgetState, clearActiveHostTimers, killActiveChildProcesses, normalizePayloadLimit, getExecutionTimeoutMs, getTimingMitigation, DEFAULT_BRIDGE_BASE64_TRANSFER_BYTES, DEFAULT_ISOLATE_JSON_PAYLOAD_BYTES, DEFAULT_SANDBOX_CWD, DEFAULT_SANDBOX_HOME, DEFAULT_SANDBOX_TMPDIR } from "./isolate-bootstrap.js";
8+
import { type DriverDeps, type NodeExecutionDriverOptions, createBudgetState, clearActiveHostTimers, killActiveChildProcesses, normalizePayloadLimit, getExecutionTimeoutMs, getTimingMitigation, DEFAULT_BRIDGE_BASE64_TRANSFER_BYTES, DEFAULT_ISOLATE_JSON_PAYLOAD_BYTES, DEFAULT_MAX_TIMERS, DEFAULT_SANDBOX_CWD, DEFAULT_SANDBOX_HOME, DEFAULT_SANDBOX_TMPDIR } from "./isolate-bootstrap.js";
99
import { shouldRunAsESM } from "./module-resolver.js";
1010
import { precompileDynamicImports, runESM, setupDynamicImport } from "./esm-compiler.js";
1111
import { setupConsole, setupRequire, setupESMGlobals } from "./bridge-setup.js";
@@ -76,7 +76,7 @@ export class NodeExecutionDriver implements RuntimeDriver {
7676
isolateJsonPayloadLimitBytes,
7777
maxOutputBytes: budgets?.maxOutputBytes,
7878
maxBridgeCalls: budgets?.maxBridgeCalls,
79-
maxTimers: budgets?.maxTimers,
79+
maxTimers: budgets?.maxTimers ?? DEFAULT_MAX_TIMERS,
8080
maxChildProcesses: budgets?.maxChildProcesses,
8181
budgetState: createBudgetState(),
8282
activeHttpServerIds: new Set(),

packages/secure-exec-node/src/isolate-bootstrap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export const MIN_CONFIGURED_PAYLOAD_BYTES = 1024;
6565
export const MAX_CONFIGURED_PAYLOAD_BYTES = 64 * 1024 * 1024;
6666
export const PAYLOAD_LIMIT_ERROR_CODE = "ERR_SANDBOX_PAYLOAD_TOO_LARGE";
6767
export const RESOURCE_BUDGET_ERROR_CODE = "ERR_RESOURCE_BUDGET_EXCEEDED";
68+
export const DEFAULT_MAX_TIMERS = 10_000;
6869
export const DEFAULT_SANDBOX_CWD = "/root";
6970
export const DEFAULT_SANDBOX_HOME = "/root";
7071
export const DEFAULT_SANDBOX_TMPDIR = "/tmp";

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,66 @@ describe("NodeRuntime resource budgets", () => {
294294
expect(out).toContain("blocked:true");
295295
expect(out).toContain("created:3");
296296
});
297+
298+
it("cleared timers free slots for new ones", async () => {
299+
const capture = createConsoleCapture();
300+
proc = createTestNodeRuntime({
301+
onStdio: capture.onStdio,
302+
resourceBudgets: { maxTimers: 6 },
303+
});
304+
305+
const result = await proc.exec(`
306+
// Fill all 6 slots
307+
const ids = [];
308+
for (let i = 0; i < 6; i++) ids.push(setInterval(() => {}, 60000));
309+
// Cap reached — next one should throw
310+
let blocked = false;
311+
try { setInterval(() => {}, 60000); } catch(e) { blocked = true; }
312+
// Clear half
313+
for (let i = 0; i < 3; i++) clearInterval(ids[i]);
314+
// Now 3 slots are free — create 3 more
315+
let created = 0;
316+
for (let i = 0; i < 3; i++) {
317+
try { setInterval(() => {}, 60000); created++; } catch(e) {}
318+
}
319+
// 7th total new one should be blocked again
320+
let blocked2 = false;
321+
try { setInterval(() => {}, 60000); } catch(e) { blocked2 = true; }
322+
console.log('blocked:' + blocked);
323+
console.log('created:' + created);
324+
console.log('blocked2:' + blocked2);
325+
`);
326+
327+
expect(result.code).toBe(0);
328+
const out = capture.stdout();
329+
expect(out).toContain("blocked:true");
330+
expect(out).toContain("created:3");
331+
expect(out).toContain("blocked2:true");
332+
});
333+
334+
it("normal code with fewer than 100 timers works fine", async () => {
335+
const capture = createConsoleCapture();
336+
proc = createTestNodeRuntime({
337+
onStdio: capture.onStdio,
338+
});
339+
340+
const result = await proc.exec(`
341+
let count = 0;
342+
for (let i = 0; i < 50; i++) {
343+
setTimeout(() => {}, 60000);
344+
count++;
345+
}
346+
for (let i = 0; i < 30; i++) {
347+
setInterval(() => {}, 60000);
348+
count++;
349+
}
350+
console.log('timers:' + count);
351+
`);
352+
353+
expect(result.code).toBe(0);
354+
const out = capture.stdout();
355+
expect(out).toContain("timers:80");
356+
});
297357
});
298358

299359
// -----------------------------------------------------------------------

0 commit comments

Comments
 (0)