Skip to content
Merged
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
9 changes: 6 additions & 3 deletions eval/deepswe/run_shard.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,17 @@ def write_token_file():
"""Publish the current host token where the proxy can re-read it.

Atomic: the proxy stats this file on every request, so a partial write would
be served as a bearer. Written 0600 because it is a live credential.
be served as a bearer. Created 0600 because it is a live credential.
"""
tok = oauth_token(min_valid_s=1800)
if tok is None:
return False
tmp = TOKEN_FILE.with_suffix(".tmp")
tmp.write_text(tok)
tmp.chmod(0o600)
# 0600 at creation: chmod after writing leaves the token briefly readable
# by any user on the host.
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w") as fh:
fh.write(tok)
os.replace(tmp, TOKEN_FILE)
return True

Expand Down
16 changes: 15 additions & 1 deletion src/core/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,11 +342,25 @@ const TAB_WIDTH = 4; // standard 4-space tab stops (logs, code, tool output are
export function minifyForRender(text: string): string {
return text
.split('\n')
.map((line) => line.replace(/[ \t]+$/, ''))
.map(trimLineEnd)
.join('\n')
.replace(/\n{4,}/g, '\n\n\n'); // 4+ \n → 3 \n (max 2 blank lines)
}

/** Drop trailing spaces and tabs from one line.
* Scanned backwards: /[ \t]+$/ retries from every position in a long run of
* spaces when the line does not end in one, which is quadratic on one very
* long line. */
function trimLineEnd(line: string): string {
let end = line.length;
while (end > 0) {
const c = line.charCodeAt(end - 1);
if (c !== 32 && c !== 9) break;
end -= 1;
}
return end === line.length ? line : line.slice(0, end);
}

// --- R3 reflow -------------------------------------------------------------
//
// Marks each original hard newline with U+21B5 ↵ so the model can distinguish
Expand Down
108 changes: 97 additions & 11 deletions src/core/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,38 @@ const KNOWN_STATIC_TAGS = [
'toolUseInstructions',
] as const;

/** Tag-name and whitespace classifiers matching /[a-zA-Z]/,
* /[a-zA-Z0-9_-]/ and /\s/. */
function isTagNameStart(c: number): boolean {
return (c >= 97 && c <= 122) || (c >= 65 && c <= 90); // a-z A-Z
}

function isTagNameChar(c: number): boolean {
return (
(c >= 97 && c <= 122) || // a-z
(c >= 65 && c <= 90) || // A-Z
(c >= 48 && c <= 57) || // 0-9
c === 95 || // _
c === 45 // -
);
}

function isTagSpace(c: number): boolean {
if (c === 32 || (c >= 9 && c <= 13)) return true; // space, \t \n \v \f \r
if (c < 0xa0) return false;
return (
c === 0xa0 ||
c === 0x1680 ||
(c >= 0x2000 && c <= 0x200a) ||
c === 0x2028 ||
c === 0x2029 ||
c === 0x202f ||
c === 0x205f ||
c === 0x3000 ||
c === 0xfeff
);
}

function splitStaticDynamic(text: string): {
staticText: string;
dynamicText: string;
Expand Down Expand Up @@ -852,16 +884,55 @@ function splitStaticDynamic(text: string): {
// surfacing the tag name lets us detect it within hours of a release.
const known = new Set<string>(DYNAMIC_BLOCK_TAGS);
const knownStatic = new Set<string>(KNOWN_STATIC_TAGS);
const sniffer = /<([a-zA-Z][a-zA-Z0-9_-]*)(?:\s[^>]*)?>([\s\S]*?)<\/\1>/g;
const unknown = new Set<string>();
const staticTagContents = new Map<string, string>();
let s: RegExpExecArray | null;
while ((s = sniffer.exec(staticBuf)) !== null) {
const tag = s[1]!;
if (tag.length > 64) continue;
if (!known.has(tag) && !knownStatic.has(tag)) unknown.add(tag);
// Fold repeated tags (e.g. several <example>s) into one fingerprint.
staticTagContents.set(tag, (staticTagContents.get(tag) ?? '') + s[2]!);
// Scanned by index: matching this with a regex costs quadratic time on
// untrusted text, since both a lazy `[\s\S]*?` body and an `(?:\s[^>]*)?>`
// attribute run rescan the tail once per candidate tag.
const noCloser = new Set<string>();
let i = 0;
while (i < staticBuf.length) {
const lt = staticBuf.indexOf('<', i);
if (lt < 0) break;
let j = lt + 1;
if (!isTagNameStart(staticBuf.charCodeAt(j))) {
i = lt + 1;
continue;
}
j += 1;
while (j < staticBuf.length && isTagNameChar(staticBuf.charCodeAt(j))) j += 1;
// Either `<tag>` or `<tag ...>`; anything else is not an opening tag.
let gt: number;
if (staticBuf[j] === '>') {
gt = j;
} else if (j < staticBuf.length && isTagSpace(staticBuf.charCodeAt(j))) {
gt = staticBuf.indexOf('>', j);
// No `>` left, so no later opening can complete either.
if (gt < 0) break;
} else {
i = lt + 1;
continue;
}
const tag = staticBuf.slice(lt + 1, j);
const contentStart = gt + 1;
const closer = `</${tag}>`;
// A closer missing after one opening is missing for every later opening of
// the same tag, so record it and skip the repeated failed scan.
const end = noCloser.has(tag) ? -1 : staticBuf.indexOf(closer, contentStart);
if (end < 0) {
noCloser.add(tag);
i = lt + 1;
continue;
}
if (tag.length <= 64) {
if (!known.has(tag) && !knownStatic.has(tag)) unknown.add(tag);
// Fold repeated tags (e.g. several <example>s) into one fingerprint.
staticTagContents.set(
tag,
(staticTagContents.get(tag) ?? '') + staticBuf.slice(contentStart, end),
);
}
i = end + closer.length;
}

return {
Expand Down Expand Up @@ -1114,14 +1185,29 @@ export function firstMessageHasSystemReminder(messages: Message[] | undefined):
return false;
}

/** Body of the first `<tag>…</tag>` pair, or undefined when unpaired.
* Scanned by index: a lazy `[\s\S]*?` span rescans the tail for every
* candidate opening, so many openings and no closer costs quadratic time.
* Tag names are ASCII literals, so lowercasing suffices for a
* case-insensitive match. */
function firstTagBody(text: string, tag: string): string | undefined {
const open = `<${tag}>`;
const close = `</${tag}>`;
const haystack = text.toLowerCase();
const start = haystack.indexOf(open);
if (start < 0) return undefined;
const end = haystack.indexOf(close, start + open.length);
if (end < 0) return undefined;
return text.slice(start + open.length, end);
}

/** Parse structured fields from the dynamic slab for telemetry. Read-only. */
export function extractEnvFields(dynamicText: string): EnvFields {
const out: EnvFields = {};
if (!dynamicText) return out;

const envMatch = /<env>([\s\S]*?)<\/env>/i.exec(dynamicText);
if (envMatch) {
const body = envMatch[1]!;
const body = firstTagBody(dynamicText, 'env');
if (body !== undefined) {
const cwd = /(?:^|\n)\s*Working directory:\s*(.+?)\s*(?:\n|$)/i.exec(body);
if (cwd) out.cwd = cwd[1]!.trim();
const gitRepo = /(?:^|\n)\s*Is directory a git repo:\s*(Yes|No)\b/i.exec(body);
Expand Down
196 changes: 196 additions & 0 deletions tests/redos-guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { describe, expect, it } from 'vitest';
import { extractEnvFields, transformRequest } from '../src/core/transform.js';
import { minifyForRender } from '../src/core/render.js';

// Characterization + ReDoS regression tests for the two regex sites reported by
// code scanning (js/polynomial-redos): the <env> span in extractEnvFields and
// the static-tag sniffer in splitStaticDynamic. Both parse untrusted request
// text, so runtime has to stay near-linear in input size while the parsed
// results stay unchanged.

// Generous wall-clock budget so the suite stays stable on loaded hosts. The
// pre-fix code needed over 20s on the 640KB case, so the signal is not marginal.
const BUDGET_MS = 2000;

function elapsed(fn: () => void): number {
const t0 = performance.now();
fn();
return performance.now() - t0;
}

const WELL_FORMED_ENV = [
'<env>',
'Working directory: /tmp/proj',
'Is directory a git repo: Yes',
'Platform: darwin',
'OS Version: Darwin 24.0.0',
"Today's date: 2026-05-18",
'</env>',
].join('\n');

describe('extractEnvFields behaviour is preserved', () => {
it('extracts every field from a well-formed block', () => {
const out = extractEnvFields(WELL_FORMED_ENV);
expect(out.cwd).toBe('/tmp/proj');
expect(out.isGitRepo).toBe(true);
expect(out.platform).toBe('darwin');
expect(out.osVersion).toBe('Darwin 24.0.0');
expect(out.today).toBe('2026-05-18');
});

it('reads a No git-repo line as false', () => {
const out = extractEnvFields(WELL_FORMED_ENV.replace('git repo: Yes', 'git repo: No'));
expect(out.isGitRepo).toBe(false);
});

it('returns an empty object for empty input', () => {
expect(extractEnvFields('')).toEqual({});
});

it('reads no env fields when the closing tag is missing', () => {
const out = extractEnvFields('<env>\nWorking directory: /tmp/proj\n');
expect(out.cwd).toBeUndefined();
});

it('picks up branch outside <env>', () => {
expect(extractEnvFields('On branch feature/x').gitBranch).toBe('feature/x');
expect(extractEnvFields('Branch: main').gitBranch).toBe('main');
expect(extractEnvFields('Current branch: dev').gitBranch).toBe('dev');
});

it('keeps the first <env> block when several are present', () => {
const two = '<env>\nPlatform: darwin\n</env>\n<env>\nPlatform: linux\n</env>';
expect(extractEnvFields(two).platform).toBe('darwin');
});

it('matches the env tag case-insensitively', () => {
expect(extractEnvFields('<ENV>\nPlatform: win32\n</ENV>').platform).toBe('win32');
});

it('keeps fields that sit on the first line of the block', () => {
expect(extractEnvFields('<env>Platform: darwin\n</env>').platform).toBe('darwin');
});
});

describe('extractEnvFields stays fast on hostile input', () => {
// Worst case for an unbounded lazy span: the opening tag is present and the
// closing tag never arrives, so the engine must scan the whole tail.
it('handles a large unterminated <env> block within budget', () => {
const hostile = '<env>' + 'a'.repeat(640_000);
expect(elapsed(() => extractEnvFields(hostile))).toBeLessThan(BUDGET_MS);
});

it('handles many unterminated <env> openings within budget', () => {
const hostile = '<env>'.repeat(50_000);
expect(elapsed(() => extractEnvFields(hostile))).toBeLessThan(BUDGET_MS);
});

it('handles near-miss field lines within budget', () => {
const hostile = '<env>' + '\n' + ' '.repeat(320_000) + 'x' + '</env>';
expect(elapsed(() => extractEnvFields(hostile))).toBeLessThan(BUDGET_MS);
});

it('handles near-miss branch lines within budget', () => {
const hostile = '\n' + ' '.repeat(320_000) + 'x';
expect(elapsed(() => extractEnvFields(hostile))).toBeLessThan(BUDGET_MS);
});

it('scales sub-quadratically with input size', () => {
const small = elapsed(() => extractEnvFields('<env>' + 'a'.repeat(80_000)));
const large = elapsed(() => extractEnvFields('<env>' + 'a'.repeat(640_000)));
// 8x the input. Quadratic would be roughly 64x; the slack absorbs timing
// noise while still failing loudly on a genuine blowup.
expect(large).toBeLessThan(Math.max(small, 1) * 24);
});
});

describe('static tag sniffer behaviour is preserved', () => {
async function unknownTagsFor(extra: string): Promise<string[]> {
// Dense slab keeps the compression gate green so the sniffer runs.
const slab = 'claude.md ground truth. '.repeat(2200);
const body = new TextEncoder().encode(
JSON.stringify({
model: 'claude',
messages: [{ role: 'user', content: 'hi' }],
system: slab + '\n' + extra,
}),
);
const { info } = await transformRequest(body);
return info.unknownStaticTags ?? [];
}

it('surfaces an unknown tag-shaped block in the static slab', async () => {
expect(await unknownTagsFor('<brandNewTag>whatever</brandNewTag>')).toContain('brandNewTag');
});

it('does not report known static tags', async () => {
expect(await unknownTagsFor('<types>some types</types>')).not.toContain('types');
});

it('does not report a tag whose closing tag is absent', async () => {
expect(await unknownTagsFor('<danglingTag>no closer here')).not.toContain('danglingTag');
});

it('reports a tag carrying attributes', async () => {
expect(await unknownTagsFor('<withAttrs id="1">body</withAttrs>')).toContain('withAttrs');
});

it('reports several distinct unknown tags', async () => {
const tags = await unknownTagsFor('<alphaTag>a</alphaTag>\n<betaTag>b</betaTag>');
expect(tags).toContain('alphaTag');
expect(tags).toContain('betaTag');
});

it('stays within budget on unterminated tag openings', async () => {
// '<A ' repeated: every opening starts an attribute run that finds no '>'.
// Quadratic against a regex opening scan, linear against an index scan.
// A regex scan needs ~18s here; an index scan needs well under a
// millisecond, so the budget separates the two by a wide margin.
const hostile = '<A '.repeat(80_000);
const t0 = performance.now();
await unknownTagsFor(hostile);
expect(performance.now() - t0).toBeLessThan(5_000);
});

it('treats an exotic whitespace separator as a tag opening', async () => {
// \u00a0 satisfies /\s/, so the previous regex accepted it after the name.
expect(await unknownTagsFor('<oddSpaceTag\u00a0x="1">body</oddSpaceTag>')).toContain(
'oddSpaceTag',
);
});

it('stays within budget on a hostile static slab', async () => {
// Many unclosed tag openings: the pathological shape for the sniffer.
const hostile = '<aTag>'.repeat(20_000);
const t0 = performance.now();
await unknownTagsFor(hostile);
expect(performance.now() - t0).toBeLessThan(10_000);
});
});

describe('minifyForRender stays fast on hostile input', () => {
// A single very long line of spaces that does not end in one: the trailing
// whitespace strip has to reject at every position in the run.
it('handles one huge run of trailing spaces within budget', () => {
const hostile = ' '.repeat(400_000) + 'x';
expect(elapsed(() => minifyForRender(hostile))).toBeLessThan(BUDGET_MS);
});

it('handles a huge run of tabs within budget', () => {
const hostile = '\t'.repeat(400_000) + 'x';
expect(elapsed(() => minifyForRender(hostile))).toBeLessThan(BUDGET_MS);
});

it('scales sub-quadratically with line length', () => {
const small = elapsed(() => minifyForRender(' '.repeat(50_000) + 'x'));
const large = elapsed(() => minifyForRender(' '.repeat(400_000) + 'x'));
expect(large).toBeLessThan(Math.max(small, 1) * 24);
});

it('still strips trailing whitespace and collapses blank runs', () => {
expect(minifyForRender('a \nb\t\nc')).toBe('a\nb\nc');
expect(minifyForRender('a\n\n\n\n\n\nb')).toBe('a\n\n\nb');
expect(minifyForRender(' indent kept ')).toBe(' indent kept');
expect(minifyForRender('mid line spaces')).toBe('mid line spaces');
});
});