Coverage Report

Created: 2026-09-14 07:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}