fix(codemode): bound BigInt operations

This commit is contained in:
Aiden Cline
2026-07-20 17:11:33 -05:00
parent 009003002b
commit 07d8315430
4 changed files with 121 additions and 6 deletions
+3 -1
View File
@@ -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.
+37 -3
View File
@@ -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<string> = []): Array<string> => {
switch (pattern.type) {
case "Identifier":
@@ -1342,6 +1372,7 @@ export class Interpreter<R> {
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<R> {
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(() => {
+15 -2
View File
@@ -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
}
@@ -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")
}
})
})