Skip to content

Commit 776967a

Browse files
waleedlatif1claude
andcommitted
fix(executor): remove quadratic backtracking in env var reference patterns
The `{{ENV_VAR}}` body was `[^}]+` in the executor's reference patterns, the code-placeholder compiler, and two client surfaces. Because the class admits `{`, a run of unmatched braces restarts a full backtracking scan at every offset. Unlike the renderer case this runs on the execution path, so the CPU burned is a worker's: 100k braces took 12.1s through `resolveEnvVarReferences`. Excludes `{` from the body, matching the reason `createReferencePattern` already excludes both angle brackets. Env var names are `PATTERNS.ENV_VAR_NAME` (`[A-Za-z_][A-Za-z0-9_]*`), enforced by the secrets manager on every key, so no representable name is affected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0139x4ngJzcoYrjwsS9B2ZG4
1 parent 568a539 commit 776967a

6 files changed

Lines changed: 126 additions & 7 deletions

File tree

apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export interface McpServerFormModalProps {
6767
domainPolicyError?: string
6868
}
6969

70-
const ENV_VAR_PATTERN = /\{\{[^}]+\}\}/
70+
const ENV_VAR_PATTERN = /\{\{[^{}]+\}\}/
7171

7272
function hasEnvVarInHostname(url: string): boolean {
7373
const globalPattern = new RegExp(ENV_VAR_PATTERN.source, 'g')

apps/sim/app/workspace/[workspaceId]/w/components/preview/components/preview-editor/preview-editor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ function extractAllReferencesFromSubBlocks(
128128
}
129129
}
130130

131-
const envMatches = value.match(/\{\{([^}]+)\}\}/g)
131+
const envMatches = value.match(/\{\{([^{}]+)\}\}/g)
132132
if (envMatches) {
133133
envMatches.forEach((match) => envVars.add(match))
134134
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* @vitest-environment node
3+
*
4+
* Differential test: narrowing the `{{ENV_VAR}}` body from `[^}]` to `[^{}]`
5+
* must not change resolution for any name the product can represent.
6+
*
7+
* The narrowing exists to remove polynomial backtracking — a class admitting
8+
* its own opening delimiter lets every offset in a run of `{` restart a full
9+
* scan. That is a real change to the accepted language, not a refactor, and it
10+
* runs on the execution path, so the divergence is pinned down here rather than
11+
* argued in a review comment.
12+
*
13+
* The one accepted divergence is enumerated below: a reference whose name
14+
* contains `{`. `PATTERNS.ENV_VAR_NAME` forbids that character, so no such name
15+
* can be stored through the secrets manager, and the divergence is unreachable.
16+
*/
17+
18+
import { describe, expect, it } from 'vitest'
19+
import { PATTERNS, REFERENCE } from '@/executor/constants'
20+
import { createEnvVarPattern, resolveEnvVarReferences } from '@/executor/utils/reference-validation'
21+
22+
/** The pattern as it stood before the narrowing. */
23+
function createLegacyEnvVarPattern(): RegExp {
24+
return new RegExp(`\\${REFERENCE.ENV_VAR_START}([^}]+)\\${REFERENCE.ENV_VAR_END}`, 'g')
25+
}
26+
27+
function matchesOf(pattern: RegExp, text: string): string[] {
28+
pattern.lastIndex = 0
29+
const found: string[] = []
30+
let match: RegExpExecArray | null
31+
while ((match = pattern.exec(text)) !== null) found.push(`${match.index}:${match[0]}`)
32+
return found
33+
}
34+
35+
/**
36+
* Complete up to character equivalence: `{`, `}`, `<`, `>` are the delimiters,
37+
* and every other character behaves identically in both bodies. `_`, `A` and
38+
* the space cover the name, trim and separator cases.
39+
*/
40+
const ALPHABET = ['{', '}', '<', '>', 'A', '_', ' ']
41+
const MAX_LEN = 6
42+
43+
function* corpus(): Generator<string> {
44+
let level = ['']
45+
yield ''
46+
for (let length = 1; length <= MAX_LEN; length++) {
47+
const next: string[] = []
48+
for (const prefix of level) {
49+
for (const character of ALPHABET) {
50+
const candidate = prefix + character
51+
next.push(candidate)
52+
yield candidate
53+
}
54+
}
55+
level = next
56+
}
57+
}
58+
59+
/** The enumerated divergence: a `{{` whose body reaches a `{` before any `}`. */
60+
const NAME_CONTAINS_BRACE = /\{\{[^}]*\{/
61+
62+
describe('env var pattern narrowing', () => {
63+
it('diverges only where the reference name contains an opening brace', () => {
64+
const legacy = createLegacyEnvVarPattern()
65+
const current = createEnvVarPattern()
66+
const unexplained: string[] = []
67+
let differed = 0
68+
69+
for (const text of corpus()) {
70+
const before = matchesOf(legacy, text)
71+
const after = matchesOf(current, text)
72+
if (before.join('|') === after.join('|')) continue
73+
differed++
74+
if (!NAME_CONTAINS_BRACE.test(text) && unexplained.length < 20) unexplained.push(text)
75+
}
76+
77+
expect(unexplained).toEqual([])
78+
expect(differed).toBeGreaterThan(0)
79+
})
80+
81+
it('never diverges on a representable environment-variable name', () => {
82+
const legacy = createLegacyEnvVarPattern()
83+
const current = createEnvVarPattern()
84+
const names = ['A', '_', 'API_KEY', 'a1', '_x9', 'X'.repeat(64)]
85+
const surroundings = ['%s', 'a%sb', '%s%s', 'x {{%s}} y', '{{%s}} {{%s}}']
86+
87+
for (const name of names) {
88+
expect(PATTERNS.ENV_VAR_NAME.test(name)).toBe(true)
89+
for (const shape of surroundings) {
90+
const text = shape.replaceAll('%s', `{{${name}}}`).replaceAll('{{{{', '{{')
91+
expect(matchesOf(current, text)).toEqual(matchesOf(legacy, text))
92+
}
93+
}
94+
})
95+
96+
it('still resolves references, including padded and embedded ones', () => {
97+
const envVars = { API_KEY: 'secret', OTHER: 'value' }
98+
expect(resolveEnvVarReferences('{{API_KEY}}', envVars)).toBe('secret')
99+
expect(resolveEnvVarReferences('{{ API_KEY }}', envVars)).toBe('secret')
100+
expect(resolveEnvVarReferences('a-{{API_KEY}}-b', envVars)).toBe('a-secret-b')
101+
expect(resolveEnvVarReferences('{{API_KEY}}/{{OTHER}}', envVars)).toBe('secret/value')
102+
})
103+
104+
it('scans an unterminated brace run in linear time', () => {
105+
const started = performance.now()
106+
resolveEnvVarReferences('{'.repeat(100_000), { API_KEY: 'secret' })
107+
expect(performance.now() - started).toBeLessThan(1_000)
108+
})
109+
})

apps/sim/executor/utils/reference-validation.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import { REFERENCE } from '@/executor/constants'
22

3+
/**
4+
* Body of an `{{ENV_VAR}}` reference. Excludes both braces for the same reason
5+
* `createReferencePattern` excludes both angle brackets: a class that admits its
6+
* own opening delimiter lets every offset in a run of `{` restart a full
7+
* backtracking scan, which is quadratic in the length of the run. Env var names
8+
* are `PATTERNS.ENV_VAR_NAME` (`[A-Za-z_][A-Za-z0-9_]*`), so no representable
9+
* name is excluded by this.
10+
*/
11+
const ENV_VAR_BODY = '[^{}]+'
12+
313
/**
414
* Creates a regex pattern for matching variable references.
515
* Uses [^<>]+ to prevent matching across nested brackets (e.g., "<3 <real.ref>" matches separately).
@@ -15,7 +25,7 @@ export function createReferencePattern(): RegExp {
1525
* Creates a regex pattern for matching environment variables {{variable}}
1626
*/
1727
export function createEnvVarPattern(): RegExp {
18-
return new RegExp(`\\${REFERENCE.ENV_VAR_START}([^}]+)\\${REFERENCE.ENV_VAR_END}`, 'g')
28+
return new RegExp(`\\${REFERENCE.ENV_VAR_START}(${ENV_VAR_BODY})\\${REFERENCE.ENV_VAR_END}`, 'g')
1929
}
2030

2131
export interface EnvVarResolveOptions {
@@ -66,7 +76,7 @@ export function resolveEnvVarReferences(
6676
if (typeof value === 'string') {
6777
if (resolveExactMatch) {
6878
const exactMatchPattern = new RegExp(
69-
`^\\${REFERENCE.ENV_VAR_START}([^}]+)\\${REFERENCE.ENV_VAR_END}$`
79+
`^\\${REFERENCE.ENV_VAR_START}(${ENV_VAR_BODY})\\${REFERENCE.ENV_VAR_END}$`
7080
)
7181
const exactMatch = exactMatchPattern.exec(value)
7282
if (exactMatch) {
@@ -140,7 +150,7 @@ export function createWorkflowVariablePattern(): RegExp {
140150
export function createCombinedPattern(): RegExp {
141151
return new RegExp(
142152
`${REFERENCE.START}[^${REFERENCE.START}${REFERENCE.END}]+${REFERENCE.END}|` +
143-
`\\${REFERENCE.ENV_VAR_START}[^}]+\\${REFERENCE.ENV_VAR_END}`,
153+
`\\${REFERENCE.ENV_VAR_START}${ENV_VAR_BODY}\\${REFERENCE.ENV_VAR_END}`,
144154
'g'
145155
)
146156
}

apps/sim/lib/execution/code-placeholders/shared.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
} from '@/lib/execution/code-placeholders/types'
1212

1313
const MAX_PLACEHOLDERS = 10_000
14-
const PLACEHOLDER_PATTERN = /\{\{([^}]+)\}\}/g
14+
const PLACEHOLDER_PATTERN = /\{\{([^{}]+)\}\}/g
1515

1616
export class CodePlaceholderCompileError extends Error {
1717
readonly line?: number

apps/sim/lib/mcp/domain-check.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ vi.mock('dns/promises', () => ({
1313
}))
1414

1515
vi.mock('@/executor/utils/reference-validation', () => ({
16-
createEnvVarPattern: () => /\{\{([^}]+)\}\}/g,
16+
createEnvVarPattern: () => /\{\{([^{}]+)\}\}/g,
1717
}))
1818

1919
import {

0 commit comments

Comments
 (0)