From 07d83154305411bede67a9509477c2c0c3377be3 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 20 Jul 2026 17:11:33 -0500 Subject: [PATCH] fix(codemode): bound BigInt operations --- packages/codemode/interpreter-support.md | 4 +- packages/codemode/src/interpreter/runtime.ts | 40 ++++++++++- packages/codemode/src/tool-runtime.ts | 17 ++++- packages/codemode/test/bigint-test262.test.ts | 66 +++++++++++++++++++ 4 files changed, 121 insertions(+), 6 deletions(-) diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 41af2fec45..3916396ae0 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -38,7 +38,9 @@ ultimate source of truth. - [x] BigInt literals (decimal, binary, octal, hexadecimal, and numeric separators) and BigInt behavior for the supported arithmetic, bitwise, unary, update, assignment, equality, and ordering operators. Mixed Number/BigInt arithmetic throws. BigInt remains invalid in program results, `JSON.stringify`, and tool arguments/results; the - `BigInt` constructor and other BigInt-specific standard-library APIs are not exposed. + `BigInt` constructor and other BigInt-specific standard-library APIs are not exposed. In-interpreter BigInts are + limited to 4,096 bits; multiplication, exponentiation, and size-increasing shifts conservatively reject before + native evaluation when their result may exceed that limit. - [ ] Symbol primitive values and symbol-keyed properties. - [ ] Tagged-template calls. - [ ] Getter and setter definitions in object literals. diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index f577115dcc..65e95ddf24 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -1,5 +1,12 @@ import { Cause, Effect } from "effect" -import { isBlockedMember, ToolReference, ToolRuntimeError, type SafeObject } from "../tool-runtime.js" +import { + bigintBitLength, + isBlockedMember, + MAX_BIGINT_BITS, + ToolReference, + ToolRuntimeError, + type SafeObject, +} from "../tool-runtime.js" import { type AstNode, asNode, @@ -171,6 +178,29 @@ const instanceofValue = (lhs: unknown, rhs: unknown, node: AstNode): boolean => ) } +const assertSafeBigIntOperation = (operator: string, lhs: unknown, rhs: unknown, node: AstNode): void => { + if (typeof lhs !== "bigint" || typeof rhs !== "bigint") return + const leftBits = bigintBitLength(lhs) + const rightBits = bigintBitLength(rhs) + const exceedsLimit = (() => { + if (operator === "*") return lhs !== 0n && rhs !== 0n && leftBits + rightBits > MAX_BIGINT_BITS + if (operator === "**") { + if (rhs <= 0n || lhs === 0n || lhs === 1n || lhs === -1n) return false + return rhs > BigInt(Math.floor(MAX_BIGINT_BITS / leftBits)) + } + if (operator !== "<<" && operator !== ">>") return false + if (lhs === 0n) return false + const growth = operator === "<<" ? rhs : -rhs + return growth > BigInt(MAX_BIGINT_BITS - leftBits) + })() + if (!exceedsLimit) return + throw new InterpreterRuntimeError( + `BigInt operation '${operator}' may exceed CodeMode's ${MAX_BIGINT_BITS}-bit limit.`, + node, + "InvalidDataValue", + ) +} + const collectPatternNames = (pattern: AstNode, out: Array = []): Array => { switch (pattern.type) { case "Identifier": @@ -1342,6 +1372,7 @@ export class Interpreter { const bothObjects = lhs !== null && typeof lhs === "object" && rhs !== null && typeof rhs === "object" const l = coerceOperand(lhs) const r = coerceOperand(rhs) + assertSafeBigIntOperation(operator, l, r, node) switch (operator) { case "+": return (l as string) + (r as string) @@ -1544,8 +1575,11 @@ export class Interpreter { return typeof current === "bigint" ? current : coerceToNumber(current) } - const update = (current: number | bigint): number | bigint => - typeof current === "bigint" ? current + (increment === 1 ? 1n : -1n) : current + increment + const update = (current: number | bigint): unknown => + boundedData( + typeof current === "bigint" ? current + (increment === 1 ? 1n : -1n) : current + increment, + "Update expression result", + ) if (argument.type === "Identifier") { return Effect.sync(() => { diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts index 64df92bdd7..62d348c588 100644 --- a/packages/codemode/src/tool-runtime.ts +++ b/packages/codemode/src/tool-runtime.ts @@ -101,6 +101,13 @@ export class ToolReference { } const MAX_VALUE_DEPTH = 32 +export const MAX_BIGINT_BITS = 4_096 + +export const bigintBitLength = (value: bigint): number => { + if (value === 0n) return 0 + const binary = value.toString(2) + return binary[0] === "-" ? binary.length - 1 : binary.length +} export class ToolRuntimeError extends Error { constructor( @@ -147,13 +154,19 @@ const copyBounded = ( if (depth > MAX_VALUE_DEPTH) { throw new ToolRuntimeError("InvalidDataValue", `${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`) } + if (preserveCodeModeValues && typeof value === "bigint") { + if (bigintBitLength(value) > MAX_BIGINT_BITS) { + throw new ToolRuntimeError("InvalidDataValue", `${label} exceeds CodeMode's ${MAX_BIGINT_BITS}-bit BigInt limit.`) + } + return value + } + if ( value === null || value === undefined || typeof value === "string" || typeof value === "boolean" || - typeof value === "number" || - (preserveCodeModeValues && typeof value === "bigint") + typeof value === "number" ) { return value } diff --git a/packages/codemode/test/bigint-test262.test.ts b/packages/codemode/test/bigint-test262.test.ts index 05936d603f..5f42bbfac3 100644 --- a/packages/codemode/test/bigint-test262.test.ts +++ b/packages/codemode/test/bigint-test262.test.ts @@ -206,3 +206,69 @@ describe("BigInt JSON-like boundaries", () => { if (!result.ok) expect(result.error.kind).toBe("InvalidToolOutput") }) }) + +describe("BigInt resource bound", () => { + test("limits in-interpreter BigInts to 4,096 bits", async () => { + const maximum = await execute(`return ${`0b1${"0".repeat(4_095)}n`} === ${`0b1${"0".repeat(4_095)}n`}`) + expect(maximum.ok).toBe(true) + if (maximum.ok) expect(maximum.value).toBe(true) + + const oversized = await execute(`return ${`0b1${"0".repeat(4_096)}n`} === 0n`) + expect(oversized.ok).toBe(false) + if (!oversized.ok) { + expect(oversized.error.kind).toBe("InvalidDataValue") + expect(oversized.error.message).toContain("4096-bit BigInt limit") + } + }) + + test("rejects expensive operations before native evaluation", async () => { + for (const code of [ + `const value = 1n << 2_048n; return value * value`, + `return 16n ** 1_024n`, + `return 1n << 4_096n`, + `return 1n >> -4_096n`, + ]) { + const result = await execute(code) + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.error.kind).toBe("InvalidDataValue") + expect(result.error.message).toContain("may exceed CodeMode's 4096-bit limit") + } + } + }) + + test("rejects oversized results from non-preflighted operations", async () => { + for (const code of [ + `const value = 1n << 4_095n; return value + value`, + `let value = ${`0b${"1".repeat(4_096)}n`}; value++; return true`, + ]) { + const result = await execute(code) + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.error.kind).toBe("InvalidDataValue") + expect(result.error.message).toContain("exceeds CodeMode's 4096-bit BigInt limit") + } + } + }) + + test("does not let synchronous BigInt work bypass a one-millisecond timeout", async () => { + const result = await Effect.runPromise( + CodeMode.execute({ + code: ` + for (let index = 0; index < 20; index++) { + const huge = 1n << 499_999n + huge * huge + } + return true + `, + tools: {}, + limits: { timeoutMs: 1 }, + }), + ) + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.error.kind).toBe("InvalidDataValue") + expect(result.error.message).toContain("may exceed CodeMode's 4096-bit limit") + } + }) +})