Skip to content

fix(prelude): throw instead of crashing on Intl.Segmenter with small-icu - #290

Merged
robertsLando merged 1 commit into
mainfrom
fix/intl-segmenter-segfault
Jul 27, 2026
Merged

fix(prelude): throw instead of crashing on Intl.Segmenter with small-icu#290
robertsLando merged 1 commit into
mainfrom
fix/intl-segmenter-segfault

Conversation

@robertsLando

Copy link
Copy Markdown
Member

Closes #289

Problem

pkg-fetch base binaries are built with --with-intl=small-icu, which ships no ICU break-iterator data. In JSSegmenter::New V8 only DCHECKs the result of icu::BreakIterator::create{Character,Word,Sentence}Instance, and DCHECKs are compiled out of release builds — so new Intl.Segmenter() succeeds, stores a null icu::BreakIterator, and the first .segment() call dereferences it in JSSegments::Create.

The result is an uncatchable SIGSEGV with 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.Segmenter directly. string-width v7+ uses it to measure text and is a transitive dependency of ora, boxen, inquirer and cli-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-width v8 has an ASCII fast path that never reaches the segmenter, and a spinner with stdout piped never animates.

Fix

patchIntlSegmenter() in prelude/bootstrap-shared.js replaces Intl.Segmenter.prototype.segment with the RangeError upstream intends to throw once the V8 fix lands. Wired into bootstrap.js (traditional, main + worker threads), sea-bootstrap-core.js (SEA main) and sea-worker-entry.js (SEA workers get their own Intl).

Two gates, in cost order:

if (variables.icu_small !== true) return;              // free property read
if (DTF('de').locale === 'de' && DTF('ja').locale === 'ja') return;

The second one matters: icu_small stays true when the user supplies working data through NODE_ICU_DATA / --icu-data-dir, and Segmenter then 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-width v7 (index.js:5) and v8 (index.js:16) both construct a Segmenter at 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 nodePath can be a small-icu distro build (Fedora and RHEL ship one).

Verification

Tested against a real icudt78l.dat from ICU 78.3 rather than reasoning about the NODE_ICU_DATA path:

binary icu_small ICU data before after
standard true none exit 139 RangeError, exit 0
standard true real, via NODE_ICU_DATA segments segments (15) — patch stays out of the way
standard + string-width v7 true none exit 139 RangeError, exit 0
--sea false full-icu segments segments (15) — untouched

The stack now names the caller instead of vanishing:

RangeError: pkg: Intl.Segmenter is unavailable in this executable. …
    at Segmenter.segment (pkg/prelude/bootstrap.js:2725:11)

Suites run: unit 257/257, plus test-00-sea, test-93-sea-compress and test-94-sea-esm-import-meta on node22 — the last two exercise sea-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.sync already fails on any signal, so a regression back to SIGSEGV fails the test without an explicit assertion. Confirmed non-vacuous: it takes the BROKEN:true branch on Linux.
  • test/unit/intl-segmenter.test.ts — locks in the regression that would otherwise be silent: on a healthy runtime the patch must leave Intl.Segmenter.prototype.segment untouched.

Docs

New troubleshooting section keyed on the error text, covering the three ways out: build with --sea, ship icudt<N>l.dat and point NODE_ICU_DATA at it, or avoid the API (string-width v6 / a shim).

Not in scope

pkg-fetch keeps small-icu everywhere except Windows node24+, where full-icu is 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

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

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.23%. Comparing base (5e648f7) to head (4d8822e).

Additional details and impacted files

Impacted file tree graph

@@            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              

see 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() in prelude/bootstrap-shared.js that gates on process.config.variables.icu_small and locale-probing to detect missing break-iterator data, then replaces Intl.Segmenter.prototype.segment to throw a RangeError.
  • 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).

@robertsLando
robertsLando merged commit cbc7629 into main Jul 27, 2026
56 of 57 checks passed
@robertsLando
robertsLando deleted the fix/intl-segmenter-segfault branch July 27, 2026 15:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Standard (non-SEA) binaries SIGSEGV on Intl.Segmenter.segment() — small-icu base binaries

2 participants