Repository navigation
Harden the vendored libraries and the runtime's stack - #3442
Conversation
Build the runtime units, and the vendored zlib and mbedtls archives that parse untrusted input, with zero-initialized locals up to 64 bytes, trapping bounds checks, stack canaries (except WASI), stack-clash probes on Linux and _FORTIFY_SOURCE=2 for optimized flavors outside musl and WASI. QuickJS and libregexp keep their flags. runtimeHardeningFlags selects the flags per target for both pack builders. On Linux x64 bench-runtime, the release and speed geomeans change by +0.6% and executables grow by a median 3.4%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Android NDK 27 clang rejects -ftrivial-auto-var-init-max-size. The pack builders now probe the compiler for the size cap and omit zero-initialization where it is missing, since the unlimited form slowed the HTTP benchmark by 11%. Every other CI toolchain accepts the cap. Clang's array-bounds checks emit unused sanitizer metadata even in trap mode, and without optimization it stays in the object with absolute source paths that -ffile-prefix-map does not rewrite. Dev flavors now use only local-bounds, which is checked in LLVM IR and embeds nothing, so the darwin-arm64 dev objects pass verify.mjs again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Harden the vendored brotli archive like zlib and mbedtls: it also parses untrusted compressed input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Static PIE links run IFUNC resolvers while relocating, before libc sets up thread-local storage. -fstack-protector-strong gave the CPU probe a canary read from %fs:0x28, so --static executables that call floor, ceil or trunc crashed in _dl_relocate_static_pie. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Linux x64 runtime pack's speed units are optimized with the committed runtime profile, and the hardening flags change the control flow of the runtime functions. The pack test reported 768 of the profile's 4005 runtime functions changed since it was collected, so the profile was regenerated with scripts/refresh-runtime-profile.mjs on the hardened runtime: the runtime benchmark workloads at --optimization=speed plus an instrumented tsc-ts checking its own sources. Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main dropped import bitcode from the runtime pack build (c1ce07e); the hardening flags this branch adds to the runtime units and the zlib/brotli/mbedtls archives are unaffected, so the merged build keeps the hardening imports and main's bitcode-free unit plan. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
scripts/refresh-runtime-profile.mjs on Linux x64 against the hardened runtime pack: the runtime benchmark workloads (48 workloads, 3 runs each) plus an instrumented tsc-ts self-check trained 333 raw profiles, merged with --sparse --keep-functions so the profile names only runtime functions (76,604 bytes gzipped). runtime-profile.test.ts and hardening.test.ts pass against the rebuilt pack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The C runtime packs are compiled with compiler protections (#1197). Conflict, resolved by reading both sides: - packages/runtime-pack-common/scripts/build.mjs, the import list: the train's import of the runtime unit helpers (with hostHelperPath) and the branch's import of the hardening flags, both kept. Co-Authored-By: Claude Code <noreply@anthropic.com>
A proposal for measurement, not yet agreed. Instruction counts on 24 benchmark workloads, per protection, against main: bounds traps +2.15 % (one workload +25 %), stack canaries +0.86 %, FORTIFY level 2 +0.15 % (one workload +1.4 %), zero-initialization up to 64 bytes +0.07 %, stack-clash probes 0.00 %. The runtime units are on the path of every typed program, so they keep only zero-initialization and the stack-clash probes. The vendored archives that parse untrusted input keep every protection, and zstd, which main gained after the branch was cut, joins zlib, brotli and mbedtls. Co-Authored-By: Claude Code <noreply@anthropic.com>
Merges the runtime-pack hardening of #1197 in the form that was decided after measuring each protection alone (instructions executed on 24 benchmark workloads, against main): bounds traps +2.15 %, stack canaries +0.86 %, _FORTIFY_SOURCE=2 +0.15 %, zero-initialization of locals up to 64 bytes +0.07 %, stack-clash probes 0.00 %. - The vendored zlib, brotli, mbedtls and zstd archives, which parse untrusted input, are compiled with all five protections. - The runtime units, which every typed program runs through, get the stack-clash probes and the zero-initialization only. Canaries, fortified calls and bounds traps stay off them. The commit that says "Proposal" in this branch is that split; it is the decided set. Co-Authored-By: Claude Code <noreply@anthropic.com>
No flag changes. The comments in hardening.mjs and the two pack build scripts said the runtime units carry every protection and named QuickJS, whose archive the packs no longer build; they now say what is compiled: zero-initialized small locals and stack-clash probes on the runtime units, all five protections on the vendored zlib, brotli, zstd and mbedtls archives, libregexp as before. The note beside the math IFUNC resolvers says why they keep no_stack_protector although the packs build the runtime units without canaries. hardening.test.ts is formatted with oxfmt and its two unoptimized cases are named after the vendored builds they describe. Co-Authored-By: Claude Code <noreply@anthropic.com>
A follow-up from the review of the pinned downloads: a checkout that converts line endings (Windows with core.autocrlf) would hand sh and sha256sum a carriage return at the end of every line of scripts/verify-toolchain.sh and scripts/toolchain.sha256. The Windows job passes today; .gitattributes makes that independent of the runner's git configuration, and the supply-chain test checks the two lines. Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
CI on this PR's head Main's run on the merge commit: green, 103 jobs, among them both native bootstraps with the hardened pack (plain: commands, rebuild; sanitized: commands, rebuild-emit, rebuild-frontend). This PR's run: every Linux and Windows job is green (67 jobs), including the pack builds and tests of the other targets: Linux x64 native packages, the musl LLVM-artifacts lane,
The aggregate 🤖 Generated with Claude Code |
Main gained train cli 21 (#3426), the timer test programs (#3441, #3443) and the hardened runtime (#3442). No conflict. Seven files changed on both sides and merge by position: binary.ts, generic-functions.ts, lower-calls.ts, lower-stmts.ts, lowerer.ts, coverage.test.ts and the island value matrix's table. Co-Authored-By: Claude Code <noreply@anthropic.com>
The runtime packs are compiled with compiler protections, split by where they cost: the vendored libraries that parse untrusted input get all of them, the runtime units get the two that cost typed code almost nothing. Supersedes #1197, which applied all five everywhere.
What is compiled with what
-fsanitize=bounds -fsanitize-trap=bounds-fstack-protector-strong-D_FORTIFY_SOURCE=2-ftrivial-auto-var-init=zero -ftrivial-auto-var-init-max-size=64-fstack-clash-protectionA compiler without the zero-initialization size cap (the Android NDK 27 clang) gets no zero-initialization rather than the unlimited form;
probeHardeningSupportasks the pack's compiler once per build.Not done, and why
The runtime's own units get no stack canaries, no fortified libc calls and no bounds traps. Each was measured alone and each makes typed programs execute more instructions: bounds traps +2.15% across the benchmark set and +25.5% on one workload, canaries +0.86% (up to +2.5%),
_FORTIFY_SOURCE=2+0.15% (up to +1.4%). With all five on the runtime units, as #1197 had it, the same set executed +3.16% more instructions, four benchmarks read slower in wall-clock time (up to +4.9%), and every executable grew by a median 3.29%. Typed programs are meant to pay nothing they can avoid, so these three stay off the code every typed program runs through. Any of them can be turned on for the runtime units inruntimeHardeningFlags(one list inpackages/runtime-pack-common/scripts/hardening.mjs) at the cost in the table.What rides, and why
--sanitize, the sanitized test lanes), these flags are not applied; the packs are what release executables link.scr_glibc_math.c: the IFUNC resolvers and the CPU probe they call carryno_stack_protector. They run before libc sets up thread-local storage, where a canary read would fault; the packs build the runtime units without canaries, and the attribute keeps that true under a compiler that enables them by default.What the linux-x64-gnu pack's objects carry
__stack_chk_fail__*_chk)ud1)Cost
This changes the bytes of every executable, by design: every program links runtime units compiled with the two flags. It is recorded as one whole-runtime exception to the zero-cost gate.
--gate-corpus=all). IR and LLVM are identical on every one; 5,868 executables differ (every program that builds), 356 fail to build on both sides, none differs in IR or LLVM, no executable gains a dynamic-tier symbol. The compiler itself is byte-identical to main's.Validation
All in Vercel Sandboxes (Linux x64). The head is
e46c1a5562= the validated tree2d0e27507eplus a merge of main6c1e5ac877that touches none of this PR's files.2d0e27507eagainst main783c54deee: type check; lanes plain 25,531 passed, 0 failed; sanitized 22,059 passed, 5 failed, all five on time limits (below); both native bootstraps to the end (plain: commands, rebuild; sanitized: commands, rebuild-emit, rebuild-frontend) with the hardened pack; the zero-cost gate over all 6,224 programs as described under Cost; bench-runtime as above; Test262 (Function, Reflect, JSON, Proxy, Object): 0 regressed.chalk4 (Node island)andcommander-calcinnpm.test.tsandnpm-shape-overlay.test.ts(120 s each) and the Effect generator case ofnpm-static.test.ts(900 s). A sanitized build compiles the runtime from source with its own flags and never links the packs this PR changes, and the built compiler is byte-identical to main's. Run alone with the lane's environment:chalk4passes in 76 s and the Effect case in 469 s on this tree;commander-calcruns into its 120 s limit on this tree and on main's own tree783c54deeealike (111 to 116 s on main four merges earlier).e46c1a5562: build and type check,pnpm lint,pnpm format:check,hardening.test.ts(16 tests),runtime-profile.test.tswithout regenerating the profile (55 of 1,458 functions mismatch, 3.77%), all ofpackages/runtime-pack-common/test, the supply-chain, pack cache-key, CI-lane and code-quality tests (20 files, 169 tests). The zero-cost gate again on this head against main6c1e5ac877: 6,234 programs compared, 5,876 executables differ (every program that builds), 358 fail on both sides, IR and LLVM identical on every one, sizes +0.02% to +3.04%, median +0.33%, compile time +0.18% [-0.92%, +1.05%].runtime-linux-x64-musl(canaries in zlib, brotli, zstd and mbedtls; no fortified calls, musl has none; none of either in the runtime units) andruntime-wasm32-wasi(no canaries, as designed). Eight programs that run through zlib, brotli, zstd and mbedtls, built for musl with that pack, match Node in stdout, stderr and exit status.Also in this PR
.gitattributeskeepsscripts/verify-toolchain.shandscripts/toolchain.sha256LF in every checkout, the follow-up from the review of #3430 (a Windows checkout that converts line endings would handshandsha256suma carriage return per line);tests/harness/ci-supply-chain.test.tschecks the two lines.Supersedes #1197.
🤖 Generated with Claude Code