184 lines
7.7 KiB
Zig
184 lines
7.7 KiB
Zig
//! Seed corpus for `dns_fuzz.zig`.
|
|
//!
|
|
//! The packets below are copies of the fixtures `src/dns/packet.zig` keeps
|
|
//! private to its own tests, plus the malformed shapes worth reaching on the
|
|
//! first run rather than after the fuzzer rediscovers them. They are copies on
|
|
//! purpose: a fuzz seed that changes whenever a unit-test fixture is edited is
|
|
//! a corpus that silently loses coverage.
|
|
//!
|
|
//! `std.testing.Smith` does not consume a corpus entry as raw parser input. It
|
|
//! reads a byte stream: a slice is a little-endian `u32` length followed by
|
|
//! that many bytes, and an integer is a little-endian `u64`. `sliceInput` and
|
|
//! `sliceIntInput` encode packets into that stream, which is why every entry in
|
|
//! `inputs` is length-prefixed.
|
|
|
|
const std = @import("std");
|
|
const dns = @import("dns");
|
|
const smith_encode = @import("smith_encode.zig");
|
|
|
|
const sliceInput = smith_encode.sliceInput;
|
|
|
|
/// A query for example.com A with an EDNS(0) OPT record advertising 4096
|
|
/// bytes: id 0x1234, RD set, one question, one additional.
|
|
pub const query =
|
|
"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01" ++
|
|
"\x07example\x03com\x00\x00\x01\x00\x01" ++
|
|
"\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x00";
|
|
|
|
/// The matching response: a CNAME to www.example.com and its A record, both
|
|
/// with compressed owner names, plus the echoed OPT record.
|
|
pub const response =
|
|
"\x12\x34\x81\x80\x00\x01\x00\x02\x00\x00\x00\x01" ++
|
|
"\x07example\x03com\x00\x00\x01\x00\x01" ++
|
|
"\xc0\x0c\x00\x05\x00\x01\x00\x00\x01\x2c\x00\x06\x03www\xc0\x0c" ++
|
|
"\xc0\x29\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04\x5d\xb8\xd8\x22" ++
|
|
"\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x00";
|
|
|
|
/// One record in every section, all with root owner names and A rdata, so the
|
|
/// walk crosses all three record sections.
|
|
pub const all_sections = "\x00\x01\x81\x80\x00\x01\x00\x01\x00\x01\x00\x01" ++
|
|
"\x00\x00\x01\x00\x01" ++
|
|
"\x00\x00\x01\x00\x01\x00\x00\x00\x64\x00\x04\x01\x01\x01\x01" ++
|
|
"\x00\x00\x01\x00\x01\x00\x00\x00\x1e\x00\x04\x02\x02\x02\x02" ++
|
|
"\x00\x00\x01\x00\x01\x00\x00\x00\x50\x00\x04\x03\x03\x03\x03";
|
|
|
|
/// An OPT record whose TTL word carries the DO bit, which `decrementTtls` must
|
|
/// leave alone.
|
|
pub const opt_do_bit = "\x12\x34\x81\x80\x00\x00\x00\x01\x00\x00\x00\x01" ++
|
|
"\x00\x00\x01\x00\x01\x00\x00\x00\x64\x00\x04\x01\x02\x03\x04" ++
|
|
"\x00\x00\x29\x10\x00\x00\x00\x80\x00\x00\x00";
|
|
|
|
/// A query whose OPT record carries an EDNS Client Subnet option for
|
|
/// 192.168.1.0/24, so the option list and the ECS parser both get exercised.
|
|
pub const opt_ecs = "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x01" ++
|
|
"\x07example\x03com\x00\x00\x01\x00\x01" ++
|
|
"\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x0b" ++
|
|
"\x00\x08\x00\x07\x00\x01\x18\x00\xc0\xa8\x01";
|
|
|
|
/// A question name that points at itself: no chain can terminate.
|
|
pub const pointer_loop = "\x00\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
|
|
"\xc0\x0c\x00\x01\x00\x01";
|
|
|
|
/// A question name that points forwards, which the strictly-decreasing rule
|
|
/// rejects.
|
|
pub const forward_pointer = "\x00\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
|
|
"\xc0\x12\x00\x01\x00\x01\x03com\x00";
|
|
|
|
/// A leading 0x40 control byte: a reserved label type, and also what a 64-byte
|
|
/// label encodes to.
|
|
pub const reserved_label = "\x00\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
|
|
"\x40abc\x00\x00\x01\x00\x01";
|
|
|
|
/// An RDLENGTH of 0x40 with four bytes of rdata behind it.
|
|
pub const rdlength_overrun = "\x00\x01\x81\x80\x00\x00\x00\x01\x00\x00\x00\x00" ++
|
|
"\x00\x00\x01\x00\x01\x00\x00\x00\x0a\x00\x40\x01\x02\x03\x04";
|
|
|
|
/// ANCOUNT claims two answers and one follows.
|
|
pub const count_too_large = "\x00\x01\x81\x80\x00\x00\x00\x02\x00\x00\x00\x00" ++
|
|
"\x00\x00\x01\x00\x01\x00\x00\x00\x0a\x00\x04\x01\x02\x03\x04";
|
|
|
|
/// A header and nothing else, which is well-formed.
|
|
pub const header_only = "\x00\x01\x81\x83" ++ "\x00" ** 8;
|
|
|
|
pub const packets = [_][]const u8{
|
|
query,
|
|
response,
|
|
all_sections,
|
|
opt_do_bit,
|
|
opt_ecs,
|
|
pointer_loop,
|
|
forward_pointer,
|
|
reserved_label,
|
|
rdlength_overrun,
|
|
count_too_large,
|
|
header_only,
|
|
};
|
|
|
|
/// A name at offset 0 followed by `link_count` compression pointers, each
|
|
/// pointing at the one before it. Parsing from the topmost link follows
|
|
/// `link_count` jumps, so a chain longer than `max_compression_jumps` is the
|
|
/// shortest input that reaches the jump cap on a chain that is otherwise legal.
|
|
fn pointerChain(comptime link_count: usize) *const [3 + 2 * link_count]u8 {
|
|
return &struct {
|
|
const value: [3 + 2 * link_count]u8 = blk: {
|
|
var buf: [3 + 2 * link_count]u8 = undefined;
|
|
buf[0..3].* = "\x01a\x00".*;
|
|
for (0..link_count) |i| {
|
|
const target: u16 = if (i == 0) 0 else @intCast(3 + 2 * (i - 1));
|
|
std.mem.writeInt(u16, buf[3 + 2 * i ..][0..2], 0xc000 | target, .big);
|
|
}
|
|
break :blk buf;
|
|
};
|
|
}.value;
|
|
}
|
|
|
|
fn chainTopOffset(comptime link_count: usize) u64 {
|
|
return 3 + 2 * (link_count - 1);
|
|
}
|
|
|
|
const max_jumps = dns.types.max_compression_jumps;
|
|
|
|
/// A chain that ends exactly at the jump cap, and one link past it.
|
|
pub const chain_at_cap = pointerChain(max_jumps);
|
|
pub const chain_past_cap = pointerChain(max_jumps + 1);
|
|
|
|
/// Encodes `bytes` as a `Smith.slice` value followed by one integer, which the
|
|
/// name target reads as an offset and the TTL target as an elapsed time.
|
|
fn sliceIntInput(comptime bytes: []const u8, comptime int: u64) *const [12 + bytes.len]u8 {
|
|
return &struct {
|
|
const value: [12 + bytes.len]u8 = blk: {
|
|
var buf: [12 + bytes.len]u8 = undefined;
|
|
buf[0 .. 4 + bytes.len].* = sliceInput(bytes).*;
|
|
std.mem.writeInt(u64, buf[4 + bytes.len ..][0..8], int, .little);
|
|
break :blk buf;
|
|
};
|
|
}.value;
|
|
}
|
|
|
|
/// Every seed, in the byte stream `Smith` reads. All three targets take the
|
|
/// same corpus: each starts with one slice, and the two that read a trailing
|
|
/// integer fall back to the low end of its range when an entry has none.
|
|
pub const inputs = [_][]const u8{
|
|
sliceInput(query),
|
|
sliceInput(response),
|
|
sliceInput(all_sections),
|
|
sliceInput(opt_do_bit),
|
|
sliceInput(opt_ecs),
|
|
sliceInput(pointer_loop),
|
|
sliceInput(forward_pointer),
|
|
sliceInput(reserved_label),
|
|
sliceInput(rdlength_overrun),
|
|
sliceInput(count_too_large),
|
|
sliceInput(header_only),
|
|
// The compressed response, read from the offset of each compressed owner
|
|
// name rather than from zero.
|
|
sliceIntInput(response, 29),
|
|
sliceIntInput(response, 47),
|
|
sliceIntInput(chain_at_cap, chainTopOffset(max_jumps)),
|
|
sliceIntInput(chain_past_cap, chainTopOffset(max_jumps + 1)),
|
|
// A response aged past every TTL it carries.
|
|
sliceIntInput(response, 1_000_000),
|
|
};
|
|
|
|
test "the pointer chain has the documented shape" {
|
|
try std.testing.expectEqual(@as(usize, 3 + 2 * max_jumps), chain_at_cap.len);
|
|
try std.testing.expectEqual(@as(u64, 3 + 2 * (max_jumps - 1)), chainTopOffset(max_jumps));
|
|
// The topmost link points at the one below it.
|
|
try std.testing.expectEqual(
|
|
@as(u16, 0xc000 | (3 + 2 * (max_jumps - 2))),
|
|
std.mem.readInt(u16, chain_at_cap[chainTopOffset(max_jumps)..][0..2], .big),
|
|
);
|
|
// The lowest link points at the name itself.
|
|
try std.testing.expectEqual(@as(u16, 0xc000), std.mem.readInt(u16, chain_at_cap[3..5], .big));
|
|
}
|
|
|
|
test "a slice-plus-integer input carries the length, the bytes and the integer" {
|
|
const encoded = sliceIntInput(query, 29);
|
|
try std.testing.expectEqual(@as(u32, query.len), std.mem.readInt(u32, encoded[0..4], .little));
|
|
try std.testing.expectEqualSlices(u8, query, encoded[4 .. 4 + query.len]);
|
|
try std.testing.expectEqual(
|
|
@as(u64, 29),
|
|
std.mem.readInt(u64, encoded[4 + query.len ..][0..8], .little),
|
|
);
|
|
}
|