SIGN IN SIGN UP
oven-sh / bun UNCLAIMED

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one

0 0 150 Rust

Fix CSS color fallback panic for system colors and currentColor in compound values (#31305)

### Problem

Found by fuzzing (`panic: infallible: fallback implies convertible`),
but reachable through the public API with completely valid CSS. Any
compound CSS value that mixes a color needing a browser fallback
(`lch()`, `lab()`, `oklch()`, …) with a color that cannot be converted
to a concrete colorspace (`currentColor` or a system color like
`background`, `buttonface`, `canvas`) panics the CSS minifier whenever
browser targets require color fallbacks — including the default targets
used by `bun build`:

```sh
# app.css: .foo{background:background linear-gradient(lch(8% 76 2), lch(51% 66 6))}
bun build app.css
# panic: infallible: fallback implies convertible
```

(`background` is a deprecated CSS2 system color keyword, so the
shorthand parses as color = system color + image = lch gradient.)

Original fuzz repro (minimized; panics when browser targets are
supplied):

```sh
BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING=1 bun -e 'require("bun:internal-for-testing").cssInternals.minifyTest("{background:background linear-gradient(lch(8% 76 2), lch(51%66 6)", "")'
```

Other affected inputs (with browser targets that need color fallbacks):

- `background: linear-gradient(currentColor, lch(50% 50 180))` —
inconvertible gradient stop
- `color: light-dark(buttonface, lch(50% 50 180))` — mixed
`light-dark()`
- `text-shadow: 0 0 currentColor, 0 0 lch(50% 50 180)` — shadow list
(panics with `called Option::unwrap() on a None value`)

### Cause

`CssColor::get_fallback` in `src/css/values/color.rs` assumes the
requested fallback kind implies the color is convertible
(`.expect("infallible: fallback implies convertible")`). That holds when
the kind is derived from the color itself, but for compound values (the
`background` shorthand, gradient stop lists, shadow lists, unparsed
token lists) the fallback kind is computed from the **union of all
colors in the value** and then applied to **every** color — including
`currentColor` and system colors, whose conversions return `None`.

The same assumption exists in `CssColor::get_fallbacks` (reachable via
`light-dark()` with one inconvertible side, since its possible-fallback
set is the union of both sides) and in the text-shadow fallback path in
`src/css/small_list.rs`, which used bare `.unwrap()`.

Upstream lightningcss has the same panic (`called Result::unwrap() on an
Err value` at `src/values/color.rs:441` with the same input), so there
is no upstream behavior to match here. The box-shadow handler in this
codebase already handles the case gracefully (`unwrap_or_else(||
input.color.deep_clone(arena))`), so this brings the remaining paths in
line with it.

### Fix

- `CssColor::get_fallback`: if the conversion to the requested
colorspace isn't possible, return the color unchanged. `currentColor`
and system colors are universally supported keywords and need no
fallback, so the generated fallback declaration simply keeps them while
sibling colors get converted.
- `CssColor::get_fallbacks`: skip fallbacks that cannot be computed
(mixed `light-dark()`), instead of panicking.
- `small_list::get_fallbacks_text_shadow`: same treatment box-shadow
already had — keep the original color when conversion isn't possible.

No output changes for previously working inputs; the conversions only
fail for colors that previously caused the panic.

Output for the repro above is now:

```css
.foo {
  background: background linear-gradient(#41001b, #da3671);
  background: background linear-gradient(color(display-p3 .342311 -.157987 .0918331), color(display-p3 .787212 .27046 .444387));
  background: background linear-gradient(lch(8% 76 2), lch(51% 66 6));
}
```

### Verification

- New tests in `test/js/bun/css/css.test.ts` (`edge cases > color
fallbacks with system colors and currentColor`) covering the background
shorthand with a system color, gradients with `currentColor`/system
color stops, mixed `light-dark()`, and `text-shadow` lists, with both
RGB (`chrome 95`) and P3 (`safari 14`) fallback targets.
- Without the fix these tests crash with the fuzz signature; with the
fix `bun bd test test/js/bun/css/css.test.ts` → 1071 pass, 0 fail.
- `color.test.ts`, `doesnt_crash.test.ts`, `css-system-color-*`
regression tests, and the other CSS test files pass with the debug
build.
- The original fuzz repro and `bun build` on the CSS above no longer
panic.
R
robobun committed
8dcbb44fe60e75334e41cfcffe22250b09ba8847
Parent: 5bf4941
Committed by GitHub <noreply@github.com> on 5/24/2026, 4:07:13 AM