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/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 positive-size callbacks should be counted and checked for failure.
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
26.6k
std::uint32_t CanonicalOrdinal(std::uint32_t ordinal) noexcept {
52
26.6k
  if (ordinal == 0) {
53
0
    return 0;
54
0
  }
55
26.6k
  const std::uint32_t maximum = static_cast<std::uint32_t>(scenario_limits::kMaxAllocationOrdinal);
56
26.6k
  return 1U + ((ordinal - 1U) % maximum);
57
26.6k
}
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
26.6k
void InsertOrdinal(std::uint32_t ordinal) noexcept {
64
26.6k
  if (ordinal == 0) {
65
0
    return;
66
0
  }
67
26.6k
  const auto begin = state.ordinals.begin();
68
26.6k
  const auto end = begin + static_cast<std::ptrdiff_t>(state.ordinal_count);
69
26.6k
  const auto position = std::lower_bound(begin, end, ordinal);
70
26.6k
  if (position != end && *position == ordinal) {
71
0
    return;
72
0
  }
73
26.6k
  const std::size_t index = static_cast<std::size_t>(position - begin);
74
26.6k
  if (index >= state.ordinals.size()) {
75
0
    return;
76
0
  }
77
26.6k
  if (state.ordinal_count < state.ordinals.size()) {
78
26.6k
    ++state.ordinal_count;
79
26.6k
  }
80
26.6k
  for (std::size_t destination = state.ordinal_count - 1; destination > index; --destination) {
81
0
    state.ordinals[destination] = state.ordinals[destination - 1];
82
0
  }
83
26.6k
  state.ordinals[index] = ordinal;
84
26.6k
}
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
62.1M
bool ShouldFail(bool positive_size) noexcept {
93
62.1M
  if (!positive_size || !state.armed) {
94
59.9M
    return false;
95
59.9M
  }
96
2.19M
  ++state.allocation_attempts;
97
2.19M
  if (state.next_ordinal < state.ordinal_count && state.allocation_attempts == state.ordinals[state.next_ordinal]) {
98
22.4k
    ++state.next_ordinal;
99
22.4k
    ++state.injected_failures;
100
22.4k
    errno = ENOMEM;
101
22.4k
    return true;
102
22.4k
  }
103
2.17M
  return false;
104
2.19M
}
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
350k
void Configure(const std::uint32_t* ordinals, std::size_t count) noexcept {
138
350k
  state = ThreadState{};
139
350k
  if (ordinals == nullptr) {
140
0
    return;
141
0
  }
142
377k
  for (std::size_t index = 0; index < count; ++index) {
143
26.6k
    InsertOrdinal(CanonicalOrdinal(ordinals[index]));
144
26.6k
  }
145
350k
}
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
350k
void Arm() noexcept {
151
350k
  state.next_ordinal = 0;
152
350k
  state.allocation_attempts = 0;
153
350k
  state.injected_failures = 0;
154
350k
  state.armed = state.ordinal_count != 0;
155
350k
}
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. Fuzzer entrypoints call this
161
/// before parsing so a standalone-runner siglongjmp cannot leak an armed plan
162
/// into the next input.
163
715k
void Reset() noexcept { state = ThreadState{}; }
164
165
/// Read the calling thread's current plan and counters.
166
/// @return A value snapshot that remains valid after later controller calls.
167
0
Statistics GetStatistics() noexcept {
168
0
  return {state.allocation_attempts, state.injected_failures, state.ordinal_count, state.armed};
169
0
}
170
171
namespace internal {
172
173
/// Allocate with libc unless this positive-size attempt is selected to fail.
174
22.0M
void* Malloc(std::size_t size) noexcept { return ShouldFail(size != 0) ? nullptr : std::malloc(size); }
175
176
/// Release an allocation. Frees are never counted or rejected.
177
62.8M
void Free(void* pointer) noexcept { std::free(pointer); }
178
179
/// Resize with libc unless this positive-size attempt is selected to fail.
180
/// Rejecting the call before libc preserves ownership of the original block.
181
13.1M
void* Realloc(void* pointer, std::size_t size) noexcept {
182
13.1M
  return ShouldFail(size != 0) ? nullptr : std::realloc(pointer, size);
183
13.1M
}
184
185
/// Duplicate one string while counting the operation as one allocation.
186
0
char* Strdup(const char* string) noexcept {
187
0
  if (string == nullptr) {
188
0
    return nullptr;
189
0
  }
190
0
  const std::size_t length = std::strlen(string) + 1;
191
0
  if (ShouldFail(length != 0)) {
192
0
    return nullptr;
193
0
  }
194
0
  char* copy = static_cast<char*>(std::malloc(length));
195
0
  if (copy != nullptr) {
196
0
    std::memcpy(copy, string, length);
197
0
  }
198
0
  return copy;
199
0
}
200
201
/// Allocate zero-filled storage unless this positive-size request is selected
202
/// to fail. The request counts once rather than once per element.
203
26.9M
void* Calloc(std::size_t count, std::size_t size) noexcept {
204
26.9M
  return ShouldFail(count != 0 && size != 0) ? nullptr : std::calloc(count, size);
205
26.9M
}
206
207
}  // namespace internal
208
}  // namespace proto_fuzzer::allocation_fault_injection