Coverage Report

Created: 2025-07-23 07:04

/src/hickory-dns/fuzz/fuzz_targets/message.rs
Line
Count
Source (jump to first uncovered line)
1
#![no_main]
2
use libfuzzer_sys::fuzz_target;
3
use pretty_assertions::assert_eq;
4
5
use hickory_proto::{
6
    ProtoErrorKind,
7
    op::Message,
8
    rr::Record,
9
    serialize::binary::{BinDecodable, BinEncodable},
10
};
11
12
fuzz_target!(|data: &[u8]| {
13
    let Ok(original) = Message::from_bytes(data) else {
14
        // If we can't parse the original message, we can't re-encode it.
15
        return;
16
    };
17
18
    let reencoded = match original.to_bytes() {
19
        Ok(reencoded) => reencoded,
20
        // If we can't re-encode the original message, we can't re-parse it.
21
        Err(err) if matches!(err.kind(), ProtoErrorKind::NotAllRecordsWritten { .. }) => return,
22
        Err(err) => {
23
            eprintln!("{original:?}");
24
            panic!("Message failed to serialize: {err:?}");
25
        }
26
    };
27
28
    let reparsed = match Message::from_bytes(&reencoded) {
29
        Ok(reparsed) => reparsed,
30
        Err(e) => {
31
            eprintln!("{original:?}");
32
            panic!("Message failed to deserialize: {e:?}");
33
        }
34
    };
35
36
    if !messages_equal(&original, &reparsed) {
37
        assert_eq!(original, reparsed);
38
    }
39
});
40
41
6.67k
fn messages_equal(original: &Message, reparsed: &Message) -> bool {
42
6.67k
    if original == reparsed {
43
5.91k
        return true;
44
767
    }
45
767
46
767
    // see if there are some of the records that don't round trip properly...
47
767
    if reparsed.truncated() {
48
        // TODO: there might be a better comparison to make here.
49
767
        return true;
50
0
    }
51
0
52
0
    // compare headers
53
0
    if original.header() != reparsed.header() {
54
0
        return false;
55
0
    }
56
0
57
0
    // compare queries
58
0
    if original.queries() != reparsed.queries() {
59
0
        return false;
60
0
    }
61
0
62
0
    // now compare answers
63
0
    if !records_equal(original.answers(), reparsed.answers()) {
64
0
        return false;
65
0
    }
66
0
    if !records_equal(original.authorities(), reparsed.authorities()) {
67
0
        return false;
68
0
    }
69
0
    if !records_equal(original.additionals(), reparsed.additionals()) {
70
0
        return false;
71
0
    }
72
0
73
0
    // everything is effectively equal
74
0
    true
75
6.67k
}
76
77
0
fn records_equal(records1: &[Record], records2: &[Record]) -> bool {
78
0
    for (record1, record2) in records1.iter().zip(records2.iter()) {
79
0
        if !record_equal(record1, record2) {
80
0
            return false;
81
0
        }
82
    }
83
84
0
    true
85
0
}
86
87
/// Some RDATAs don't roundtrip elegantly, so we have custom matching rules here.
88
#[allow(clippy::single_match)]
89
0
fn record_equal(record1: &Record, record2: &Record) -> bool {
90
    use hickory_proto::rr::RData;
91
92
0
    if record1.record_type() != record2.record_type() {
93
0
        return false;
94
0
    }
95
0
96
0
    // if the record data matches, we're fine
97
0
    if record1.data() == record2.data() {
98
0
        return true;
99
0
    }
100
0
101
0
    // custom rules to match..
102
0
    match (record1.data(), record2.data()) {
103
0
        (RData::Update0(_), RData::OPT(opt)) | (RData::OPT(opt), RData::Update0(_)) => {
104
0
            if opt.as_ref().is_empty() {
105
0
                return true;
106
0
            }
107
        }
108
0
        _ => return false,
109
    }
110
111
0
    false
112
0
}