fix(prelude): throw instead of crashing on Intl.Segmenter with small-icu - #290
Merged
Conversation
pkg-fetch base binaries are built with --with-intl=small-icu and ship no ICU break-iterator data. V8 only DCHECKs the null icu::BreakIterator returned by BreakIterator::create*Instance, and DCHECKs are compiled out of release builds, so `new Intl.Segmenter()` succeeds and the first segment() call dereferences null — an uncatchable SIGSEGV with no stack (nodejs/node#51752, https://issues.chromium.org/issues/531782498). Users hit this through string-width v7+, a transitive dependency of ora, boxen, inquirer and cli-table3, so the usual symptom is a CLI that dies silently the moment a spinner draws a frame. Replace segment() with the RangeError upstream intends to throw once the V8 fix lands. Gated on icu_small plus a two-locale probe, because icu_small stays true when the user supplies working data via NODE_ICU_DATA and Segmenter then works. Only segment() is replaced: string-width builds a Segmenter at module scope, so removing the constructor would break those imports outright instead of at the point of use. SEA mode is unaffected either way — it uses the official full-icu Node.js binaries — but the patch is wired into both bootstraps since a user-supplied nodePath can be a small-icu distro build.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #290 +/- ##
==========================================
+ Coverage 87.19% 87.23% +0.03%
==========================================
Files 23 23
Lines 7929 7929
Branches 1214 1214
==========================================
+ Hits 6914 6917 +3
+ Misses 1008 1005 -3
Partials 7 7 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR adds a defensive prelude patch to prevent a hard crash (SIGSEGV) when Intl.Segmenter#segment() is used on small-icu Node.js builds without ICU break-iterator data (a common configuration for Standard-mode pkg base binaries). Instead of allowing an uncatchable process crash, the prelude replaces .segment() with a RangeError, while ensuring the patch does not activate when ICU data is actually available (e.g., via NODE_ICU_DATA).
Changes:
- Add
patchIntlSegmenter()inprelude/bootstrap-shared.jsthat gates onprocess.config.variables.icu_smalland locale-probing to detect missing break-iterator data, then replacesIntl.Segmenter.prototype.segmentto throw aRangeError. - Wire the patch into Standard bootstrap, SEA bootstrap core, and SEA worker entry to cover main + worker contexts.
- Add both an e2e regression test (survives and matches “throw vs segment” behavior) and a unit test to ensure healthy runtimes remain unmodified; document the new troubleshooting error and remediation options.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
prelude/bootstrap-shared.js |
Implements the gated Intl.Segmenter#segment patch that throws instead of allowing a SIGSEGV on broken small-icu bases. |
prelude/bootstrap.js |
Invokes the shared Segmenter patch in the Standard bootstrap flow. |
prelude/sea-bootstrap-core.js |
Invokes the shared Segmenter patch in SEA bootstrap core (covers SEA main). |
prelude/sea-worker-entry.js |
Invokes the shared Segmenter patch in SEA workers (workers have separate Intl). |
test/test-50-intl-segmenter/main.js |
New e2e test that packages and runs a repro and asserts “throws vs segments” while ensuring the process stays alive. |
test/test-50-intl-segmenter/test-x-index.js |
New packaged repro script that logs whether the runtime is “broken” and whether it throws or segments. |
test/unit/intl-segmenter.test.ts |
Unit test ensuring the patch is a no-op on healthy (break-data-present) runtimes. |
docs-site/guide/troubleshooting.md |
Documents the new RangeError: pkg: Intl.Segmenter is unavailable... and mitigation paths (--sea, NODE_ICU_DATA, avoiding the API). |
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.
Closes #289
Problem
pkg-fetchbase binaries are built with--with-intl=small-icu, which ships no ICU break-iterator data. InJSSegmenter::NewV8 onlyDCHECKs the result oficu::BreakIterator::create{Character,Word,Sentence}Instance, andDCHECKs are compiled out of release builds — sonew Intl.Segmenter()succeeds, stores a nullicu::BreakIterator, and the first.segment()call dereferences it inJSSegments::Create.The result is an uncatchable
SIGSEGVwith no output and a symbol-less backtrace. Upstream: nodejs/node#51752 (open since Feb 2024), filed with V8 as https://issues.chromium.org/issues/531782498. We also track it in yao-pkg/pkg-fetch#134.Almost nobody calls
Intl.Segmenterdirectly.string-widthv7+ uses it to measure text and is a transitive dependency ofora,boxen,inquirerandcli-table3— so the symptom is a CLI that dies the instant a spinner draws its first frame. It is also easy to miss in testing:string-widthv8 has an ASCII fast path that never reaches the segmenter, and a spinner with stdout piped never animates.Fix
patchIntlSegmenter()inprelude/bootstrap-shared.jsreplacesIntl.Segmenter.prototype.segmentwith theRangeErrorupstream intends to throw once the V8 fix lands. Wired intobootstrap.js(traditional, main + worker threads),sea-bootstrap-core.js(SEA main) andsea-worker-entry.js(SEA workers get their ownIntl).Two gates, in cost order:
The second one matters:
icu_smallstaystruewhen the user supplies working data throughNODE_ICU_DATA/--icu-data-dir, andSegmenterthen works — patching there would break a functioning setup. Data-less small-icu resolves every locale to the default one, so probing two unrelated locales means no single system locale can produce a false negative.Only
.segment()is replaced, never the constructor.string-widthv7 (index.js:5) and v8 (index.js:16) both construct aSegmenterat module scope, so removing the constructor would break those imports outright rather than at the point of use.SEA mode is unaffected either way — it downloads the official full-icu Node.js binaries — but the patch is wired into that bootstrap too, since a user-supplied
nodePathcan be a small-icu distro build (Fedora and RHEL ship one).Verification
Tested against a real
icudt78l.datfrom ICU 78.3 rather than reasoning about theNODE_ICU_DATApath:icu_smalltrueRangeError, exit 0trueNODE_ICU_DATAstring-widthv7trueRangeError, exit 0--seafalseThe stack now names the caller instead of vanishing:
Suites run: unit 257/257, plus
test-00-sea,test-93-sea-compressandtest-94-sea-esm-import-metaon node22 — the last two exercisesea-bootstrap-core.js.Tests
test/test-50-intl-segmenter/— computes the broken/healthy expectation independently of the prelude and asserts the matching branch, so it also passes on the full-icu Windows node24+ bases.spawn.syncalready fails on any signal, so a regression back toSIGSEGVfails the test without an explicit assertion. Confirmed non-vacuous: it takes theBROKEN:truebranch on Linux.test/unit/intl-segmenter.test.ts— locks in the regression that would otherwise be silent: on a healthy runtime the patch must leaveIntl.Segmenter.prototype.segmentuntouched.Docs
New troubleshooting section keyed on the error text, covering the three ways out: build with
--sea, shipicudt<N>l.datand pointNODE_ICU_DATAat it, or avoid the API (string-widthv6 / a shim).Not in scope
pkg-fetchkeepssmall-icueverywhere except Windows node24+, wherefull-icuis a build workaround tracked in yao-pkg/pkg-fetch#108. Full ICU data costs roughly +34 MB per binary, which is why this fixes the failure mode rather than the cause.🤖 Generated with Claude Code