/src/curl_fuzzer/full_coverage/legacy_mutator.cc
Line | Count | Source |
1 | | /* |
2 | | * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al. |
3 | | * |
4 | | * SPDX-License-Identifier: curl |
5 | | */ |
6 | | |
7 | | #include "full_coverage/legacy_mutator.h" |
8 | | |
9 | | #include "legacy_tlv_mutator.h" |
10 | | |
11 | | #include <stdint.h> |
12 | | |
13 | | #include <algorithm> |
14 | | #include <array> |
15 | | #include <cstring> |
16 | | #include <vector> |
17 | | |
18 | | extern "C" size_t LLVMFuzzerCustomMutator(uint8_t *data, size_t size, |
19 | | size_t max_size, |
20 | | unsigned int seed); |
21 | | extern "C" size_t LLVMFuzzerCustomCrossOver( |
22 | | const uint8_t *data1, size_t size1, const uint8_t *data2, size_t size2, |
23 | | uint8_t *out, size_t max_out_size, unsigned int seed); |
24 | | |
25 | | namespace full_coverage { |
26 | | namespace { |
27 | | |
28 | | constexpr size_t kCapacity = 8192; |
29 | | constexpr unsigned int kSeedCount = 64; |
30 | | using Input = std::vector<uint8_t>; |
31 | | |
32 | | void AppendRecord(Input *input, uint16_t type, const uint8_t *value, |
33 | 0 | size_t size) { |
34 | 0 | const uint32_t length = static_cast<uint32_t>(size); |
35 | 0 | input->push_back(static_cast<uint8_t>(type >> 8)); |
36 | 0 | input->push_back(static_cast<uint8_t>(type)); |
37 | 0 | input->push_back(static_cast<uint8_t>(length >> 24)); |
38 | 0 | input->push_back(static_cast<uint8_t>(length >> 16)); |
39 | 0 | input->push_back(static_cast<uint8_t>(length >> 8)); |
40 | 0 | input->push_back(static_cast<uint8_t>(length)); |
41 | 0 | if(size) |
42 | 0 | input->insert(input->end(), value, value + size); |
43 | 0 | } |
44 | | |
45 | 0 | void AppendString(Input *input, uint16_t type, const char *value) { |
46 | 0 | AppendRecord(input, type, reinterpret_cast<const uint8_t *>(value), |
47 | 0 | std::strlen(value)); |
48 | 0 | } |
49 | | |
50 | 0 | std::vector<Input> MakeInputs() { |
51 | 0 | Input transfer; |
52 | 0 | AppendString(&transfer, 1, "http://127.0.0.1/"); |
53 | 0 | const Input url = transfer; |
54 | 0 | AppendString(&transfer, 2, |
55 | 0 | "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); |
56 | | |
57 | | // One ordinary record from every value class, plus repeatable records and |
58 | | // canonical routing strings. These remain data for the mutator only. |
59 | 0 | Input options = transfer; |
60 | 0 | AppendString(&options, 6, "X-Coverage: 1"); |
61 | 0 | AppendString(&options, 6, "X-Coverage: 2"); |
62 | 0 | AppendString(&options, 8, "upload"); |
63 | 0 | AppendString(&options, 11, "coverage@example.test"); |
64 | 0 | Input mime; |
65 | 0 | AppendString(&mime, 14, "field"); |
66 | 0 | AppendString(&mime, 15, "value"); |
67 | 0 | AppendRecord(&options, 13, mime.data(), mime.size()); |
68 | 0 | const uint8_t numeric[] = {0, 0, 0, 80}; |
69 | 0 | AppendRecord(&options, 200, numeric, sizeof(numeric)); |
70 | 0 | for(const uint16_t type : {53, 102, 105, 149}) |
71 | 0 | AppendString(&options, type, |
72 | 0 | legacy_tlv_mutator::CanonicalRoutingValue(type)); |
73 | |
|
74 | 0 | Input header; |
75 | 0 | AppendString(&header, 6, "X-Only: 1"); |
76 | 0 | Input response; |
77 | 0 | AppendString(&response, 2, "HTTP/1.1 204 OK\r\n\r\n"); |
78 | | |
79 | | // Short values exercise canonicalization with and without growth space. |
80 | 0 | Input routing = transfer; |
81 | 0 | for(const uint16_t type : {53, 102, 105, 149}) |
82 | 0 | AppendString(&routing, type, ""); |
83 | |
|
84 | 0 | Input trailing = options; |
85 | 0 | trailing.push_back(0); |
86 | | |
87 | | // Known framing and semantic rejection paths also enter prefix repair and |
88 | | // byte crossover. All storage is local; none of these records reaches curl. |
89 | 0 | Input truncated = options; |
90 | 0 | truncated.pop_back(); |
91 | 0 | Input duplicate = transfer; |
92 | 0 | AppendString(&duplicate, 1, "http://127.0.0.1/"); |
93 | 0 | Input short_numeric = transfer; |
94 | 0 | AppendRecord(&short_numeric, 200, numeric, 1); |
95 | 0 | Input unknown = transfer; |
96 | 0 | AppendString(&unknown, 0xffff, ""); |
97 | 0 | Input invalid_mime = transfer; |
98 | 0 | AppendRecord(&invalid_mime, 13, header.data(), header.size()); |
99 | 0 | Input truncated_mime = transfer; |
100 | 0 | const uint8_t short_part[] = {0, 15, 0, 0, 0, 2, 'v'}; |
101 | 0 | AppendRecord(&truncated_mime, 13, short_part, sizeof(short_part)); |
102 | | |
103 | | // The parser has a 512-record work limit. Repeated empty headers exercise |
104 | | // acceptance at that limit and the bounded repair path immediately above it. |
105 | 0 | Input at_record_limit; |
106 | 0 | for(size_t index = 0; index < 512; ++index) |
107 | 0 | AppendString(&at_record_limit, 6, ""); |
108 | 0 | Input above_record_limit = at_record_limit; |
109 | 0 | AppendString(&above_record_limit, 6, ""); |
110 | |
|
111 | 0 | return {{}, transfer, options, url, header, response, routing, trailing, |
112 | 0 | truncated, duplicate, short_numeric, unknown, invalid_mime, |
113 | 0 | truncated_mime, at_record_limit, above_record_limit}; |
114 | 0 | } |
115 | | |
116 | | // A volatile result keeps validation and immutable routing queries visible |
117 | | // even when link-time optimization can see that this is a coverage exercise. |
118 | 0 | void ObserveValidation(const uint8_t *data, size_t size) { |
119 | 0 | volatile bool valid = legacy_tlv_mutator::IsStructurallyValid(data, size); |
120 | 0 | (void)valid; |
121 | 0 | } |
122 | | |
123 | | } // namespace |
124 | | |
125 | 0 | void ExerciseLegacyMutatorCoverage() { |
126 | 0 | const std::vector<Input> inputs = MakeInputs(); |
127 | 0 | std::array<uint8_t, kCapacity> scratch = {}; |
128 | 0 | std::array<uint8_t, kCapacity> child = {}; |
129 | | |
130 | | // Replay does not call libFuzzer's ABI adapters. Cover them explicitly as |
131 | | // well as their ordinary namespaced implementations below. |
132 | 0 | const Input &first = inputs[1]; |
133 | 0 | const Input &second = inputs[2]; |
134 | 0 | std::copy(first.begin(), first.end(), scratch.begin()); |
135 | 0 | const size_t mutated_size = LLVMFuzzerCustomMutator( |
136 | 0 | scratch.data(), first.size(), scratch.size(), 1); |
137 | 0 | ObserveValidation(scratch.data(), mutated_size); |
138 | 0 | const size_t crossed_size = LLVMFuzzerCustomCrossOver( |
139 | 0 | first.data(), first.size(), second.data(), second.size(), child.data(), |
140 | 0 | child.size(), 1); |
141 | 0 | ObserveValidation(child.data(), crossed_size); |
142 | | |
143 | | // Empty parents still use real storage. Reversing the existing empty-first |
144 | | // case exercises the one-byte fallback from the first parent; two empty |
145 | | // parents have no byte to inherit even when output capacity is available. |
146 | 0 | const Input &header = inputs[4]; |
147 | 0 | const size_t reversed_size = LLVMFuzzerCustomCrossOver( |
148 | 0 | header.data(), header.size(), scratch.data(), 0, child.data(), 1, 7); |
149 | 0 | ObserveValidation(child.data(), reversed_size); |
150 | 0 | const size_t empty_size = LLVMFuzzerCustomCrossOver( |
151 | 0 | scratch.data(), 0, scratch.data(), 0, child.data(), 1, 7); |
152 | 0 | ObserveValidation(child.data(), empty_size); |
153 | | |
154 | | // These framed IDs are explicitly excluded by the top-level type policy. |
155 | 0 | for(const uint16_t type : {15, 51, 212, 322}) { |
156 | 0 | Input rejected; |
157 | 0 | AppendRecord(&rejected, type, scratch.data(), 0); |
158 | 0 | ObserveValidation(rejected.data(), rejected.size()); |
159 | 0 | } |
160 | | |
161 | | // With exact capacity, a nonempty short routing value must shrink to the |
162 | | // disabled empty value when its canonical spelling cannot fit. Seed 8 |
163 | | // selects duplication; scalar-only records leave no room for insertion. |
164 | 0 | Input short_routing = first; |
165 | 0 | AppendString(&short_routing, 53, "1"); |
166 | 0 | std::copy(short_routing.begin(), short_routing.end(), scratch.begin()); |
167 | 0 | const size_t routing_size = legacy_tlv_mutator::Mutate( |
168 | 0 | scratch.data(), short_routing.size(), short_routing.size(), 8); |
169 | 0 | ObserveValidation(scratch.data(), routing_size); |
170 | |
|
171 | 0 | for(const uint16_t type : {53, 102, 105, 129, 149, 0xffff}) { |
172 | 0 | const char *volatile routing = |
173 | 0 | legacy_tlv_mutator::CanonicalRoutingValue(type); |
174 | 0 | (void)routing; |
175 | 0 | } |
176 | |
|
177 | 0 | for(size_t index = 0; index < inputs.size(); ++index) { |
178 | 0 | const Input &input = inputs[index]; |
179 | 0 | const Input &other = inputs[(index + 1) % inputs.size()]; |
180 | 0 | ObserveValidation(input.data(), input.size()); |
181 | | |
182 | | // Zero/tiny capacities exercise the documented no-op/fallback behavior; |
183 | | // exact capacity and spare capacity distinguish replacement from growth. |
184 | 0 | const size_t capacities[] = {0, 1, 6, 96, input.size(), kCapacity}; |
185 | 0 | for(const size_t capacity : capacities) { |
186 | 0 | for(unsigned int seed = 0; seed < kSeedCount; ++seed) { |
187 | 0 | std::copy(input.begin(), input.end(), scratch.begin()); |
188 | 0 | const size_t size = legacy_tlv_mutator::Mutate( |
189 | 0 | scratch.data(), input.size(), capacity, seed); |
190 | 0 | ObserveValidation(scratch.data(), size); |
191 | |
|
192 | 0 | const size_t child_size = legacy_tlv_mutator::CrossOver( |
193 | 0 | input.data(), input.size(), other.data(), other.size(), |
194 | 0 | child.data(), capacity, seed); |
195 | 0 | ObserveValidation(child.data(), child_size); |
196 | 0 | } |
197 | 0 | } |
198 | 0 | } |
199 | 0 | } |
200 | | |
201 | | } // namespace full_coverage |