/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 | } |