Skip to content

docs(security): track bundled browser URI parser exposure - #236

Merged
iperev merged 13 commits into
mainfrom
chain/d1-fast-uri
Oct 25, 2026
Merged

iperev merged 13 commits into
mainfrom
chain/d1-fast-uri

Conversation

@iperev

@iperev iperev commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

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.json changes by three lines, and BACKLOG.md gains one BLOCKED row.

Why

The osv / source advisory job (osv-scanner v2.6.0) fails on #232 to #235, first in run 40059109279. It reports fast-uri@3.1.8 in package-lock.json as 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.

Advisory CVE GitHub severity (CVSS 3.1) Affected 3.x Defect
GHSA-333x-w4r6-6crp CVE-2026-110725 low (3.7) >= 3.0.0, < 3.1.9 With the non-default option reference: 'suffix', serialize, normalize and resolve leave a colon in the first path segment unescaped, so the output reparses with a scheme (javascript:).
GHSA-w74x-v6f9-j3xg CVE-2026-110306 moderate (6.1) >= 3.0.0, < 3.1.9 The scheme is percent-decoded before it is validated: %6Aavascript: becomes javascript:.
GHSA-wjr9-j795-jxcr CVE-2026-110180 moderate (4.8) >= 3.1.3, < 3.1.9 For http, https, ws, wss and domainHost schemes, the host is replaced by the WHATWG parser's host without a check that it parsed the whole host: a@b@evil.example becomes evil.example, and TAB, CR and LF are dropped.
GHSA-xhq7-xmqj-769h CVE-2026-110095 low (3.7) >= 3.0.0, < 3.1.9 normalize and serialize remove dot segments from a relative reference: a/../b becomes /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). main locks the same 3.1.8, so its next scan fails the same way.

What changes

  • package-lock.json: the node_modules/fast-uri entry changes from 3.1.8 to 3.1.9: version, resolved tarball URL and integrity.
    • Made with the pinned npm 12.2.0: npm update fast-uri --package-lock-only --ignore-scripts. No other entry changes.
    • ajv 8.20.0 still requires fast-uri through ^3.0.1. 3.1.9 is the highest 3.x release (dist-tag three); 4.x lies outside that range.
    • npm install --package-lock-only then leaves the lock byte-identical, so it agrees with package.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.

  • The tarball fetched with curl hashes to the lock's SHA-512 integrity, and its SHA-1 equals the registry's shasum.
  • npm audit signatures verifies the registry signatures of all 11 installed packages.
  • It also verifies the SLSA provenance attestation of fast-uri 3.1.9. The attestation was built by fastify/fast-uri's .github/workflows/release.yml from refs/heads/v3.x at a1d0da2f…, 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.json declares no runtime dependency.

Shipped bytes. npm run package:artifact on 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 contains fast-uri, fastify or the name of another ajv dependency. The packaged package.json names ajv only under devDependencies, 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.mjs and schema-completeness.test.mjs. They run through five scripts: compact-contract:check, receipt-contract:check, spec-tree-contract:check, graph-contract:check and boundary-contract:check. The local gate, the CI source job and the release workflow run those scripts. Nothing else loads node_modules/fast-uri or node_modules/ajv: not Playwright, axe-core, TypeScript, the browser tests or any Go code.

What reaches it. ajv calls fast-uri's parse, serialize and resolve only 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:

  • Calls: 729,573 calls from all 34 test files: parse with one argument, serialize with one argument and resolve with two. No call passed options.
  • Arguments: 621 distinct strings with 25 distinct base identifiers.
    • 16 are ajv's own: the https://json-schema.org/… IDs and the relative meta/… references of its bundled draft 2020-12 meta-schemas, and the empty ID that ajv uses for a schema without $id.
    • 9 are urn:proofkit:… schema IDs that proofkit/cli-contract.v2.json and the Go structure sources declare.
    • The rest are JSON-pointer fragments of those schemas: #/$defs/…, three #/properties/… and #.
  • Advisory triggers, absent from every argument:
    • the option reference: 'suffix' (GHSA-333x);
    • a % anywhere (GHSA-w74x);
    • a host other than json-schema.org, an @ or whitespace (GHSA-wjr9);
    • a dot segment (GHSA-xhq7).

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_*_CONTRACT or PROOFKIT_*_BASELINE variable 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.

  • What it is: playwright-core 1.63.0 inlines fast-uri 3.1.3, together with ajv and the Model Context Protocol (MCP) SDK, into node_modules/playwright-core/lib/utilsBundle.js. Its utilsBundle.js.LICENSE lists 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).
  • Not fixed here: this PR does not change that copy, and no lock entry can.
  • Not scanned: osv-scanner and npm audit read the lock, so they do not see it.
  • Upstream: the latest release, 1.64.1, and the next prerelease of 2026-10-24 both inline fast-uri 3.1.7, which still has five: the four above and GHSA-hrr3-gc8f-f4qj.
  • Callers: in that file, 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 MCP Client and Server constructors use.
  • Browser suite: a full browser:test run with NODE_V8_COVERAGE passed 561/561. In each of the 31 Node processes that loaded utilsBundle.js, the three fast-uri module bodies ran once and none of the other 34 fast-uri functions ran.
  • Control: constructing an MCP Client against the same file runs parse, serialize, removeDotSegments and others, so the coverage does see calls when they happen.
  • Conclusion: the browser tests load that copy, which runs its three module initializers, but never call its functions.

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 outside package-lock.json contains the string fast-uri except the new backlog row. Nothing needed a re-pin.

Surface Relation to package-lock.json Change
Release SBOM (release:sbom) built from go list -m all and the release files; no npm component none
Package gate (packageverify) pins the four direct devDependencies of package.json none
Python packaging no Python lock is tracked none
proofkit/witness-plan.json lists the path as an input selector of eight witnesses, without a digest none
Browser runtime and visual proof manifests list the path as an input; inputDigest is computed from current bytes at run time and not tracked none
tests/visual/environment.json binds the Playwright version, which TestVisualEnvironmentBindsThePinnedPlaywrightToolchain reads from the lock; unchanged none
Self-hosting package receipt dependencyDigest covers go.mod and go.sum; lockfileDigest is null; the source snapshot digest is computed at run time none
releasemanifest packageLockSha256 the lock of the registry consumer install, not this lock none

Who reads the lock. To find every reader, go test -p 2 -count=1 ./... ran under a macOS sandbox profile that refuses any read of package-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, through git 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-policy and package:artifact (its source snapshot) fail too.

Compatibility

Not breaking. No shipped byte changes: package-lock.json and BACKLOG.md are 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 check sequence on darwin/arm64 passed (exit 0) on tree 90a6ec033706 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.

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.json and differed only by the BACKLOG.md row. The OSV scans, source-hygiene, text-policy and git diff --check ran again on this tree. The schema suites, browser tests, Go tests and package:artifact ran on the first version.

  • OSV, before and after.
    • osv-scanner v2.6.0 was built from the module cache, and its module hash matches 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.
    • Base tree and this tree, each a git archive export: 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.
    • Base tree: 4 findings, exactly the four advisories above, exit 1. This tree: no issues, exit 0.
    • This worktree: osv-scanner scan source --offline --include-git-root --recursive --format sarif --output-file <file> ./ exits 0 with 0 results. Offline, it extracted only go.mod and package-lock.json, no .git package.
    • The hosted job runs scan source --include-git-root --recursive --format sarif --output-file artifacts/osv/osv-results.sarif ./, online.
    • The OSV API querybatch, sent with curl, covered every package of the lock and every module of go 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.
  • Suites that load fast-uri. After 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:check and boundary-contract:check (358 tests). browser:static-check passes as well (72 tests).
  • Browser tests: npm run browser:test with NODE_V8_COVERAGE set passed 561/561 in Chromium, Firefox and WebKit with zero retries. It took 13.1 min.
  • Go tests that read the lock: go test -p 2 -count=1 ./internal/tools/browserproofverify ./internal/tools/commandoracle ./scripts passes.
  • Other commands that read the lock: npm run source-hygiene, npm run text-policy (1421 text files) and npm run package:artifact pass.
  • Whitespace: git diff --check, git diff --cached --check and git diff --check HEAD~1 HEAD are clean on this tree.
  • Lock identity: this tree's package-lock.json is byte-identical to the first version's (blob 7770ca24).

Non-claims

  • Exploitability. It is not claimed that any of the four defects was reachable here. The trace covers the inputs of these suites in their tracked invocations; a later schema, or an operator-set contract or baseline variable, could bring other identifiers.
  • Scope. Only the locked fast-uri is updated; no other development dependency was re-evaluated beyond the scans above.
  • Playwright's copy. The fast-uri 3.1.3 inlined in playwright-core stays affected by every fast-uri advisory fixed after 3.1.3 (12 in OSV on 2026-10-24, seven rated high); the BACKLOG.md row BROWSER-BUNDLE-01 records it. That none of its exported functions is called is shown for this tree's browser suite only. Any use of Playwright's MCP client or server, or of a tool built on them, would call it. No Playwright release yet inlines a fixed fast-uri.
  • Unrelated finding. The OSV API also reports GO-2026-5932 (the unmaintained golang.org/x/crypto/openpgp package) for golang.org/x/crypto 0.58.0. That module is in the go list -m all graph, but the main module does not need it, it is in neither go.mod nor go.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 scope excluded.
  • Dates. The OSV results hold for the database state of 2026-10-24: about 05:15Z for the scans and the batch query, and 06:55Z for the per-version counts of the inlined copy. Later advisories are not covered.
  • Reader discovery. The sandbox only finds readers that fail when the read is refused. A reader that ignores the refusal would not appear.
  • Hosted run. Not run for this commit; nothing was pushed.

…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.
@iperev iperev changed the title fix(deps): lock fast-uri 3.1.9 to clear four advisories docs(security): track bundled browser URI parser exposure Oct 25, 2026
@iperev

iperev commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Delivery scope clarification

The exact locked fast-uri3.1.9 change was moved into PR228 so that earlier
intermediates could satisfy mandatory OSV checks. PR228 is now merged with
verified source/CI evidence. The remaining semantic delta of this PR is the
separate BROWSER-BUNDLE-01 backlog obligation: updating the lockfile does not
replace the URI parser independently inlined by the browser toolchain.

The original analysis above remains historical evidence for the proposed lock
repair, not a claim that this later edge still delivers that repair or that
all browser-tool dependency exposure is closed. Full fresh CI remains required
for the integrated candidate; no release/publication claim is made.

Base automatically changed from chain/r2-backlog-gate-diff to main October 25, 2026 22:57
@iperev
iperev marked this pull request as draft October 25, 2026 22:57
@iperev
iperev marked this pull request as ready for review October 25, 2026 23:16
@iperev
iperev merged commit 42447d8 into main Oct 25, 2026
15 checks passed
@iperev
iperev deleted the chain/d1-fast-uri branch October 25, 2026 23:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant