Coverage Report

Created: 2026-10-15 06:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/curl_fuzzer/proto_fuzzer/fuzzer_main.cc
Line
Count
Source
1
/*
2
 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3
 *
4
 * SPDX-License-Identifier: curl
5
 */
6
7
/// @file
8
/// @brief Shared LPM mutation and execution for profile-split protobuf targets.
9
10
#include "proto_fuzzer/fuzzer_main.h"
11
12
#include <curl/curl.h>
13
#include <libprotobuf-mutator/src/libfuzzer/libfuzzer_macro.h>
14
15
#include <array>
16
#include <cassert>
17
#include <csignal>
18
#include <cstdio>
19
#include <cstdlib>
20
21
#include "curl_fuzzer.pb.h"
22
#include "proto_fuzzer/allocation_fault_injection.h"
23
#include "proto_fuzzer/scenario_runner.h"
24
#include "proto_fuzzer/target_policy.h"
25
26
namespace {
27
28
constexpr bool kUseBinaryFormat = true;
29
30
/// Register the fixed lane's normalizer before asking LPM to parse or mutate
31
/// its first input. LoadProtoInput runs LPM's Fix() only for direct corpus
32
/// loads; a just-mutated input is recovered from LPM's cache and has already
33
/// passed the same postprocessor. Ordering the registration here therefore
34
/// covers both paths without normalizing cached mutations twice.
35
///
36
/// One libFuzzer process exposes exactly one entrypoint and therefore one
37
/// profile. Capturing the first profile lets the shared runtime use LPM's
38
/// process-wide postprocessor registry without hiding target selection in a
39
/// compiler definition. The compatibility target deliberately registers
40
/// nothing, preserving its historical mixed-corpus mutation semantics.
41
364k
void EnsureTargetPostProcessor(proto_fuzzer::TargetProfile profile) {
42
364k
  if (profile == proto_fuzzer::TargetProfile::kCompatibility) {
43
28.7k
    return;
44
28.7k
  }
45
46
335k
  static const proto_fuzzer::TargetProfile registered_profile = profile;
47
335k
  static const protobuf_mutator::libfuzzer::PostProcessorRegistration<curl::fuzzer::proto::Scenario>
48
335k
      policy_registration([](curl::fuzzer::proto::Scenario* scenario, unsigned int /*seed*/) {
49
322k
        proto_fuzzer::NormalizeScenarioForTarget(scenario, registered_profile);
50
322k
      });
51
52
  // A second profile in one process would cause LPM's global registry to
53
  // enforce the wrong lane. Real fuzz binaries cannot do this; keep the
54
  // assertion to make misuse by future in-process callers immediately clear.
55
335k
  assert(profile == registered_profile);
56
335k
  (void)policy_registration;
57
335k
}
58
59
// Wire curl_global_init once so repeated fuzz iterations don't pay for it on every call. libFuzzer reuses the process;
60
// static ctors run once.
61
struct CurlGlobalBootstrap {
62
42
  CurlGlobalBootstrap() {
63
    // Open curl's TLS keylog at backend init. Curl_tls_keylog_open() runs once
64
    // inside curl_global_init, so setting this after bootstrap would be too
65
    // late. Keep the output in /dev/null and let a reproducer override it.
66
42
    (void)setenv("SSLKEYLOGFILE", "/dev/null", 0);
67
68
    // Keep parity with the legacy harness. libcurl normally suppresses
69
    // SIGPIPE for its own writes, but fuzzed connection lifecycles also race
70
    // mock-peer teardown; those failures should be reported as socket errors,
71
    // not mistaken for process crashes.
72
42
    std::signal(SIGPIPE, SIG_IGN);
73
42
    if (!proto_fuzzer::allocation_fault_injection::InitializeCurl()) {
74
0
      std::fputs("warning: libcurl allocation fault injection is unavailable\n", stderr);
75
0
      (void)curl_global_init(CURL_GLOBAL_ALL);
76
0
    }
77
42
  }
78
};
79
const CurlGlobalBootstrap kGlobalBootstrap;
80
81
#if defined(PROTO_FUZZER_OBSERVE_STATE)
82
/// Exercise the mutation ABI once in the profiling-only timing lane. Corpus
83
/// replay invokes test inputs but does not ask libFuzzer to mutate or cross
84
/// them, leaving the two target hooks invisible to coverage and introspection.
85
/// A fixed, bounded Scenario keeps the probe deterministic and also checks the
86
/// timing policy's follow-on backpressure normalization without executing the
87
/// generated children.
88
1
void ProbeTimingMutationHelpers() {
89
1
  constexpr std::size_t kBufferSize = 1024;
90
1
  constexpr unsigned int kMutationSeed = 0x4355524cU;
91
92
1
  curl::fuzzer::proto::Scenario scenario;
93
1
  scenario.set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
94
1
  scenario.set_host_path("timing.test/profile");
95
1
  scenario.mutable_connection();
96
1
  scenario.add_subsequent_connections()->mutable_backpressure()->set_drain_limit(1);
97
1
  proto_fuzzer::NormalizeScenarioForTarget(&scenario, proto_fuzzer::TargetProfile::kTiming);
98
99
1
  std::array<std::uint8_t, kBufferSize> first = {};
100
1
  const std::size_t original_size = scenario.ByteSizeLong();
101
1
  (void)scenario.SerializeWithCachedSizesToArray(first.data());
102
103
1
  std::array<std::uint8_t, kBufferSize> second = first;
104
1
  std::array<std::uint8_t, kBufferSize> child = {};
105
1
  const std::size_t mutated_size = proto_fuzzer::ProtoFuzzerCustomMutator(
106
1
      proto_fuzzer::TargetProfile::kTiming, first.data(), original_size, first.size(), kMutationSeed);
107
1
  const std::size_t crossed_size =
108
1
      proto_fuzzer::ProtoFuzzerCustomCrossOver(proto_fuzzer::TargetProfile::kTiming, first.data(), mutated_size,
109
1
                                               second.data(), original_size, child.data(), child.size(), kMutationSeed);
110
1
  const volatile std::size_t result_sizes[] = {mutated_size, crossed_size};
111
1
  (void)result_sizes;
112
1
}
113
#endif
114
115
}  // namespace
116
117
namespace proto_fuzzer {
118
119
/// Mutate a serialized Scenario while preserving the selected lane's policy.
120
/// @param profile Mutation and execution policy selected by the entrypoint.
121
/// @param data Mutable serialized Scenario storage.
122
/// @param size Current serialized size.
123
/// @param max_size Capacity of data.
124
/// @param seed Mutation random seed supplied by libFuzzer.
125
/// @return Serialized size after mutation.
126
std::size_t ProtoFuzzerCustomMutator(TargetProfile profile, std::uint8_t* data, std::size_t size, std::size_t max_size,
127
1
                                     unsigned int seed) {
128
1
  allocation_fault_injection::Reset();
129
1
  EnsureTargetPostProcessor(profile);
130
1
  curl::fuzzer::proto::Scenario scenario;
131
1
  return protobuf_mutator::libfuzzer::CustomProtoMutator(kUseBinaryFormat, data, size, max_size, seed, &scenario);
132
1
}
133
134
/// Cross two serialized Scenarios while preserving the selected lane's policy.
135
/// @param profile Mutation and execution policy selected by the entrypoint.
136
/// @param data1 First serialized parent.
137
/// @param size1 Size of data1.
138
/// @param data2 Second serialized parent.
139
/// @param size2 Size of data2.
140
/// @param out Destination storage.
141
/// @param max_out_size Capacity of out.
142
/// @param seed Crossover random seed supplied by libFuzzer.
143
/// @return Serialized child size.
144
std::size_t ProtoFuzzerCustomCrossOver(TargetProfile profile, const std::uint8_t* data1, std::size_t size1,
145
                                       const std::uint8_t* data2, std::size_t size2, std::uint8_t* out,
146
1
                                       std::size_t max_out_size, unsigned int seed) {
147
1
  allocation_fault_injection::Reset();
148
1
  EnsureTargetPostProcessor(profile);
149
1
  curl::fuzzer::proto::Scenario scenario1;
150
1
  curl::fuzzer::proto::Scenario scenario2;
151
1
  return protobuf_mutator::libfuzzer::CustomProtoCrossOver(kUseBinaryFormat, data1, size1, data2, size2, out,
152
1
                                                           max_out_size, seed, &scenario1, &scenario2);
153
1
}
154
155
/// Decode and run one serialized Scenario. Keeping this behind an ordinary
156
/// function allows each binary to expose a discoverable, same-named libFuzzer
157
/// entrypoint while retaining one mutation and execution implementation. The
158
/// profile is explicit so target identity remains visible at the entrypoint
159
/// instead of being hidden in per-translation-unit compiler definitions.
160
/// @param profile Mutation and execution policy selected by the entrypoint.
161
/// @param data Serialized binary Scenario bytes.
162
/// @param size Number of bytes available at data.
163
/// @return Always zero, as required by libFuzzer.
164
364k
int ProtoFuzzerTestOneInput(TargetProfile profile, const std::uint8_t* data, std::size_t size) {
165
364k
  allocation_fault_injection::Reset();
166
364k
  EnsureTargetPostProcessor(profile);
167
364k
#if defined(PROTO_FUZZER_OBSERVE_STATE)
168
364k
  if (profile == TargetProfile::kTiming) {
169
20.0k
    static const bool mutation_helpers_probed = [] {
170
1
      ProbeTimingMutationHelpers();
171
1
      return true;
172
1
    }();
173
20.0k
    (void)mutation_helpers_probed;
174
20.0k
  }
175
364k
#endif
176
364k
  curl::fuzzer::proto::Scenario scenario;
177
364k
  if (protobuf_mutator::libfuzzer::LoadProtoInput(kUseBinaryFormat, data, size, &scenario)) {
178
350k
    NormalizeScenarioForExecution(&scenario);
179
350k
    allocation_fault_injection::Configure(scenario.fail_allocations().data(),
180
350k
                                          static_cast<std::size_t>(scenario.fail_allocations_size()));
181
350k
    allocation_fault_injection::Arm();
182
350k
    proto_fuzzer::RunScenario(scenario, RunModeFor(profile));
183
350k
    allocation_fault_injection::Reset();
184
350k
  }
185
364k
  return 0;
186
364k
}
187
188
}  // namespace proto_fuzzer