Skip to content

Commit 491ffcf

Browse files
authored
Merge pull request lidge-jun#3347 from lidge-jun/codex/promote-preview-2410-v3
[WRONG BRANCH] release: refresh preview onto the audit-clean dev head for v2.41.0-preview.20260903
2 parents d7de528 + 1017867 commit 491ffcf

5 files changed

Lines changed: 95 additions & 20 deletions

File tree

‎.github/scripts/pr-carry-attribution.cjs‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,23 @@ const TRAILER_RE = /^[ \t]*co-authored-by:[ \t]*(.+)$/gim;
5656

5757
const FENCED_CODE_RE = /^[ \t]*(\u0060{3,}|~{3,})[\s\S]*?^[ \t]*\1[ \t]*$/gm;
5858
const INLINE_CODE_RE = /\u0060[^\u0060\n]*\u0060/g;
59-
const HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
59+
/**
60+
* HTML comments, which GitHub never renders.
61+
*
62+
* The `(?:-->|$)` alternative is load-bearing and matches `pr-quality.cjs`: an
63+
* UNCLOSED comment runs to the end of the text, because that is what GitHub
64+
* does with it. Without the alternative, `<!--` with no terminator matched
65+
* nothing, so everything after it stayed in the scanned text while GitHub
66+
* rendered none of it — an author could write a carry claim that the gate reads
67+
* and no human ever sees, or bury one the gate misses in text that renders.
68+
* Either direction is a divergence between what is enforced and what is shown.
69+
*
70+
* CodeQL flagged the same shape as `js/incomplete-multi-character-sanitization`
71+
* on #3342. The alert's own framing (HTML element injection) does not apply —
72+
* this output is matched by regex, never rendered — but the underlying
73+
* observation, that the strip is incomplete, is correct for this gate's purpose.
74+
*/
75+
const HTML_COMMENT_RE = /<!--[\s\S]*?(?:-->|$)/g;
6076

6177
/**
6278
* Carry language inside a fenced block, an inline span, or an HTML comment is

‎.github/scripts/pr-carry-attribution.test.cjs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,35 @@ describe("assessCarryAttribution", () => {
120120
);
121121
});
122122

123+
it("ignores carry language after an unclosed HTML comment", () => {
124+
// GitHub renders nothing after an unterminated `<!--`, so neither does the
125+
// gate. The closing-delimiter-only pattern used to match nothing here and
126+
// leave the whole tail in the scanned text, which is the divergence CodeQL
127+
// flagged on #3342: enforced text and rendered text stopped agreeing.
128+
assert.deepEqual(
129+
assessCarryAttribution(
130+
base({
131+
body: ["This is an ordinary fix.", "", "<!-- supersedes #2797"].join("\n"),
132+
}),
133+
),
134+
[],
135+
);
136+
});
137+
138+
it("still reads carry language that follows a CLOSED comment", () => {
139+
// The guard above must not swallow the rest of the body wholesale: a
140+
// properly closed comment ends at its own `-->`, and a real claim after it
141+
// is still a claim.
142+
assert.equal(
143+
assessCarryAttribution(
144+
base({
145+
body: ["<!-- a note -->", "", "Supersedes #2797."].join("\n"),
146+
}),
147+
).length,
148+
1,
149+
);
150+
});
151+
123152
it("passes an ordinary pull request with no carry language", () => {
124153
assert.deepEqual(
125154
assessCarryAttribution(base({ body: "Closes #2797." })),

‎bun.lock‎

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,10 @@
7474
},
7575
"overrides": {
7676
"@hono/node-server": "2.1.0",
77-
"fast-uri": "^3.1.5",
77+
"fast-uri": "^3.1.7",
7878
"hono": "4.13.1",
79-
"ip-address": "^10.4.0"
79+
"ip-address": "^10.4.0",
80+
"qs": "^6.16.0"
8081
},
8182
"keywords": [
8283
"codex",

‎tests/strict-semver.test.ts‎

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,63 @@ import { parseStrictSemver } from "../src/lib/strict-semver";
1010
*
1111
* The length ceiling did not help. It only chose where on the curve the input landed.
1212
*/
13+
/**
14+
* Every timing assertion here measures the BEST of several runs, not a single one.
15+
*
16+
* A first call carries one-time cost the parse itself does not: regex compilation, JIT
17+
* warm-up, and whatever the shared CI runner was doing during that millisecond. On a
18+
* loaded macOS runner that noise reached 53.77ms against a 50ms budget and failed a
19+
* suite whose subject is three orders of magnitude away from the regression it guards
20+
* (522ms). A gate that fires on runner weather rather than on the defect teaches
21+
* everyone to re-run it, which is how a real ReDoS regression would get waved through.
22+
*
23+
* The minimum is the right statistic for this question. Superlinear backtracking is a
24+
* property of the pattern, so it reproduces on EVERY iteration; scheduler noise does
25+
* not. If the exponential path returns, no run is fast.
26+
*
27+
* That claim was measured rather than assumed. Running the semver.org prerelease
28+
* pattern this module replaced against the same inputs, three runs each:
29+
*
30+
* reps=20 len=68 17.6ms 17.4ms 17.4ms
31+
* reps=30 len=98 545.4ms 521.2ms 500.0ms
32+
* reps=39 len=125 492.3ms 493.5ms 491.3ms
33+
* reps=45 len=128 495.3ms 507.9ms 527.8ms
34+
*
35+
* The blowup is on every run, not the first, so a best-of-N below 50ms still fails
36+
* loudly if it comes back. The spread across runs is under 10%, which is what a
37+
* deterministic cost looks like next to the 4ms of scheduler jitter that broke the
38+
* single-sample form.
39+
*/
40+
function fastestParseMs(input: string, runs = 5): number {
41+
let best = Infinity;
42+
for (let i = 0; i < runs; i++) {
43+
const started = performance.now();
44+
parseStrictSemver(input);
45+
const elapsed = performance.now() - started;
46+
if (elapsed < best) best = elapsed;
47+
}
48+
return best;
49+
}
50+
1351
describe("parseStrictSemver ReDoS resistance", () => {
1452
test("the flagged attack shape stays linear at the length ceiling", () => {
1553
// "0.0.0-0." followed by repetitions of "--." is the input CodeQL named.
1654
const attack = ("0.0.0-0." + "--.".repeat(45)).slice(0, 128);
1755
expect(attack.length).toBe(128);
1856

19-
const started = performance.now();
2057
expect(parseStrictSemver(attack)).toBeNull();
21-
const elapsed = performance.now() - started;
2258

2359
// The vulnerable pattern took ~522ms for this input. Anything in that region means the
2460
// superlinear path is back; a linear parse lands three orders of magnitude below it.
25-
expect(elapsed).toBeLessThan(50);
61+
expect(fastestParseMs(attack)).toBeLessThan(50);
2662
});
2763

2864
test("cost does not grow with the number of repetitions", () => {
29-
const measure = (reps: number): number => {
30-
const input = ("0.0.0-0." + "--.".repeat(reps)).slice(0, 128);
31-
const started = performance.now();
32-
parseStrictSemver(input);
33-
return performance.now() - started;
34-
};
65+
const inputFor = (reps: number): string => ("0.0.0-0." + "--.".repeat(reps)).slice(0, 128);
3566

3667
// Under the old pattern, going from 20 to 39 repetitions moved 16ms to 524ms.
37-
measure(20);
38-
const short = measure(20);
39-
const long = measure(39);
40-
expect(short).toBeLessThan(50);
41-
expect(long).toBeLessThan(50);
68+
expect(fastestParseMs(inputFor(20))).toBeLessThan(50);
69+
expect(fastestParseMs(inputFor(39))).toBeLessThan(50);
4270
});
4371

4472
test("the length guard still rejects before any matching work", () => {

0 commit comments

Comments
 (0)