mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-21 10:16:03 +00:00
fix(codemode): bound BigInt operations
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user