/src/ada-url/fuzz/serializers.cc
Line | Count | Source |
1 | | #include <fuzzer/FuzzedDataProvider.h> |
2 | | |
3 | | #include <array> |
4 | | #include <cassert> |
5 | | #include <cstdio> |
6 | | #include <string> |
7 | | |
8 | | #include "ada.cpp" |
9 | | #include "ada.h" |
10 | | |
11 | | // ============================================================ |
12 | | // Fuzzer for IP address serializers, fast IPv4 parser, and |
13 | | // percent encode/decode utilities. |
14 | | // |
15 | | // These code paths are exercised indirectly by parse.cc but |
16 | | // never directly. Targeting them independently lets the fuzzer |
17 | | // discover edge cases in the serialization and encoding layers |
18 | | // without depending on a valid URL to reach them. |
19 | | // ============================================================ |
20 | | |
21 | 2.87k | extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { |
22 | 2.87k | FuzzedDataProvider fdp(data, size); |
23 | | |
24 | | // ===== IPv4 Serialization ===== |
25 | | // ipv4() takes a uint64_t (valid addresses are 0..0xFFFFFFFF, but the |
26 | | // function accepts any value – out-of-range inputs should still be safe). |
27 | 2.87k | uint64_t ipv4_addr = fdp.ConsumeIntegral<uint64_t>(); |
28 | 2.87k | std::string ipv4_str = ada::serializers::ipv4(ipv4_addr); |
29 | 2.87k | volatile size_t ipv4_len = ipv4_str.size(); |
30 | 2.87k | (void)ipv4_len; |
31 | | |
32 | | // ===== IPv6 Serialization ===== |
33 | 2.87k | std::array<uint16_t, 8> ipv6_addr{}; |
34 | 23.0k | for (auto& piece : ipv6_addr) { |
35 | 23.0k | piece = fdp.ConsumeIntegral<uint16_t>(); |
36 | 23.0k | } |
37 | | |
38 | | // find_longest_sequence_of_ipv6_pieces: basic invariants must hold. |
39 | 2.87k | size_t compress = 0, compress_length = 0; |
40 | 2.87k | ada::serializers::find_longest_sequence_of_ipv6_pieces(ipv6_addr, compress, |
41 | 2.87k | compress_length); |
42 | | // The longest run cannot exceed 8 pieces. |
43 | 2.87k | assert(compress_length <= 8); |
44 | | // If a run was found (length > 0) its start index must be in-bounds. |
45 | 2.87k | if (compress_length > 0) { |
46 | 505 | assert(compress < 8); |
47 | 505 | assert(compress + compress_length <= 8); |
48 | 505 | } |
49 | | |
50 | 2.87k | std::string ipv6_str = ada::serializers::ipv6(ipv6_addr); |
51 | 2.87k | volatile size_t ipv6_len = ipv6_str.size(); |
52 | 2.87k | (void)ipv6_len; |
53 | | |
54 | | // Serialized IPv6 must always be a non-empty string. |
55 | 2.87k | assert(!ipv6_str.empty()); |
56 | | |
57 | | // ===== Fast IPv4 Parser ===== |
58 | | // try_parse_ipv4_fast() should agree with the full parsing pipeline on its |
59 | | // output: if it succeeds (result <= 0xFFFFFFFF), re-serializing with |
60 | | // ipv4() and parsing again must yield the same address value. |
61 | 2.87k | { |
62 | 2.87k | std::string ip_candidate = fdp.ConsumeRandomLengthString(32); |
63 | 2.87k | uint64_t fast_result = ada::checkers::try_parse_ipv4_fast(ip_candidate); |
64 | 2.87k | if (fast_result <= 0xFFFFFFFF) { |
65 | | // Serialize the parsed address and re-parse. |
66 | 38 | std::string canonical = ada::serializers::ipv4(fast_result); |
67 | 38 | uint64_t recheck = ada::checkers::try_parse_ipv4_fast(canonical); |
68 | 38 | if (recheck != fast_result) { |
69 | 0 | printf( |
70 | 0 | "try_parse_ipv4_fast round-trip failure:\n" |
71 | 0 | " input='%s' result=%llu canonical='%s' recheck=%llu\n", |
72 | 0 | ip_candidate.c_str(), (unsigned long long)fast_result, |
73 | 0 | canonical.c_str(), (unsigned long long)recheck); |
74 | 0 | abort(); |
75 | 0 | } |
76 | 38 | } |
77 | 2.87k | } |
78 | | |
79 | | // ===== Percent Encode / Decode ===== |
80 | 2.87k | { |
81 | 2.87k | std::string source = fdp.ConsumeRandomLengthString(128); |
82 | | |
83 | | // Exercise all six standard character sets used by the URL parser. |
84 | 2.87k | const uint8_t* sets[] = { |
85 | 2.87k | ada::character_sets::C0_CONTROL_PERCENT_ENCODE, |
86 | 2.87k | ada::character_sets::PATH_PERCENT_ENCODE, |
87 | 2.87k | ada::character_sets::QUERY_PERCENT_ENCODE, |
88 | 2.87k | ada::character_sets::FRAGMENT_PERCENT_ENCODE, |
89 | 2.87k | ada::character_sets::USERINFO_PERCENT_ENCODE, |
90 | 2.87k | ada::character_sets::SPECIAL_QUERY_PERCENT_ENCODE, |
91 | 2.87k | }; |
92 | | |
93 | 17.2k | for (const uint8_t* charset : sets) { |
94 | | // Two-argument percent_encode: returns the encoded string. |
95 | 17.2k | std::string encoded = ada::unicode::percent_encode(source, charset); |
96 | 17.2k | volatile size_t enc_len = encoded.size(); |
97 | 17.2k | (void)enc_len; |
98 | | |
99 | | // Encoded output must be at least as long as the input (each byte |
100 | | // either stays the same or expands to %XX – three bytes). |
101 | 17.2k | assert(encoded.size() >= source.size()); |
102 | | |
103 | | // Three-argument percent_encode: starts encoding from a given index. |
104 | 17.2k | size_t start_idx = fdp.ConsumeIntegralInRange<size_t>(0, source.size()); |
105 | 17.2k | std::string encoded_from = |
106 | 17.2k | ada::unicode::percent_encode(source, charset, start_idx); |
107 | 17.2k | volatile size_t enf_len = encoded_from.size(); |
108 | 17.2k | (void)enf_len; |
109 | | |
110 | | // Template form: percent_encode<false> (replace). |
111 | 17.2k | { |
112 | 17.2k | std::string out; |
113 | 17.2k | bool changed = |
114 | 17.2k | ada::unicode::percent_encode<false>(source, charset, out); |
115 | 17.2k | volatile bool c = changed; |
116 | 17.2k | (void)c; |
117 | | // When encoding was needed 'out' holds the encoded string; when not, |
118 | | // 'out' is unchanged (empty). If changed, 'out' must not be shorter. |
119 | 17.2k | if (changed) { |
120 | 5.49k | assert(out.size() >= source.size()); |
121 | 5.49k | } |
122 | 17.2k | } |
123 | | |
124 | | // Template form: percent_encode<true> (append). |
125 | 17.2k | { |
126 | 17.2k | std::string out = "prefix_"; |
127 | 17.2k | size_t prefix_len = out.size(); |
128 | 17.2k | ada::unicode::percent_encode<true>(source, charset, out); |
129 | | // Append mode: 'out' must grow by at least source.size() bytes if |
130 | | // encoding was needed, or stay unchanged if it wasn't. |
131 | 17.2k | assert(out.size() >= prefix_len); |
132 | 17.2k | } |
133 | 17.2k | } |
134 | | |
135 | | // Percent decode: feed raw fuzz input (may contain invalid sequences). |
136 | 2.87k | { |
137 | 2.87k | size_t pct_pos = source.find('%'); |
138 | 2.87k | if (pct_pos != std::string::npos) { |
139 | 115 | std::string decoded = ada::unicode::percent_decode(source, pct_pos); |
140 | | // Decoded output can't be longer than the input. |
141 | 115 | assert(decoded.size() <= source.size()); |
142 | 115 | volatile size_t dec_len = decoded.size(); |
143 | 115 | (void)dec_len; |
144 | 115 | } |
145 | 2.87k | } |
146 | | |
147 | | // Round-trip: encode with PATH set, then decode the result; the decoded |
148 | | // form must equal the original (encoding then decoding is an identity). |
149 | 2.87k | { |
150 | 2.87k | std::string encoded = ada::unicode::percent_encode( |
151 | 2.87k | source, ada::character_sets::PATH_PERCENT_ENCODE); |
152 | 2.87k | size_t pct_pos = encoded.find('%'); |
153 | 2.87k | if (pct_pos != std::string::npos) { |
154 | 917 | std::string decoded = ada::unicode::percent_decode(encoded, pct_pos); |
155 | 917 | if (decoded != source) { |
156 | 0 | printf( |
157 | 0 | "percent_encode/decode round-trip failure!\n" |
158 | 0 | " source='%s'\n encoded='%s'\n decoded='%s'\n", |
159 | 0 | source.c_str(), encoded.c_str(), decoded.c_str()); |
160 | 0 | abort(); |
161 | 0 | } |
162 | 1.95k | } else { |
163 | | // No encoding was needed; the output should equal the input. |
164 | 1.95k | assert(encoded == source); |
165 | 1.95k | } |
166 | 2.87k | } |
167 | | |
168 | | // percent_encode_index: the returned index must be within [0, size]. |
169 | 2.87k | { |
170 | 2.87k | size_t idx = ada::unicode::percent_encode_index( |
171 | 2.87k | source, ada::character_sets::PATH_PERCENT_ENCODE); |
172 | 2.87k | assert(idx <= source.size()); |
173 | 2.87k | } |
174 | 2.87k | } |
175 | | |
176 | | // ===== Checker and Unicode Utility Functions ===== |
177 | | // These are internal helpers used throughout the parser. Fuzzing them |
178 | | // directly (rather than only through the URL parsing pipeline) ensures |
179 | | // every edge case is reachable without a valid URL structure. |
180 | 2.87k | { |
181 | 2.87k | std::string util_input = fdp.ConsumeRandomLengthString(128); |
182 | | |
183 | | // has_tabs_or_newline: any string is valid input. |
184 | 2.87k | volatile bool has_tn = ada::unicode::has_tabs_or_newline(util_input); |
185 | 2.87k | (void)has_tn; |
186 | | |
187 | | // is_ipv4: must not crash on arbitrary input. Cross-check against URL |
188 | | // parsing: if is_ipv4 reports true, embedding the string as a hostname |
189 | | // in an http:// URL must succeed (it must be a parseable IPv4 address). |
190 | 2.87k | volatile bool is_v4 = ada::checkers::is_ipv4(util_input); |
191 | 2.87k | (void)is_v4; |
192 | 2.87k | if (is_v4) { |
193 | 2.11k | std::string ipv4_url = "http://" + util_input + "/"; |
194 | 2.11k | auto parsed = ada::parse<ada::url_aggregator>(ipv4_url); |
195 | | // is_ipv4 reports true only for strings that look like IPv4 addresses; |
196 | | // the full parser may still reject them (e.g. out-of-range octets), but |
197 | | // if it accepts them the host type must be IPv4. |
198 | 2.11k | if (parsed) { |
199 | 797 | volatile bool v = parsed->validate(); |
200 | 797 | (void)v; |
201 | 797 | } |
202 | 2.11k | } |
203 | | |
204 | | // path_signature: returns a bitmask; must not crash. |
205 | 2.87k | volatile uint8_t sig = ada::checkers::path_signature(util_input); |
206 | 2.87k | (void)sig; |
207 | | |
208 | | // is_windows_drive_letter: must not crash on short or long inputs. |
209 | 2.87k | volatile bool is_wdl = ada::checkers::is_windows_drive_letter(util_input); |
210 | 2.87k | (void)is_wdl; |
211 | | |
212 | | // is_normalized_windows_drive_letter |
213 | 2.87k | volatile bool is_nwdl = |
214 | 2.87k | ada::checkers::is_normalized_windows_drive_letter(util_input); |
215 | 2.87k | (void)is_nwdl; |
216 | | |
217 | | // Consistency: a normalised Windows drive letter is a subset of |
218 | | // Windows drive letters. |
219 | 2.87k | if (is_nwdl && !is_wdl) { |
220 | 0 | printf( |
221 | 0 | "is_normalized_windows_drive_letter implies is_windows_drive_letter" |
222 | 0 | " but got inconsistent results for '%s'\n", |
223 | 0 | util_input.c_str()); |
224 | 0 | abort(); |
225 | 0 | } |
226 | | |
227 | | // to_lower_ascii: works in-place; must not crash. |
228 | 2.87k | { |
229 | 2.87k | std::string lower_copy = util_input; |
230 | 2.87k | volatile bool all_ascii = |
231 | 2.87k | ada::unicode::to_lower_ascii(lower_copy.data(), lower_copy.size()); |
232 | 2.87k | (void)all_ascii; |
233 | | // The result should be at most as long as the input. |
234 | 2.87k | assert(lower_copy.size() == util_input.size()); |
235 | 2.87k | } |
236 | | |
237 | | // contains_forbidden_domain_code_point: must not crash. |
238 | 2.87k | volatile bool has_forbidden = |
239 | 2.87k | ada::unicode::contains_forbidden_domain_code_point(util_input.data(), |
240 | 2.87k | util_input.size()); |
241 | 2.87k | (void)has_forbidden; |
242 | | |
243 | | // contains_forbidden_domain_code_point_or_upper: must not crash. |
244 | 2.87k | volatile uint8_t forbidden_or_upper = |
245 | 2.87k | ada::unicode::contains_forbidden_domain_code_point_or_upper( |
246 | 2.87k | util_input.data(), util_input.size()); |
247 | 2.87k | (void)forbidden_or_upper; |
248 | | |
249 | | // verify_dns_length: must not crash; also check consistency with |
250 | | // to_ascii output. |
251 | 2.87k | volatile bool dns_ok = ada::checkers::verify_dns_length(util_input); |
252 | 2.87k | (void)dns_ok; |
253 | 2.87k | } |
254 | | |
255 | | // ===== Integration: embed serialized addresses into real URLs ===== |
256 | | // This exercises the full parsing pipeline with our synthetic addresses |
257 | | // and verifies that serialization output is always round-trip safe. |
258 | 0 | { |
259 | | // Only valid IPv4 address range (0..0xFFFFFFFF) should embed cleanly. |
260 | 2.87k | if (ipv4_addr <= 0xFFFFFFFF) { |
261 | 146 | std::string url_str = "http://" + ipv4_str + "/path?q=1"; |
262 | 146 | auto parsed = ada::parse<ada::url_aggregator>(url_str); |
263 | 146 | if (parsed) { |
264 | 146 | volatile bool v = parsed->validate(); |
265 | 146 | (void)v; |
266 | | // The URL's hostname must be the canonical dotted-decimal address. |
267 | 146 | std::string host = std::string(parsed->get_hostname()); |
268 | | // host == ipv4_str (not asserted here because the URL parser may |
269 | | // normalise the address differently for non-standard values, but it |
270 | | // must always successfully parse our serialized form). |
271 | 146 | (void)host; |
272 | 146 | } |
273 | 146 | } |
274 | | |
275 | | // IPv6: wrap in brackets. |
276 | 2.87k | std::string ipv6_url_str = "http://[" + ipv6_str + "]/path"; |
277 | 2.87k | auto parsed_ipv6 = ada::parse<ada::url_aggregator>(ipv6_url_str); |
278 | 2.87k | if (parsed_ipv6) { |
279 | 0 | volatile bool v = parsed_ipv6->validate(); |
280 | 0 | (void)v; |
281 | | // Re-parse the href – must be idempotent. |
282 | 0 | std::string href = std::string(parsed_ipv6->get_href()); |
283 | 0 | auto reparsed = ada::parse<ada::url_aggregator>(href); |
284 | 0 | if (!reparsed) { |
285 | 0 | printf("IPv6 URL re-parse failure: '%s'\n", href.c_str()); |
286 | 0 | abort(); |
287 | 0 | } |
288 | 0 | if (std::string(reparsed->get_href()) != href) { |
289 | 0 | printf("IPv6 URL re-parse href mismatch: '%s' vs '%s'\n", href.c_str(), |
290 | 0 | std::string(reparsed->get_href()).c_str()); |
291 | 0 | abort(); |
292 | 0 | } |
293 | 0 | } |
294 | 2.87k | } |
295 | | |
296 | 2.87k | return 0; |
297 | 2.87k | } |