diff --git a/src/css/values/color.rs b/src/css/values/color.rs index d87fe6982446..c714d993f54b 100644 --- a/src/css/values/color.rs +++ b/src/css/values/color.rs @@ -2459,6 +2459,13 @@ pub fn parse_color_mix(input: &mut css::Parser) -> CssResult { }; // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm + // The grammar is ; values outside that range are invalid. + if first_percent.is_some_and(|p| !(0.0..=1.0).contains(&p)) + || second_percent.is_some_and(|p| !(0.0..=1.0).contains(&p)) + { + return Err(input.new_custom_error(css::ParserError::invalid_value)); + } + let (p1, p2): (f32, f32) = if first_percent.is_none() && second_percent.is_none() { (0.5, 0.5) } else { diff --git a/test/js/bun/css/color.test.ts b/test/js/bun/css/color.test.ts index bafcf5863505..d523d86404f8 100644 --- a/test/js/bun/css/color.test.ts +++ b/test/js/bun/css/color.test.ts @@ -536,3 +536,49 @@ describe("input forms", () => { expect(color("#f00", "[rgb]")).toEqual([255, 0, 0]); }); }); + +// https://drafts.csswg.org/css-color-5/#color-mix — the grammar is +// , so a value outside that range is a parse error. +describe("color-mix() percentage range", () => { + // fuzz repro: -9% drove HSL saturation negative and tripped a debug assertion + // in hsl_to_rgb; release builds produced out-of-gamut garbage. + test("does not crash on a negative mix percentage", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `process.stdout.write(String(Bun.color("color-mix(in hsl,red -9%,color(display-p3 0 0 0)", "lab")))`, + ], + env: bunEnv, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, exitCode, stderr: exitCode === 0 ? undefined : stderr }).toEqual({ stdout: "null", exitCode: 0 }); + }); + + test.each([ + "color-mix(in hsl, red -9%, blue)", + "color-mix(in hsl, red 150%, blue)", + "color-mix(in hsl, -10% red, blue)", + "color-mix(in hsl, red, blue -9%)", + "color-mix(in hsl, red, blue 150%)", + "color-mix(in hsl, red, 150% blue)", + "color-mix(in srgb, red -1%, blue)", + "color-mix(in srgb, red 100.001%, blue)", + "color-mix(in lab, red -9%, blue)", + "color-mix(in hwb, red -9%, blue)", + "color-mix(in oklch, red 150%, blue)", + ])("rejects %s", input => { + expect(color(input, "css")).toBeNull(); + }); + + test.each([ + ["color-mix(in hsl, red 0%, blue)", "#00f"], + ["color-mix(in hsl, red 100%, blue)", "red"], + ["color-mix(in hsl, red 50%, blue 50%)", "#f0f"], + ["color-mix(in hsl, red, blue 0%)", "red"], + ["color-mix(in hsl, red, blue 100%)", "#00f"], + ])("accepts %s", (input, expected) => { + expect(color(input, "css")).toBe(expected); + }); +});