Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions projects/postcss/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
################################################################################

FROM gcr.io/oss-fuzz-base/base-builder-javascript

COPY build.sh $SRC/

# PostCSS's default branch is `main`. Pin it explicitly so the integration
# does not silently break if the default ever changes.
RUN git clone --depth 1 -b main https://github.com/postcss/postcss

# postcss-parser-tests is the upstream-maintained collection of CSS test
# cases. We use its `cases/` directory as the seed corpus so the fuzzer
# starts mutating from real, parser-shaped inputs.
RUN git clone --depth 1 -b main https://github.com/postcss/postcss-parser-tests

COPY fuzz_parse.js fuzz_parse.dict $SRC/postcss/

WORKDIR $SRC/postcss
36 changes: 36 additions & 0 deletions projects/postcss/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/bin/bash -eu
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
################################################################################

# Install runtime dependencies only. PostCSS's devDependencies pull in tools
# that have peer-dep conflicts and are unrelated to the library's runtime
# behavior, so we skip them.
npm install --omit=dev --ignore-scripts --legacy-peer-deps
npm install --save-dev --legacy-peer-deps @jazzer.js/core

# Build a seed corpus from the upstream postcss-parser-tests CSS cases so
# the fuzzer starts mutating from realistic, parser-shaped inputs rather
# than from empty bytes.
mkdir -p "$WORK/seed_corpus"
cp "$SRC"/postcss-parser-tests/cases/*.css "$WORK/seed_corpus/"
(cd "$WORK/seed_corpus" && zip -q -r "$OUT/fuzz_parse_seed_corpus.zip" .)

# Ship the CSS dictionary alongside the fuzzer so libFuzzer can splice in
# common CSS tokens during mutation.
cp "$SRC/postcss/fuzz_parse.dict" "$OUT/fuzz_parse.dict"

# Build Fuzzers.
compile_javascript_fuzzer postcss fuzz_parse.js -i postcss --sync
101 changes: 101 additions & 0 deletions projects/postcss/fuzz_parse.dict
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Dictionary of common CSS tokens for libFuzzer.
# Reference: https://www.w3.org/TR/css-syntax-3/

# At-rules
"@charset "
"@import "
"@media "
"@supports "
"@font-face "
"@keyframes "
"@page "
"@namespace "
"@document "
"@layer "
"@container "
"@property "
"@scope "
"@counter-style "
"@font-feature-values "

# Structural punctuation
"{"
"}"
";"
":"
","
"("
")"
"["
"]"
"/*"
"*/"
"!important"
"--"

# Selectors
"*"
">"
"+"
"~"
"::"
"&"

# Common pseudo-classes / pseudo-elements
":hover"
":focus"
":active"
":root"
":not("
":is("
":where("
":has("
"::before"
"::after"

# Common properties
"color:"
"background:"
"background-color:"
"width:"
"height:"
"margin:"
"padding:"
"border:"
"display:"
"position:"
"font-size:"
"font-family:"
"transform:"
"transition:"
"animation:"
"grid-template-columns:"
"flex:"

# Values / units
"px"
"em"
"rem"
"%"
"vh"
"vw"
"deg"
"rgb("
"rgba("
"hsl("
"hsla("
"calc("
"var("
"url("
"linear-gradient("
"none"
"auto"
"inherit"
"initial"
"unset"
"revert"

# Strings / escapes
"\""
"'"
"\\"
123 changes: 123 additions & 0 deletions projects/postcss/fuzz_parse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

const { FuzzedDataProvider } = require('@jazzer.js/core');
const postcss = require('./lib/postcss');

module.exports.fuzz = function (data) {
const provider = new FuzzedDataProvider(data);

// The CSS input itself is randomized: every byte the fuzzer produces (or
// mutates from the seed corpus) flows directly into `cssString` via
// consumeRemainingAsString(). The option flags below are read from the
// *back* of the buffer (jazzer.js consumes integrals/booleans from the
// tail), so seed CSS files from postcss-parser-tests are fed into the
// parser nearly verbatim, with only their last few bytes nibbled off as
// option control.
const useMap = provider.consumeBoolean();
const useFrom = provider.consumeBoolean();
const useProcessor = provider.consumeBoolean();
const splitMode = provider.consumeIntegralInRange(0, 2);
const cssString = provider.consumeRemainingAsString();

const parseOptions = {};
if (useFrom) parseOptions.from = 'fuzz.css';
if (useMap) parseOptions.map = { inline: false, annotation: false };

let root;
try {
root = postcss.parse(cssString, parseOptions);
} catch (e) {
if (e instanceof postcss.CssSyntaxError) return;
throw e;
}

// Walk the AST and exercise common node accessors. This also stresses
// raws/source bookkeeping for any node returned by the parser.
try {
root.walk(node => {
void node.type;
void node.toString();
if (typeof node.error === 'function') {
// Generating an error message touches input/source-map machinery.
node.error('fuzz').message;
}
});
} catch (e) {
if (!isExpected(e, postcss)) throw e;
}

// Round-trip via stringify and re-parse. Output should itself be parseable.
let serialized;
try {
serialized = root.toString();
} catch (e) {
if (!isExpected(e, postcss)) throw e;
return;
}

try {
postcss.parse(serialized);
} catch (e) {
if (!(e instanceof postcss.CssSyntaxError)) throw e;
}

// Exercise the JSON serialization round-trip.
try {
const json = root.toJSON();
postcss.fromJSON(json);
} catch (e) {
if (!isExpected(e, postcss)) throw e;
}

// Exercise the main public entry point: postcss().process(). This drives
// the LazyResult / NoWorkResult pipeline that real plugin chains use.
if (useProcessor) {
try {
const result = postcss().process(cssString, parseOptions);
void result.css;
void result.warnings();
} catch (e) {
if (!isExpected(e, postcss)) throw e;
}
}

// Exercise the list helpers, which have their own quoting/escape logic.
try {
if (splitMode === 0) {
postcss.list.comma(cssString);
} else if (splitMode === 1) {
postcss.list.space(cssString);
} else {
postcss.list.split(cssString, [',', ' '], false);
}
} catch (e) {
if (!isExpected(e, postcss)) throw e;
}
};

function isExpected(error, postcss) {
if (error instanceof postcss.CssSyntaxError) return true;
if (!error || typeof error.message !== 'string') return false;
// Some legitimate inputs reach known-shaped TypeErrors during stringify or
// walk because the CSS allows constructs whose textual form is ambiguous.
// Suppress only those well-defined cases so real bugs still surface.
const benign = [
'Unknown node type',
'Unknown word',
];
return benign.some(msg => error.message.indexOf(msg) !== -1);
}
8 changes: 8 additions & 0 deletions projects/postcss/project.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
homepage: https://postcss.org/
language: javascript
primary_contact: "andrey@sitnik.es"
main_repo: https://github.com/postcss/postcss
fuzzing_engines:
- libfuzzer
sanitizers:
- none