mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
255 lines
8.0 KiB
TypeScript
255 lines
8.0 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { isCI, isDebug } from "harness";
|
|
|
|
interface InvalidFuzzOptions {
|
|
maxLength: number;
|
|
strategy: "syntax" | "structure" | "encoding" | "memory" | "all";
|
|
iterations: number;
|
|
}
|
|
|
|
const shutup = process.env.CSS_FUZZ_SHUTUP === "1";
|
|
const log = shutup ? () => {} : console.log;
|
|
|
|
// Collection of invalid CSS generation strategies
|
|
const invalidGenerators = {
|
|
// Syntax errors
|
|
syntax: {
|
|
unclosedRules: () => `
|
|
.test { color: red
|
|
.another { padding: 10px }`,
|
|
invalidSelectors: () => [
|
|
"}{color:red}",
|
|
"&*#@.class{color:red}",
|
|
"..double.dot{color:red}",
|
|
".{color:red}",
|
|
"#{color:red}",
|
|
],
|
|
malformedProperties: () => [
|
|
".test{color:}",
|
|
".test{:red}",
|
|
".test{color::red}",
|
|
".test{;color:red}",
|
|
".test{color:red;;;}",
|
|
],
|
|
unclosedComments: () => [
|
|
"/* unclosed comment .test{color:red}",
|
|
".test{color:red} /* unclosed",
|
|
"/**//**//* .test{color:red}",
|
|
],
|
|
} as const,
|
|
|
|
// Structural errors
|
|
structure: {
|
|
nestedRules: () => [
|
|
".outer { .inner { color: red } }", // Invalid nesting without @rules
|
|
"@media screen { @media print { } ", // Unclosed nested at-rule
|
|
"@keyframes { @keyframes { } }", // Invalid nesting of @keyframes
|
|
],
|
|
malformedAtRules: () => ["@media ;", "@import url('test.css'", "@{color:red}", "@media screen and and {color:red}"],
|
|
invalidImports: () => ["@import 'file' 'screen';", "@import url(;", "@import url('test.css') print"],
|
|
} as const,
|
|
|
|
// Encoding and character issues
|
|
encoding: {
|
|
invalidUTF8: () => [
|
|
`.test{content:"${Buffer.from([0xc0, 0x80]).toString()}"}`,
|
|
`.test{content:"${Buffer.from([0xe0, 0x80, 0x80]).toString()}"}`,
|
|
`.test{content:"${Buffer.from([0xf0, 0x80, 0x80, 0x80]).toString()}"}`,
|
|
],
|
|
nullBytes: () => [`.test{color:red${"\0"};}`, `.te${"\0"}st{color:red}`, `${"\0"}.test{color:red}`],
|
|
controlCharacters: () => {
|
|
const controls = Array.from({ length: 32 }, (_, i) => String.fromCharCode(i));
|
|
return controls.map(char => `.test{color:${char}red}`);
|
|
},
|
|
} as const,
|
|
|
|
// Memory and resource stress
|
|
memory: {
|
|
deepNesting: (depth: number = 300) => {
|
|
let css = "";
|
|
for (let i = 0; i < depth; i++) {
|
|
css += "@media screen {";
|
|
}
|
|
css += ".test{color:red}";
|
|
for (let i = 0; i < depth; i++) {
|
|
css += "}";
|
|
}
|
|
return css;
|
|
},
|
|
longSelectors: (length: number = 100000) => {
|
|
const selector = ".test".repeat(length);
|
|
return `${selector}{color:red}`;
|
|
},
|
|
manyProperties: (count: number = 10000) => {
|
|
const properties = Array(count).fill("color:red;").join("\n");
|
|
return `.test{${properties}}`;
|
|
},
|
|
} as const,
|
|
} as const;
|
|
|
|
// Helper to randomly corrupt CSS
|
|
function corruptCSS(css: string): string {
|
|
const corruptions = [
|
|
(s: string) => (s + "").replace(/{/g, "}"),
|
|
(s: string) => (s + "").replace(/}/g, "{"),
|
|
(s: string) => (s + "").replace(/:/g, ";"),
|
|
(s: string) => (s + "").replace(/;/g, ":"),
|
|
(s: string) => (s + "").slice(Math.floor(Math.random() * (s + "").length)),
|
|
(s: string) => s + "" + "}}".repeat(Math.floor(Math.random() * 5)),
|
|
(s: string) => (s + "").split("").reverse().join(""),
|
|
(s: string) => (s + "").replace(/[a-z]/g, c => String.fromCharCode(97 + Math.floor(Math.random() * 26))),
|
|
];
|
|
|
|
const numCorruptions = Math.floor(Math.random() * 3) + 1;
|
|
let corrupted = css;
|
|
|
|
for (let i = 0; i < numCorruptions; i++) {
|
|
const corruption = corruptions[Math.floor(Math.random() * corruptions.length)];
|
|
corrupted = corruption(corrupted);
|
|
}
|
|
|
|
return corrupted;
|
|
}
|
|
|
|
// TODO:
|
|
if (!isCI) {
|
|
// Main fuzzing test suite for invalid inputs
|
|
test.each(
|
|
[["syntax", 1000], ["structure", 1000], ["encoding", 500], !isDebug ? ["memory", 100] : []].filter(
|
|
xs => xs.length > 0,
|
|
),
|
|
)(
|
|
"CSS Parser Invalid Input Fuzzing - %s (%d iterations)",
|
|
async (strategy, iterations) => {
|
|
const options: InvalidFuzzOptions = {
|
|
maxLength: 10000,
|
|
strategy: strategy as any,
|
|
iterations,
|
|
};
|
|
|
|
let crashCount = 0;
|
|
let errorCount = 0;
|
|
const startTime = performance.now();
|
|
|
|
for (let i = 0; i < options.iterations; i++) {
|
|
let invalidCSS = "";
|
|
|
|
switch (strategy) {
|
|
case "syntax":
|
|
invalidCSS =
|
|
invalidGenerators.syntax[
|
|
Object.keys(invalidGenerators.syntax)[
|
|
Math.floor(Math.random() * Object.keys(invalidGenerators.syntax).length)
|
|
]
|
|
]()[Math.floor(Math.random() * 5)];
|
|
break;
|
|
|
|
case "structure":
|
|
invalidCSS =
|
|
invalidGenerators.structure[
|
|
Object.keys(invalidGenerators.structure)[
|
|
Math.floor(Math.random() * Object.keys(invalidGenerators.structure).length)
|
|
]
|
|
]()[Math.floor(Math.random() * 3)];
|
|
break;
|
|
|
|
case "encoding":
|
|
invalidCSS =
|
|
invalidGenerators.encoding[
|
|
Object.keys(invalidGenerators.encoding)[
|
|
Math.floor(Math.random() * Object.keys(invalidGenerators.encoding).length)
|
|
]
|
|
]()[0];
|
|
break;
|
|
|
|
case "memory":
|
|
const memoryFuncs = Object.keys(invalidGenerators.memory);
|
|
const selectedFunc = memoryFuncs[Math.floor(Math.random() * memoryFuncs.length)];
|
|
invalidCSS = invalidGenerators.memory[selectedFunc]();
|
|
break;
|
|
}
|
|
|
|
// Further corrupt the CSS randomly
|
|
if (Math.random() < 0.3) {
|
|
invalidCSS = corruptCSS(invalidCSS);
|
|
}
|
|
|
|
log("--- CSS Fuzz ---");
|
|
invalidCSS = invalidCSS + "";
|
|
log(JSON.stringify(invalidCSS, null, 2));
|
|
await Bun.write("invalid.css", invalidCSS);
|
|
|
|
try {
|
|
const result = await Bun.build({
|
|
entrypoints: ["invalid.css"],
|
|
});
|
|
|
|
// We expect the parser to either throw an error or return a valid result
|
|
// If it returns undefined/null, that's a potential issue
|
|
if (result === undefined || result === null) {
|
|
crashCount++;
|
|
console.error(`Parser returned ${result} for input:\n${invalidCSS.slice(0, 100)}...`);
|
|
}
|
|
} catch (error) {
|
|
// Expected behavior for invalid CSS
|
|
errorCount++;
|
|
|
|
// Check for specific error types we want to track
|
|
if (error instanceof RangeError || error instanceof TypeError) {
|
|
console.warn(`Unexpected error type: ${error.constructor.name} for input:\n${invalidCSS.slice(0, 100)}...`);
|
|
}
|
|
}
|
|
|
|
// Memory check every 100 iterations
|
|
if (i % 100 === 0) {
|
|
const heapUsed = process.memoryUsage().heapUsed / 1024 / 1024;
|
|
expect(heapUsed).toBeLessThan(500); // Alert if memory usage exceeds 500MB
|
|
}
|
|
}
|
|
|
|
const endTime = performance.now();
|
|
const duration = endTime - startTime;
|
|
|
|
console.log(`
|
|
Strategy: ${strategy}
|
|
Total iterations: ${iterations}
|
|
Crashes: ${crashCount}
|
|
Expected errors: ${errorCount}
|
|
Duration: ${duration.toFixed(2)}ms
|
|
Average time per test: ${(duration / iterations).toFixed(2)}ms
|
|
`);
|
|
|
|
// We expect some errors for invalid input, but no crashes
|
|
expect(crashCount).toBe(0);
|
|
expect(errorCount).toBeGreaterThan(0);
|
|
},
|
|
10 * 1000,
|
|
);
|
|
|
|
// Additional test for mixed valid/invalid input
|
|
test("CSS Parser Mixed Input Fuzzing", async () => {
|
|
const validCSS = ".test{color:red}";
|
|
|
|
for (let i = 0; i < 100; i++) {
|
|
const mixedCSS = `
|
|
${validCSS}
|
|
${corruptCSS(validCSS)}
|
|
${validCSS}
|
|
`;
|
|
|
|
console.log("--- Mixed CSS ---");
|
|
console.log(JSON.stringify(mixedCSS, null, 2));
|
|
await Bun.write("invalid.css", mixedCSS);
|
|
|
|
try {
|
|
await Bun.build({
|
|
entrypoints: ["invalid.css"],
|
|
});
|
|
} catch (error) {
|
|
// Expected to throw, but shouldn't crash
|
|
expect(error).toBeDefined();
|
|
}
|
|
}
|
|
});
|
|
}
|