/src/curl_fuzzer/fuzz_doh.cc
Line | Count | Source |
1 | | /* |
2 | | * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al. |
3 | | * |
4 | | * SPDX-License-Identifier: curl |
5 | | */ |
6 | | |
7 | | // Direct fuzz harness for curl's DOH (DNS-over-HTTPS) parser entrypoints. |
8 | | // Targets the attack surface exposed to a compromised DOH server: raw DNS |
9 | | // wire-format bytes flowing into doh_resp_decode, HTTPS RR RDATA flowing into |
10 | | // doh_resp_decode_httpsrr, plus the request-side doh_req_encode. Deliberately |
11 | | // skips the network-plumbing paths around Curl_doh / doh_probe_run - those are |
12 | | // reachable via curl_fuzzer_proto if / when we wire them up separately. |
13 | | // |
14 | | // doh.h cannot be included directly because it pulls in urldata.h and most |
15 | | // of curl's internal header tree. Instead we forward-declare the handful of |
16 | | // symbols and constants we need; the real definitions live in the curl static |
17 | | // lib, exported because the repo-level build now compiles curl with -DUNITTESTS |
18 | | // (see CMakeLists.txt). Internal structs are treated as opaque: dohentry gets |
19 | | // aligned storage sized well above the real struct, while Curl_https_rrinfo is |
20 | | // allocated and destroyed entirely by curl. |
21 | | |
22 | | #include <stddef.h> |
23 | | #include <stdint.h> |
24 | | #include <cstring> |
25 | | #include <signal.h> |
26 | | #include <string> |
27 | | #include <vector> |
28 | | |
29 | | #include <curl/curl.h> |
30 | | |
31 | | extern "C" { |
32 | | |
33 | | // Mirror curl's DNStype enum values (lib/doh.h). |
34 | | typedef enum { |
35 | | CURL_DNS_TYPE_A = 1, |
36 | | CURL_DNS_TYPE_AAAA = 28, |
37 | | CURL_DNS_TYPE_HTTPS = 65 |
38 | | } DNStype; |
39 | | |
40 | | // Match DOH_MAX_DNSREQ_SIZE from lib/doh.h — size of the request buffer |
41 | | // doh_req_encode writes into. The real caller in doh.c uses the same. |
42 | | #define FUZZ_DOH_MAX_DNSREQ_SIZE (256 + 16) |
43 | | |
44 | | // Opaque dohentry — forward-declared so we can pass pointers into curl. |
45 | | struct dohentry; |
46 | | struct Curl_easy; |
47 | | struct Curl_https_rrinfo; |
48 | | |
49 | | void de_init(struct dohentry *de); |
50 | | void de_cleanup(struct dohentry *d); |
51 | | int doh_resp_decode(const unsigned char *doh, size_t dohlen, |
52 | | DNStype dnstype, struct dohentry *d); |
53 | | int doh_req_encode(const char *host, DNStype dnstype, |
54 | | unsigned char *dnsp, size_t len, size_t *olen); |
55 | | CURLcode doh_resp_decode_httpsrr(struct Curl_easy *data, |
56 | | const unsigned char *cp, size_t len, |
57 | | struct Curl_https_rrinfo **hrr); |
58 | | void Curl_httpsrr_destroy(struct Curl_https_rrinfo *rrinfo); |
59 | | |
60 | | } // extern "C" |
61 | | |
62 | | namespace { |
63 | | |
64 | | constexpr uint8_t kHttpsRrRdataSelector = 4; |
65 | | |
66 | | // The inner decoder only needs a handle for curl's optional trace plumbing; |
67 | | // it does not mutate transfer state. Reuse one process-lifetime handle so the |
68 | | // high-throughput parser target does not allocate an entire easy handle for |
69 | | // every HTTPS RR input. |
70 | | class HttpsRrTraceHandle { |
71 | | public: |
72 | 1 | HttpsRrTraceHandle() : easy_(curl_easy_init()) {} |
73 | 1 | ~HttpsRrTraceHandle() { |
74 | 1 | if (easy_) { |
75 | 1 | curl_easy_cleanup(easy_); |
76 | 1 | } |
77 | 1 | } |
78 | | |
79 | | HttpsRrTraceHandle(const HttpsRrTraceHandle &) = delete; |
80 | | HttpsRrTraceHandle &operator=(const HttpsRrTraceHandle &) = delete; |
81 | | |
82 | 1.00k | CURL *get() const { return easy_; } |
83 | | |
84 | | private: |
85 | | CURL *easy_; |
86 | | }; |
87 | | |
88 | 1.00k | CURL *GetHttpsRrTraceHandle() { |
89 | 1.00k | static HttpsRrTraceHandle handle; |
90 | 1.00k | return handle.get(); |
91 | 1.00k | } |
92 | | |
93 | | // Run de_init / doh_resp_decode / de_cleanup against a byte payload for a |
94 | | // given DNS record type. Uses an aligned byte buffer as opaque storage for |
95 | | // the dohentry (real size is ~660 bytes in our DEBUGBUILD; 4 KiB with 16-byte |
96 | | // alignment is generous headroom for any future layout growth). |
97 | 1.22k | void RunDecode(const uint8_t *body, size_t len, DNStype dnstype) { |
98 | 1.22k | alignas(16) unsigned char de_storage[4096]; |
99 | 1.22k | auto *de = reinterpret_cast<struct dohentry *>(de_storage); |
100 | 1.22k | de_init(de); |
101 | 1.22k | (void)doh_resp_decode(body, len, dnstype, de); |
102 | 1.22k | de_cleanup(de); |
103 | 1.22k | } |
104 | | |
105 | | // Exercise the request-encoder with the payload interpreted as a hostname. |
106 | | // doh_req_encode measures the host with strlen() and asserts the result is |
107 | | // non-zero (DEBUGASSERT(hostlen) at doh.c:105). The real caller in doh.c is |
108 | | // fed from name resolution, which can't produce an empty string, so guard on |
109 | | // the strlen here too — including the leading-NUL case where the payload is |
110 | | // non-empty but the C-string representation is zero-length. |
111 | 153 | void RunEncode(const uint8_t *body, size_t len) { |
112 | 153 | std::string host(reinterpret_cast<const char *>(body), len); |
113 | 153 | if (std::strlen(host.c_str()) == 0) { |
114 | 8 | return; |
115 | 8 | } |
116 | 145 | unsigned char req[FUZZ_DOH_MAX_DNSREQ_SIZE]; |
117 | 145 | size_t olen = 0; |
118 | 145 | (void)doh_req_encode(host.c_str(), CURL_DNS_TYPE_A, req, sizeof(req), &olen); |
119 | 145 | } |
120 | | |
121 | | // Decode the RDATA portion of an HTTPS resource record. A real easy handle is |
122 | | // required because verbose builds trace each successfully decoded SvcParam |
123 | | // through the handle. Curl owns every allocation inside the opaque result. |
124 | 1.01k | void RunHttpsRrDecode(const uint8_t *body, size_t len) { |
125 | | // The outer DNS decoder stores RDLENGTH in 16 bits before handing this |
126 | | // exact byte range to the inner parser. |
127 | 1.01k | if (len > UINT16_MAX) { |
128 | 4 | return; |
129 | 4 | } |
130 | | |
131 | 1.00k | CURL *easy = GetHttpsRrTraceHandle(); |
132 | 1.00k | if (!easy) { |
133 | 0 | return; |
134 | 0 | } |
135 | | |
136 | 1.00k | struct Curl_https_rrinfo *hrr = nullptr; |
137 | 1.00k | (void)doh_resp_decode_httpsrr(reinterpret_cast<struct Curl_easy *>(easy), |
138 | 1.00k | body, len, &hrr); |
139 | 1.00k | Curl_httpsrr_destroy(hrr); |
140 | 1.00k | } |
141 | | |
142 | | } // namespace |
143 | | |
144 | | // Fuzzing entry point. First byte selects the parser target; remaining bytes |
145 | | // are the payload fed to that parser. One byte of in-band selection lets |
146 | | // libFuzzer cross-pollinate between targets from a shared corpus rather than |
147 | | // maintaining one corpus per entrypoint. We read the selector from data[0] |
148 | | // directly rather than through FuzzedDataProvider because the latter consumes |
149 | | // integrals from the tail of the buffer, which would put the selector in the |
150 | | // wrong place for seeds authored with the selector up front. |
151 | 2.38k | extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { |
152 | | // Ignore SIGPIPE in case any decoder path touches stderr through curl's |
153 | | // trace machinery and hits a closed pipe during fuzzer harness runs. |
154 | 2.38k | signal(SIGPIPE, SIG_IGN); |
155 | | |
156 | 2.38k | if (size < 1) { |
157 | 0 | return 0; |
158 | 0 | } |
159 | | // Values 0 through 3 retain their original meanings. HTTPS RR RDATA uses a |
160 | | // new value rather than widening the old mask, which would silently remap |
161 | | // existing corpus inputs with high selector bits set. |
162 | 2.38k | const uint8_t raw_selector = data[0]; |
163 | 2.38k | const uint8_t *payload = data + 1; |
164 | 2.38k | const size_t payload_len = size - 1; |
165 | | |
166 | 2.38k | if (raw_selector == kHttpsRrRdataSelector) { |
167 | 1.01k | RunHttpsRrDecode(payload, payload_len); |
168 | 1.01k | return 0; |
169 | 1.01k | } |
170 | | |
171 | 1.37k | const uint8_t selector = raw_selector & 0x03; |
172 | 1.37k | switch (selector) { |
173 | 590 | case 0: |
174 | 590 | RunDecode(payload, payload_len, CURL_DNS_TYPE_A); |
175 | 590 | break; |
176 | 381 | case 1: |
177 | 381 | RunDecode(payload, payload_len, CURL_DNS_TYPE_AAAA); |
178 | 381 | break; |
179 | 250 | case 2: |
180 | 250 | RunDecode(payload, payload_len, CURL_DNS_TYPE_HTTPS); |
181 | 250 | break; |
182 | 153 | default: |
183 | 153 | RunEncode(payload, payload_len); |
184 | 153 | break; |
185 | 1.37k | } |
186 | | |
187 | 1.37k | return 0; |
188 | 1.37k | } |