//! Fuzz target for the regex engine (`src/filter/regex.zig`, milestone-21 //! ruling 10). //! //! The contract: any byte string is a legal pattern, so `compile` may reject it //! however it likes but must return — never panic, never loop forever, never //! read out of bounds. Where it returns a program the target then checks what //! the filter is entitled to rely on: //! //! - the program obeys ruling 5's limits: it is non-empty, at most //! `max_program_len` instructions, and it came from a pattern of at most //! `max_pattern_len` bytes; //! - `matches` terminates on any input, and the VM's step count never exceeds //! program length × (input length + 1), which is the linearity claim the //! whole design rests on; //! - compile-then-match is deterministic: the same pattern compiled twice //! gives the same program length and the same verdict on the same input, //! and two runs of one program agree step for step. //! //! `regex.zig` imports only `std`, so this target's module roots directly at //! that file — no aggregator needed. //! //! Runner semantics: under a plain `zig build test` the target runs once per //! corpus entry plus once on empty input, which makes the corpus a regression //! suite. `zig build test --fuzz=` gives it `n` generated inputs. const std = @import("std"); const regex = @import("regex"); const smith_encode = @import("smith_encode.zig"); const sliceInput = smith_encode.sliceInput; const pairInput = smith_encode.pairInput; const Smith = std.testing.Smith; /// Twice `regex.max_pattern_len`, so `error.PatternTooLong` is reachable rather /// than the only thing the target ever sees. const max_pattern = 2 * regex.max_pattern_len; /// Past the 253 bytes of the longest text name, which is the longest input the /// filter ever hands the engine. const max_name = 512; /// `Smith` entity ids: the pattern and the name it is matched against. const pattern_hash: u32 = 1; const name_hash: u32 = 2; const fuzz_options: std.testing.FuzzInputOptions = .{ .corpus = &corpus }; test "fuzz regex.compile and regex.matches" { try std.testing.fuzz({}, regexTarget, fuzz_options); } fn regexTarget(_: void, smith: *Smith) anyerror!void { var pattern_buf: [max_pattern]u8 = undefined; var name_buf: [max_name]u8 = undefined; const pattern = pattern_buf[0..smith.sliceWithHash(&pattern_buf, pattern_hash)]; const input = name_buf[0..smith.sliceWithHash(&name_buf, name_hash)]; var prog = (try compileOnce(pattern)) orelse return; defer prog.deinit(std.testing.allocator); // Ruling 5's limits, read off the program the compiler agreed to build. try std.testing.expect(pattern.len <= regex.max_pattern_len); try std.testing.expect(prog.insts.len > 0); try std.testing.expect(prog.insts.len <= regex.max_program_len); const first = regex.run(&prog, input); try expectLinear(first, prog.insts.len, input.len); try std.testing.expectEqual(first.matched, regex.matches(&prog, input)); // The empty name is the cheapest way to reach the position-zero closure with // no consuming step behind it, so every pattern is run against it too. try expectLinear(regex.run(&prog, ""), prog.insts.len, 0); const again = regex.run(&prog, input); try std.testing.expectEqual(first.matched, again.matched); try std.testing.expectEqual(first.steps, again.steps); // Compiling is a pure function of the pattern bytes: the second program // matches the first instruction for instruction and answers the same. var second = (try compileOnce(pattern)) orelse return error.TestSecondCompileFailed; defer second.deinit(std.testing.allocator); try std.testing.expectEqual(prog.insts.len, second.insts.len); try std.testing.expectEqual(prog.classes.len, second.classes.len); try std.testing.expectEqual(first.matched, regex.matches(&second, input)); } /// One compile, or null when the engine rejected the pattern. Every member of /// `regex.Error` is a legitimate rejection: unparsable syntax, a pattern past /// the byte limit, a program past the instruction limit, and an allocator that /// ran out. fn compileOnce(pattern: []const u8) anyerror!?regex.Program { return regex.compile(std.testing.allocator, pattern) catch |err| switch (err) { error.OutOfMemory, error.BadPattern, error.PatternTooLong, error.PatternTooComplex, => null, }; } fn expectLinear(result: regex.Run, program_len: usize, input_len: usize) !void { try std.testing.expect(result.steps <= program_len * (input_len + 1)); } // --------------------------------------------------------------------------- // corpus // --------------------------------------------------------------------------- // // `Smith` does not consume a corpus entry as raw input, so every entry below // goes through the `smith_encode.zig` encoders. An entry that carries only the // pattern leaves the name empty, which is the position-zero closure on its own. /// The backtracker killers: exponential for a backtracking engine, linear here. const nested_plus = "(a+)+b"; const nested_alternation = "^(a|aa)+$"; const nested_star = "^(a*)*(b*)*$"; /// An epsilon cycle: the loop body consumes nothing, so only the VM's /// one-admission-per-position rule ends the walk. const empty_loop = "^((a*)*)*$"; /// A name long enough that a quadratic step count would show against the bound. const long_name = "a" ** 252 ++ "X"; /// Emits nothing at all, so only the emitter's visit budget ends the compile. const empty_body_blowup = "((((x{0}){900}){900}){900}){900}"; const corpus = [_][]const u8{ pairInput(nested_plus, "a" ** 20 ++ "X"), pairInput(nested_alternation, long_name), pairInput(nested_star, long_name), pairInput(empty_loop, long_name), pairInput("^ad[0-9]+-", "ad42-serve.example.com"), pairInput("^(ads|track)\\.example\\.(com|net)$", "track.example.net"), pairInput("[^.]+\\.doubleclick\\.net$", "static.doubleclick.net"), pairInput("^\\w{1,8}\\.\\d{2}\\.example$", "ads_42.13.example"), // Every rejection path, so the corpus replays them rather than waiting on a // discovery: bad syntax, an over-long pattern, an over-large program, and a // compile that only the visit budget stops. sliceInput("(a"), sliceInput("[z-a]"), sliceInput("a{2,1}"), sliceInput("ads\\"), sliceInput("(?:ab)"), sliceInput("a+?"), sliceInput("a" ** (regex.max_pattern_len + 1)), sliceInput("(abcd){400}"), sliceInput(empty_body_blowup), };