fix(security): make the shell sensitive-path gate linear and bounded - #8277
Closed
bolichen97 wants to merge 1 commit into
Closed
fix(security): make the shell sensitive-path gate linear and bounded#8277bolichen97 wants to merge 1 commit into
bolichen97 wants to merge 1 commit into
Conversation
A cron whose agent emitted a ~9 KB bash command full of https:// URLs took the gateway down every hour: is_sensitive_bash_command ran for 25+ seconds on the event loop and the loop-stall watchdog hard-exited the process. The gate is synchronous under that watchdog, so its worst case is the gateway's worst case, and it was quadratic in the command. Three constructs were each quadratic on their own, and their costs multiply, so each is measured and fixed separately (10 KB / 40 KB, pattern tier alone, before -> after): * The redirect alternative `.*[<>|]\s*<path>`: a `.*` tried at every offset of every line whatever the content, the one term whose cost did not depend on the input looking like a path. 0.30 s / 4.8 s -> 5 ms / 19 ms. Dropped to `[<>|]\s*<path>`, redundant under re.search exactly like the token anchors kirodotdev#7941 already fixed. * The UNC anchor `\\\\[^\s'"]+` followed by the generalized separator: the greedy run backtracks one character at a time and re-walks the whole `\X\..` chain from every separator it lands on. 0.06 s / 0.8 s on one UNC token -> 11 ms / 43 ms. It now takes a plain separator, which matches the same strings: the run already absorbs every character a no-op chain contains. * The verb-anchored branch `verb.*<path>` (and `interp ... open( ... <path>`): re-walks the line from every verb occurrence. 12 ms / 130 ms on a verb-dense line -> 4 ms / 18 ms. No regex spelling of "a verb earlier on this line" is linear, so it is a Python walk now: `.*` never crosses a newline, so per line the statement is "earliest verb end" + "path search from there", two linear searches accepting exactly the old language. `_sensitive_pattern_hit` runs both halves. Whole gate at the crash size (10-12 KB, every adversarial shape tried: doubled-separator paths, URL-dense JSON, UNC chains, verb-dense lines, 24 000-backslash runs): 20-80 ms, against 15-36 s on the shipped build. On top of that, MAX_SCANNABLE_COMMAND_CHARS (20 KiB) is a hard ceiling: a longer command is refused with a reason, never scanned or skipped. The tool_input tier in llm_helpers already refused at that size; it now aliases the same constant so the two cannot drift. Zero verdict change: a 381 474-command generated differential corpus (verbs x preceding characters x paths x terminators, script-open shapes, multi-line, UNC and %VAR% spellings) produced no difference against origin/main and against the tree before kirodotdev#7941. The tests pin the cases where each rewritten construct was the ONLY matching branch. Seven mutants (drop the cap, drop the verb half, accept a path before the verb, `.*` back on the redirect, gsep back on UNC, no line split, `open(` before the interpreter) each turn a test red; the two spelling mutants are also caught by the timing pins alone.
Collaborator
Author
|
Superseded by #8282, which carries this change together with its two companions as one PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem / Motivation
An hourly cron whose agent emitted a ~9 KB bash command full of
https://URLs took a user's gateway down every hour. The crash dump's main thread sat insecurity.pyinsideis_sensitive_bash_commandfor 25 s and the loop-stall watchdog hard-exited the process. The gate is synchronous on the event loop under that watchdog, so its worst case is the gateway's worst case — and it was quadratic in the command. The user's own benchmark of the installed gate: 1.2 KB 0.6 s, 2.3 KB 2.3 s, 4.6 KB 9.1 s, 9.1 KB 35.8 s.Why it matters
Any surface that streams a permission request (cron, Slack, dashboard side panel, workflows) and any caller of
hooks.on_tool_callruns this gate inline. A long command from the model is enough to kill the gateway, every in-flight turn with it, and — for a cron — to do it again on the next run. #7941 removed the eleven.*token anchors, which cut the constant ~20x, but the growth was still quadratic on every shape measured (10 KB 0.3 s, 40 KB 5 s, pattern tier alone).What changed (motivation → approach → change)
Three constructs were each quadratic on their own and their costs multiply, so each was measured and fixed separately (harness in a subprocess under
timeout; pattern tier alone, 10 KB / 40 KB, before → after):\\\\[^\s'"]++ generalized separator\X\..hopsverb.*<path>,interp … open( … <path>.never crosses a newline, so per line it decomposes into "earliest verb end" + "path search from there" (_verb_anchored_sensitive_hit, two linear searches, same language)._sensitive_pattern_hitruns both halves and is the only entry to the pattern tierFixing the first alone leaves the UNC shape quadratic (0.8 s at 40 KB); fixing the second alone changes nothing visible because the first dominates; fixing both leaves the verb term (×3.5 per doubling). All three are needed.
Whole gate at the crash size (10–12 KB) on every adversarial shape tried — doubled-separator paths, URL-dense JSON, UNC chains, verb-dense lines,
~-runs, 24 000-backslash runs: 20–80 ms, against 15–36 s on the shipped build. With the ceiling lifted for measurement, 20 → 40 → 80 KB scales ×2 per doubling on every shape.On top of that
MAX_SCANNABLE_COMMAND_CHARS(20 KiB) is a hard ceiling in the gate itself: a longer command is refused with a reason, never scanned partially and never let through unscanned — a denied long command is recoverable, a stalled gateway and an unscanned command are not.llm_helpers._MAX_SCANNABLE_TOOL_INPUT_CHARSalready refused at that size; it now aliases the same constant so the two tiers cannot drift. Two later passes (_ENV_CRED_PATTERNS, the normalizer) remain O(k·n) in verb tokens and are bounded by the ceiling (≤ 60 ms at 20 KB); they were not on the crash path and are left as is.Zero verdict change. A 381 474-command generated differential corpus (verbs × preceding characters × paths × terminators, script-open shapes, multi-line, UNC and
%VAR%spellings, plus every golden from the existing tests) produced no difference againstorigin/mainand against the tree before #7941.Spec:
docs/system-specs/modules/security.mdgains the bounded-cost invariant.Tests
test/test_security_gate_liveness.py:open(after the interpreter; UNC with hops), plus negatives (path before the verb, verb on another line,open(before the interpreter)Seven mutants each turn a test red (drop the ceiling; drop the verb half; accept a path before the verb;
.*back on the redirect; generalized separator back on UNC; no line split;open(before the interpreter). The two spelling mutants are also caught by the timing pins alone with the source guard deselected.test/test_security_regex_linearity.py::test_long_nonshell_line_does_not_blow_upis resized under the new ceiling (it fed 22.5 KB and asserted "allowed"; that is now refused by design).Manual verification
Reproduced the field curve on the shipped build (0.6.0.10 internal / pre-#7941): 10.7 KB double-separator command 14.8 s. Measured
re.searchGIL behaviour separately (a worker's 16–23 s search leaves the main thread ticking on 3.10/3.11/3.12) — relevant to the companion offload PR, not to this one.Related Issues
Companion PRs: #8278 (title-tier off-loop scan in
_resolve_permission) and #8279 (cron loop-stall attribution + breaker + doctor). Each is independently mergeable.Pattern harvest
Rule candidate: review-prompt
Pattern: a leading
.*(or a greedy run followed by a starred group) inside an alternation evaluated withre.searchon agent-supplied text — redundant for existence, quadratic in the subject; measure each.*-bearing branch alone, because their costs multiply and fixing one hides the rest.Checklist