milestone 15: make a green run mean a real pass
CI / test (push) Failing after 1m12s
CI / test-aarch64 (push) Failing after 2m27s
CI / frontend (push) Successful in 1m28s
CI / cross (push) Failing after 27s
CI / docker (push) Failing after 24s

This commit is contained in:
2026-08-07 01:28:13 +02:00
parent f2898479d4
commit 9f8a5cd753
16 changed files with 1070 additions and 31 deletions
+57 -1
View File
@@ -10,7 +10,10 @@
//! - a `Name` that `parse` accepted ends inside the packet and survives a
//! round trip through presentation form;
//! - a buffer that `decrementTtls` aged still parses, and no record it aged
//! holds a TTL below the minimum it reported.
//! holds a TTL below the minimum it reported;
//! - a query that `stripEcs` rewrote still parses, still carries a valid OPT
//! record, no longer carries an ECS option, and kept all four of its
//! section counts.
//!
//! The targets stay inside the documented safe entry points. `setId` is called
//! only on a buffer long enough to hold a header, because it asserts that
@@ -54,6 +57,10 @@ test "fuzz packet.decrementTtls" {
try std.testing.fuzz({}, ttlTarget, fuzz_options);
}
test "fuzz edns.stripEcs" {
try std.testing.fuzz({}, stripEcsTarget, fuzz_options);
}
fn parseTarget(_: void, smith: *Smith) anyerror!void {
var buf: [max_input]u8 = undefined;
const bytes = buf[0..smith.slice(&buf)];
@@ -136,6 +143,55 @@ fn ttlTarget(_: void, smith: *Smith) anyerror!void {
}
}
/// `stripEcs` is the only attacker-facing entry point that rewrites a packet, so
/// it is the only one where a finding can be a wrong output rather than a crash.
///
/// The input is derived exactly as `parseTarget` derives it, because `stripEcs`
/// asserts its preconditions rather than returning an error: `query` must be the
/// same bytes `pkt` was parsed from, and `out` must not overlap them. `out` is a
/// separate stack buffer for that reason, and tripping either assertion from a
/// hand-built argument would report a fault no packet can cause.
fn stripEcsTarget(_: void, smith: *Smith) anyerror!void {
var buf: [max_input]u8 = undefined;
const bytes = buf[0..smith.slice(&buf)];
const p = packet.parse(bytes) catch return;
const opt_record = packet.findOptRecord(p) orelse return;
const opt = edns.parseOpt(bytes, opt_record) catch return;
// Removing an option only ever shortens the query, so a buffer the size of
// the input always holds the rewrite.
var out: [max_input]u8 = undefined;
const result = edns.stripEcs(bytes, p, opt, &out) catch return;
const rewritten = switch (result) {
.unchanged => return,
.rewritten => |message| message,
};
try std.testing.expect(rewritten.len <= bytes.len);
const stripped = try packet.parse(rewritten);
try std.testing.expectEqual(p.header.id, stripped.header.id);
try std.testing.expectEqual(p.header.qdcount, stripped.header.qdcount);
try std.testing.expectEqual(p.header.ancount, stripped.header.ancount);
try std.testing.expectEqual(p.header.nscount, stripped.header.nscount);
try std.testing.expectEqual(p.header.arcount, stripped.header.arcount);
const stripped_record = packet.findOptRecord(stripped) orelse
return error.TestOptRecordLost;
const stripped_opt = try edns.parseOpt(rewritten, stripped_record);
try std.testing.expectEqual(opt.udp_payload_size, stripped_opt.udp_payload_size);
try std.testing.expectEqual(opt.do_bit, stripped_opt.do_bit);
var options = edns.options(rewritten, stripped_opt);
while (try options.next()) |option| {
try std.testing.expect(option.code != edns.ecs_option_code);
}
try std.testing.expect(
(try edns.findOption(rewritten, stripped_opt, edns.ecs_option_code)) == null,
);
}
/// Runs every typed RDATA accessor over a record. Each one rejects a record of
/// the wrong type or a truncated RDATA, so only a panic is a finding here.
fn sweepRdata(bytes: []const u8, rec: record.Record) void {