From 38f0844e3533069fd9c9a209c57b19979313bf9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Anic=CC=81?= Date: Thu, 8 Feb 2024 15:31:35 +0100 Subject: [PATCH] add fuzzing tests Using fuzzing tests from zig-std-lib-fuzzing project: https://github.com/squeek502/zig-std-lib-fuzzing/tree/master/inputs/deflate Pointed by squeek502 in issues #1, #2, #3, #4. Fixes #3, #4 --- src/bit_reader.zig | 30 +++--- src/inflate.zig | 100 ++++++++++++++---- src/testdata/fuzzing/deflate-stream | 3 + .../fuzzing/empty-distance-alphabet01 | Bin 0 -> 12 bytes .../fuzzing/empty-distance-alphabet02 | Bin 0 -> 13 bytes src/testdata/fuzzing/end-of-stream | 1 + src/testdata/fuzzing/invalid-distance | Bin 0 -> 6 bytes src/testdata/fuzzing/invalid-tree01 | 1 + src/testdata/fuzzing/invalid-tree02 | Bin 0 -> 14 bytes src/testdata/fuzzing/invalid-tree03 | Bin 0 -> 12 bytes src/testdata/fuzzing/lengths-overflow | 1 + src/testdata/fuzzing/out-of-codes | Bin 0 -> 9 bytes src/testdata/fuzzing/puff01 | Bin 0 -> 5 bytes src/testdata/fuzzing/puff02 | Bin 0 -> 5 bytes src/testdata/fuzzing/puff03 | Bin 0 -> 6 bytes src/testdata/fuzzing/puff04 | 1 + src/testdata/fuzzing/puff05 | 1 + src/testdata/fuzzing/puff06 | 1 + src/testdata/fuzzing/puff07 | Bin 0 -> 14 bytes src/testdata/fuzzing/puff08 | Bin 0 -> 14 bytes src/testdata/fuzzing/puff09 | Bin 0 -> 3 bytes src/testdata/fuzzing/puff10 | 1 + src/testdata/fuzzing/puff11 | Bin 0 -> 12 bytes src/testdata/fuzzing/puff12 | Bin 0 -> 3 bytes src/testdata/fuzzing/puff13 | Bin 0 -> 4 bytes src/testdata/fuzzing/puff14 | Bin 0 -> 4 bytes src/testdata/fuzzing/puff15 | 1 + src/testdata/fuzzing/puff16 | Bin 0 -> 6 bytes src/testdata/fuzzing/puff17 | Bin 0 -> 6 bytes 29 files changed, 106 insertions(+), 35 deletions(-) create mode 100644 src/testdata/fuzzing/deflate-stream create mode 100644 src/testdata/fuzzing/empty-distance-alphabet01 create mode 100644 src/testdata/fuzzing/empty-distance-alphabet02 create mode 100644 src/testdata/fuzzing/end-of-stream create mode 100644 src/testdata/fuzzing/invalid-distance create mode 100644 src/testdata/fuzzing/invalid-tree01 create mode 100644 src/testdata/fuzzing/invalid-tree02 create mode 100644 src/testdata/fuzzing/invalid-tree03 create mode 100644 src/testdata/fuzzing/lengths-overflow create mode 100644 src/testdata/fuzzing/out-of-codes create mode 100644 src/testdata/fuzzing/puff01 create mode 100644 src/testdata/fuzzing/puff02 create mode 100644 src/testdata/fuzzing/puff03 create mode 100644 src/testdata/fuzzing/puff04 create mode 100644 src/testdata/fuzzing/puff05 create mode 100644 src/testdata/fuzzing/puff06 create mode 100644 src/testdata/fuzzing/puff07 create mode 100644 src/testdata/fuzzing/puff08 create mode 100644 src/testdata/fuzzing/puff09 create mode 100644 src/testdata/fuzzing/puff10 create mode 100644 src/testdata/fuzzing/puff11 create mode 100644 src/testdata/fuzzing/puff12 create mode 100644 src/testdata/fuzzing/puff13 create mode 100644 src/testdata/fuzzing/puff14 create mode 100644 src/testdata/fuzzing/puff15 create mode 100644 src/testdata/fuzzing/puff16 create mode 100644 src/testdata/fuzzing/puff17 diff --git a/src/bit_reader.zig b/src/bit_reader.zig index 8a7a6d8..59b3387 100644 --- a/src/bit_reader.zig +++ b/src/bit_reader.zig @@ -103,7 +103,7 @@ pub fn BitReader(comptime ReaderType: type) type { 0 => { // `normal` read try self.fill(n); // ensure that there are n bits in the buffer const u: U = @truncate(self.bits); // get n bits - self.shift(n); // advance buffer for n + try self.shift(n); // advance buffer for n return u; }, (flag.peek) => { // no shift, leave bits in the buffer @@ -112,13 +112,13 @@ pub fn BitReader(comptime ReaderType: type) type { }, flag.buffered => { // no fill, assume that buffer has enought bits const u: U = @truncate(self.bits); - self.shift(n); + try self.shift(n); return u; }, (flag.reverse) => { // same as 0 with bit reverse try self.fill(n); const u: U = @truncate(self.bits); - self.shift(n); + try self.shift(n); return @bitReverse(u); }, (flag.peek | flag.reverse) => { @@ -127,7 +127,7 @@ pub fn BitReader(comptime ReaderType: type) type { }, (flag.buffered | flag.reverse) => { const u: U = @truncate(self.bits); - self.shift(n); + try self.shift(n); return @bitReverse(u); }, (flag.peek | flag.buffered) => { @@ -151,13 +151,13 @@ pub fn BitReader(comptime ReaderType: type) type { } const mask: u16 = (@as(u16, 1) << n) - 1; const u: u16 = @as(u16, @truncate(self.bits)) & mask; - self.shift(n); + try self.shift(n); return u; } // Advance buffer for n bits. - pub inline fn shift(self: *Self, n: u6) void { - assert(n <= self.nbits); + pub inline fn shift(self: *Self, n: u6) !void { + if (n > self.nbits) return error.EndOfStream; self.bits >>= n; self.nbits -= n; } @@ -166,7 +166,7 @@ pub fn BitReader(comptime ReaderType: type) type { pub inline fn skipBytes(self: *Self, n: u16) !void { for (0..n) |_| { try self.fill(8); - self.shift(8); + try self.shift(8); } } @@ -178,7 +178,7 @@ pub fn BitReader(comptime ReaderType: type) type { // Align stream to the byte boundary. pub inline fn alignToByte(self: *Self) void { const ab = self.alignBits(); - if (ab > 0) self.shift(ab); + if (ab > 0) self.shift(ab) catch unreachable; } // Skip zero terminated string. @@ -232,7 +232,7 @@ test "BitReader" { try testing.expect(try br.readF(u8, F.peek) == 0b0001_1110); try testing.expect(try br.readF(u9, F.peek) == 0b1_0001_1110); - br.shift(9); + try br.shift(9); try testing.expectEqual(@as(u8, 36), br.nbits); try testing.expectEqual(@as(u3, 4), br.alignBits()); @@ -240,9 +240,9 @@ test "BitReader" { try testing.expectEqual(@as(u8, 32), br.nbits); try testing.expectEqual(@as(u3, 0), br.alignBits()); - br.shift(1); + try br.shift(1); try testing.expectEqual(@as(u3, 7), br.alignBits()); - br.shift(1); + try br.shift(1); try testing.expectEqual(@as(u3, 6), br.alignBits()); br.alignToByte(); try testing.expectEqual(@as(u3, 0), br.alignBits()); @@ -285,18 +285,18 @@ test "BitReader init" { var br = bitReader(fbs.reader()); try testing.expectEqual(@as(u64, 0x08_07_06_05_04_03_02_01), br.bits); - br.shift(8); + try br.shift(8); try testing.expectEqual(@as(u64, 0x00_08_07_06_05_04_03_02), br.bits); try br.fill(60); // fill with 1 byte try testing.expectEqual(@as(u64, 0x01_08_07_06_05_04_03_02), br.bits); - br.shift(8 * 4 + 4); + try br.shift(8 * 4 + 4); try testing.expectEqual(@as(u64, 0x00_00_00_00_00_10_80_70), br.bits); try br.fill(60); // fill with 4 bytes (shift by 4) try testing.expectEqual(@as(u64, 0x00_50_40_30_20_10_80_70), br.bits); try testing.expectEqual(@as(u8, 8 * 7 + 4), br.nbits); - br.shift(@intCast(br.nbits)); // clear buffer + try br.shift(@intCast(br.nbits)); // clear buffer try br.fill(8); // refill with the rest of the bytes try testing.expectEqual(@as(u64, 0x00_00_00_00_00_08_07_06), br.bits); } diff --git a/src/inflate.zig b/src/inflate.zig index 257c232..02b9a47 100644 --- a/src/inflate.zig +++ b/src/inflate.zig @@ -123,7 +123,7 @@ pub fn Inflate(comptime container: Container, comptime ReaderType: type) type { } inline fn decodeLength(self: *Self, code: u8) !u16 { - assert(code <= 28); + if (code > 28) return error.CorruptInput; const ml = Token.matchLength(code); return if (ml.extra_bits == 0) // 0 - 5 extra bits ml.base @@ -132,7 +132,7 @@ pub fn Inflate(comptime container: Container, comptime ReaderType: type) type { } inline fn decodeDistance(self: *Self, code: u8) !u16 { - assert(code <= 29); + if (code > 29) return error.CorruptInput; const md = Token.matchDistance(code); return if (md.extra_bits == 0) // 0 - 13 extra bits md.base @@ -158,7 +158,7 @@ pub fn Inflate(comptime container: Container, comptime ReaderType: type) type { var pos: usize = 0; while (pos < hlit) { const sym = cl_h.find(try self.bits.peekF(u7, F.reverse)); - self.bits.shift(sym.code_bits); + try self.bits.shift(sym.code_bits); pos += try self.dynamicCodeLength(sym.symbol, &lit_l, pos); } @@ -167,7 +167,7 @@ pub fn Inflate(comptime container: Container, comptime ReaderType: type) type { pos = 0; while (pos < hdist) { const sym = cl_h.find(try self.bits.peekF(u7, F.reverse)); - self.bits.shift(sym.code_bits); + try self.bits.shift(sym.code_bits); pos += try self.dynamicCodeLength(sym.symbol, &dst_l, pos); } @@ -234,7 +234,7 @@ pub fn Inflate(comptime container: Container, comptime ReaderType: type) type { inline fn decodeSymbol(self: *Self, decoder: anytype) !hfd.Symbol { const sym = decoder.find(try self.bits.peekF(u15, F.buffered | F.reverse)); if (sym.code_bits == 0) return error.CorruptInput; - self.bits.shift(sym.code_bits); + try self.bits.shift(sym.code_bits); return sym; } @@ -474,21 +474,81 @@ test "zlib decompress" { } } -test "lengths overflow" { - const data = "\xed\x1d$\xe9\xff\xff9\x0e"; - - var fb = std.io.fixedBufferStream(data); - var al = std.ArrayList(u8).init(testing.allocator); - defer al.deinit(); - - try testing.expectError(error.CorruptInput, decompress(.raw, fb.reader(), al.writer())); -} +test "fuzzing tests" { + const cases = [_]struct { + in: []const u8, + out: []const u8 = "", + err: ?anyerror = null, + }{ + .{ .in = @embedFile("testdata/fuzzing/deflate-stream"), .out = + \\[ + \\ { id: "brieflz", + \\ name: "BriefLZ", + \\ libraryUrl: "https://github.com/jibsen/brieflz", + \\ license: "MIT", + \\ revision: "bcaa6a1ee7ccf005512b5c23aa92b40cf75f9ed1", + \\ codecs: [ { name: "brieflz" } ], }, + \\ { id: "brotli", + \\ name: "Brotli", + \\ libraryUrl: "https://github.com/google/brotli", + \\ license: "Apache 2.0", + \\ revision: "1dd66ef114fd244778d9dcb5da09c28b49a0df33", + \\ codecs: [ { name: "brotli", + \\ levels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + \\ streaming: true } ], }, + \\ { id: "bsc", + \\ name: "bsc", + \\ libraryUrl: "http://libbsc.com/", + \\ license: "Apache 2.0", + \\ revision: "b2b07421381b19b2fada8b291f3cdead10578abc", + \\ codecs: [ { name: "bsc" } ] } + \\] + \\ + }, + .{ .in = @embedFile("testdata/fuzzing/empty-distance-alphabet01"), .err = error.EndOfStream }, + .{ .in = @embedFile("testdata/fuzzing/empty-distance-alphabet02"), .out = "" }, + .{ .in = @embedFile("testdata/fuzzing/end-of-stream"), .err = error.EndOfStream }, + .{ .in = @embedFile("testdata/fuzzing/invalid-distance"), .err = error.EndOfStream }, // panic + .{ .in = @embedFile("testdata/fuzzing/invalid-tree01"), .err = error.EndOfStream }, + .{ .in = @embedFile("testdata/fuzzing/invalid-tree02"), .err = error.EndOfStream }, + .{ .in = @embedFile("testdata/fuzzing/invalid-tree03"), .err = error.EndOfStream }, + .{ .in = @embedFile("testdata/fuzzing/lengths-overflow"), .err = error.CorruptInput }, + //.{ .in = "\xed\x1d$\xe9\xff\xff9\x0e", .err = error.CorruptInput }, + + .{ .in = @embedFile("testdata/fuzzing/out-of-codes"), .err = error.CorruptInput }, + //.{ .in = "\x950\x00\x0000000", .err = error.CorruptInput }, + .{ .in = @embedFile("testdata/fuzzing/puff01"), .err = error.DeflateWrongNlen }, + .{ .in = @embedFile("testdata/fuzzing/puff02"), .err = error.EndOfStream }, + .{ .in = @embedFile("testdata/fuzzing/puff03"), .out = &[_]u8{0xa} }, + .{ .in = @embedFile("testdata/fuzzing/puff04"), .err = error.CorruptInput }, // this was panicking + .{ .in = @embedFile("testdata/fuzzing/puff05"), .err = error.EndOfStream }, + .{ .in = @embedFile("testdata/fuzzing/puff06"), .err = error.EndOfStream }, + //.{ .in = @embedFile("testdata/fuzzing/puff07") }, // panic + .{ .in = @embedFile("testdata/fuzzing/puff08"), .err = error.CorruptInput }, + //.{ .in = "\x04\xc0\x81\x08\x00\x00\x00\x00 \x7f\xeb\x0b\x00\x00", .err = error.CorruptInput }, + .{ .in = @embedFile("testdata/fuzzing/puff09"), .out = "P" }, + .{ .in = @embedFile("testdata/fuzzing/puff10"), .err = error.DeflateInvalidCode }, + .{ .in = @embedFile("testdata/fuzzing/puff11"), .err = error.EndOfStream }, + + .{ .in = @embedFile("testdata/fuzzing/puff12"), .err = error.EndOfStream }, + .{ .in = @embedFile("testdata/fuzzing/puff13"), .err = error.CorruptInput }, + //.{ .in = "\x04\x00\xfe\xff", .err = error.CorruptInput }, + .{ .in = @embedFile("testdata/fuzzing/puff14"), .err = error.CorruptInput }, + .{ .in = @embedFile("testdata/fuzzing/puff15"), .err = error.EndOfStream }, + .{ .in = @embedFile("testdata/fuzzing/puff16"), .err = error.EndOfStream }, // panic + .{ .in = @embedFile("testdata/fuzzing/puff17"), .err = error.EndOfStream }, + }; -test "fix: infinite loop during 'out of codes'" { - const data = "\x950\x00\x0000000"; + for (cases) |c| { + var fb = std.io.fixedBufferStream(c.in); + var al = std.ArrayList(u8).init(testing.allocator); + defer al.deinit(); - var fb = std.io.fixedBufferStream(data); - var al = std.ArrayList(u8).init(testing.allocator); - defer al.deinit(); - try testing.expectError(error.CorruptInput, decompress(.raw, fb.reader(), al.writer())); + if (c.err) |expected_err| { + try testing.expectError(expected_err, decompress(.raw, fb.reader(), al.writer())); + } else { + try decompress(.raw, fb.reader(), al.writer()); + try testing.expectEqualStrings(c.out, al.items); + } + } } diff --git a/src/testdata/fuzzing/deflate-stream b/src/testdata/fuzzing/deflate-stream new file mode 100644 index 0000000..a0ed06f --- /dev/null +++ b/src/testdata/fuzzing/deflate-stream @@ -0,0 +1,3 @@ +=o0+NڭRK}>W!AI@j+{ +| literal 0 HcmV?d00001 diff --git a/src/testdata/fuzzing/empty-distance-alphabet02 b/src/testdata/fuzzing/empty-distance-alphabet02 new file mode 100644 index 0000000000000000000000000000000000000000..32a51d3180bb3430260b85ccc15fffc005033257 GIT binary patch literal 13 ScmWfAz{tq}1OhSp^$Y+Ii~@-O literal 0 HcmV?d00001 diff --git a/src/testdata/fuzzing/end-of-stream b/src/testdata/fuzzing/end-of-stream new file mode 100644 index 0000000..05e05dc --- /dev/null +++ b/src/testdata/fuzzing/end-of-stream @@ -0,0 +1 @@ +=o00 \ No newline at end of file diff --git a/src/testdata/fuzzing/invalid-distance b/src/testdata/fuzzing/invalid-distance new file mode 100644 index 0000000000000000000000000000000000000000..fd05e4165377eed3169237659d3ed35267f79692 GIT binary patch literal 6 NcmXReDPl`x000M=0l@$O literal 0 HcmV?d00001 diff --git a/src/testdata/fuzzing/invalid-tree01 b/src/testdata/fuzzing/invalid-tree01 new file mode 100644 index 0000000..2a6b195 --- /dev/null +++ b/src/testdata/fuzzing/invalid-tree01 @@ -0,0 +1 @@ +000 \ No newline at end of file diff --git a/src/testdata/fuzzing/invalid-tree02 b/src/testdata/fuzzing/invalid-tree02 new file mode 100644 index 0000000000000000000000000000000000000000..a4725951a278a73da4a293fed95364aef52fa621 GIT binary patch literal 14 VcmZR5#mLFPz`)25vtN&s0RR#Z0!;t_ literal 0 HcmV?d00001 diff --git a/src/testdata/fuzzing/invalid-tree03 b/src/testdata/fuzzing/invalid-tree03 new file mode 100644 index 0000000000000000000000000000000000000000..a6b335a1054b4aae8fbe34193c2e1c6b51163f57 GIT binary patch literal 12 RcmZR5!05;T1OhSp^#Bh$0)YSk literal 0 HcmV?d00001 diff --git a/src/testdata/fuzzing/lengths-overflow b/src/testdata/fuzzing/lengths-overflow new file mode 100644 index 0000000..af0620e --- /dev/null +++ b/src/testdata/fuzzing/lengths-overflow @@ -0,0 +1 @@ +$9 \ No newline at end of file diff --git a/src/testdata/fuzzing/out-of-codes b/src/testdata/fuzzing/out-of-codes new file mode 100644 index 0000000000000000000000000000000000000000..86c8c7be0299570b0ad83db7b0f62b50b42173d0 GIT binary patch literal 9 NcmbPgz`$St0ssk?0k!}D literal 0 HcmV?d00001 diff --git a/src/testdata/fuzzing/puff01 b/src/testdata/fuzzing/puff01 new file mode 100644 index 0000000000000000000000000000000000000000..40b450dd9d8187f90cf9f13a80c3ded26f8ecfd7 GIT binary patch literal 5 KcmZQz00IC21pom6 literal 0 HcmV?d00001 diff --git a/src/testdata/fuzzing/puff02 b/src/testdata/fuzzing/puff02 new file mode 100644 index 0000000000000000000000000000000000000000..8aed0c90bd93dedd3428d60464830cfd4f21652c GIT binary patch literal 5 McmZQzWcc?V00RU8{{R30 literal 0 HcmV?d00001 diff --git a/src/testdata/fuzzing/puff03 b/src/testdata/fuzzing/puff03 new file mode 100644 index 0000000000000000000000000000000000000000..3e5998658d5de876896345ce22533d46026f9efb GIT binary patch literal 6 NcmZQ%Wcc@=3jhTZ0tx^C literal 0 HcmV?d00001 diff --git a/src/testdata/fuzzing/puff04 b/src/testdata/fuzzing/puff04 new file mode 100644 index 0000000..843dc9c --- /dev/null +++ b/src/testdata/fuzzing/puff04 @@ -0,0 +1 @@ +~ \ No newline at end of file diff --git a/src/testdata/fuzzing/puff05 b/src/testdata/fuzzing/puff05 new file mode 100644 index 0000000..25cb955 --- /dev/null +++ b/src/testdata/fuzzing/puff05 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/testdata/fuzzing/puff06 b/src/testdata/fuzzing/puff06 new file mode 100644 index 0000000..5750c48 --- /dev/null +++ b/src/testdata/fuzzing/puff06 @@ -0,0 +1 @@ +I$I$ \ No newline at end of file diff --git a/src/testdata/fuzzing/puff07 b/src/testdata/fuzzing/puff07 new file mode 100644 index 0000000000000000000000000000000000000000..de6786f0d2425f81d9d3a744f2b32913b5202748 GIT binary patch literal 14 UcmZQ+@SLOqM1}wVPZneV04H??mH+?% literal 0 HcmV?d00001 diff --git a/src/testdata/fuzzing/puff08 b/src/testdata/fuzzing/puff08 new file mode 100644 index 0000000000000000000000000000000000000000..19402ac49f55b9957413e18d342401bb9faeae9e GIT binary patch literal 14 TcmZQM(8$361Pb-9xfvJ$7jgpQ literal 0 HcmV?d00001 diff --git a/src/testdata/fuzzing/puff09 b/src/testdata/fuzzing/puff09 new file mode 100644 index 0000000000000000000000000000000000000000..ef731eb5e7ebc3247c5522710fa35b58c9ffc496 GIT binary patch literal 3 Kcmd;PU;qFBBmfKm literal 0 HcmV?d00001 diff --git a/src/testdata/fuzzing/puff10 b/src/testdata/fuzzing/puff10 new file mode 100644 index 0000000..bb26068 --- /dev/null +++ b/src/testdata/fuzzing/puff10 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/testdata/fuzzing/puff11 b/src/testdata/fuzzing/puff11 new file mode 100644 index 0000000000000000000000000000000000000000..138da04b6156677bc588548efb768ac0a2b3561b GIT binary patch literal 12 Rcmd-%(8vG+6aHtj000yH155w_ literal 0 HcmV?d00001 diff --git a/src/testdata/fuzzing/puff12 b/src/testdata/fuzzing/puff12 new file mode 100644 index 0000000000000000000000000000000000000000..cb896978fb1471b9ea98014ba6accb9b302176ee GIT binary patch literal 3 KcmeyvzyJUO_W=C> literal 0 HcmV?d00001 diff --git a/src/testdata/fuzzing/puff13 b/src/testdata/fuzzing/puff13 new file mode 100644 index 0000000000000000000000000000000000000000..644f6437a1eb9d9184cd13579f2470f296c96906 GIT binary patch literal 4 LcmZQ!`1c