Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

## 2025-02-13 - Regex recompilation overhead in hot paths
**Learning:** Terminal input validation (`validateInput`) runs on a very hot path for every incoming payload. Recompiling regular expressions inside the loop caused a significant performance overhead.
**Action:** Always pre-compile or cache (`Map<string, RegExp>`) regular expressions used in hot paths like continuous input validation, rather than instantiating `new RegExp()` on every call.
41 changes: 41 additions & 0 deletions packages/terminal/src/permissions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, test } from "bun:test";
import { validateInput, DEFAULT_TERMINAL_POLICY } from "./permissions.ts";

describe("permissions.ts", () => {
describe("validateInput", () => {
test("allows normal input", () => {
const result = validateInput("echo 'hello'");
expect(result.allowed).toBe(true);
});

test("blocks rm -rf /", () => {
const result = validateInput("rm -rf /");
expect(result.allowed).toBe(false);
expect(result.reason).toContain("Input blocked by policy pattern:");
});

test("blocks fork bomb", () => {
const result = validateInput(":(){ :|:& };:");
expect(result.allowed).toBe(false);
expect(result.reason).toContain("Input blocked by policy pattern:");
});

test("blocks dd zero to dev", () => {
const result = validateInput("dd if=/dev/zero of=/dev/sda");
expect(result.allowed).toBe(false);
expect(result.reason).toContain("Input blocked by policy pattern:");
});

test("handles custom policy", () => {
const customPolicy = {
...DEFAULT_TERMINAL_POLICY,
inputBlockPatterns: ["^\\s*sudo\\s+su\\s*$"]
};
const result1 = validateInput("sudo su", customPolicy);
expect(result1.allowed).toBe(false);

const result2 = validateInput("echo 'sudo su'", customPolicy);
expect(result2.allowed).toBe(true);
});
});
});
15 changes: 14 additions & 1 deletion packages/terminal/src/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ export function validateShell(
return { allowed: true };
}

// Cache for pre-compiled regular expressions to avoid recompilation overhead.
// Bound size to prevent unbounded memory growth if policies ever become dynamic.
const MAX_REGEX_CACHE_SIZE = 1000;
const inputRegexCache = new Map<string, RegExp>();

/**
* Check if input text contains blocked patterns.
* This is a best-effort filter β€” not a security boundary.
Expand All @@ -116,7 +121,15 @@ export function validateInput(
): AccessCheckResult {
for (const pattern of policy.inputBlockPatterns) {
try {
const re = new RegExp(pattern);
let re = inputRegexCache.get(pattern);
if (!re) {
// Prevent theoretical memory leak from unbounded dynamic patterns
if (inputRegexCache.size >= MAX_REGEX_CACHE_SIZE) {
inputRegexCache.clear();
}
re = new RegExp(pattern);
inputRegexCache.set(pattern, re);
}
if (re.test(input)) {
return {
allowed: false,
Expand Down