Repository navigation
fix(test): give spawned CLI children their input as a file, not a socket - #232
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.
This was referenced Oct 24, 2026
iperev
marked this pull request as draft
October 25, 2026 21:05
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(test): give spawned CLI children their input as a file, not a socket
Part of the UI-DESIGN-01 chain. Base branch:
chain/v1b-media-emulation(#231, the media-emulation assertion). This PR is one commit on that branch's head, and it goes into the chain before the design-token PR. The next change in this chain is stacked on this one and fixes the shipped TypeScript adapter's hang (see Non-claims). Merge after #231.Why
The Node test suites run the Proofkit CLI with
spawnSync(binary, argv, {input, timeout}). On a macOS host, a small share of these calls never ended on their own. They failed withETIMEDOUT, a different test each time, and this failed full local gates.For the
inputoption, Node'sspawnSyncgives the child one end of a Unix-domain socket pair. It writes the input, callsshutdown(SHUT_WR)on its end and keeps that end open until the child exits or the timeout kills it. On macOS, a child blocked inread(2)on that socket is sometimes not woken by the shutdown. The socket already holds end of file, but the read returns only when the parent closes its end, whichspawnSyncdoes at its timeout.Evidence, darwin/arm64:
recv(MSG_PEEK|MSG_DONTWAIT)return 0 (end of file) every 500 ms until the timeout, while itsread(2)stayed blocked: 66 stalls, 330 probes, all 0.shutdown(fd, SHUT_WR) = 02 to 3 ms after the spawn, and thecloseabout 3000 ms later, at the timeout.shutdown, descriptor kept open) stalled 8 times in 3000 calls. With acloseinstead of theshutdown, with apipe(2)or with a regular file, it stalled 0 times in 3000 calls each.io.ReadAllor its read loop, 8 to 52 of 3,000 calls stalled per run.subprocessuses pipes, and a separate run on the same host saw 0 stalls in 472 runs with it. Node's asynchronousspawnwithstdin.end()also shuts the socket down, but it closes it within 16 ms, and it stalled 0 times in 3,000 calls.So the defect is in the macOS kernel's wake-up of a reader blocked on a Unix-domain socket, not in the CLI and not in Node's or libuv's logic. Node's
spawnSyncexposes it because it keeps its end open after the shutdown. No upstream report naming this mechanism was found.What changes
One transport helper.
spawnWithInputFile(file, args, options)inscripts/cli-process.mjs:spawnSync(file, args, options), except thatoptions.inputis written to a temporary file that the child gets as its standard input. A regular file ends at its size, so the child always reads end of file.rmSyncof the file, thenrmdirSyncof the directory), so it needs no descriptor of its own; if it fails, the input descriptor is closed and the call throws.status,signal,error,stdout,stderr,timeout,maxBuffer,encoding,env,cwdandkillSignalkeepspawnSync's meaning. Writing the input can no longer fail with EPIPE.options.encoding, and a falsy input is no input, as inspawnSync.maxBuffer: undefinedwould have liftedspawnSync's 1 MiB default bound, and a test pins that.stdioexcept["ignore", "pipe", "pipe"], is refused. So is anyargsthat is not an array, including the two-argument formspawnWithInputFile(file, options):spawnSyncwould read the options from the args position and drop the third argument, so the input would go through the socket again.stdinUnreadBytes: the input bytes past the file offset that the child shares with the parent when it exits.read(2)andlseek(2)on the inherited descriptor move it;pread(2),mmap(2)and, on Linux, reopening/dev/stdindo not.Every site. All 42
spawnSynccalls withinput, in 29 test files underscripts/, now call the helper. Each change is the call name and the import. Apart from the early-refusal test below, no timeout, argument, expected value or assertion changed. The otherspawnSyncandexecFileSynccalls underscripts/andtests/pass no input, for example Go builds,go version -m,--help, file inputs, the integration-structure invocations, the git andtsccalls, the visual environment probes and argv-only rows with an ignored stdin.The EPIPE oracle, kept.
assertCLITransportkeeps its EPIPE rule, which the asynchronous pipe transport ofscripts/schema-completeness.test.mjsstill needs. It applies the same rule to a non-zerostdinUnreadBytes: it is accepted only from a child that exited normally with a non-zero status and no signal. With status 0 it fails.The pipe could report EPIPE only when an exiting child left more input than the pipe buffer held. The shared file offset leaves every byte that the child did not read with
read(2)past it. So the early-refusal test inscripts/browser-server-structure.test.mjsnow asserts thatstdinUnreadBytesequals the size of its input (over 8 MiB), that is, the refusal read none of it, instead of asserting EPIPE.Tests.
scripts/cli-process.test.mjs:Uint8Array, empty and absent input;maxBufferbound and the refused options, including the two-argument form and a non-arrayargs;spawnSyncthrows, after the open throws and after the write throws; and that a failed removal closes the input descriptor (the process holds as many descriptors after the call as before it);.js,.cjs,.mjs,.ts,.ctsand.mtsfiles underscripts/andtests/refuses aninputmember in the arguments of a directspawnSync,execFileSyncorexecSynccall, also when the function is called as a member (cp.spawnSync(…)). That covers plain, quoted, string-literal computed and shorthand keys, methods with a plain or quoted name, and object literals spread or nested in the arguments. The scan skips comments, strings, template text and regular expressions, and it has its own positive and negative cases.Declared inputs. In
proofkit/witness-plan.json, theproofkit.boundary-contract-checkpolicy now declaresscriptsandtests, which the source check reads.witness-scheduler-planadmits the plan (passed). Any change underscripts/ortests/now selects this policy in a selective run; its 37 listedscripts/entries are now redundant. No CI workflow selects from this plan.Requirements
No requirement text changes. No requirement or binding names the test transport. The REQ-PROOFKIT-QUALITY rows cover coverage metrics, sanitizers and the visual reference gate. The bindings name the witness files, which keep their tests and names. Each touched suite asserts the same CLI behavior, and its transport check is stricter. No clause was false before this change or becomes false with it.
Compatibility
Not breaking. CLI behavior, JSON contracts, exit codes and Go code are unchanged.
The one shipped byte change is
proofkit/witness-plan.json, which gains two input selectors of the boundary-contract-check policy. That changes its proof node digest, which no record pins.The test helper is Node-only and adds no dependency.
Proof
Proof: A local
npm run checksequence on darwin/arm64 passed (exit 0) on tree600c4bf80032with 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.Narrow evidence on the darwin/arm64 host, Node 26.11.1 (CI's version):
io.ReadAllchild, 440,000 bytes, 3 s timeout.spawnSyncwithinput: 52 stalls in 3,000 calls in one run, 44 in 3,000 in another, and 21 in 1,000 in the same window as the helper run below.spawnWithInputFile: 0 stalls in 3,000 calls, and 0 in 30,000 with two variants of the helper that differ from it only in when and how the input file is removed.spec-proof-bundle-admissionon the 59 captured ~430 KB inputs, 5 s timeout.spawnSyncwithinput: 9 stalls in 590 calls.spawnWithInputFile: 0 in 590, and 0 in 5,900 with those two variants.admission-records-structure,report-admission-structureandimpact-structure: 0 failures in 3 runs each, and in 15 runs each with those two variants. In interleaved runs, a copy of the base tree failedadmission-records-structureonce andreport-admission-structureonce, each on one test with anETIMEDOUTdiagnostic.scripts/cli-process.test.mjsand the three formerly flaky files passed 3 times each, andcommand-contract:check,source-hygiene,text-policyand the Go tests that read the witness plan passed. With one of those two variants, the file lists ofcompact-contract:check,receipt-contract:check,spec-tree-contract:checkandboundary-contract:checkpassed 5 times each (run with--test-concurrency=3on the shared host).spawnSync'sEMFILEresult with no descriptor leaked and no input left on disk, on this host and in the pinned Playwright arm64 image (Node 24.20.0, as root and as uid 1000). A variant that removed the directory with a recursivermSyncthrew instead, leaked the input descriptor and left the input file, with 1 free descriptor on both systems and with 2 on macOS.cli-process.test.mjs:spawnSync({input}), as a shorthand and as aninput:key;execFileSyncwith a quoted"input"key added undertests/;spawnSync's input;maxBuffer: undefinedpassed;trythat removes the input when it throws;rmSyncof the directory;argscheck removed, or narrowed to the two-argument object form;cli-process.test.mjs: a recursivermSyncof the directory that still closes the descriptor when it fails. The suite does not exhaust descriptors. The descriptor-limit probe refuses this mutant, because it leaves the input file at 1 and 2 free descriptors on macOS.git diff --checkover the range: clean.Non-claims
stdinUnreadBytesreads falsely for a child that reads its input only withpread(2)ormmap(2), or by reopening/dev/stdinon Linux (it reports bytes it read as unread), and for a child that seeks past input it did not read (it reports that input as read). On macOS, reopening/dev/stdinshares the offset and reads correctly. No current caller depends on these cases: its children are Proofkit CLI builds and the Node probes ofcli-process.test.mjs, which read their standard input withread(2)or not at all.scripts/write-browser-proof.mjsandscripts/write-browser-visual-proof.mjspassspawnSyncandexecFileSyncas values, and their callee passes no input. Nor does it see calls through.call,.apply,Reflect.applyor an optional call (spawnSync?.(…)), a bracketed callee (cp["spawnSync"](…)), a TypeScript type-argument call (spawnSync<T>(…)), getter, setter, async or generator members, methods with a computed key ({["input"]() {…}}), computed keys other than string literals, or escaped key names. Its lexer chooses between a division and a regular expression from the previous token alone, so it misreads some code: for example a/after++,--, a template literal, an object literal, a property named like a keyword that a regular expression can follow (x.in / y) or a name with a non-ASCII letter (π / 2), a regular expression after)orexport default(export default /'/), and a hashbang line with an unbalanced quote. A misreading can hide the code after it. On this tree everyspawnSync,execFileSyncandexecSyncname outsidecli-process.*is lexed as a name, and no regular-expression token lies inside any call's arguments.schema-completeness, two browser-server session tests) are unchanged. Node's asynchronous transport closes the socket right after the shutdown, and it stalled 0 times in 3,000 reproducer calls. That is evidence, not proof that a lost wake-up can never delay these sites.json-report-cli-adapter-source, a shipped product surface, still passes its input throughspawnSync'sinputon this tree. It sets no timeout, andProofkitCommandRunOptionsoffers none, so on macOS the same lost wake-up can block a consumer's call, and with it the consumer's event loop, until the CLI child is killed.TestGeneratedTypeScriptAdapterExecutesCoreSemantics(internal/command/jsonreportcliadaptersource/json_report_cli_adapter_source_test.go) runs that adapter undergo testwith small inputs and no timeout of its own. This PR does not change product output; the adapter hang is fixed by the next change in this chain.