Coverage Report

Created: 2026-04-28 07:09

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, 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
}