Repository navigation
docs(security): track bundled browser URI parser exposure - #236
Merged
Merged
Conversation
…y vulnerability govulncheck reports GO-2026-6809 (crypto/x509 distrusts compromised intermediate certificates) as reachable in Go 1.27.2 through the CLI's diagnostic output path, so the release candidate and every source gate fail at go:vulncheck. Go 1.27.3 contains the fix. - go.mod: toolchain go1.27.3; the go directive and every module requirement are unchanged. - release/change-record.v2.json: the toolchain addition now states that release binaries are built with Go 1.27.3 and why; the pinned copy in internal/tools/releasechange/record_test.go is regenerated from it. Proof on this tree: A local npm run check sequence on darwin/arm64 passed (exit 0) on tree f7cfc5f with npm 12.2.0, CPython 3.15.0 and Go 1.27.3: 561/561 browser cases in Chromium, Firefox and WebKit with zero retries. One recorded deviation: the host firewall blocks Go-built binaries from the network, so the go:vulncheck step ran govulncheck against a vulnerability database snapshot fetched with curl (modified 2026-10-21T00:27:29Z) and found no vulnerabilities. With the same snapshot, Go 1.27.2 is reported affected by GO-2026-6809.
Add the required job "quality / platform smoke / linux-arm64" on the GitHub-hosted ubuntu-24.04-arm runner. Its first step fails unless uname -m reports aarch64. It then sets up the same pinned Node 26.11.1, verified npm, Go and Python 3.15.0 toolchains as the macOS platform jobs, runs the rootpath, repositorytransaction, processgroup and projectstatus packages natively, runs npm run npm:version and npm run platform:smoke, builds the complete current release wheel set, and runs the CPython 3.11.0 installed-wheel smoke, which selects the manylinux_2_17_aarch64 wheel and the pinned linux/arm64 CPython 3.11.0 image digest. The required aggregate needs the job and requires its success. The workflow oracles pin the job's name, runner, 15-minute timeout and exact ordered steps, the five required leaf jobs and the aggregate shell. They refuse mutants that move the job to another runner, drop, move or weaken its architecture check, drop or narrow its invariant, smoke, wheel-set or CPython 3.11.0 steps, or remove it from the aggregate needs or result checks. The Python runtime oracle counts the job as a pinned Python consumer. README states that required CI executes Linux arm64 on ubuntu-24.04-arm and x64 on ubuntu-24.04, that CI does not execute a host at the manylinux 2.17 floor, and that the CPython 3.11.0 smoke runs on both Linux architectures; its projection test follows. REQ-PROOFKIT-QUALITY-011 now names the Linux arm64 platform job, its architecture check and its CPython 3.11.0 step, and five required leaf jobs instead of four. CONTRIBUTING lists ubuntu-24.04-arm among the source-check targets. Proof on this tree: A local npm run check sequence on darwin/arm64 passed (exit 0) on tree 5573507 with npm 12.2.0, CPython 3.15.0 and Go 1.27.3: 561/561 browser cases in Chromium, Firefox and WebKit with zero retries. One recorded deviation: the host firewall blocks Go-built binaries from the network, so go:vulncheck ran against a vulnerability database snapshot fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities. The new arm64 job's success is proven only by hosted CI on this PR.
Add a visual reference gate for the rendered requirement views. npm run browser:visual, which npm run check does not run, compares fourteen views (the source, spec-tree, structured proof, compact proof and coverage static renderings in Cards and Table modes, and the Specifications, Coverage, Diff and Traceability workspace tabs) in Chromium, Firefox and Playwright WebKit, light and dark, at 320 and 1440 CSS pixels, plus one Chromium README scene: 169 references. A reference is a digest, not an image. tests/visual/references.json binds, for each reference ID <engine>/<view>.<scheme>.<width>, the width, the height and the SHA-256 of the canonical pixel encoding that REQ-PROOFKIT-QUALITY-028 specifies. No reference image is stored in the repository. tests/visual/environment.json records the reference environment: the mcr.microsoft.com/playwright:v1.63.0-noble image with its index and platform digests on linux/arm64, and the observed OS identity, machine architecture, image marker, fontconfig, FreeType, HarfBuzz and font package versions, the SHA-256 of the committed tests/visual/fonts.conf, the fc-match results for monospace, sans-serif, serif, system-ui and ui-monospace, the installed engine revisions, the absence of emulation and the launched browser versions. fc-match reads the text after an unescaped hyphen as a point size, so each family is queried by its whole name with every hyphen escaped. fonts.conf includes the system configuration and prefers Liberation Sans for sans-serif and system-ui, Liberation Serif for serif and Liberation Mono for monospace and ui-monospace, which have real bold faces. The system configuration appends sans-serif to every pattern that names no generic family, so the strong sans-serif preference makes each family fontconfig does not know a strong alias of Liberation Sans, which WebKit accepts for the first family that WebKit looks up in fontconfig; without the ui-monospace mapping WebKit drew the product's monospace stacks in Liberation Sans. The gate copies fonts.conf into its run directory and sets the copy as FONTCONFIG_FILE before observing, so fc-match and every browser read the hashed bytes. The engine fixture requires the copy to equal the test snapshot's fonts.conf and requires that no project sets its own browser environment, so every browser inherits that FONTCONFIG_FILE. references.json also binds the SHA-256 of the environment record's canonical JSON. The gate observes the environment, writes it with the CPU vendor, model and features to observed-environment.json in its run directory and prints the CPU before launching any browser. On another environment it records the state unverified with the divergent fields and exits 0; --require-reference-environment makes that state exit 1. Firefox drops color-scheme emulation at a navigation into or out of a document served with Cross-Origin-Opener-Policy: same-origin unless both documents have the same origin and send that header, including the first navigation from about:blank; emulation applied after the last such navigation holds across reload and across navigation within one fixture. The gate therefore applies the color scheme and reduced motion after the first navigation, reloads, applies them again after the reload, the last navigation of every captured view, and asserts both through the matching media queries before each capture. It fits the window to the document height, cut at 2400 CSS pixels, captures each view twice, requires byte-equal PNG captures, decodes them with node:zlib, refusing tRNS outside palette images and a second IHDR, and requires the dimensions and the canonical digest to equal the inventory entry; a failure names the ID and both digests and sizes. Every reference-environment run, passing or failing, keeps its captures, a candidate inventory and the observed environment below its run directory, and a run after which the materialized tests/visual tree gained, lost or changed a file fails. Six controls per engine serve a copied workspace stylesheet: a routed unchanged copy must keep the admitted digest, so routing alone cannot change the pixels, and a one-unit text color change, a one-pixel shift, a font-weight change from 400 to 700, a change of the sans-serif family to serif and a change of the monospace family to sans-serif must each change it. A font-face check per engine reads the font stacks of the workspace and static roots and of the workspace and static monospace elements from their rendered views, draws each beside the named Liberation faces in regular and bold, and requires exact pixel equality with Liberation Sans for the roots and with Liberation Mono for the monospace stacks, which must also be monospaced. The visual Chromium project adds --disable-partial-raster, without which repeated launches rasterized rounded corners differently, and the visual Firefox project adds browser.chrome.site_icons=false; the runtime harness is unchanged. One runtime-harness site with the same Firefox pattern remains: tests/browser/workspace-evidence-layout.spec.mjs emulates the color scheme before its first navigation, so its two dark Firefox rows render light; the runtime harness site is fixed by the next change in this chain, test(browser): assert media emulation in the document under test. browserproofverify selects its input manifest, witness command, record path, admissible states and run-directory retention from a closed table. The runtime manifest, witness command, record path, record format and run cleanup are unchanged. The visual row keeps its run directory, writes artifacts/proofkit/browser-visual-proof.json with the state passed or unverified, recomputes the environment divergence itself, requires each engine to pass exactly its expected comparisons, controls and font-face check with zero retries, and refuses an inventory with a missing, extra, duplicate or malformed entry, dimensions out of bounds or a binding to another environment. Any failure exits non-zero without writing a record. References are admitted by the owner action --admit-references --from <run directory>, which the gate never calls. It admits only a run whose observed environment equals the recorded one and that holds exactly one regular capture file per ID and no unstable directory, recomputes every digest from the PNG captures through the same canonical encoding, refuses a run whose candidate inventory disagrees or whose observed environment claims a divergence, and only then writes references.json. It cannot authenticate the run directory as unmodified gate output; only a passing gate on the admitting tree proves the admitted digests. browsertestserver gains a compact proof selector and a README project selector, which materializes a committed synthetic example.dispatch project, serves it through the same project inspection path as the view command and removes the temporary project on every exit path. A test admits that project through the project plan. CI gains the required job "quality / browser visual references" on ubuntu-24.04-arm in the digest-pinned image with --user 1001 --ipc=host --init. It sets up Node 26.11.1, the verified npm, Go and the locked tooling, runs npm run browser:visual -- --require-reference-environment, uploads the captures, the candidate inventory and the observed environment after every outcome as browser-visual-captures-<sha>-<attempt> for 30 days, and uploads the record only after success. The required aggregate needs the job, and REQ-PROOFKIT-QUALITY-011 now counts six required leaf jobs in the aggregate. REQ-PROOFKIT-QUALITY-025 admits exactly this job container and requires every workflow container image reference to be pinned by a sha256 digest; the workflow oracles pin the job's container, steps, uploads, runner, timeout and aggregate membership and refuse an unpinned image. REQ-PROOFKIT-QUALITY-028 states the reference gate, the canonical pixel encoding, the font-face check and the admission with its bindings, coverage inventory entries, overview line and non-claims: weights 400 and 500, and weights 600 to 900, each render identically because only regular and bold faces are installed; a family name that WebKit looks up and fontconfig does not know, other than ui-monospace, resolves to Liberation Sans in WebKit; pixel identity is claimed only on the calibrated Apple M1 and Arm Neoverse N2 CPU models; admission does not authenticate its run directory; admitted images are reproducible only while the pinned image and the pinned toolchain and dependency downloads remain available. The binding record carries the requirement's source non-claims. The witness plan adds the browser-visual command and policy, the container-linux-arm64-browser environment class and the browser-visual parallel group, and the go-test policy lists the visual records and the digest script its tests read. CONTRIBUTING.md explains how to view an admitted image from a clone in the pinned image with checksum-verified toolchains and how references are re-admitted, and requires, before merge, a passing gate on each CPU model named in the non-claims or a change of those non-claims. The UI-DESIGN-01 backlog row now says that reference screenshot digests are committed and the screenshots are reproducible in the recorded environment, as the owner decided. Proof on this tree: A local npm run check sequence on darwin/arm64 passed (exit 0) on tree f67bb17 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3: 561/561 runtime browser cases in Chromium, Firefox and WebKit with zero retries. One recorded deviation: the host firewall blocks Go-built binaries from the network, so go:vulncheck ran against a vulnerability-database snapshot fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities. npm run check does not run the visual gate. Its proof is two fresh-container runs in the pinned arm64 image on Apple M1 (190/190 each, candidates equal to references.json) and three hosted CI runs of this tree on Arm Neoverse N2 (190/190 each, all jobs green).
Pinned Firefox drops color-scheme emulation at a navigation into or out of a document served with Cross-Origin-Opener-Policy: same-origin, unless both documents have the same origin and send that header. That includes the first navigation from about:blank, a navigation to another fixture (each fixture server has its own port) and history traversal across such a boundary; every fixture document sends the header. Emulation applied after the last such navigation holds across reload and across navigation between documents of the same fixture. Chromium and WebKit are unaffected, and reduced-motion and forced-colors emulation survived every such navigation tested in every engine. Playwright's default light scheme is applied before navigation as well, so pinned Firefox drops it; tests that do not call the helper run in Firefox under its own preference. The reflow spec tests/browser/workspace-evidence-layout.spec.mjs, the native reflow witness of REQ-PROOFKIT-SPEC-039 and REQ-PROOFKIT-SPEC-040, emulated before openWorkspace, so its two Firefox dark rows (390x844 and 1280x800) ran in the light scheme. No emulation site asserted that its emulation took effect. - tests/browser/workspace-navigation-harness.mjs: emulateMediaInEffect applies page.emulateMedia and then asserts, in one observation, that the page holds a served http document and that matchMedia matches every given color-scheme, reduced-motion and forced-colors value. It refuses any other feature or value, and the http check refuses a call made in the initial about:blank document, where a match says nothing about the next document. Every pinned engine reflects forced-colors emulation in matchMedia, so that feature is asserted in all three. The assertion proves the media feature, which switches the author forced-colors rules, not an engine forced-color palette, which WebKit does not apply. - tests/browser/workspace-evidence-layout.spec.mjs: each reflow row emulates after openWorkspace through the helper, without a reload. The workspace reads the color scheme and the motion preference only through CSS (media queries and the root color-scheme property), which follows emulation in the loaded document. - tests/browser/workspace.spec.mjs (rendered light and dark contrast), tests/browser/static-view.spec.mjs (static control contrast) and tests/browser/workspace-graph.spec.mjs (dark and forced-colors graph paint) emulate through the helper. They already emulated after navigation. Test titles, requirement statements, bindings and the browser proof input manifest are unchanged. No statement of REQ-PROOFKIT-SPEC-039 or REQ-PROOFKIT-SPEC-040 names a color scheme, the light and dark contrast oracles of REQ-PROOFKIT-QUALITY-022 already ran under the intended scheme, and the runtime proof inputs select tests/browser as a whole. Proof on this tree: A local npm run check sequence on darwin/arm64 passed (exit 0) on tree 14809a0 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3: 561/561 runtime browser cases in Chromium, Firefox and WebKit with zero retries. One recorded deviation: the host firewall blocks Go-built binaries from the network, so go:vulncheck ran against a vulnerability-database snapshot fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities.
The Node suites passed CLI input with spawnSync's `input` option. Node then gives the child one end of a Unix-domain socket pair, writes the input, calls shutdown(SHUT_WR) on its end and keeps that end open until the child exits or the timeout kills it. On macOS, a child blocked in read(2) on that socket is sometimes not woken by the shutdown, although the socket already holds end of file, so the call ends only at its timeout. Node 22, 24 and 26 stall this way on darwin/arm64, and so does the same syscall sequence written in C, without Node or libuv. With a pipe, with a close instead of the shutdown or with a regular file, that reproducer did not stall, and neither did Node on Linux. The suites saw the stall as an occasional spawnSync ETIMEDOUT of one CLI call. - scripts/cli-process.mjs adds spawnWithInputFile(file, args, options). It is spawnSync(file, args, options) except that options.input is written to a temporary file that the child gets as its standard input. The file has mode 0600 in a fresh directory of mode 0700, and the directory is removed as soon as the file is open, before the child starts, or at once when writing or opening the file throws. The removal uses path calls only; if it fails, the input descriptor is closed and the call throws. Status, signal, error, stdout, stderr, timeout, maxBuffer and the other passed options keep spawnSync's meaning, except that writing the input can no longer fail with EPIPE. An unsupported option is refused, and so is any args that is not an array, including the two-argument form spawnWithInputFile(file, options), which spawnSync would read as its options while it drops the third argument. The result also reports stdinUnreadBytes, the input bytes past the file offset that the child shares with the parent when it exits: read(2) and lseek(2) on the inherited descriptor move it; pread(2), mmap(2) and, on Linux, reopening /dev/stdin do not. - All 42 spawnSync calls with input in 29 test files under scripts/ use it. - assertCLITransport accepts a non-zero stdinUnreadBytes under the rule it already applies to EPIPE: only from a child that exited normally with a non-zero status and no signal. EPIPE stays accepted for the asynchronous pipe transport of the schema-completeness test. - The early-refusal test of the browser server asserts that stdinUnreadBytes equals the size of its input, so the refusal read none of it, instead of asserting EPIPE. - scripts/cli-process.test.mjs covers the file transport, the unread-input and EPIPE rules through both transports, the kept and refused options, the removal of the input file before the child runs and after a throw, a failed write included, and the closing of the input descriptor after a failed removal. It adds a lexical source check that refuses an `input` member in the arguments of a direct spawnSync, execFileSync or execSync call under scripts/ and tests/. - proofkit/witness-plan.json: the boundary-contract-check policy declares scripts and tests, which that source check reads. No timeout, retry or assertion is relaxed. Proof on this tree: A local npm run check sequence on darwin/arm64 passed (exit 0) on tree 600c4bf with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3, with no Go test verdict replayed from the test cache: 561/561 runtime browser cases in Chromium, Firefox and WebKit with zero retries. One recorded deviation: the host firewall blocks Go-built binaries from the network, so go:vulncheck ran against a vulnerability-database snapshot fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities.
The TypeScript adapter that json-report-cli-adapter-source generates ran
the CLI with spawnSync(binaryPath, args, {cwd, env, input, maxBuffer}) and
no timeout. On Unix-like systems, Node gives the child one end of a
Unix-domain socket pair for the input option, writes the input, shuts down
writing and keeps its end open until the child exits. On macOS, a child
blocked in read(2) on that socket is sometimes not woken by the shutdown,
so an adapter call that passed input could block forever. With the adapter
generated by the parent commit, Node 26.11.1 and the CLI on 59 captured
spec-proof-bundle-admission inputs (58 of them 514 to 585 KB as stable
JSON), 9 of 3540 calls on darwin/arm64 did not return within 5 seconds.
The one held for 60 seconds stayed blocked, with a Unix socket as its
standard input. The adapter generated by this commit returned in all of
2950 such calls.
- The generated runProofkitCommand writes the stable JSON input to a file
created with O_EXCL and mode 0600 in a fresh mkdtemp directory under
os.tmpdir() of the calling process, opens it read-only and gives that
descriptor to spawnSync as stdio[0]. Except on Windows, it then removes
the file and the directory by path (rmSync of the file, rmdirSync of
the directory) before the CLI starts, so the CLI reads a file that no
longer has a name, and a signal that ends the calling process during
the call leaves no input on disk. When the call returns or throws, the
descriptor is closed and whatever is not yet removed is removed: on
Windows, where an open file can keep its name until it is closed, after
a throw before the early removal, and after a failed early removal. The
removal needs no descriptor of its own, and a failed removal fails the
call. Errors of this transport are thrown through
formatProofkitCliError, as the errors that spawnSync returns already
were.
- A call without input (inputMode "none") gives the child the null device
instead of a shut-down socket. The adapter passes no --input flag for
such a call, so the CLI does not read its standard input.
- cwd, env and maxBuffer are passed as before, and stdout, stderr, status
and the errors that spawnSync returns are handled as before. Argument
errors that spawnSync throws itself, such as a negative maxBuffer or a
NUL byte in binaryPath, cwd or an env value, are now thrown as plain
Errors carrying formatProofkitCliError's redacted, bounded message,
without Node's error class or code. A secret-shaped env value that
Node's message quotes is therefore redacted, where it was shown before.
A child that exits without reading a large input no longer makes the
call throw EPIPE; the call reports the child's status and diagnostic
instead.
- The generated source is 1189 lines with digest
sha256:e782b2cb6e17f1671a43418cf224a566365a4ebe90730a40699adbffbd1b0ac4.
The bundle keeps its record shape, generatorId
proofkit.json-report-cli-adapter-source.typescript.v2 and contractId
proofkit.json-report-cli-adapter-source.output.v1. Neither the CLI
contract nor a bound requirement statement specifies how the adapter
feeds the CLI's standard input, so under the contract-identity rule of
docs/proofkit-contract-map.md this is a defect correction.
- proofkit/cli-contract.v2.json re-pins the native-source digest of the
command's output contract. The generated contract projections and the
public CLI ABI hash follow from that digest.
- scripts/generated-starter-structure.test.mjs declares the byte deltas of
the three adapter bundle rows from the frozen predecessor streams: the
imports, the transport, the source digest and the pretty and compact
line count. The corpus is unchanged.
- A source check requires that the single spawnSync call passes exactly
cwd, env, maxBuffer and a stdin descriptor, with no input member, and
that the input file is removed by path right after it is opened, except
on Windows. The generated-adapter semantics test probes the child's
standard input: a regular file of mode 0600 with no remaining link that
holds exactly the stable JSON, alone in a mode 0700 directory when it is
opened and gone before the child runs, or the null device for a call
without input, although the harness's own standard input is a pipe.
The Windows branch, run by reporting win32 as the platform, keeps the
file under its name until the call returns. Simulated failures of the
read-only open, of either removal call and of the end-of-call removal
make the call throw and leave no descriptor open; only the failed
end-of-call removal leaves the directory. The test also covers an early
refusal, maxBuffer, a negative maxBuffer, a secret-shaped env value, a
missing binary and a missing temporary directory.
- README.md states where a call with input writes it, that the caller
needs a writable temporary directory, when the input is removed on
Windows and elsewhere, and when it can remain on disk.
No timeout is added.
Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
a17845a with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3,
with no Go test verdict replayed from the test cache: 561/561 runtime
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability-database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities.
go:test ran `go test ./...`. Go's test cache replays a package's passing result while the test binary, the files the test process opened and the environment variables it read are unchanged. Many tests here run a child process, such as node, git or a built CLI, and the files that child reads are not part of that key. So a full gate could report a cached pass for a test that fails on the tree under test. On the parent commit's tree, with scripts/source-hygiene.mjs changed to exit 0 before it checks anything, `go test ./scripts -run '^TestSourceHygieneRejectsMalformedUTF8$'` reported `ok (cached)`; the same command with -count=1 failed. Recent local gates served 120 to 143 of the 148 test packages from this cache. The witness plan already declares proofkit.go-test, like each of its commands, with cachePolicy "disabled", which witness-scheduler-plan treats as not cacheable; the command now behaves as declared. - package.json: go:test is `go test -count=1 ./...`. -count=1 is the flag Go documents for disabling the test cache, so every test package runs on the tree under test. The build cache is still used. - proofkit/witness-plan.json: the proofkit.go-test argv is ["go", "test", "-count=1", "./..."]. witness-scheduler-plan admits the plan, and of its node digests only that of proofkit.go-test changes; no tracked file pins it. - proofkit/requirement-bindings.json: the command bound to proofkit.go-test is `go test -count=1 ./...`, as the existing witness-command identity test requires. - scripts/workflow_source_oracles_test.go requires go:test to be that command and the witness argv and the bound command to equal it. It refuses each of the three without -count=1, a narrower package pattern, a repeated count and a masked failure. A second test requires a -count= flag on every Go test invocation it finds in the package scripts, the witness plan argv and the YAML files under .github. It reads shell text lexically and finds an invocation only as adjacent go and test fields: it misses one in a subshell or in backquotes, one through an absolute go path and `go -C . test`, and it accepts -count=1 in a comment and -count=0. The other invocations it finds already carry -count=: go:bench, the legacy Unicode positive witness, and the CI workflow's legacy Unicode, cross-filesystem deletion and platform invariant steps. CI's "Run all Go tests" step and the release workflow's full check run go:test itself. The Go code that starts `go test` itself already passes -count=1: the command oracle behind self:coverage, the schema-completeness mutation witnesses and one coverage-metrics test. Hosted CI time is unchanged: recent hosted quality / source jobs and the release candidate's check served none of the test packages from the cache, even when they restored it, so they already ran every package. A local gate whose test packages the cache used to serve now links and runs all of them, so it takes longer, and on a loaded host it comes closer to Go's per-package test timeout and to the 600-second bound declared for proofkit.go-test. Test results are not made deterministic by this change. Proof on this tree: A local npm run check sequence on darwin/arm64 passed (exit 0) on tree 18bff04 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3, with no Go test verdict replayed from the test cache: 561/561 runtime browser cases in Chromium, Firefox and WebKit with zero retries. One recorded deviation: the host firewall blocks Go-built binaries from the network, so go:vulncheck ran against a vulnerability-database snapshot fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities. The gate runner already passes -count=1 to every Go command, so this run does not discriminate the change; the oracle tests and the stale-verdict reproduction above do.
GATE-DIFF-01 asked for a closeout gate that inherits the verdicts of unchanged steps from a passing parent tree. Measured on this repository's recent commits and gates, its rules save a mean of at most 0.2 % of a hosted gate, and the trace check they need on every gate would cost more than that: any overhead above 0.2 % does, and Linux micro-benchmarks of its cheapest suitable mode showed factors of 1.5 and 11. The row is deferred with its measurement, the premises found false and a reopen condition. - GATE-DIFF-01 moves from NEXT to DEFERRED. Its scope keeps the unsoundness statement and adds the measurement: over the 40 first-parent commits from 6d19004 to 834f6ae, the last seven of them not yet on main when measured, the rules would have saved a mean of at most 2.1 s per commit, 0.2 % of a 22.7-minute hosted gate, because 34 of the 40 commits hit a full-gate trigger and the costliest steps cannot be inherited. It lists the premises found false: whole-worktree reads by browser:check, by the ./... steps and by the CLI builds of three node contract steps; same-tree verdict flips of browser:check and boundary-contract:check; Firefox under sandbox-exec on macOS; release:closeout without selectors; an incomplete toolchain list; and go:test verdicts replayed from Go's test cache, which the parent commit fixes. - The scope records that the known causes of the boundary-contract:check flips are fixed: the early-refusal EPIPE race by the shared CLI transport assertion, and the lost macOS Unix-socket wake-up by giving spawned CLI children their input as a file. It also records the browser:check flips: two WebKit navigations that did not reach their target document and six Firefox and Chromium page operations that did not complete, all at the 30-second test timeout, in local full gates whose browser tests took 2.6 to 4.6 times as long as on the passing reruns, with no load sampled during them. - The completion condition starts with the reopen condition. Its first clause asks for 20 consecutive full gates of one unchanged tree that pass browser:check and boundary-contract:check with zero retries inside a load envelope: the 1-minute load average per CPU, sampled at least once a minute during those steps, has a median of at most 6 and a maximum of at most 12, and at least five of the gates have a median of at least 3. A gate outside the envelope is recorded but neither counts nor restarts the count, and no timeout is raised, no retry added and no CSP, console or security-policy check weakened. The other clauses stay: fewer than half of the last 40 first-parent commits on a full-gate trigger, and a trace overhead measured on the real hosted gate below the mean saving then measured. A reopened row first traces every step's reads on its own tree, recomputes the saving and resolves each false premise. - The requirement text stays, except that its toolchain list also names Python, git, shellcheck and pyflakes, and its release:closeout trigger names the witness command that release:closeout must first get. No row is added for browser:check. With fixed 30-second timeouts any test fails under enough contention, so its verdict can be a function of the tree only inside a stated load envelope. None of the recorded failures is known to lie inside one, and the reopen condition above is the only consumer of that claim. AGENTS.md and every product surface are unchanged. Proof on this tree: A local npm run check sequence on darwin/arm64 passed (exit 0) on tree 99c57d7 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3, with no Go test verdict replayed from the test cache: 561/561 runtime browser cases in Chromium, Firefox and WebKit with zero retries. One recorded deviation: the host firewall blocks Go-built binaries from the network, so go:vulncheck ran against a vulnerability-database snapshot fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities.
The OSV source scan of the parent tree fails: package-lock.json locks fast-uri 3.1.8, which four advisories affect: GHSA-333x-w4r6-6crp (CVE-2026-110725), GHSA-w74x-v6f9-j3xg (CVE-2026-110306), GHSA-wjr9-j795-jxcr (CVE-2026-110180) and GHSA-xhq7-xmqj-769h (CVE-2026-110095). Each names 3.1.9 as the first fixed 3.x version. - package-lock.json: fast-uri 3.1.9 with its registry integrity. No other entry changes; ajv 8.20.0 still requires fast-uri through ^3.0.1. - BACKLOG.md: a BLOCKED row records the fast-uri copy that the pinned browser-test SDK inlines, which no lock entry can change. fast-uri is a development dependency only. In the lock, ajv is its only dependent, and only the Node schema tests load that ajv. There, ajv passes fast-uri only schema identifiers and references, with default options. In every tracked invocation they come from its bundled meta-schemas and this repository's contract schemas; an operator-set PROOFKIT_*_CONTRACT or PROOFKIT_*_BASELINE variable substitutes that file's schemas. Validated instance data never reaches it. The npm package declares no runtime dependency, and neither it nor the Python wheels contain fast-uri. playwright-core 1.63.0 inlines a separate fast-uri 3.1.3 into lib/utilsBundle.js. This change does not touch that copy, and the OSV scan does not see it. Loading that file runs only the copy's module initializers. Only the Model Context Protocol client and server inlined in the same file call its exported functions; the browser suite never does. The copy stays affected by every fast-uri advisory fixed after 3.1.3, and no Playwright release inlines a fixed fast-uri yet. No other file pins the lock or a digest of it. The release SBOM is built from the Go module graph and the release files, the package gate pins only the direct development dependencies in package.json, there is no Python lock, and the witness plan and the browser proof manifests list package-lock.json only as an input path. Proof on this tree: A local npm run check sequence on darwin/arm64 passed (exit 0) on tree 90a6ec0 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3, with no Go test verdict replayed from the test cache: 561/561 runtime browser cases in Chromium, Firefox and WebKit with zero retries. One recorded deviation: the host firewall blocks Go-built binaries from the network, so go:vulncheck ran against a vulnerability-database snapshot fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities. An earlier full gate on a candidate of this change failed once in an unchanged package, internal/tools/releasepreflight, on a timing race in a counterexample test that uncached Go test runs now expose; the next change in this chain fixes that race.
This was referenced Oct 24, 2026
Contributor
Author
Delivery scope clarificationThe exact locked fast-uri3.1.9 change was moved into PR228 so that earlier The original analysis above remains historical evidence for the proposed lock |
iperev
marked this pull request as draft
October 25, 2026 22:57
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.
fix(deps): lock fast-uri 3.1.9 to clear four advisories
Part of the UI-DESIGN-01 chain, stacked on it; the change itself is a dependency fix, not UI work. Base branch:
chain/r2-backlog-gate-diff(#235, "docs(backlog): park differential closeout on its measured cost"), which sits on #234, #233, #232 and #231. This PR is one commit on that branch's head; merge it after #235.package-lock.jsonchanges by three lines, andBACKLOG.mdgains one BLOCKED row.Why
The
osv / source advisoryjob (osv-scanner v2.6.0) fails on #232 to #235, first in run 40059109279. It reportsfast-uri@3.1.8inpackage-lock.jsonas affected by four advisories. GitHub published them on 2026-10-23 between 21:36 and 21:42 UTC, about an hour after the scan of #231 passed at 20:26 UTC.reference: 'suffix',serialize,normalizeandresolveleave a colon in the first path segment unescaped, so the output reparses with a scheme (javascript:).%6Aavascript:becomesjavascript:.http,https,ws,wssanddomainHostschemes, the host is replaced by the WHATWG parser's host without a check that it parsed the whole host:a@b@evil.examplebecomesevil.example, and TAB, CR and LF are dropped.normalizeandserializeremove dot segments from a relative reference:a/../bbecomes/b.All four advisories name 3.1.9 as the first fixed 3.x version, both in the GitHub advisory database and in OSV (fetched 2026-10-24).
mainlocks the same 3.1.8, so its next scan fails the same way.What changes
package-lock.json: thenode_modules/fast-urientry changes from 3.1.8 to 3.1.9: version, resolved tarball URL and integrity.npm update fast-uri --package-lock-only --ignore-scripts. No other entry changes.^3.0.1. 3.1.9 is the highest 3.x release (dist-tagthree); 4.x lies outside that range.npm install --package-lock-onlythen leaves the lock byte-identical, so it agrees withpackage.json.BACKLOG.md: the BLOCKED row BROWSER-BUNDLE-01 records the fast-uri copy that the pinned browser-test SDK inlines (see "A second copy, inside Playwright" below). No lock entry can change that copy, and no Playwright release inlines a fixed fast-uri yet. The row's completion condition is a pinned SDK whose inlined fast-uri has no OSV advisory and which passes the BROWSER-TOOLCHAIN-01 qualification. Any use of the SDK's MCP client or server here before then reopens the row.Registry integrity.
shasum.npm audit signaturesverifies the registry signatures of all 11 installed packages.fastify/fast-uri's.github/workflows/release.ymlfromrefs/heads/v3.xata1d0da2f…, and its subject digest equals the lock's integrity. The registry lists no provenance attestation for 3.1.8.Exposure
fast-uri is a development dependency only (
"dev": true). In the lock, ajv is its only dependent.package.jsondeclares no runtime dependency.Shipped bytes.
npm run package:artifacton the first version of this commit (the same files except the backlog row, which neither package ships) built the npm tarball (31 files) and four Python wheels (10 files each). No file in any of them containsfast-uri,fastifyor the name of another ajv dependency. The packagedpackage.jsonnames ajv only underdevDependencies, which installers do not install for a dependency. The release SBOM has no npm component; its components are 31 Go modules, 5 binaries and 4 files.Who loads the locked copy. All 34 files that import ajv are Node test files,
scripts/*-structure.test.mjs,schema-definitions.test.mjsandschema-completeness.test.mjs. They run through five scripts:compact-contract:check,receipt-contract:check,spec-tree-contract:check,graph-contract:checkandboundary-contract:check. The local gate, the CI source job and the release workflow run those scripts. Nothing else loadsnode_modules/fast-uriornode_modules/ajv: not Playwright, axe-core, TypeScript, the browser tests or any Go code.What reaches it. ajv calls fast-uri's
parse,serializeandresolveonly to resolve schema identifiers and references while it adds and compiles schemas, and to build a missing-reference error. It does not pass the validated data to fast-uri. To check this, a recorder preloaded into every Node process of the five scripts logged each call into fast-uri on the first version of this commit, which has the same lock and test sources:parsewith one argument,serializewith one argument andresolvewith two. No call passed options.https://json-schema.org/…IDs and the relativemeta/…references of its bundled draft 2020-12 meta-schemas, and the empty ID that ajv uses for a schema without$id.urn:proofkit:…schema IDs thatproofkit/cli-contract.v2.jsonand the Go structure sources declare.#/$defs/…, three#/properties/…and#.reference: 'suffix'(GHSA-333x);%anywhere (GHSA-w74x);json-schema.org, an@or whitespace (GHSA-wjr9);No untrusted input reaches fast-uri. In every tracked invocation its inputs are ajv's own meta-schemas and schemas in this repository; an operator-set
PROOFKIT_*_CONTRACTorPROOFKIT_*_BASELINEvariable substitutes that file's schemas. A pull request that changes those schemas can change the tests that compile them just as easily.A second copy, inside Playwright.
node_modules/playwright-core/lib/utilsBundle.js. ItsutilsBundle.js.LICENSElists them. 3.1.3 is affected by the four advisories and by eight older ones fixed in 3.1.4 to 3.1.8: 12 in OSV on 2026-10-24, seven rated high (CVSS 7.5: host confusion, server-side request forgery, authority injection).osv-scannerandnpm auditread the lock, so they do not see it.nextprerelease of 2026-10-24 both inline fast-uri 3.1.7, which still has five: the four above and GHSA-hrr3-gc8f-f4qj.require_fast_uri()is called only by two inlined ajv copies, and an Ajv instance is created only by the MCP SDK's validator, which the MCPClientandServerconstructors use.browser:testrun withNODE_V8_COVERAGEpassed 561/561. In each of the 31 Node processes that loadedutilsBundle.js, the three fast-uri module bodies ran once and none of the other 34 fast-uri functions ran.Clientagainst the same file runsparse,serialize,removeDotSegmentsand others, so the coverage does see calls when they happen.Pins and digests of the lock
No tracked file contains the SHA-256, the Git blob ID or the base64 SHA-256 of either lock or of either
BACKLOG.md, and no file outsidepackage-lock.jsoncontains the stringfast-uriexcept the new backlog row. Nothing needed a re-pin.package-lock.jsonrelease:sbom)go list -m alland the release files; no npm componentpackageverify)devDependenciesofpackage.jsonproofkit/witness-plan.jsoninputDigestis computed from current bytes at run time and not trackedtests/visual/environment.jsonTestVisualEnvironmentBindsThePinnedPlaywrightToolchainreads from the lock; unchangeddependencyDigestcoversgo.modandgo.sum;lockfileDigestis null; the source snapshot digest is computed at run timereleasemanifestpackageLockSha256Who reads the lock. To find every reader,
go test -p 2 -count=1 ./...ran under a macOS sandbox profile that refuses any read ofpackage-lock.json. 144 of the 148 test packages passed. The failing tests:internal/tools/browserproofverify: input manifest closure, the visual environment's Playwright pin and visual record admission.internal/tools/commandoracle: the source snapshot, throughgit ls-files.scripts: the generator fixture, which copies every tracked file.internal/app: one setuid/setgid output-mode test. It also fails under an allow-all sandbox profile and passes outside one, so the cause is the sandbox, not a read of the lock.Under the same profile,
source-hygiene,text-policyandpackage:artifact(its source snapshot) fail too.Compatibility
Not breaking. No shipped byte changes:
package-lock.jsonandBACKLOG.mdare in neither the npm package nor the wheels. Only the Node schema tests run different fast-uri code, and they all pass.Proof
Proof: A local
npm run checksequence on darwin/arm64 passed (exit 0) on tree90a6ec033706with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3, with no Go test verdict replayed from the test cache: 561/561 runtime browser cases in Chromium, Firefox and WebKit with zero retries. One recorded deviation: the host firewall blocks Go-built binaries from the network, sogo:vulncheckran against a vulnerability-database snapshot fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities. An earlier full gate on a candidate of this change failed once in an unchanged package, internal/tools/releasepreflight, on a timing race in a counterexample test that uncached Go test runs now expose; the next change in this chain fixes that race.Narrow evidence on darwin/arm64 with Node 26.11.1 (as in CI), npm 12.2.0 and Go 1.27.3. The host is shared. The first version of this commit had the same
package-lock.jsonand differed only by theBACKLOG.mdrow. The OSV scans,source-hygiene,text-policyandgit diff --checkran again on this tree. The schema suites, browser tests, Go tests andpackage:artifactran on the first version.sum.golang.org. It read the OSV npm and Go database snapshots fetched with curl at 2026-10-24 05:14Z, whose MD5 still matched the live bucket when these scans ran.git archiveexport:osv-scanner scan source --offline --recursive ./, and again with--format sarif --output-file <file>. These runs did not pass the CI flag--include-git-root, because an export has no.git.osv-scanner scan source --offline --include-git-root --recursive --format sarif --output-file <file> ./exits 0 with 0 results. Offline, it extracted onlygo.modandpackage-lock.json, no.gitpackage.scan source --include-git-root --recursive --format sarif --output-file artifacts/osv/osv-results.sarif ./, online.querybatch, sent with curl, covered every package of the lock and every module ofgo list -m all. Before: the four fast-uri advisories. After: none for any npm package.npm audit: before, one moderate entry, fast-uri, through all four advisories; after, 0 vulnerabilities.npm ci, all five pass on the first version of this commit, with and without the recorder:compact-contract:check,receipt-contract:check,spec-tree-contract:check,graph-contract:checkandboundary-contract:check(358 tests).browser:static-checkpasses as well (72 tests).npm run browser:testwithNODE_V8_COVERAGEset passed 561/561 in Chromium, Firefox and WebKit with zero retries. It took 13.1 min.go test -p 2 -count=1 ./internal/tools/browserproofverify ./internal/tools/commandoracle ./scriptspasses.npm run source-hygiene,npm run text-policy(1421 text files) andnpm run package:artifactpass.git diff --check,git diff --cached --checkandgit diff --check HEAD~1 HEADare clean on this tree.package-lock.jsonis byte-identical to the first version's (blob7770ca24).Non-claims
golang.org/x/crypto/openpgppackage) forgolang.org/x/crypto0.58.0. That module is in thego list -m allgraph, but the main module does not need it, it is in neithergo.modnorgo.sum, osv-scanner does not report it, and this change does not touch it. The advisory has no fixed version, and the release SBOM lists the module with scopeexcluded.