From 8807a9b8542f34bf9780a0236dfeac8232bbce25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 1 Aug 2026 12:51:33 +0200 Subject: [PATCH 1/4] fix: clear the polynomial-redos class across main Three sites of the same CodeQL js/polynomial-redos family: - packages/replay-test session-test-artifacts/-discovery slugs trimmed edge dashes with /^-+|-+$/g, which backtracks polynomially on long dash runs built from caller-supplied paths (alerts #27/#28). Replaced with a shared linear trimEdgeDashes. - src/replay/target-identity.ts's target-v1 annotation line regex had the \s+(.*) ambiguity (the shape flagged as alert #29 on the #1536 copy). Anchored the payload group on \S so the split point is unique; the only caller matches against trimmed lines, so behavior is unchanged. Adversarial regression test on the slug path (100k-char dash run, sub-second); the annotation-regex adversarial case is covered on the #1536 package copy and the frozen replay-compat corpus passes here unchanged. Co-Authored-By: Claude --- .../__tests__/session-test-discovery.test.ts | 13 +++++++++- .../src/internal/session-test-artifacts.ts | 24 +++++++++++++++---- .../src/internal/session-test-discovery.ts | 11 +++------ src/replay/target-identity.ts | 2 +- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/packages/replay-test/src/internal/__tests__/session-test-discovery.test.ts b/packages/replay-test/src/internal/__tests__/session-test-discovery.test.ts index ebc2eff1b9..b13e17b7bd 100644 --- a/packages/replay-test/src/internal/__tests__/session-test-discovery.test.ts +++ b/packages/replay-test/src/internal/__tests__/session-test-discovery.test.ts @@ -1,7 +1,10 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import { AppError } from '@agent-device/kernel/errors'; -import { discoverReplayTestEntries } from '../session-test-discovery.ts'; +import { + discoverReplayTestEntries, + buildReplayTestInvocationId, +} from '../session-test-discovery.ts'; import type { ReplayTestManifest, ReplayTestSource } from '../session-test-types.ts'; // Scheduler-owned discovery policy (#1478 P3b): which sources a --platform filter runs, which @@ -75,3 +78,11 @@ test('a suite that matched nothing after filtering is rejected', () => { (error: unknown) => error instanceof AppError && /No replay tests matched/.test(error.message), ); }); + +test('slug building stays linear on adversarial dash runs (CodeQL js/polynomial-redos)', () => { + const adversarial = `${'-'.repeat(50_000)}x${'-'.repeat(50_000)}`; + const startedAt = performance.now(); + const slugged = buildReplayTestInvocationId(adversarial); + assert.ok(performance.now() - startedAt < 1000); + assert.ok(slugged.startsWith('x')); +}); diff --git a/packages/replay-test/src/internal/session-test-artifacts.ts b/packages/replay-test/src/internal/session-test-artifacts.ts index 92d360bfe7..1c65e4c9dc 100644 --- a/packages/replay-test/src/internal/session-test-artifacts.ts +++ b/packages/replay-test/src/internal/session-test-artifacts.ts @@ -23,11 +23,12 @@ export function buildReplayTestArtifactSlug(filePath: string, cwd?: string): str ? path.basename(filePath) : relativePath; return ( - value - .toLowerCase() - .replace(/[\\/]+/g, '__') - .replace(/[^a-z0-9._-]+/g, '-') - .replace(/^-+|-+$/g, '') || 'test' + trimEdgeDashes( + value + .toLowerCase() + .replace(/[\\/]+/g, '__') + .replace(/[^a-z0-9._-]+/g, '-'), + ) || 'test' ); } @@ -135,3 +136,16 @@ function isExistingFile(filePath: string): boolean { return false; } } + +/** + * Linear-time edge trim. The regex form (`/^-+|-+$/g`) backtracks + * polynomially on long dash runs (CodeQL js/polynomial-redos #27/#28), and + * these slugs are built from caller-supplied file paths. + */ +export function trimEdgeDashes(value: string): string { + let start = 0; + let end = value.length; + while (start < end && value[start] === '-') start += 1; + while (end > start && value[end - 1] === '-') end -= 1; + return value.slice(start, end); +} diff --git a/packages/replay-test/src/internal/session-test-discovery.ts b/packages/replay-test/src/internal/session-test-discovery.ts index 0d5b2783f9..0d8550ffd1 100644 --- a/packages/replay-test/src/internal/session-test-discovery.ts +++ b/packages/replay-test/src/internal/session-test-discovery.ts @@ -1,4 +1,5 @@ import path from 'node:path'; +import { trimEdgeDashes } from './session-test-artifacts.ts'; import { AppError } from '@agent-device/kernel/errors'; import { isApplePlatform, type PlatformSelector } from '@agent-device/kernel/device'; import type { @@ -89,20 +90,14 @@ export function buildReplayTestSessionName( attemptIndex = 0, ): string { const baseName = path.basename(filePath, path.extname(filePath)); - const slug = baseName - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, ''); + const slug = trimEdgeDashes(baseName.toLowerCase().replace(/[^a-z0-9]+/g, '-')); const testNumber = caseIndex + 1; return `${sessionName}:test:${suiteInvocationId}:${testNumber}${slug ? `-${slug}` : ''}:attempt-${attemptIndex + 1}`; } export function buildReplayTestInvocationId(requestId?: string): string { const raw = requestId?.trim() || `${process.pid}-${Date.now().toString(36)}`; - const normalized = raw - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, ''); + const normalized = trimEdgeDashes(raw.toLowerCase().replace(/[^a-z0-9]+/g, '-')); return normalized || 'suite'; } diff --git a/src/replay/target-identity.ts b/src/replay/target-identity.ts index 14ae9254ca..2b1fd2582f 100644 --- a/src/replay/target-identity.ts +++ b/src/replay/target-identity.ts @@ -13,7 +13,7 @@ import { AppError } from '@agent-device/kernel/errors'; const TARGET_ANNOTATION_TAG = 'agent-device:target-v1'; // Captures the rest of the line verbatim: a line claiming the tag with a // garbage payload is a malformed v1 annotation, never an ordinary comment. -const TARGET_ANNOTATION_LINE_RE = /^#\s*agent-device:target-v(\d+)(?:\s+(.*))?$/; +const TARGET_ANNOTATION_LINE_RE = /^#\s*agent-device:target-v(\d+)(?:\s+(\S.*))?$/; export const TARGET_ANNOTATION_MAX_FIELD_BYTES = 256; export const TARGET_ANNOTATION_MAX_PAYLOAD_BYTES = 4096; From 6aea6b483ef5ea895fcc825cb06a9b8c8922f41c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 1 Aug 2026 13:56:31 +0200 Subject: [PATCH 2/4] test: make the redos regression fail against the retired regex form The edge-run input matched the old /^-+|-+$/g in one pass; the quadratic case is an interior run (each dash restarts a -+$ attempt that fails at the trailing byte). The slug pipeline collapses runs before trimming, so the test targets trimEdgeDashes directly and asserts the input comes back byte-identical. Co-Authored-By: Claude --- .../__tests__/session-test-discovery.test.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/replay-test/src/internal/__tests__/session-test-discovery.test.ts b/packages/replay-test/src/internal/__tests__/session-test-discovery.test.ts index b13e17b7bd..908a358337 100644 --- a/packages/replay-test/src/internal/__tests__/session-test-discovery.test.ts +++ b/packages/replay-test/src/internal/__tests__/session-test-discovery.test.ts @@ -5,6 +5,7 @@ import { discoverReplayTestEntries, buildReplayTestInvocationId, } from '../session-test-discovery.ts'; +import { trimEdgeDashes } from '../session-test-artifacts.ts'; import type { ReplayTestManifest, ReplayTestSource } from '../session-test-types.ts'; // Scheduler-owned discovery policy (#1478 P3b): which sources a --platform filter runs, which @@ -79,10 +80,16 @@ test('a suite that matched nothing after filtering is rejected', () => { ); }); -test('slug building stays linear on adversarial dash runs (CodeQL js/polynomial-redos)', () => { - const adversarial = `${'-'.repeat(50_000)}x${'-'.repeat(50_000)}`; +test('edge-dash trimming stays linear on an interior dash run (CodeQL js/polynomial-redos)', () => { + // The slug pipeline collapses character runs before trimming, so only a + // direct call can carry a long interior run — which is exactly the shape + // the retired /^-+|-+$/g form re-scans quadratically (each interior dash + // restarts a -+$ attempt that fails at the trailing x). 100k dashes take + // seconds there and must stay well under a second here, with the input + // returned byte-identical since nothing sits at the edges. + const interiorRun = `x${'-'.repeat(100_000)}x`; const startedAt = performance.now(); - const slugged = buildReplayTestInvocationId(adversarial); + const trimmed = trimEdgeDashes(interiorRun); assert.ok(performance.now() - startedAt < 1000); - assert.ok(slugged.startsWith('x')); + assert.equal(trimmed, interiorRun); }); From 8039cdf5c6fbfb4548e5ee5e54403d1a426f9928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 1 Aug 2026 14:28:32 +0200 Subject: [PATCH 3/4] fix: drop the import the test rewrite orphaned Co-Authored-By: Claude --- .../src/internal/__tests__/session-test-discovery.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/replay-test/src/internal/__tests__/session-test-discovery.test.ts b/packages/replay-test/src/internal/__tests__/session-test-discovery.test.ts index 908a358337..fe33c972ca 100644 --- a/packages/replay-test/src/internal/__tests__/session-test-discovery.test.ts +++ b/packages/replay-test/src/internal/__tests__/session-test-discovery.test.ts @@ -1,10 +1,7 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import { AppError } from '@agent-device/kernel/errors'; -import { - discoverReplayTestEntries, - buildReplayTestInvocationId, -} from '../session-test-discovery.ts'; +import { discoverReplayTestEntries } from '../session-test-discovery.ts'; import { trimEdgeDashes } from '../session-test-artifacts.ts'; import type { ReplayTestManifest, ReplayTestSource } from '../session-test-types.ts'; From 2ae86d93e87ffe676b710acb87f8b126e1dc805a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 1 Aug 2026 14:41:07 +0200 Subject: [PATCH 4/4] test: pin the all-dash fallback identifiers Artifact slug falls back to 'test', invocation id to 'suite', and a session-name slug that trims to nothing is omitted without a dangling separator. Co-Authored-By: Claude --- .../__tests__/session-test-discovery.test.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/replay-test/src/internal/__tests__/session-test-discovery.test.ts b/packages/replay-test/src/internal/__tests__/session-test-discovery.test.ts index fe33c972ca..3f2a3abb6e 100644 --- a/packages/replay-test/src/internal/__tests__/session-test-discovery.test.ts +++ b/packages/replay-test/src/internal/__tests__/session-test-discovery.test.ts @@ -1,8 +1,12 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import { AppError } from '@agent-device/kernel/errors'; -import { discoverReplayTestEntries } from '../session-test-discovery.ts'; -import { trimEdgeDashes } from '../session-test-artifacts.ts'; +import { + buildReplayTestInvocationId, + buildReplayTestSessionName, + discoverReplayTestEntries, +} from '../session-test-discovery.ts'; +import { buildReplayTestArtifactSlug, trimEdgeDashes } from '../session-test-artifacts.ts'; import type { ReplayTestManifest, ReplayTestSource } from '../session-test-types.ts'; // Scheduler-owned discovery policy (#1478 P3b): which sources a --platform filter runs, which @@ -90,3 +94,13 @@ test('edge-dash trimming stays linear on an interior dash run (CodeQL js/polynom assert.ok(performance.now() - startedAt < 1000); assert.equal(trimmed, interiorRun); }); + +test('all-dash inputs land on the documented fallbacks instead of empty identifiers', () => { + assert.equal(buildReplayTestArtifactSlug('/tmp/####', '/tmp'), 'test'); + assert.equal(buildReplayTestInvocationId('----'), 'suite'); + // A session-name slug that trims to nothing is omitted entirely — no dangling separator. + assert.equal( + buildReplayTestSessionName('s', 'suite1', '/tmp/----.ad', 0), + 's:test:suite1:1:attempt-1', + ); +});