Fix crash due to invalid characters in candidate (#19829)

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
This commit is contained in:
Robin Malfait 2026-03-20 14:51:37 +01:00 • committed by GitHub
parent 7482d47a54
commit bd30a716e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 38 additions and 3 deletions

View file

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

View file

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

View file

@ -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<63>54-4725-4235-9d22-2d0ed572e924"`,
)
})
})

View file

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