/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, plus the request-side |
10 | | // doh_req_encode. Deliberately skips the network-plumbing paths around |
11 | | // Curl_doh / doh_probe_run - those are reachable via curl_fuzzer_proto if / |
12 | | // 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 |
17 | | // static lib, exported because the repo-level build now compiles curl with |
18 | | // -DUNITTESTS (see CMakeLists.txt). struct dohentry is treated as opaque - |
19 | | // we hand the parser an aligned byte buffer sized well above the real |
20 | | // struct and let the parser's compiled-in layout knowledge handle the rest. |
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 | | |
47 | | void de_init(struct dohentry *de); |
48 | | void de_cleanup(struct dohentry *d); |
49 | | int doh_resp_decode(const unsigned char *doh, size_t dohlen, |
50 | | DNStype dnstype, struct dohentry *d); |
51 | | int doh_req_encode(const char *host, DNStype dnstype, |
52 | | unsigned char *dnsp, size_t len, size_t *olen); |
53 | | |
54 | | } // extern "C" |
55 | | |
56 | | namespace { |
57 | | |
58 | | // Run de_init / doh_resp_decode / de_cleanup against a byte payload for a |
59 | | // given DNS record type. Uses an aligned byte buffer as opaque storage for |
60 | | // the dohentry (real size is ~660 bytes in our DEBUGBUILD; 4 KiB with 16-byte |
61 | | // alignment is generous headroom for any future layout growth). |
62 | 1.26k | void RunDecode(const uint8_t *body, size_t len, DNStype dnstype) { |
63 | 1.26k | alignas(16) unsigned char de_storage[4096]; |
64 | 1.26k | auto *de = reinterpret_cast<struct dohentry *>(de_storage); |
65 | 1.26k | de_init(de); |
66 | 1.26k | (void)doh_resp_decode(body, len, dnstype, de); |
67 | 1.26k | de_cleanup(de); |
68 | 1.26k | } |
69 | | |
70 | | // Exercise the request-encoder with the payload interpreted as a hostname. |
71 | | // doh_req_encode measures the host with strlen() and asserts the result is |
72 | | // non-zero (DEBUGASSERT(hostlen) at doh.c:105). The real caller in doh.c is |
73 | | // fed from name resolution, which can't produce an empty string, so guard on |
74 | | // the strlen here too — including the leading-NUL case where the payload is |
75 | | // non-empty but the C-string representation is zero-length. |
76 | 138 | void RunEncode(const uint8_t *body, size_t len) { |
77 | 138 | std::string host(reinterpret_cast<const char *>(body), len); |
78 | 138 | if (std::strlen(host.c_str()) == 0) { |
79 | 10 | return; |
80 | 10 | } |
81 | 128 | unsigned char req[FUZZ_DOH_MAX_DNSREQ_SIZE]; |
82 | 128 | size_t olen = 0; |
83 | 128 | (void)doh_req_encode(host.c_str(), CURL_DNS_TYPE_A, req, sizeof(req), &olen); |
84 | 128 | } |
85 | | |
86 | | } // namespace |
87 | | |
88 | | // Fuzzing entry point. First byte selects the parser target; remaining bytes |
89 | | // are the payload fed to that parser. One byte of in-band selection lets |
90 | | // libFuzzer cross-pollinate between targets from a shared corpus rather than |
91 | | // maintaining one corpus per entrypoint. We read the selector from data[0] |
92 | | // directly rather than through FuzzedDataProvider because the latter consumes |
93 | | // integrals from the tail of the buffer, which would put the selector in the |
94 | | // wrong place for seeds authored with the selector up front. |
95 | 1.40k | extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { |
96 | | // Ignore SIGPIPE in case any decoder path touches stderr through curl's |
97 | | // trace machinery and hits a closed pipe during fuzzer harness runs. |
98 | 1.40k | signal(SIGPIPE, SIG_IGN); |
99 | | |
100 | 1.40k | if (size < 1) { |
101 | 0 | return 0; |
102 | 0 | } |
103 | 1.40k | const uint8_t selector = data[0] & 0x03; |
104 | 1.40k | const uint8_t *payload = data + 1; |
105 | 1.40k | const size_t payload_len = size - 1; |
106 | | |
107 | 1.40k | switch (selector) { |
108 | 728 | case 0: |
109 | 728 | RunDecode(payload, payload_len, CURL_DNS_TYPE_A); |
110 | 728 | break; |
111 | 348 | case 1: |
112 | 348 | RunDecode(payload, payload_len, CURL_DNS_TYPE_AAAA); |
113 | 348 | break; |
114 | 186 | case 2: |
115 | 186 | RunDecode(payload, payload_len, CURL_DNS_TYPE_HTTPS); |
116 | 186 | break; |
117 | 138 | default: |
118 | 138 | RunEncode(payload, payload_len); |
119 | 138 | break; |
120 | 1.40k | } |
121 | | |
122 | 1.40k | return 0; |
123 | 1.40k | } |