From fe02b58a095adf54cd1c099a88a30cae9d90ae69 Mon Sep 17 00:00:00 2001 From: Psyborgs-git <49641518+Psyborgs-git@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:12:34 +0000 Subject: [PATCH] perf(terminal): cache regex patterns for input validation Introduced a `Map` in `validateInput` to cache pre-compiled regular expressions. This avoids the severe overhead of instantiating `new RegExp()` repeatedly in a hot loop (continuous input validation). Bounded the cache to a max size of 1000 to prevent unbounded memory growth from hypothetical dynamic policies. Includes corresponding performance journal entry. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .jules/bolt.md | 4 +++ packages/terminal/src/permissions.test.ts | 41 +++++++++++++++++++++++ packages/terminal/src/permissions.ts | 15 ++++++++- 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 .jules/bolt.md create mode 100644 packages/terminal/src/permissions.test.ts diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..b57ddc4 --- /dev/null +++ b/.jules/bolt.md @@ -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`) regular expressions used in hot paths like continuous input validation, rather than instantiating `new RegExp()` on every call. diff --git a/packages/terminal/src/permissions.test.ts b/packages/terminal/src/permissions.test.ts new file mode 100644 index 0000000..2fa9029 --- /dev/null +++ b/packages/terminal/src/permissions.test.ts @@ -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); + }); + }); +}); diff --git a/packages/terminal/src/permissions.ts b/packages/terminal/src/permissions.ts index 3755e8c..15a650a 100644 --- a/packages/terminal/src/permissions.ts +++ b/packages/terminal/src/permissions.ts @@ -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(); + /** * Check if input text contains blocked patterns. * This is a best-effort filter — not a security boundary. @@ -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,