Coverage Report

Created: 2026-10-28 07:17

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/curl_fuzzer/zstd_contract_shim.cc
Line
Count
Source
1
/*
2
 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3
 *
4
 * SPDX-License-Identifier: curl
5
 */
6
7
#define ZSTD_STATIC_LINKING_ONLY
8
#include <zstd.h>
9
10
#include <array>
11
#include <atomic>
12
#include <cstddef>
13
#include <cstdio>
14
#include <cstdlib>
15
#include <mutex>
16
#include <type_traits>
17
18
namespace {
19
20
struct PoisonedContext {
21
  const ZSTD_DCtx *context = nullptr;
22
  size_t error_result = 0;
23
};
24
25
// The checker must keep working when the fuzzer injects allocation failures,
26
// so its state is bounded and never allocates.
27
constexpr std::size_t kMaximumPoisonedContexts = 256;
28
std::array<PoisonedContext, kMaximumPoisonedContexts> poisoned_contexts{};
29
std::mutex poisoned_contexts_mutex;
30
std::atomic<std::size_t> poisoned_context_count{0};
31
32
// Static libzstd calls some of its public entry points internally. --wrap
33
// redirects those calls too; bypass checking while a real zstd call is active
34
// so only application-visible operations alter the tracked state.
35
thread_local unsigned real_call_depth = 0;
36
37
class RealCallScope {
38
public:
39
588k
  RealCallScope() { ++real_call_depth; }
40
588k
  ~RealCallScope() { --real_call_depth; }
41
42
  RealCallScope(const RealCallScope &) = delete;
43
  RealCallScope &operator=(const RealCallScope &) = delete;
44
};
45
46
588k
bool IsRealCallActive() { return real_call_depth != 0; }
47
48
0
bool ResetsDecompressionSession(ZSTD_ResetDirective reset) {
49
0
  return reset == ZSTD_reset_session_only ||
50
0
         reset == ZSTD_reset_session_and_parameters;
51
0
}
52
53
5.71k
PoisonedContext *FindPoisonedContext(const ZSTD_DCtx *context) {
54
735k
  for (PoisonedContext &entry : poisoned_contexts) {
55
735k
    if (entry.context == context) {
56
2.85k
      return &entry;
57
2.85k
    }
58
735k
  }
59
2.86k
  return nullptr;
60
5.71k
}
61
62
[[noreturn]] void AbortContractViolation(const char *attempted_api,
63
0
                                         const PoisonedContext &poisoned) {
64
0
  std::fprintf(stderr,
65
0
               "zstd contract assertion failed: %s called on DStream/DCtx "
66
0
               "%p after ZSTD_decompressStream returned error code %zu; "
67
0
               "successfully reset, initialize, or free the context before "
68
0
               "using it again\n",
69
0
               attempted_api, static_cast<const void *>(poisoned.context),
70
0
               poisoned.error_result);
71
0
  std::fflush(stderr);
72
0
  std::abort();
73
0
}
74
75
[[noreturn]] void AbortTableOverflow(const ZSTD_DCtx *context,
76
0
                                     size_t error_result) {
77
0
  std::fprintf(stderr,
78
0
               "zstd contract assertion failed: cannot track DStream/DCtx "
79
0
               "%p after ZSTD_decompressStream returned error code %zu; "
80
0
               "poisoned-context table is full\n",
81
0
               static_cast<const void *>(context), error_result);
82
0
  std::fflush(stderr);
83
0
  std::abort();
84
0
}
85
86
581k
void CheckUsable(const ZSTD_DCtx *context, const char *attempted_api) {
87
581k
  if (context == nullptr ||
88
581k
      poisoned_context_count.load(std::memory_order_acquire) == 0) {
89
581k
    return;
90
581k
  }
91
92
0
  PoisonedContext poisoned;
93
0
  {
94
0
    std::lock_guard<std::mutex> lock(poisoned_contexts_mutex);
95
0
    const PoisonedContext *tracked = FindPoisonedContext(context);
96
0
    if (tracked != nullptr) {
97
0
      poisoned = *tracked;
98
0
    }
99
0
  }
100
0
  if (poisoned.context != nullptr) {
101
0
    AbortContractViolation(attempted_api, poisoned);
102
0
  }
103
0
}
104
105
6.66k
void ForgetContext(const ZSTD_DCtx *context) {
106
6.66k
  if (context == nullptr ||
107
6.66k
      poisoned_context_count.load(std::memory_order_acquire) == 0) {
108
3.80k
    return;
109
3.80k
  }
110
111
2.86k
  std::lock_guard<std::mutex> lock(poisoned_contexts_mutex);
112
2.86k
  PoisonedContext *poisoned = FindPoisonedContext(context);
113
2.86k
  if (poisoned != nullptr) {
114
2.85k
    *poisoned = PoisonedContext{};
115
2.85k
    poisoned_context_count.fetch_sub(1, std::memory_order_release);
116
2.85k
  }
117
2.86k
}
118
119
2.85k
void PoisonContext(const ZSTD_DCtx *context, size_t error_result) {
120
2.85k
  if (context == nullptr) {
121
0
    return;
122
0
  }
123
124
2.85k
  bool table_full = false;
125
2.85k
  bool added = false;
126
2.85k
  {
127
2.85k
    std::lock_guard<std::mutex> lock(poisoned_contexts_mutex);
128
2.85k
    PoisonedContext *poisoned = FindPoisonedContext(context);
129
2.85k
    if (poisoned == nullptr) {
130
2.85k
      for (PoisonedContext &entry : poisoned_contexts) {
131
2.85k
        if (entry.context == nullptr) {
132
2.85k
          poisoned = &entry;
133
2.85k
          added = true;
134
2.85k
          break;
135
2.85k
        }
136
2.85k
      }
137
2.85k
    }
138
2.85k
    if (poisoned != nullptr) {
139
2.85k
      poisoned->context = context;
140
2.85k
      poisoned->error_result = error_result;
141
2.85k
      if (added) {
142
2.85k
        poisoned_context_count.fetch_add(1, std::memory_order_release);
143
2.85k
      }
144
2.85k
    } else {
145
0
      table_full = true;
146
0
    }
147
2.85k
  }
148
2.85k
  if (table_full) {
149
0
    AbortTableOverflow(context, error_result);
150
0
  }
151
2.85k
}
152
153
} // namespace
154
155
// GNU ld and lld provide each __real_* symbol when the final link uses the
156
// matching --wrap option. decltype ties these declarations to the installed
157
// zstd headers and makes signature drift a compile-time failure.
158
extern "C" decltype(ZSTD_createDCtx) __real_ZSTD_createDCtx;
159
extern "C" decltype(ZSTD_createDCtx_advanced) __real_ZSTD_createDCtx_advanced;
160
extern "C" decltype(ZSTD_createDStream) __real_ZSTD_createDStream;
161
extern "C" decltype(ZSTD_createDStream_advanced)
162
    __real_ZSTD_createDStream_advanced;
163
extern "C" decltype(ZSTD_freeDCtx) __real_ZSTD_freeDCtx;
164
extern "C" decltype(ZSTD_freeDStream) __real_ZSTD_freeDStream;
165
extern "C" decltype(ZSTD_initDStream) __real_ZSTD_initDStream;
166
extern "C" decltype(ZSTD_DCtx_reset) __real_ZSTD_DCtx_reset;
167
extern "C" decltype(ZSTD_decompressStream) __real_ZSTD_decompressStream;
168
169
0
extern "C" ZSTD_DCtx *__wrap_ZSTD_createDCtx(void) {
170
0
  if (IsRealCallActive()) {
171
0
    return __real_ZSTD_createDCtx();
172
0
  }
173
0
  ZSTD_DCtx *context;
174
0
  {
175
0
    RealCallScope real_call;
176
0
    context = __real_ZSTD_createDCtx();
177
0
  }
178
  // A zstd allocator may reuse the address of a recently freed context.
179
0
  ForgetContext(context);
180
0
  return context;
181
0
}
182
static_assert(std::is_same<decltype(ZSTD_createDCtx),
183
                           decltype(__wrap_ZSTD_createDCtx)>::value,
184
              "ZSTD_createDCtx signature changed");
185
186
extern "C" ZSTD_DCtx *
187
0
__wrap_ZSTD_createDCtx_advanced(ZSTD_customMem custom_memory) {
188
0
  if (IsRealCallActive()) {
189
0
    return __real_ZSTD_createDCtx_advanced(custom_memory);
190
0
  }
191
0
  ZSTD_DCtx *context;
192
0
  {
193
0
    RealCallScope real_call;
194
0
    context = __real_ZSTD_createDCtx_advanced(custom_memory);
195
0
  }
196
0
  ForgetContext(context);
197
0
  return context;
198
0
}
199
static_assert(std::is_same<decltype(ZSTD_createDCtx_advanced),
200
                           decltype(__wrap_ZSTD_createDCtx_advanced)>::value,
201
              "ZSTD_createDCtx_advanced signature changed");
202
203
3.33k
extern "C" ZSTD_DStream *__wrap_ZSTD_createDStream(void) {
204
3.33k
  if (IsRealCallActive()) {
205
0
    return __real_ZSTD_createDStream();
206
0
  }
207
3.33k
  ZSTD_DStream *stream;
208
3.33k
  {
209
3.33k
    RealCallScope real_call;
210
3.33k
    stream = __real_ZSTD_createDStream();
211
3.33k
  }
212
3.33k
  ForgetContext(stream);
213
3.33k
  return stream;
214
3.33k
}
215
static_assert(std::is_same<decltype(ZSTD_createDStream),
216
                           decltype(__wrap_ZSTD_createDStream)>::value,
217
              "ZSTD_createDStream signature changed");
218
219
extern "C" ZSTD_DStream *
220
0
__wrap_ZSTD_createDStream_advanced(ZSTD_customMem custom_memory) {
221
0
  if (IsRealCallActive()) {
222
0
    return __real_ZSTD_createDStream_advanced(custom_memory);
223
0
  }
224
0
  ZSTD_DStream *stream;
225
0
  {
226
0
    RealCallScope real_call;
227
0
    stream = __real_ZSTD_createDStream_advanced(custom_memory);
228
0
  }
229
0
  ForgetContext(stream);
230
0
  return stream;
231
0
}
232
static_assert(std::is_same<decltype(ZSTD_createDStream_advanced),
233
                           decltype(__wrap_ZSTD_createDStream_advanced)>::value,
234
              "ZSTD_createDStream_advanced signature changed");
235
236
0
extern "C" size_t __wrap_ZSTD_freeDCtx(ZSTD_DCtx *context) {
237
0
  if (IsRealCallActive()) {
238
0
    return __real_ZSTD_freeDCtx(context);
239
0
  }
240
0
  size_t result;
241
0
  {
242
0
    RealCallScope real_call;
243
0
    result = __real_ZSTD_freeDCtx(context);
244
0
  }
245
0
  if (!ZSTD_isError(result)) {
246
0
    ForgetContext(context);
247
0
  }
248
0
  return result;
249
0
}
250
static_assert(std::is_same<decltype(ZSTD_freeDCtx),
251
                           decltype(__wrap_ZSTD_freeDCtx)>::value,
252
              "ZSTD_freeDCtx signature changed");
253
254
3.33k
extern "C" size_t __wrap_ZSTD_freeDStream(ZSTD_DStream *stream) {
255
3.33k
  if (IsRealCallActive()) {
256
0
    return __real_ZSTD_freeDStream(stream);
257
0
  }
258
3.33k
  size_t result;
259
3.33k
  {
260
3.33k
    RealCallScope real_call;
261
3.33k
    result = __real_ZSTD_freeDStream(stream);
262
3.33k
  }
263
3.33k
  if (!ZSTD_isError(result)) {
264
3.33k
    ForgetContext(stream);
265
3.33k
  }
266
3.33k
  return result;
267
3.33k
}
268
static_assert(std::is_same<decltype(ZSTD_freeDStream),
269
                           decltype(__wrap_ZSTD_freeDStream)>::value,
270
              "ZSTD_freeDStream signature changed");
271
272
0
extern "C" size_t __wrap_ZSTD_initDStream(ZSTD_DStream *stream) {
273
0
  if (IsRealCallActive()) {
274
0
    return __real_ZSTD_initDStream(stream);
275
0
  }
276
0
  size_t result;
277
0
  {
278
0
    RealCallScope real_call;
279
0
    result = __real_ZSTD_initDStream(stream);
280
0
  }
281
0
  if (!ZSTD_isError(result)) {
282
0
    ForgetContext(stream);
283
0
  }
284
0
  return result;
285
0
}
286
static_assert(std::is_same<decltype(ZSTD_initDStream),
287
                           decltype(__wrap_ZSTD_initDStream)>::value,
288
              "ZSTD_initDStream signature changed");
289
290
extern "C" size_t __wrap_ZSTD_DCtx_reset(ZSTD_DCtx *context,
291
0
                                         ZSTD_ResetDirective reset) {
292
0
  if (IsRealCallActive()) {
293
0
    return __real_ZSTD_DCtx_reset(context, reset);
294
0
  }
295
0
  size_t result;
296
0
  {
297
0
    RealCallScope real_call;
298
0
    result = __real_ZSTD_DCtx_reset(context, reset);
299
0
  }
300
0
  if (!ZSTD_isError(result) && ResetsDecompressionSession(reset)) {
301
0
    ForgetContext(context);
302
0
  }
303
0
  return result;
304
0
}
305
static_assert(std::is_same<decltype(ZSTD_DCtx_reset),
306
                           decltype(__wrap_ZSTD_DCtx_reset)>::value,
307
              "ZSTD_DCtx_reset signature changed");
308
309
extern "C" size_t __wrap_ZSTD_decompressStream(ZSTD_DStream *stream,
310
                                               ZSTD_outBuffer *output,
311
581k
                                               ZSTD_inBuffer *input) {
312
581k
  if (IsRealCallActive()) {
313
0
    return __real_ZSTD_decompressStream(stream, output, input);
314
0
  }
315
581k
  CheckUsable(stream, "ZSTD_decompressStream");
316
581k
  size_t result;
317
581k
  {
318
581k
    RealCallScope real_call;
319
581k
    result = __real_ZSTD_decompressStream(stream, output, input);
320
581k
  }
321
581k
  if (ZSTD_isError(result)) {
322
2.85k
    PoisonContext(stream, result);
323
2.85k
  }
324
581k
  return result;
325
581k
}
326
static_assert(std::is_same<decltype(ZSTD_decompressStream),
327
                           decltype(__wrap_ZSTD_decompressStream)>::value,
328
              "ZSTD_decompressStream signature changed");