From bd30a716e640550748f7029506f3758e9da4aecb Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Fri, 20 Mar 2026 14:51:37 +0100 Subject: [PATCH] Fix crash due to invalid characters in candidate (#19829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes an issue where the compiler can crash if it encounters an invalid codepoint. When we extract potential candidates from files, it could be that we encounter values that look like a class or a CSS variable, if it turns out that it's an invalid CSS variable we can ignore it. The problem is that sometimes there are escaped values in there that result in invalid code points crashing the compiler. This PR fixes that by gracefully handling that and making sure that invalid code points are replaced by `\uFFFD` as per the spec. The bug report (https://github.com/tailwindlabs/tailwindcss/issues/19786) has a clean example where a piece of text looks like a CSS variable, but contains invalid code points. ``` --Coding-Projects-CharacterMapper-Master-Workspace\d8819554-4725-4235-9d22-2d0ed572e924 ``` Luckily we can fix this today by ignoring the file paths that contain these strings using `@source not "…";`, but the better way is to actually fix this. To solve this, instead of blindly passing numbers to `String.fromCodePoint`, we will first validate whether it's a valid codepoint: 1. `0x0000` — `0x10FFFF` (inclusive) is the range of valid code points. See: https://infra.spec.whatwg.org/#code-point 2. `0xD800` — `0xDBFF` (inclusive) are leading surrogates. See: https://infra.spec.whatwg.org/#leading-surrogate 3. `0xDC00` — `0xDFFF` (inclusive) are trailing surrogates. See: https://infra.spec.whatwg.org/#trailing-surrogate In the code we use the `0xD800` — `0xDFFF` range because the ranges overlap. There are various references in the spec to replace surrogates (and invalid codepoints) with `\uFFFD`. Here is one of them: https://drafts.csswg.org/css-syntax-3/#consume-escaped-code-point Fixes: https://github.com/tailwindlabs/tailwindcss/issues/19786 Fixes: #19801 (this issue talks about a similar invalid code point issue) ## Test plan 1. Added a regression test where the above string was used as a CSS variable 2. Added a regression test for the unescape functionality to make sure that invalid code points and surrogates are replaced by the `\uFFFD` replacement character. [ci-all] Just to verify on Windows as well --- CHANGELOG.md | 1 + packages/tailwindcss/src/index.test.ts | 8 +++++++ packages/tailwindcss/src/utils/escape.test.ts | 10 +++++++++ packages/tailwindcss/src/utils/escape.ts | 22 ++++++++++++++++--- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b639aed9..39e89832b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Improve canonicalizations for `tracking-*` utilities ([#19827](https://github.com/tailwindlabs/tailwindcss/pull/19827)) +- Fix crash due to invalid characters in candidate ([#19829](https://github.com/tailwindlabs/tailwindcss/pull/19829)) ## [4.2.2] - 2026-03-18 diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 5eb7407d8..bf1c72c42 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -1501,6 +1501,14 @@ describe('Parsing theme values from CSS', () => { `) }) + // https://github.com/tailwindlabs/tailwindcss/issues/19786 + test('out-of-range escaped CSS variable candidates do not crash the build', async () => { + // Shouldn't crash + await run([ + String.raw`--Coding-Projects-CharacterMapper-Master-Workspace\d8819554-4725-4235-9d22-2d0ed572e924`, + ]) + }) + test('`@keyframes` in `@theme` are hoisted', async () => { expect( await compileCss( diff --git a/packages/tailwindcss/src/utils/escape.test.ts b/packages/tailwindcss/src/utils/escape.test.ts index ff7715b9d..f4c8e1192 100644 --- a/packages/tailwindcss/src/utils/escape.test.ts +++ b/packages/tailwindcss/src/utils/escape.test.ts @@ -11,4 +11,14 @@ describe('unescape', () => { test('removes backslashes', () => { expect(unescape(String.raw`red-1\/2`)).toMatchInlineSnapshot(`"red-1/2"`) }) + + test('replaces out-of-range escaped code points', () => { + expect( + unescape( + String.raw`--Coding-Projects-CharacterMapper-Master-Workspace\d8819554-4725-4235-9d22-2d0ed572e924`, + ), + ).toMatchInlineSnapshot( + `"--Coding-Projects-CharacterMapper-Master-Workspace�54-4725-4235-9d22-2d0ed572e924"`, + ) + }) }) diff --git a/packages/tailwindcss/src/utils/escape.ts b/packages/tailwindcss/src/utils/escape.ts index cbcfa62a0..c430e4ad4 100644 --- a/packages/tailwindcss/src/utils/escape.ts +++ b/packages/tailwindcss/src/utils/escape.ts @@ -74,8 +74,24 @@ export function escape(value: string) { export function unescape(escaped: string) { return escaped.replace(/\\([\dA-Fa-f]{1,6}[\t\n\f\r ]?|[\S\s])/g, (match) => { - return match.length > 2 - ? String.fromCodePoint(Number.parseInt(match.slice(1).trim(), 16)) - : match[1] + if (match.length <= 2) { + return match[1] + } + + let codePoint = Number.parseInt(match.slice(1).trim(), 16) + + if ( + // Invalid codepoint: https://infra.spec.whatwg.org/#code-point + codePoint === 0x0000 || + codePoint > 0x10ffff || + // Is surrogate: https://infra.spec.whatwg.org/#leading-surrogate + // - A leading surrogate is a code point that is in the range U+D800 to U+DBFF, inclusive. + // - A trailing surrogate is a code point that is in the range U+DC00 to U+DFFF, inclusive. + (codePoint >= 0xd800 && codePoint <= 0xdfff) + ) { + return '\uFFFD' + } + + return String.fromCodePoint(codePoint) }) }