/src/ada-url/fuzz/idna.cc
Line | Count | Source |
1 | | #include <fuzzer/FuzzedDataProvider.h> |
2 | | |
3 | | #include <memory> |
4 | | #include <string> |
5 | | |
6 | | #include "ada.cpp" |
7 | | #include "ada.h" |
8 | | |
9 | 4.42k | extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { |
10 | 4.42k | FuzzedDataProvider fdp(data, size); |
11 | 4.42k | std::string source = fdp.ConsumeRandomLengthString(256); |
12 | 4.42k | std::string source2 = fdp.ConsumeRandomLengthString(64); |
13 | | |
14 | | /** |
15 | | * High-level IDNA API |
16 | | */ |
17 | 4.42k | std::string ascii_result = ada::idna::to_ascii(source); |
18 | 4.42k | std::string unicode_result = ada::idna::to_unicode(source); |
19 | | |
20 | | // Avoid dead-code elimination |
21 | 4.42k | volatile size_t length = 0; |
22 | 4.42k | length += ascii_result.size(); |
23 | 4.42k | length += unicode_result.size(); |
24 | | |
25 | | /** |
26 | | * Round-trip property: to_unicode(to_ascii(x)) should not crash. |
27 | | * We don't assert equality because IDNA may normalize/reject inputs. |
28 | | */ |
29 | 4.42k | if (!ascii_result.empty()) { |
30 | 2.71k | std::string roundtrip = ada::idna::to_unicode(ascii_result); |
31 | 2.71k | length += roundtrip.size(); |
32 | 2.71k | } |
33 | | |
34 | | /** |
35 | | * Punycode functions |
36 | | */ |
37 | 4.42k | { |
38 | 4.42k | std::u32string utf32_out; |
39 | | // punycode_to_utf32: source can be any string (it's a punycode label) |
40 | 4.42k | bool punycode_ok = ada::idna::punycode_to_utf32(source, utf32_out); |
41 | 4.42k | length += utf32_out.size(); |
42 | | |
43 | | // verify_punycode: checks if source is valid punycode |
44 | 4.42k | volatile bool is_valid_punycode = ada::idna::verify_punycode(source); |
45 | 4.42k | (void)is_valid_punycode; |
46 | | |
47 | | // utf32_to_punycode: round-trip if punycode_to_utf32 succeeded |
48 | 4.42k | if (punycode_ok && !utf32_out.empty()) { |
49 | 802 | std::string punycode_back; |
50 | 802 | volatile bool encode_ok = |
51 | 802 | ada::idna::utf32_to_punycode(utf32_out, punycode_back); |
52 | 802 | length += punycode_back.size(); |
53 | 802 | (void)encode_ok; |
54 | 802 | } |
55 | 4.42k | } |
56 | | |
57 | | /** |
58 | | * Unicode transcoding |
59 | | */ |
60 | 4.42k | { |
61 | | // UTF-8 to UTF-32 conversion |
62 | 4.42k | size_t utf32_len = |
63 | 4.42k | ada::idna::utf32_length_from_utf8(source.data(), source.size()); |
64 | 4.42k | if (utf32_len > 0 && utf32_len < 1024) { |
65 | 3.87k | std::vector<char32_t> utf32_buf(utf32_len + 1, 0); |
66 | 3.87k | size_t actual = ada::idna::utf8_to_utf32(source.data(), source.size(), |
67 | 3.87k | utf32_buf.data()); |
68 | 3.87k | length += actual; |
69 | | |
70 | | // UTF-32 to UTF-8 round-trip |
71 | 3.87k | if (actual > 0) { |
72 | 3.67k | size_t utf8_len = |
73 | 3.67k | ada::idna::utf8_length_from_utf32(utf32_buf.data(), actual); |
74 | 3.67k | if (utf8_len > 0 && utf8_len < 4096) { |
75 | 3.67k | std::string utf8_back(utf8_len, '\0'); |
76 | 3.67k | size_t written = ada::idna::utf32_to_utf8(utf32_buf.data(), actual, |
77 | 3.67k | utf8_back.data()); |
78 | 3.67k | length += written; |
79 | 3.67k | } |
80 | 3.67k | } |
81 | 3.87k | } |
82 | 4.42k | } |
83 | | |
84 | | /** |
85 | | * IDNA label validation |
86 | | */ |
87 | 4.42k | { |
88 | | // is_label_valid requires a UTF-32 string |
89 | 4.42k | std::u32string utf32_label; |
90 | 4.42k | bool ok = ada::idna::punycode_to_utf32(source2, utf32_label); |
91 | 4.42k | if (ok && !utf32_label.empty()) { |
92 | 177 | volatile bool label_valid = ada::idna::is_label_valid(utf32_label); |
93 | 177 | (void)label_valid; |
94 | 177 | } |
95 | | |
96 | | // Also test is_label_valid with direct ASCII-to-UTF32 conversion |
97 | 4.42k | std::u32string ascii_label(source2.begin(), source2.end()); |
98 | 4.42k | if (!ascii_label.empty()) { |
99 | 340 | volatile bool ascii_label_valid = ada::idna::is_label_valid(ascii_label); |
100 | 340 | (void)ascii_label_valid; |
101 | 340 | } |
102 | 4.42k | } |
103 | | |
104 | | /** |
105 | | * IDNA mapping |
106 | | */ |
107 | 4.42k | { |
108 | | // ASCII mapping: just lowercases ASCII characters |
109 | 4.42k | std::string ascii_copy = source; |
110 | 4.42k | ada::idna::ascii_map(ascii_copy.data(), ascii_copy.size()); |
111 | 4.42k | length += ascii_copy.size(); |
112 | | |
113 | | // Unicode mapping: maps UTF-32 characters according to IDNA |
114 | 4.42k | size_t utf32_len = |
115 | 4.42k | ada::idna::utf32_length_from_utf8(source.data(), source.size()); |
116 | 4.42k | if (utf32_len > 0 && utf32_len < 256) { |
117 | 3.84k | std::u32string utf32_input(utf32_len, 0); |
118 | 3.84k | size_t actual = ada::idna::utf8_to_utf32(source.data(), source.size(), |
119 | 3.84k | utf32_input.data()); |
120 | 3.84k | if (actual > 0) { |
121 | 3.65k | utf32_input.resize(actual); |
122 | 3.65k | std::u32string mapped = ada::idna::map(utf32_input); |
123 | 3.65k | length += mapped.size(); |
124 | 3.65k | } |
125 | 3.84k | } |
126 | 4.42k | } |
127 | | |
128 | | /** |
129 | | * Domain code point validation |
130 | | */ |
131 | 4.42k | { |
132 | 4.42k | volatile bool has_forbidden = |
133 | 4.42k | ada::idna::contains_forbidden_domain_code_point(source); |
134 | 4.42k | (void)has_forbidden; |
135 | | |
136 | | // is_ascii checks |
137 | 4.42k | volatile bool is_ascii_str = |
138 | 4.42k | ada::idna::is_ascii(std::string_view(source.data(), source.size())); |
139 | 4.42k | (void)is_ascii_str; |
140 | 4.42k | } |
141 | | |
142 | | /** |
143 | | * Normalization |
144 | | */ |
145 | 4.42k | { |
146 | 4.42k | size_t utf32_len = |
147 | 4.42k | ada::idna::utf32_length_from_utf8(source.data(), source.size()); |
148 | 4.42k | if (utf32_len > 0 && utf32_len < 256) { |
149 | 3.84k | std::u32string utf32_input(utf32_len, 0); |
150 | 3.84k | size_t actual = ada::idna::utf8_to_utf32(source.data(), source.size(), |
151 | 3.84k | utf32_input.data()); |
152 | 3.84k | if (actual > 0) { |
153 | 3.65k | utf32_input.resize(actual); |
154 | 3.65k | ada::idna::normalize(utf32_input); |
155 | 3.65k | length += utf32_input.size(); |
156 | 3.65k | } |
157 | 3.84k | } |
158 | 4.42k | } |
159 | | |
160 | | /** |
161 | | * IDNA stability property. |
162 | | * |
163 | | * Applying to_ascii twice must be idempotent: if to_ascii(x) produces a |
164 | | * non-empty result, then to_ascii(to_ascii(x)) must equal to_ascii(x). |
165 | | * A correctly normalised ACE label is already its own fixed point. |
166 | | * |
167 | | * We allow the second call to return an empty string only if the first |
168 | | * result was itself not a valid IDNA domain (some implementations return |
169 | | * empty on failure). If the first result is non-empty and looks like a |
170 | | * valid domain the second application must match. |
171 | | */ |
172 | 4.42k | { |
173 | 4.42k | if (!ascii_result.empty()) { |
174 | 2.71k | std::string ascii_result2 = ada::idna::to_ascii(ascii_result); |
175 | 2.71k | if (!ascii_result2.empty() && ascii_result2 != ascii_result) { |
176 | 0 | printf( |
177 | 0 | "IDNA to_ascii not idempotent!\n" |
178 | 0 | " input: %s\n first: %s\n second: %s\n", |
179 | 0 | source.c_str(), ascii_result.c_str(), ascii_result2.c_str()); |
180 | 0 | abort(); |
181 | 0 | } |
182 | 2.71k | } |
183 | 4.42k | } |
184 | | |
185 | | /** |
186 | | * to_unicode stability. |
187 | | * |
188 | | * Applying to_unicode twice should also be idempotent: once a domain is in |
189 | | * its Unicode presentation form, converting again should give the same |
190 | | * result. |
191 | | */ |
192 | 4.42k | { |
193 | 4.42k | if (!unicode_result.empty()) { |
194 | 3.89k | std::string unicode_result2 = ada::idna::to_unicode(unicode_result); |
195 | 3.89k | if (!unicode_result2.empty() && unicode_result2 != unicode_result) { |
196 | 0 | printf( |
197 | 0 | "IDNA to_unicode not idempotent!\n" |
198 | 0 | " input: %s\n first: %s\n second: %s\n", |
199 | 0 | source.c_str(), unicode_result.c_str(), unicode_result2.c_str()); |
200 | 0 | abort(); |
201 | 0 | } |
202 | 3.89k | } |
203 | 4.42k | } |
204 | | |
205 | | /** |
206 | | * Long domain names near the DNS length limit (253 characters). |
207 | | * |
208 | | * The IDNA and DNS-length checking code has special handling for domains |
209 | | * close to or exceeding 253/255 characters and 63-character labels. Feed |
210 | | * the fuzzer inputs of a controlled length to maximise coverage of those |
211 | | * boundary checks. |
212 | | */ |
213 | 4.42k | { |
214 | 4.42k | std::string long_domain = fdp.ConsumeRandomLengthString(270); |
215 | 4.42k | std::string long_ascii = ada::idna::to_ascii(long_domain); |
216 | 4.42k | length += long_ascii.size(); |
217 | | |
218 | | // verify_dns_length on the long input (already called for `source` above, |
219 | | // but we want to exercise the boundary cases separately). |
220 | 4.42k | volatile bool long_dns_ok = ada::checkers::verify_dns_length(long_domain); |
221 | 4.42k | (void)long_dns_ok; |
222 | | |
223 | 4.42k | if (!long_ascii.empty()) { |
224 | 195 | volatile bool long_ascii_dns_ok = |
225 | 195 | ada::checkers::verify_dns_length(long_ascii); |
226 | 195 | (void)long_ascii_dns_ok; |
227 | 195 | } |
228 | 4.42k | } |
229 | | |
230 | | /** |
231 | | * Punycode round-trip on arbitrary binary blobs. |
232 | | * |
233 | | * Feed random bytes directly into punycode_to_utf32, then if that succeeds |
234 | | * encode the result back with utf32_to_punycode and verify the round-trip. |
235 | | */ |
236 | 4.42k | { |
237 | 4.42k | std::string blob = fdp.ConsumeRandomLengthString(128); |
238 | 4.42k | std::u32string decoded; |
239 | 4.42k | bool ok = ada::idna::punycode_to_utf32(blob, decoded); |
240 | 4.42k | if (ok && !decoded.empty()) { |
241 | 41 | std::string reencoded; |
242 | 41 | bool enc_ok = ada::idna::utf32_to_punycode(decoded, reencoded); |
243 | 41 | (void)enc_ok; |
244 | 41 | length += reencoded.size(); |
245 | | |
246 | | // Re-decode the re-encoded form; it must match the first decoded form. |
247 | 41 | if (enc_ok && !reencoded.empty()) { |
248 | 38 | std::u32string redecoded; |
249 | 38 | bool redec_ok = ada::idna::punycode_to_utf32(reencoded, redecoded); |
250 | 38 | if (redec_ok && redecoded != decoded) { |
251 | 0 | printf("Punycode round-trip mismatch!\n"); |
252 | 0 | abort(); |
253 | 0 | } |
254 | 38 | } |
255 | 41 | } |
256 | 4.42k | } |
257 | | |
258 | 4.42k | return 0; |
259 | 4.42k | } |