Coverage Report

Created: 2026-10-22 06:56

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 <cerrno>
14
#include <cstdlib>
15
#include <cstring>
16
#include <mutex>
17
18
#include "proto_fuzzer/scenario_limits.h"
19
20
namespace proto_fuzzer::allocation_fault_injection {
21
namespace {
22
23
/// Keep one thread's bounded failure plan and allocation counters without
24
/// allocating memory in the controller itself.
25
struct ThreadState {
26
  /// Sorted, distinct one-based ordinals; only ordinal_count entries are valid.
27
  std::array<std::uint32_t, scenario_limits::kMaxAllocationFailures> ordinals{};
28
  /// Number of valid ordinals retained in the fixed-capacity plan.
29
  std::size_t ordinal_count = 0;
30
  /// Index of the next configured ordinal that has not yet failed.
31
  std::size_t next_ordinal = 0;
32
  /// Positive-size allocation attempts observed while this thread is armed.
33
  std::uint64_t allocation_attempts = 0;
34
  /// Allocation attempts rejected at configured ordinals.
35
  std::uint64_t injected_failures = 0;
36
  /// Whether the plan is armed; suppression can still bypass its callbacks.
37
  bool armed = false;
38
};
39
40
/// Isolate each calling thread's failure plan and counters.
41
thread_local ThreadState state;
42
/// Install and verify libcurl's process-wide allocator callbacks only once.
43
std::once_flag initialize_once;
44
/// Remember whether the initialization probe reached the installed callbacks.
45
bool callbacks_active = false;
46
47
/// Fold a positive ordinal into the bounded one-based allocation range.
48
/// @param ordinal Requested allocation attempt number; zero disables the entry.
49
/// @return Zero for a disabled entry, otherwise a value in
50
/// 1..scenario_limits::kMaxAllocationOrdinal.
51
39.0k
std::uint32_t CanonicalOrdinal(std::uint32_t ordinal) noexcept {
52
39.0k
  if (ordinal == 0) {
53
0
    return 0;
54
0
  }
55
39.0k
  const std::uint32_t maximum = static_cast<std::uint32_t>(scenario_limits::kMaxAllocationOrdinal);
56
39.0k
  return 1U + ((ordinal - 1U) % maximum);
57
39.0k
}
58
59
/// Insert an ordinal into the calling thread's sorted, unique plan.
60
/// When the plan is full, retain the smallest ordinals by dropping its largest
61
/// entry if the new value sorts before it. Zero and duplicates are ignored.
62
/// @param ordinal Canonical allocation attempt number to retain.
63
39.0k
void InsertOrdinal(std::uint32_t ordinal) noexcept {
64
39.0k
  if (ordinal == 0) {
65
0
    return;
66
0
  }
67
39.0k
  const auto begin = state.ordinals.begin();
68
39.0k
  const auto end = begin + static_cast<std::ptrdiff_t>(state.ordinal_count);
69
39.0k
  const auto position = std::lower_bound(begin, end, ordinal);
70
39.0k
  if (position != end && *position == ordinal) {
71
0
    return;
72
0
  }
73
39.0k
  const std::size_t index = static_cast<std::size_t>(position - begin);
74
39.0k
  if (index >= state.ordinals.size()) {
75
0
    return;
76
0
  }
77
39.0k
  if (state.ordinal_count < state.ordinals.size()) {
78
39.0k
    ++state.ordinal_count;
79
39.0k
  }
80
39.0k
  for (std::size_t destination = state.ordinal_count - 1; destination > index; --destination) {
81
0
    state.ordinals[destination] = state.ordinals[destination - 1];
82
0
  }
83
39.0k
  state.ordinals[index] = ordinal;
84
39.0k
}
85
86
/// Count an armed positive-size allocation and reject a selected attempt.
87
/// Each configured ordinal fails once; rejected calls advance the plan and
88
/// failure counter and set errno to ENOMEM. Zero-size and disarmed calls leave
89
/// the controller state unchanged.
90
/// @param positive_size Whether the allocation requests nonzero storage.
91
/// @return true when the caller should reject this allocation.
92
61.2M
bool ShouldFail(bool positive_size) noexcept {
93
61.2M
  if (!positive_size || !state.armed || internal::FailuresSuppressed()) {
94
57.2M
    return false;
95
57.2M
  }
96
4.05M
  ++state.allocation_attempts;
97
4.05M
  if (state.next_ordinal < state.ordinal_count && state.allocation_attempts == state.ordinals[state.next_ordinal]) {
98
32.2k
    ++state.next_ordinal;
99
32.2k
    ++state.injected_failures;
100
32.2k
    errno = ENOMEM;
101
32.2k
    return true;
102
32.2k
  }
103
4.01M
  return false;
104
4.05M
}
105
106
}  // namespace
107
108
/// Install the allocator callbacks before any other libcurl global
109
/// initialization. The callbacks remain installed for the process lifetime;
110
/// individual inputs only change thread-local state. A small allocation
111
/// verifies that a pre-existing global initialization did not silently keep
112
/// the default callbacks.
113
/// @return true when libcurl invoked the installed callbacks.
114
42
bool InitializeCurl() noexcept {
115
42
  std::call_once(initialize_once, [] {
116
42
    if (curl_global_init_mem(CURL_GLOBAL_ALL, internal::Malloc, internal::Free, internal::Realloc, internal::Strdup,
117
42
                             internal::Calloc) != CURLE_OK) {
118
0
      return;
119
0
    }
120
121
42
    state.armed = true;
122
42
    char* probe = curl_escape("allocator callback probe", 0);
123
42
    callbacks_active = state.allocation_attempts != 0;
124
42
    state.armed = false;
125
42
    curl_free(probe);
126
42
    state = ThreadState{};
127
42
  });
128
42
  return callbacks_active;
129
42
}
130
131
/// Store a bounded, sorted, unique allocation plan for the calling thread.
132
/// Values outside the useful range are folded into it. Retaining the smallest
133
/// ordinals makes oversized compatibility inputs deterministic without heap
134
/// allocation in the controller itself.
135
/// @param ordinals One-based positive allocation attempt numbers.
136
/// @param count Number of values available at ordinals.
137
356k
void Configure(const std::uint32_t* ordinals, std::size_t count) noexcept {
138
356k
  state = ThreadState{};
139
356k
  if (ordinals == nullptr) {
140
0
    return;
141
0
  }
142
395k
  for (std::size_t index = 0; index < count; ++index) {
143
39.0k
    InsertOrdinal(CanonicalOrdinal(ordinals[index]));
144
39.0k
  }
145
356k
}
146
147
/// Begin counting positive-size allocation attempts on the calling thread.
148
/// Each configured ordinal can fail once. A later ordinal is evaluated on the
149
/// execution path already changed by every earlier injected failure.
150
356k
void Arm() noexcept {
151
356k
  state.next_ordinal = 0;
152
356k
  state.allocation_attempts = 0;
153
356k
  state.injected_failures = 0;
154
356k
  state.armed = state.ordinal_count != 0;
155
356k
}
156
157
/// Stop fault injection while retaining counters for diagnostics and tests.
158
0
void Disarm() noexcept { state.armed = false; }
159
160
/// Clear all thread-local controller state and invalidate live suppression
161
/// guards. Fuzzer entrypoints call this before parsing so a standalone-runner
162
/// siglongjmp cannot leak an armed plan or suppression into the next input.
163
727k
void Reset() noexcept {
164
727k
  state = ThreadState{};
165
727k
  internal::ResetFailureSuppression();
166
727k
}
167
168
/// Read the calling thread's current plan and counters.
169
/// @return A value snapshot that remains valid after later controller calls.
170
0
Statistics GetStatistics() noexcept {
171
0
  return {state.allocation_attempts, state.injected_failures, state.ordinal_count,
172
0
          state.armed && !internal::FailuresSuppressed()};
173
0
}
174
175
namespace internal {
176
177
/// Allocate with libc unless this positive-size attempt is selected to fail.
178
21.9M
void* Malloc(std::size_t size) noexcept { return ShouldFail(size != 0) ? nullptr : std::malloc(size); }
179
180
/// Release an allocation. Frees are never counted or rejected.
181
62.1M
void Free(void* pointer) noexcept { std::free(pointer); }
182
183
/// Resize with libc unless this positive-size attempt is selected to fail.
184
/// Rejecting the call before libc preserves ownership of the original block.
185
12.5M
void* Realloc(void* pointer, std::size_t size) noexcept {
186
12.5M
  return ShouldFail(size != 0) ? nullptr : std::realloc(pointer, size);
187
12.5M
}
188
189
/// Duplicate one string while counting the operation as one allocation.
190
0
char* Strdup(const char* string) noexcept {
191
0
  if (string == nullptr) {
192
0
    return nullptr;
193
0
  }
194
0
  const std::size_t length = std::strlen(string) + 1;
195
0
  if (ShouldFail(length != 0)) {
196
0
    return nullptr;
197
0
  }
198
0
  char* copy = static_cast<char*>(std::malloc(length));
199
0
  if (copy != nullptr) {
200
0
    std::memcpy(copy, string, length);
201
0
  }
202
0
  return copy;
203
0
}
204
205
/// Allocate zero-filled storage unless this positive-size request is selected
206
/// to fail. The request counts once rather than once per element.
207
26.8M
void* Calloc(std::size_t count, std::size_t size) noexcept {
208
26.8M
  return ShouldFail(count != 0 && size != 0) ? nullptr : std::calloc(count, size);
209
26.8M
}
210
211
}  // namespace internal
212
}  // namespace proto_fuzzer::allocation_fault_injection