Coverage Report

Created: 2026-10-28 07:17

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/curl_fuzzer/proto_fuzzer/allocation_fault_injection.cc
Line
Count
Source
1
/*
2
 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3
 *
4
 * SPDX-License-Identifier: curl
5
 */
6
7
#include "proto_fuzzer/allocation_fault_injection.h"
8
9
#include <curl/curl.h>
10
11
#include <algorithm>
12
#include <array>
13
#include <atomic>
14
#include <cerrno>
15
#include <cstdlib>
16
#include <cstring>
17
#include <mutex>
18
19
#include "proto_fuzzer/scenario_limits.h"
20
21
namespace proto_fuzzer::allocation_fault_injection {
22
namespace {
23
24
/// Keep one thread's bounded failure plan and allocation counters without
25
/// allocating memory in the controller itself.
26
struct ThreadState {
27
  /// Sorted, distinct one-based ordinals; only ordinal_count entries are valid.
28
  std::array<std::uint32_t, scenario_limits::kMaxAllocationFailures> ordinals{};
29
  /// Number of valid ordinals retained in the fixed-capacity plan.
30
  std::size_t ordinal_count = 0;
31
  /// Index of the next configured ordinal that has not yet failed.
32
  std::size_t next_ordinal = 0;
33
  /// Positive-size allocation attempts observed while this thread is armed.
34
  std::uint64_t allocation_attempts = 0;
35
  /// Allocation attempts rejected at configured ordinals.
36
  std::uint64_t injected_failures = 0;
37
  /// Whether the plan is armed; suppression can still bypass its callbacks.
38
  bool armed = false;
39
};
40
41
/// Isolate each calling thread's failure plan and counters.
42
thread_local ThreadState state;
43
/// Install and verify libcurl's process-wide allocator callbacks only once.
44
std::once_flag initialize_once;
45
/// Remember whether the initialization probe reached the installed callbacks.
46
bool callbacks_active = false;
47
/// Remember whether this module owns one successful curl global initialization.
48
bool curl_initialized = false;
49
/// Transfer that one initialization reference to at most one lifecycle owner.
50
std::atomic<bool> curl_initialization_claimed{false};
51
52
/// Fold a positive ordinal into the bounded one-based allocation range.
53
/// @param ordinal Requested allocation attempt number; zero disables the entry.
54
/// @return Zero for a disabled entry, otherwise a value in
55
/// 1..scenario_limits::kMaxAllocationOrdinal.
56
66.5k
std::uint32_t CanonicalOrdinal(std::uint32_t ordinal) noexcept {
57
66.5k
  if (ordinal == 0) {
58
0
    return 0;
59
0
  }
60
66.5k
  const std::uint32_t maximum = static_cast<std::uint32_t>(scenario_limits::kMaxAllocationOrdinal);
61
66.5k
  return 1U + ((ordinal - 1U) % maximum);
62
66.5k
}
63
64
/// Insert an ordinal into the calling thread's sorted, unique plan.
65
/// When the plan is full, retain the smallest ordinals by dropping its largest
66
/// entry if the new value sorts before it. Zero and duplicates are ignored.
67
/// @param ordinal Canonical allocation attempt number to retain.
68
66.5k
void InsertOrdinal(std::uint32_t ordinal) noexcept {
69
66.5k
  if (ordinal == 0) {
70
0
    return;
71
0
  }
72
66.5k
  const auto begin = state.ordinals.begin();
73
66.5k
  const auto end = begin + static_cast<std::ptrdiff_t>(state.ordinal_count);
74
66.5k
  const auto position = std::lower_bound(begin, end, ordinal);
75
66.5k
  if (position != end && *position == ordinal) {
76
0
    return;
77
0
  }
78
66.5k
  const std::size_t index = static_cast<std::size_t>(position - begin);
79
66.5k
  if (index >= state.ordinals.size()) {
80
0
    return;
81
0
  }
82
66.5k
  if (state.ordinal_count < state.ordinals.size()) {
83
66.5k
    ++state.ordinal_count;
84
66.5k
  }
85
66.5k
  for (std::size_t destination = state.ordinal_count - 1; destination > index; --destination) {
86
0
    state.ordinals[destination] = state.ordinals[destination - 1];
87
0
  }
88
66.5k
  state.ordinals[index] = ordinal;
89
66.5k
}
90
91
/// Count an armed positive-size allocation and reject a selected attempt.
92
/// Each configured ordinal fails once; rejected calls advance the plan and
93
/// failure counter and set errno to ENOMEM. Zero-size and disarmed calls leave
94
/// the controller state unchanged.
95
/// @param positive_size Whether the allocation requests nonzero storage.
96
/// @return true when the caller should reject this allocation.
97
33.8M
bool ShouldFail(bool positive_size) noexcept {
98
33.8M
  if (!positive_size || !state.armed || internal::FailuresSuppressed()) {
99
28.3M
    return false;
100
28.3M
  }
101
5.51M
  ++state.allocation_attempts;
102
5.51M
  if (state.next_ordinal < state.ordinal_count && state.allocation_attempts == state.ordinals[state.next_ordinal]) {
103
49.7k
    ++state.next_ordinal;
104
49.7k
    ++state.injected_failures;
105
49.7k
    errno = ENOMEM;
106
49.7k
    return true;
107
49.7k
  }
108
5.46M
  return false;
109
5.51M
}
110
111
}  // namespace
112
113
/// Install the allocator callbacks before any other libcurl global
114
/// initialization. The callbacks remain installed for the process lifetime;
115
/// individual inputs only change thread-local state. A small allocation
116
/// verifies that a pre-existing global initialization did not silently keep
117
/// the default callbacks.
118
/// @return true when libcurl invoked the installed callbacks.
119
120
bool InitializeCurl() noexcept {
120
120
  std::call_once(initialize_once, [] {
121
60
    if (curl_global_init_mem(CURL_GLOBAL_ALL, internal::Malloc, internal::Free, internal::Realloc, internal::Strdup,
122
60
                             internal::Calloc) != CURLE_OK) {
123
0
      return;
124
0
    }
125
60
    curl_initialized = true;
126
127
60
    state.armed = true;
128
60
    char* probe = curl_escape("allocator callback probe", 0);
129
60
    callbacks_active = state.allocation_attempts != 0;
130
60
    state.armed = false;
131
60
    curl_free(probe);
132
60
    state = ThreadState{};
133
60
  });
134
120
  return callbacks_active;
135
120
}
136
137
/// Reinstall the same allocator callbacks after a deliberate, balanced
138
/// curl_global_cleanup(). Call only when no curl handles or workers remain.
139
/// @return true when libcurl accepted the callbacks.
140
143
bool ReinitializeCurl() noexcept {
141
143
  return curl_global_init_mem(CURL_GLOBAL_ALL, internal::Malloc, internal::Free, internal::Realloc, internal::Strdup,
142
143
                              internal::Calloc) == CURLE_OK;
143
143
}
144
145
/// Report whether InitializeCurl owns a successful global initialization.
146
/// This is distinct from its return value: allocation callbacks can be
147
/// unavailable even though curl_global_init_mem itself succeeded.
148
0
bool CurlInitializationSucceeded() noexcept {
149
0
  (void)InitializeCurl();
150
0
  return curl_initialized;
151
0
}
152
153
/// Claim ownership of InitializeCurl's retained global reference exactly once.
154
/// A failed initialization has no reference to claim, and a reference remains
155
/// claimed after cleanup because initialize_once cannot create it again.
156
/// @return true only for the caller that acquires the retained reference.
157
60
bool ClaimCurlInitializationReference() noexcept {
158
60
  (void)InitializeCurl();
159
60
  if (!curl_initialized) {
160
0
    return false;
161
0
  }
162
60
  bool expected = false;
163
60
  return curl_initialization_claimed.compare_exchange_strong(expected, true, std::memory_order_relaxed,
164
60
                                                             std::memory_order_relaxed);
165
60
}
166
167
/// Store a bounded, sorted, unique allocation plan for the calling thread.
168
/// Values outside the useful range are folded into it. Retaining the smallest
169
/// ordinals makes oversized compatibility inputs deterministic without heap
170
/// allocation in the controller itself.
171
/// @param ordinals One-based positive allocation attempt numbers.
172
/// @param count Number of values available at ordinals.
173
240k
void Configure(const std::uint32_t* ordinals, std::size_t count) noexcept {
174
240k
  state = ThreadState{};
175
240k
  if (ordinals == nullptr) {
176
0
    return;
177
0
  }
178
307k
  for (std::size_t index = 0; index < count; ++index) {
179
66.5k
    InsertOrdinal(CanonicalOrdinal(ordinals[index]));
180
66.5k
  }
181
240k
}
182
183
/// Begin counting positive-size allocation attempts on the calling thread.
184
/// Each configured ordinal can fail once. A later ordinal is evaluated on the
185
/// execution path already changed by every earlier injected failure.
186
240k
void Arm() noexcept {
187
240k
  state.next_ordinal = 0;
188
240k
  state.allocation_attempts = 0;
189
240k
  state.injected_failures = 0;
190
240k
  state.armed = state.ordinal_count != 0;
191
240k
}
192
193
/// Stop fault injection while retaining counters for diagnostics and tests.
194
0
void Disarm() noexcept { state.armed = false; }
195
196
/// Clear all thread-local controller state and invalidate live suppression
197
/// guards. Fuzzer entrypoints call this before parsing so a standalone-runner
198
/// siglongjmp cannot leak an armed plan or suppression into the next input.
199
486k
void Reset() noexcept {
200
486k
  state = ThreadState{};
201
486k
  internal::ResetFailureSuppression();
202
486k
}
203
204
/// Read the calling thread's current plan and counters.
205
/// @return A value snapshot that remains valid after later controller calls.
206
0
Statistics GetStatistics() noexcept {
207
0
  return {state.allocation_attempts, state.injected_failures, state.ordinal_count,
208
0
          state.armed && !internal::FailuresSuppressed()};
209
0
}
210
211
namespace internal {
212
213
/// Allocate with libc unless this positive-size attempt is selected to fail.
214
13.0M
void* Malloc(std::size_t size) noexcept { return ShouldFail(size != 0) ? nullptr : std::malloc(size); }
215
216
/// Release an allocation. Frees are never counted or rejected.
217
34.2M
void Free(void* pointer) noexcept { std::free(pointer); }
218
219
/// Resize with libc unless this positive-size attempt is selected to fail.
220
/// Rejecting the call before libc preserves ownership of the original block.
221
6.19M
void* Realloc(void* pointer, std::size_t size) noexcept {
222
6.19M
  return ShouldFail(size != 0) ? nullptr : std::realloc(pointer, size);
223
6.19M
}
224
225
/// Duplicate one string while counting the operation as one allocation.
226
0
char* Strdup(const char* string) noexcept {
227
0
  if (string == nullptr) {
228
0
    return nullptr;
229
0
  }
230
0
  const std::size_t length = std::strlen(string) + 1;
231
0
  if (ShouldFail(length != 0)) {
232
0
    return nullptr;
233
0
  }
234
0
  char* copy = static_cast<char*>(std::malloc(length));
235
0
  if (copy != nullptr) {
236
0
    std::memcpy(copy, string, length);
237
0
  }
238
0
  return copy;
239
0
}
240
241
/// Allocate zero-filled storage unless this positive-size request is selected
242
/// to fail. The request counts once rather than once per element.
243
14.6M
void* Calloc(std::size_t count, std::size_t size) noexcept {
244
14.6M
  return ShouldFail(count != 0 && size != 0) ? nullptr : std::calloc(count, size);
245
14.6M
}
246
247
}  // namespace internal
248
}  // namespace proto_fuzzer::allocation_fault_injection