/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 |