Repository navigation
build(nix): distrust compromised eMudhra CAs via Node 24.22.1 and Bun 1.4.4 - #1218
Merged
Merged
Conversation
… 1.4.4 On 2026-10-19 the private keys of eight eMudhra (emSign) intermediate CAs were found to be compromised. Root stores have shipped updates, but Node and Bun carry their own root stores, so the nix-darwin `security.pki.caCertificateBlacklist` fix never reaches them. The pinned nixpkgs still ships Node 24.14.1 and Bun 1.3.13, and the repo pinned Bun 1.4.2; all three still connect to the test site that chains through a compromised intermediate. Dev shell / Nix package: - Add nix/nodejs-bin.nix (official nodejs.org 24.22.1 tarball, darwin-only) and nix/emudhra-overlay.nix, which substitutes that node and Bun 1.4.4 (official GitHub release zip) for `nodejs*`/`bun` on macOS. Linux keeps nixpkgs' packages untouched. - The overlay gates each replacement on `lib.versionOlder` against the pinned nixpkgs version, so it retires itself once `nix flake update` brings nodejs_24 >= 24.22.1 and bun >= 1.4.4. Attribute names never depend on `prev`, and the node gate reads `prev.nodejs-slim_24` because `nodejs_24` is a symlinkJoin that resolves `version` through `final`. - Wire the overlay into flake.nix's single `import nixpkgs` call. Bun pin: - Bump `packageManager`, the `bun` dependency, and `@types/bun` (1.4.3, the latest) in both package.json files; refresh bun.lock and nix/bun.lock.nix; update nix/package.nix's bunCompilerArchives to the @oven/bun-*@1.4.4 integrity hashes (x64 uses the -baseline archives, as since #1214); bump oven-sh/setup-bun pins and CONTRIBUTING. - bunfig's `minimumReleaseAge` (7 days) blocks resolving a same-day Bun release, so the lockfile was resolved once with `--minimum-release-age=0`; `bun install --frozen-lockfile` (what CI runs) passes under the default policy because only new resolutions are gated. Removal: once nixpkgs' nodejs_24 >= 24.22.1 and bun >= 1.4.4, delete nix/emudhra-overlay.nix, nix/nodejs-bin.nix, and the `overlays` line in flake.nix. Keep the bun@1.4.4 pin. Refs: - https://blog.cloudflare.com/emudhra-certificate-compromise/ - https://github.com/nodejs/node/releases/tag/v24.22.1 - https://github.com/oven-sh/bun/releases/tag/bun-v1.4.4
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Contributor
[Medium impact] The PR appears safe to merge; no actionable defect was established. SummaryUpdates Bun to 1.4.4 across dependencies, locks, CI, and the compiler embedded in published binaries. Adds a temporary macOS Nix overlay for Bun 1.4.4 and Node 24.22.1.
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
Pins["Bun 1.4.4 package pins"] --> CI["CI and release builds"]
Pins --> Compiler["Nix compiler archive"]
Compiler --> Binary["Hunk binary with embedded Bun"]
Flake["Nix package import"] --> Gate{"macOS and older package?"}
Gate -->|Yes| Replacement["Node 24.22.1 / Bun 1.4.4"]
Gate -->|No| Original["Original nixpkgs packages"]
Replacement --> Shell["Development shell"]
Original --> Shell
Reviews (1) · Last reviewed commit: "build(nix): distrust compromised eMudhra..." · Reviewed by Greptile |
…g exits on Bun 1.4.3+ With Bun 1.4.3 and later, `git diff | hunk pager` (and any run whose stdin is not a terminal) no longer exited on `q`: the terminal looked stuck until the user pressed Enter. CI caught it on the Bun 1.4.4 bump; `main` on Bun 1.4.2 was unaffected. Bun implements `tty.ReadStream(fd)` as `fs.ReadStream`, so every read on the `/dev/tty` descriptor Hunk opened for keyboard input is a blocking `read(2)` on a worker thread, and `destroy()` waits for that read to return. The app tears down on a later tick than the keypress that requested it; Bun 1.4.3 started issuing the next blocking read before that tick, so the teardown parked behind a read that only the next line of input could satisfy. Only `process.stdin` gets Bun's poll-driven native reader. `ControllingTerminalInput` rebuilds that path for an arbitrary terminal device: bytes come from `Bun.file(devicePath).stream()`, whose pending read is poll-driven and whose `cancel()` releases the event loop, while raw mode goes through a dormant `tty.ReadStream` on a second descriptor since termios state belongs to the device. The device must be the real node (`/dev/ttys003`, `/dev/pts/4`): macOS kqueue rejects the `/dev/tty` alias with EINVAL, so `resolveControllingTerminalDevice` follows `/proc/self/fd` on Linux and matches `rdev` among `/dev` terminal entries elsewhere. When no device resolves, startup keeps its existing "no interactive terminal" fallback. Verified in the PTY harness on Bun 1.4.2 and 1.4.4: `git diff | hunk pager`, `echo | hunk diff`, `hunk pager < file`, and `hunk diff` all exit on `q`; `test/pty/pager.test.ts` passes on both.
Bun bundles its own root store on every platform, so a Linux `nix develop`
shell was still handing out nixpkgs' bun 1.3.13, which trusts the
compromised eMudhra intermediates. Add the bun-linux-x64 and
bun-linux-aarch64 release assets (the same flavors nixpkgs selects, hashes
from the bun-v1.4.4 SHASUMS256.txt) and gate only the node override on
darwin, where the prebuilt nodejs.org tarball applies.
Verified by evaluation: devShells.{x86_64,aarch64}-linux resolve bun-1.4.4
from the matching asset while keeping nixpkgs' nodejs-24.14.1; the darwin
shell is unchanged (bun 1.4.4, node 24.22.1, probe still rejects the chain).
No Linux builder is available locally, so the Linux derivation itself was
not built here; it reuses nixpkgs' unzip + autoPatchelf recipe unchanged.
…erminal The comment on the renderer-stdin cast said OpenTUI only uses "the Readable surface plus setRawMode", which leaves out `isTTY`. OpenTUI gates its terminal palette (OSC) queries on `stdin.isTTY`, and Hunk's disconnect watcher reads it too, so dropping it from the controlling-terminal input would silently disable palette detection rather than fail. Name the members OpenTUI 0.5.x actually reads (audited in the Bun build of `CliRenderer` and `TerminalPalette`) and note that the cast cannot catch upstream drift, so `CliRenderer`'s stdin use needs a re-audit on `@opentui/core` bumps.
`runInteractiveApp` cast the controlling-terminal input to `NodeJS.ReadStream` before handing it to the session runner. Because the cast covered the whole union, removing `setRawMode` (or `isTTY`) from `ControllingTerminalInput` still compiled, and OpenTUI guards its raw-mode calls with `if (stdin.setRawMode)`, so the regression would have shipped as a terminal silently left in cooked mode. Add `RendererInput`, the members OpenTUI 0.5.x and the disconnect watcher actually read, and use it for `HunkSessionRunnerOptions.stdin`. Both `process.stdin` and `ControllingTerminalInput` satisfy it without a cast; the one remaining cast sits at the `createCliRenderer` call, where OpenTUI's declared type is wider than what it uses. Verified the guard: renaming `setRawMode` on the class now fails `bun run typecheck` at `runInteractiveApp.tsx`. The type still cannot catch OpenTUI starting to read a new stdin member, so its doc comment asks for a re-audit on `@opentui/core` bumps.
…aw-mode invariants The `ref()` comment said the device stream keeps the event loop alive "only while a read is pending". In Bun 1.4.4 it holds the loop from the first read until `destroy()` cancels the reader; completed reads, `pause()`, and `unref()` do not release it. That is what keeps `hunk pager` alive while idle (OpenTUI's keep-alive timer only runs for `process.stdin`), and it means exit depends on `ControllingTerminal.close`, so say so instead. Also record the two invariants the dormant raw-mode `tty.ReadStream` relies on, both verified against Bun 1.4.3/1.4.4 source and in a PTY: it must never be read from (a flowing handle reintroduces the blocking read and steals keystrokes), and its descriptor is closed with `closeSync`, so raw mode must be left before destroy and the handle not touched after. Comment-only; no behavior change.
The header said the flake's nixpkgs pin carries Node 24.21.0; it carries 24.14.1 (`nodejs_24` and `nodejs-slim_24` both evaluate to 24.14.1 at the locked revision), matching the PR description and the Linux dev shell. Comment-only; no evaluation change.
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.
Problem
On 2026-10-19 the private keys of eight eMudhra (emSign) intermediate CAs were found to be compromised (Cloudflare write-up). OS and browser root stores have been updated, but Node and Bun ship their own root stores, so the usual system-level fix (
security.pki.caCertificateBlackliston nix-darwin, etc.) never reaches them.The first upstream releases that distrust the chain are Node v24.22.1 (NSS 3.131.1 roots + blocked-SPKI check) and Bun v1.4.4. Today the repo's
nix developshell gives Node 24.14.1 / Bun 1.3.13 (the pinned nixpkgs), the repo pins Bun 1.4.2, and the compiledhunkembeds that Bun — all of which still connect to a site chained through a compromised intermediate.Approach
Dev shell (temporary, self-retiring)
nix/nodejs-bin.nix: official nodejs.org 24.22.1 tarball as a derivation (darwin only).nix/emudhra-overlay.nix: substitutes Bun 1.4.4 forbunon all four systems (official GitHub release zips — the same asset flavor nixpkgs picks per system — reusing nixpkgs' bun derivation hooks viaoverrideAttrs), and that node fornodejs*on darwin only (Linux keeps nixpkgs' node, which already distrusts the chain through the distro trust store; see Why node is only replaced on macOS). Each replacement applies only while the pinned nixpkgs is older than the fixed release (lib.versionOlder). Attribute names never depend onprev, and the node gate readsprev.nodejs-slim_24becausenodejs_24is a symlinkJoin whoseversionresolves throughfinal.import nixpkgsinflake.nix.Bun pin → 1.4.4 (this is what reaches users: the published binary embeds this runtime, so
hunk ghand extension fetches go through it)packageManager,bun,@types/bun(1.4.3 is the latest) in bothpackage.jsons;bun.lock;nix/bun.lock.nix(vianix run .#update-bun-lock);nix/package.nixbunCompilerArchiveswith the@oven/bun-*@1.4.4integrity hashes (x64 →-baseline, matching build(nix): compile through build-bin.ts with Bun's baseline x64 runtime #1214), all re-verified withnpm view … dist.integrity;oven-sh/setup-bunpins in workflows;CONTRIBUTING.md.hunkdiffpatch.minimumReleaseAgegate: Bun 1.4.4 is a same-day release. Scope and justification are in Security exception tominimumReleaseAge.Non-goals: not changing the
minimumReleaseAgepolicy inbunfig.toml(this PR makes a one-time, scoped security exception to it; see Security exception tominimumReleaseAge), not replacing node on Linux (not needed; see Why node is only replaced on macOS), not bumping other deps.Why node is only replaced on macOS
Linux shells keep nixpkgs' node: it is built with
--openssl-use-def-ca-store, so it trusts the distro's/etc/ssl/certsbundle and inherits the distro's eMudhra distrust (e.g. Ubuntu's ca-certificates 20260816ubuntu0.24.04.1 blacklists "emSign ECC Root CA - G3"). On macOS nix's OpenSSL reads Nix's own bundle instead of the Keychain, so only Node ≥ 24.22.1's built-in SPKI block protects it.Probed against the canary site (
https://dv-g3b-emudhra.we-have-a-problem.com/) with the flake's pinned nixpkgsnodejs_24(24.14.1):--use-bundled-ca20260816ubuntu0.24.04.1UNABLE_TO_GET_ISSUER_CERT_LOCALLYCERT_REVOKEDThe Linux result depends on the host's CA bundle having the distrust: Linux machines whose bundle hasn't caught up (an older NixOS
cacert, a distro without the update yet) stay exposed until it does, or untilnix flake updatebrings nixpkgs' node to ≥ 24.22.1 (it merged into nixpkgsstagingon 2026-10-21). That window is narrow and dev-shell only, so this PR documents it rather than packaging Linux node tarballs.Bun 1.4.3+ regression this bump exposed (fixed in the second commit)
The first CI run failed on every stdin-pipe pager path: after
q,git diff | hunk pagerstayed alive until the user pressed Enter.mainon Bun 1.4.2 is fine; the same source hangs on 1.4.3 and 1.4.4 (bisected by swapping only the Bun binary).Root cause: Bun implements
tty.ReadStream(fd)asfs.ReadStream, so each read on the/dev/ttydescriptor Hunk opens for keyboard input (when stdin is a pipe) is a blockingread(2)on a worker thread, anddestroy()waits for it. Hunk tears down on a later tick than the keypress that requested it; from 1.4.3 Bun issues the next blocking read before that tick, so teardown parks behind a read that only the next line of input can satisfy. Onlyprocess.stdingets Bun's poll-driven native reader (Bun's own source notestty.ReadStream"is supposed to extend from net.Socket, but we haven't made that work yet"). Minimal repro (hangs on 1.4.3+, exits on 1.4.2):Fix (
packages/hunk/src/core/process/terminal.ts):ControllingTerminalInput, aReadablefed byBun.file(<real device>).stream()— poll-driven reads, andcancel()releases the event loop — with raw mode delegated to a dormanttty.ReadStreamon a second descriptor (termios is per device). The device has to be the real node (/dev/ttys003,/dev/pts/4) because macOS kqueue rejects the/dev/ttyalias with EINVAL;resolveControllingTerminalDevicefollows/proc/self/fdon Linux and matchesrdevin/develsewhere. If nothing resolves, startup keeps its existing "no interactive terminal" fallback. Verified in the PTY harness on Bun 1.4.2 and 1.4.4:git diff | hunk pager,echo | hunk diff,hunk pager < file,hunk diffall exit onq;test/pty/pager.test.ts12/12 on both. Worth an upstream Bun issue; happy to file it.Remove when nixpkgs catches up
Once
nix flake updategivesnodejs_24 >= 24.22.1andbun >= 1.4.4, deletenix/emudhra-overlay.nix,nix/nodejs-bin.nix, and theoverlaysline inflake.nix. Keep thebun@1.4.4pin. Nothing breaks if it's left in place — the gates just evaluate to the nixpkgs packages. Watch:Verification (aarch64-darwin, macOS 26.5.1)
Dev shell probe against the canary site:
Baseline for comparison, the pinned nixpkgs bun 1.3.13:
CONNECTED (BAD) Your client successfully connected to the site. That's bad!Nix package:
nix build .#hunk✅ (compiles withbun-compiler-1.4.4); the embedded runtime of the resulting binary (BUN_BE_BUN=1 …/bin/.hunk-wrapped probe.ts) →REJECTED (GOOD) certificate revoked.Overlay gates:
nix evalofdevShells.{x86_64,aarch64}-linux.default.buildInputs→bun-1.4.4 nodejs-24.14.1(bun frombun-linux-{x64,aarch64}.zip, hashes checked against the releaseSHASUMS256.txt; node untouched);aarch64-darwin→bun-1.4.4 nodejs-bin-24.22.1. Simulating a caught-up nixpkgs (pre-overlay that sets bun/nodejs-slim_24 to the fixed versions) yields nixpkgs' own derivations (bun src hash = nixpkgs', node namenodejs-24.22.1notnodejs-bin), i.e. the overlay turns itself off.Repo checks (with the worktree's Bun 1.4.4):
bun run typecheck✅bun run deps:check✅bun install --frozen-lockfilefrom a cleannode_modulesunder the defaultbunfig.toml✅ (see note)bun run test(after the fix): 5145 pass / 55 skip / 0 fail. Two local environment artifacts are unrelated to this change: a symlinkedTMPDIR(/var/foldersvs/private/var/…) breaks realpath-equality assertions in 9 tests, and nix's bash 5.3.9 fails the 5 bash-completion tests identically on Bun 1.4.2 (they pass withHUNK_TEST_BASH_EXECUTABLE=/bin/bash); the run above used a canonicalTMPDIRand/bin/bash.bun run test:integration(PTY): 266 pass / 1 skip / 1 fail — the failure (Neovim accepts j without Enter) is nvim 0.12.1's--cleanDSR warning under the test PTY and fails identically on Bun 1.4.2.script(Linux-only) — relying on CI for it.test:session-broker-nodenot run (no broker changes).nix developon Linux.Security exception to
minimumReleaseAgeThis PR makes a deliberate, one-time exception to the repo's 7-day package age gate in order to ship the eMudhra fix. The policy itself is unchanged:
bunfig.tomlis not modified andminimumReleaseAge = 604800still applies to every future resolution.Why. The age gate exists to give the ecosystem time to catch a compromised or broken release before we adopt it. Here the trade-off runs the other way: Bun 1.4.4 (published 2026-10-22) is the first Bun release whose root store rejects the compromised eMudhra intermediates, and the published
hunkbinary embeds whatever Bun we pin. Waiting 7 days means shipping a runtime that still trusts a known-compromised CA for that whole window. The risk we accept in exchange is narrow: a same-day release from the Bun maintainers' ownbun/@oven/bun-*packages, with integrity hashes pinned in bothbun.lockandnix/bun.lock.nix.How. The lockfile was resolved once with
bun install --minimum-release-age=0(a command-line override, not a config change). CI'sbun install --frozen-lockfilepasses under the default policy because the gate applies only to new resolutions, not to entries already in the lockfile. Re-verified from a cleannode_modulesat the PR head with the unmodifiedbunfig.toml.Scope.
--minimum-release-age=0disables the gate for the whole resolution, not just Bun, so the lockfile was audited to confirm nothing else came along:bun@1.4.4and its 12 platform packages@oven/bun-{darwin,linux,freebsd,windows}-*@1.4.4, plus the@oven/bun-{darwin,linux}-x64-baseline@1.4.4compiler archives thatnix/package.nixfetches directly. All published 2026-10-22 by the Bun release.@types/bun1.4.1 → 1.4.3 (published 2026-10-12) and its dependencybun-types1.4.1 → 1.4.3 (2026-10-10).bun.lockhas 375 package entries before and after: none added, none removed, no other version or integrity changed, no top-leveloverrides/patchedDependencies/trustedDependencieschanges.bun-types@1.4.3declares a newundici-types: "*"dependency, which resolves to theundici-types@7.18.2already in the lockfile, so it adds no package.mainwith only this PR'spackage.jsonchanges produces the same 375 keys with identical versions and integrity hashes. Every changed integrity hash matchesregistry.npmjs.org, and all 364 registry entries inbun.lockmatchnix/bun.lock.nix.If reviewers prefer to wait out the 7 days instead, the Nix overlay half of this PR is independent and still protects the dev shell in the meantime.