From fb535974c8d680a72c14479a4a7cffd8f57c5276 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 26 Jul 2026 07:21:55 +0000 Subject: [PATCH 1/2] Intl.Locale: throw RangeError for structurally valid tags that exceed ICU capacity new Intl.Locale(tag) threw TypeError ("failed to initialize Locale") for a structurally valid BCP-47 tag once it overflowed ICU's ULOC_FULLNAME_CAPACITY buffer (23+ variant subtags, ~186 chars). Every other Intl entry point (DateTimeFormat, NumberFormat, Collator, getCanonicalLocales, localeCompare) throws RangeError for the same input via canonicalizeLocaleList, and so does V8/Node. The source change lives in JavaScriptCore (IntlLocale.cpp); this bumps WEBKIT_VERSION to the oven-sh/WebKit#350 preview tarball and adds a test asserting every Intl path agrees on RangeError for an oversized-but-valid tag. --- scripts/build/deps/webkit.ts | 7 +++++- test/js/web/intl/intl.test.ts | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index f84dd77e0e19..70c8dfe60425 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,12 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "549170099226f816a4b204ea1d8fa102fb79eefa"; +// oven-sh/WebKit#350 preview, branched from 549170099226f816 (the previous +// WEBKIT_VERSION): IntlLocale::initializeLocale throws RangeError (not +// TypeError) when ICU canonicalization fails on a structurally valid tag, +// matching canonicalizeLocaleList and V8. Revert to the merged-main sha once +// #350 lands. +export const WEBKIT_VERSION = "autobuild-preview-pr-350-db4518f8"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/test/js/web/intl/intl.test.ts b/test/js/web/intl/intl.test.ts index 86dc3758a8e8..2dc148dfdfdc 100644 --- a/test/js/web/intl/intl.test.ts +++ b/test/js/web/intl/intl.test.ts @@ -223,6 +223,47 @@ describe("URL IDNA", () => { }); }); +describe("Intl.Locale", () => { + test("structurally valid tag exceeding ICU capacity throws RangeError, consistent with every other Intl entry point", () => { + // 23 distinct 7-char variant subtags; each is a well-formed `unicode_variant_subtag`, so the + // whole tag is a structurally valid BCP-47 language tag. It only fails because it overflows + // ICU's internal ULOC_FULLNAME_CAPACITY buffer during canonicalization. + const tag = "en" + Array.from({ length: 23 }, (_, i) => "-" + String(i).padStart(5, "v") + "ab").join(""); + const ctor = (f: () => unknown) => { + try { + f(); + return "no throw"; + } catch (e) { + return (e as Error).constructor.name; + } + }; + // Every Intl path that canonicalizes this tag must agree on RangeError so that a + // `catch (e) { if (e instanceof RangeError) ... }` validation guard is not constructor-dependent. + expect({ + "Intl.Locale": ctor(() => new Intl.Locale(tag)), + "Intl.Locale+options": ctor(() => new Intl.Locale(tag, { language: "en" })), + "Intl.DateTimeFormat": ctor(() => new Intl.DateTimeFormat(tag)), + "Intl.NumberFormat": ctor(() => new Intl.NumberFormat(tag)), + "Intl.Collator": ctor(() => new Intl.Collator(tag)), + "Intl.getCanonicalLocales": ctor(() => Intl.getCanonicalLocales(tag)), + "String#localeCompare": ctor(() => "a".localeCompare("b", tag)), + }).toEqual({ + "Intl.Locale": "RangeError", + "Intl.Locale+options": "RangeError", + "Intl.DateTimeFormat": "RangeError", + "Intl.NumberFormat": "RangeError", + "Intl.Collator": "RangeError", + "Intl.getCanonicalLocales": "RangeError", + "String#localeCompare": "RangeError", + }); + }); + + test("22 variants (under ICU capacity) still constructs", () => { + const tag = "en" + Array.from({ length: 22 }, (_, i) => "-" + String(i).padStart(5, "v") + "ab").join(""); + expect(() => new Intl.Locale(tag)).not.toThrow(); + }); +}); + describe("Intl.getCanonicalLocales", () => { test("deprecated BCP-47 tags map to modern equivalents", () => { // ICU ships .res bundles under the deprecated tag names; canonicalization From b71ad1aca8a6d09e49e210700c3fe0cf5cddb7d4 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 26 Jul 2026 08:21:46 +0000 Subject: [PATCH 2/2] test: probe for ICU canonicalization capacity instead of hard-coding 23 variants Windows ICU 73.2 grows its canonicalization buffer and accepts arbitrarily long structurally-valid tags, so the 23-variant tag constructs there and the hard-coded RangeError assertion showed 'no throw' for every entry. The capacity is an ICU-internal detail (Linux/macOS fail at 23, Windows never does); probe for it via getCanonicalLocales and skip the RangeError assertion on builds without a limit. A Linux-only guard ensures the probe never silently degrades to a skip on the platform where the bug reproduces. --- test/js/web/intl/intl.test.ts | 95 ++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/test/js/web/intl/intl.test.ts b/test/js/web/intl/intl.test.ts index 2dc148dfdfdc..4370c3f91b90 100644 --- a/test/js/web/intl/intl.test.ts +++ b/test/js/web/intl/intl.test.ts @@ -224,43 +224,68 @@ describe("URL IDNA", () => { }); describe("Intl.Locale", () => { - test("structurally valid tag exceeding ICU capacity throws RangeError, consistent with every other Intl entry point", () => { - // 23 distinct 7-char variant subtags; each is a well-formed `unicode_variant_subtag`, so the - // whole tag is a structurally valid BCP-47 language tag. It only fails because it overflows - // ICU's internal ULOC_FULLNAME_CAPACITY buffer during canonicalization. - const tag = "en" + Array.from({ length: 23 }, (_, i) => "-" + String(i).padStart(5, "v") + "ab").join(""); - const ctor = (f: () => unknown) => { - try { - f(); - return "no throw"; - } catch (e) { - return (e as Error).constructor.name; - } - }; - // Every Intl path that canonicalizes this tag must agree on RangeError so that a - // `catch (e) { if (e instanceof RangeError) ... }` validation guard is not constructor-dependent. - expect({ - "Intl.Locale": ctor(() => new Intl.Locale(tag)), - "Intl.Locale+options": ctor(() => new Intl.Locale(tag, { language: "en" })), - "Intl.DateTimeFormat": ctor(() => new Intl.DateTimeFormat(tag)), - "Intl.NumberFormat": ctor(() => new Intl.NumberFormat(tag)), - "Intl.Collator": ctor(() => new Intl.Collator(tag)), - "Intl.getCanonicalLocales": ctor(() => Intl.getCanonicalLocales(tag)), - "String#localeCompare": ctor(() => "a".localeCompare("b", tag)), - }).toEqual({ - "Intl.Locale": "RangeError", - "Intl.Locale+options": "RangeError", - "Intl.DateTimeFormat": "RangeError", - "Intl.NumberFormat": "RangeError", - "Intl.Collator": "RangeError", - "Intl.getCanonicalLocales": "RangeError", - "String#localeCompare": "RangeError", - }); + // N distinct 7-char variant subtags; each is a well-formed `unicode_variant_subtag`, so the + // whole tag is a structurally valid BCP-47 language tag for any N. + const makeTag = (n: number) => + "en" + Array.from({ length: n }, (_, i) => "-" + String(i).padStart(5, "v") + "ab").join(""); + + // The capacity at which ICU's canonicalization fails is an internal detail that varies by ICU + // build: bundled ICU 75.1 (Linux) and Apple's libicucore (macOS) fail at 23 variants / 186 bytes; + // Windows ICU 73.2 grows its buffer and accepts arbitrarily long tags. Probe for the threshold + // via getCanonicalLocales (which already throws RangeError) rather than hard-coding it. + let overflowTag: string | undefined; + for (let n = 20; n <= 200; n++) { + try { + Intl.getCanonicalLocales(makeTag(n)); + } catch { + overflowTag = makeTag(n); + break; + } + } + + const hasLimit = overflowTag !== undefined ? test : test.skip; + hasLimit( + "structurally valid tag exceeding ICU capacity throws RangeError, consistent with every other Intl entry point", + () => { + const ctor = (f: () => unknown) => { + try { + f(); + return "no throw"; + } catch (e) { + return (e as Error).constructor.name; + } + }; + // Every Intl path that canonicalizes this tag must agree on RangeError so that a + // `catch (e) { if (e instanceof RangeError) ... }` validation guard is not constructor-dependent. + expect({ + "Intl.Locale": ctor(() => new Intl.Locale(overflowTag!)), + "Intl.Locale+options": ctor(() => new Intl.Locale(overflowTag!, { language: "en" })), + "Intl.DateTimeFormat": ctor(() => new Intl.DateTimeFormat(overflowTag!)), + "Intl.NumberFormat": ctor(() => new Intl.NumberFormat(overflowTag!)), + "Intl.Collator": ctor(() => new Intl.Collator(overflowTag!)), + "Intl.getCanonicalLocales": ctor(() => Intl.getCanonicalLocales(overflowTag!)), + "String#localeCompare": ctor(() => "a".localeCompare("b", overflowTag!)), + }).toEqual({ + "Intl.Locale": "RangeError", + "Intl.Locale+options": "RangeError", + "Intl.DateTimeFormat": "RangeError", + "Intl.NumberFormat": "RangeError", + "Intl.Collator": "RangeError", + "Intl.getCanonicalLocales": "RangeError", + "String#localeCompare": "RangeError", + }); + }, + ); + + // Linux ICU 75.1 has the lowest observed capacity (23 variants); the probe must find it there so + // the RangeError assertion above never silently degrades to a skip on the platform where the bug + // was reported. + test.skipIf(!isLinux)("probe finds the capacity limit on bundled ICU", () => { + expect(overflowTag).toBeDefined(); }); - test("22 variants (under ICU capacity) still constructs", () => { - const tag = "en" + Array.from({ length: 22 }, (_, i) => "-" + String(i).padStart(5, "v") + "ab").join(""); - expect(() => new Intl.Locale(tag)).not.toThrow(); + test("valid tag with a handful of variants still constructs", () => { + expect(() => new Intl.Locale(makeTag(5))).not.toThrow(); }); });