Coverage Report

Created: 2026-09-01 07:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/curl_fuzzer/proto_fuzzer/tls_mock_server.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 Nonblocking OpenSSL server transport for structured HTTPS inputs.
9
10
#include "proto_fuzzer/tls_mock_server.h"
11
12
#include <openssl/err.h>
13
#include <openssl/pem.h>
14
#include <openssl/ssl.h>
15
#include <sys/socket.h>
16
17
#include <cstddef>
18
#include <string>
19
20
#include "proto_fuzzer/tls_test_credentials.h"
21
22
namespace proto_fuzzer {
23
24
namespace {
25
26
/// Isolate the server and curl client even though both OpenSSL instances run
27
/// on one thread. SSL_get_error requires an empty queue before its I/O call;
28
/// clearing at both boundaries also prevents a server failure from changing
29
/// curl's subsequent error classification.
30
class OpenSslErrorQueueGuard {
31
 public:
32
26.2k
  OpenSslErrorQueueGuard() { ERR_clear_error(); }
33
34
26.2k
  ~OpenSslErrorQueueGuard() { ERR_clear_error(); }
35
36
  OpenSslErrorQueueGuard(const OpenSslErrorQueueGuard&) = delete;
37
  OpenSslErrorQueueGuard& operator=(const OpenSslErrorQueueGuard&) = delete;
38
};
39
40
/// Select HTTP/1.1 when curl offers it. Keeping the server preference fixed
41
/// lets the existing HTTP corpus reach successful application traffic; seeds
42
/// that force HTTP/2 still exercise negotiation and failure handling.
43
int SelectAlpn(SSL* /*ssl*/, const unsigned char** selected, unsigned char* selected_length,
44
3.62k
               const unsigned char* client_protocols, unsigned int client_protocols_length, void* /*userdata*/) {
45
  // OpenSSL may return a pointer into the server preference list. Function-
46
  // local static storage keeps that result valid for the rest of the
47
  // handshake without reintroducing transport policy as a namespace global.
48
3.62k
  static constexpr unsigned char http11_alpn[] = {8, 'h', 't', 't', 'p', '/', '1', '.', '1'};
49
3.62k
  unsigned char* match = nullptr;
50
3.62k
  unsigned char match_length = 0;
51
3.62k
  const int result = SSL_select_next_proto(&match, &match_length, http11_alpn, sizeof(http11_alpn), client_protocols,
52
3.62k
                                           client_protocols_length);
53
3.62k
  if (result != OPENSSL_NPN_NEGOTIATED) {
54
2
    return SSL_TLSEXT_ERR_NOACK;
55
2
  }
56
3.62k
  *selected = match;
57
3.62k
  *selected_length = match_length;
58
3.62k
  return SSL_TLSEXT_ERR_OK;
59
3.62k
}
60
61
}  // namespace
62
63
/// Own the certificate, key, session cache, ALPN policy, and observations
64
/// shared by every bounded connection in one Scenario. Keeping both session
65
/// state and its measurements here makes redirects related while preventing
66
/// one fuzz input from affecting the next.
67
class TlsServerContext {
68
 public:
69
  TlsServerContext()
70
3.73k
      : context_(nullptr),
71
3.73k
        negotiated_tls_version_(0),
72
3.73k
        completed_handshake_count_(0),
73
3.73k
        reused_session_count_(0),
74
3.73k
        write_retry_count_(0) {
75
3.73k
    OpenSslErrorQueueGuard error_guard;
76
3.73k
    context_ = SSL_CTX_new(TLS_server_method());
77
3.73k
    if (context_ == nullptr || !LoadCredentials()) {
78
0
      SSL_CTX_free(context_);
79
0
      context_ = nullptr;
80
0
      return;
81
0
    }
82
83
3.73k
    (void)SSL_CTX_set_min_proto_version(context_, TLS1_2_VERSION);
84
3.73k
    (void)SSL_CTX_set_options(context_, SSL_OP_NO_COMPRESSION);
85
3.73k
    (void)SSL_CTX_set_session_cache_mode(context_, SSL_SESS_CACHE_SERVER);
86
3.73k
    constexpr unsigned char session_id_context[] = "curl-fuzzer";
87
3.73k
    (void)SSL_CTX_set_session_id_context(context_, session_id_context, sizeof(session_id_context) - 1);
88
3.73k
    SSL_CTX_set_alpn_select_cb(context_, &SelectAlpn, nullptr);
89
3.73k
  }
90
91
3.73k
  ~TlsServerContext() {
92
3.73k
    OpenSslErrorQueueGuard error_guard;
93
3.73k
    SSL_CTX_free(context_);
94
3.73k
  }
95
96
  TlsServerContext(const TlsServerContext&) = delete;
97
  TlsServerContext& operator=(const TlsServerContext&) = delete;
98
99
  /// @return the configured context, or nullptr when credential setup failed.
100
3.62k
  SSL_CTX* get() const { return context_; }
101
102
  /// Retain only scalar handshake results; SSL itself remains connection-owned.
103
  /// @param ssl connection that has just completed SSL_accept.
104
3.58k
  void RecordHandshake(SSL* ssl) {
105
3.58k
    if (ssl == nullptr) {
106
0
      return;
107
0
    }
108
3.58k
    negotiated_tls_version_ = SSL_version(ssl);
109
3.58k
    ++completed_handshake_count_;
110
3.58k
    if (SSL_session_reused(ssl) == 1) {
111
0
      ++reused_session_count_;
112
0
    }
113
3.58k
  }
114
115
  /// Record that OpenSSL requires an identical application-write retry.
116
0
  void RecordWriteRetry() { ++write_retry_count_; }
117
118
  /// @return protocol version from the most recent completed handshake.
119
0
  int negotiated_tls_version() const { return negotiated_tls_version_; }
120
  /// @return number of connections that completed their handshake.
121
0
  std::size_t completed_handshake_count() const { return completed_handshake_count_; }
122
  /// @return number of completed handshakes that reused a session.
123
0
  std::size_t reused_session_count() const { return reused_session_count_; }
124
  /// @return number of application writes that OpenSSL asked to retry.
125
0
  std::size_t write_retry_count() const { return write_retry_count_; }
126
127
 private:
128
  /// Parse the checked-in test-only PEM values entirely in memory.
129
3.73k
  bool LoadCredentials() {
130
3.73k
    BIO* certificate_bio = BIO_new_mem_buf(tls_test_credentials::kCertificatePem, -1);
131
3.73k
    BIO* key_bio = BIO_new_mem_buf(tls_test_credentials::kPrivateKeyPem, -1);
132
3.73k
    if (certificate_bio == nullptr || key_bio == nullptr) {
133
0
      BIO_free(certificate_bio);
134
0
      BIO_free(key_bio);
135
0
      return false;
136
0
    }
137
138
3.73k
    X509* certificate = PEM_read_bio_X509(certificate_bio, nullptr, nullptr, nullptr);
139
3.73k
    EVP_PKEY* key = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, nullptr);
140
3.73k
    BIO_free(certificate_bio);
141
3.73k
    BIO_free(key_bio);
142
3.73k
    if (certificate == nullptr || key == nullptr) {
143
0
      X509_free(certificate);
144
0
      EVP_PKEY_free(key);
145
0
      return false;
146
0
    }
147
148
3.73k
    const bool loaded = SSL_CTX_use_certificate(context_, certificate) == 1 &&
149
3.73k
                        SSL_CTX_use_PrivateKey(context_, key) == 1 && SSL_CTX_check_private_key(context_) == 1;
150
3.73k
    X509_free(certificate);
151
3.73k
    EVP_PKEY_free(key);
152
3.73k
    return loaded;
153
3.73k
  }
154
155
  SSL_CTX* context_;
156
  int negotiated_tls_version_;
157
  std::size_t completed_handshake_count_;
158
  std::size_t reused_session_count_;
159
  std::size_t write_retry_count_;
160
};
161
162
namespace {
163
164
/// TLS-aware MockConnection. Application writes are queued before the client
165
/// starts, then encrypted only after SSL_accept has completed. This preserves
166
/// MockServer's useful initial_response semantics without ever putting those
167
/// plaintext bytes onto the TLS wire.
168
class TlsMockConnection final : public MockConnection {
169
 public:
170
  explicit TlsMockConnection(TlsServerContext* context)
171
3.62k
      : ssl_(nullptr),
172
3.62k
        context_(context),
173
3.62k
        pending_offset_(0),
174
3.62k
        pending_write_end_(0),
175
3.62k
        handshake_complete_(false),
176
3.62k
        shutdown_requested_(false),
177
3.62k
        write_shutdown_(false),
178
3.62k
        failed_(false) {
179
3.62k
    OpenSslErrorQueueGuard error_guard;
180
3.62k
    SSL_CTX* ssl_context = context_ == nullptr ? nullptr : context_->get();
181
3.62k
    ssl_ = ssl_context == nullptr ? nullptr : SSL_new(ssl_context);
182
3.62k
    if (ssl_ != nullptr) {
183
3.62k
      SSL_set_mode(ssl_, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
184
3.62k
      if (SSL_set_fd(ssl_, server_fd()) != 1) {
185
0
        SSL_free(ssl_);
186
0
        ssl_ = nullptr;
187
3.62k
      } else {
188
3.62k
        SSL_set_accept_state(ssl_);
189
3.62k
      }
190
3.62k
    }
191
3.62k
  }
192
193
3.62k
  ~TlsMockConnection() override {
194
3.62k
    OpenSslErrorQueueGuard error_guard;
195
3.62k
    if (ssl_ != nullptr) {
196
3.62k
      SSL_free(ssl_);
197
3.62k
    }
198
3.62k
  }
199
200
  /// Require both the socketpair and its OpenSSL wrapper to be usable.
201
15.1k
  bool ok() const override { return MockConnection::ok() && ssl_ != nullptr; }
202
203
  /// Queue bounded application bytes until the handshake can encrypt them.
204
1.05k
  bool WriteAll(const unsigned char* data, std::size_t size) override {
205
1.05k
    if (failed_ || (data == nullptr && size != 0) || size > pending_plaintext_.max_size() - pending_plaintext_.size()) {
206
0
      return false;
207
0
    }
208
1.05k
    if (size != 0) {
209
1.05k
      pending_plaintext_.append(reinterpret_cast<const char*>(data), size);
210
1.05k
    }
211
1.05k
    return true;
212
1.05k
  }
213
214
  /// Advance the handshake, consume encrypted request records, flush queued
215
  /// response records, and finish a requested close without ever waiting.
216
  /// @return decrypted/application bytes plus state transitions made this turn.
217
11.5k
  std::size_t DrainIncoming() override {
218
11.5k
    if (!ok() || failed_) {
219
0
      return 0;
220
0
    }
221
222
11.5k
    OpenSslErrorQueueGuard error_guard;
223
11.5k
    std::size_t progress = 0;
224
11.5k
    if (!handshake_complete_) {
225
11.1k
      const int state_before = static_cast<int>(SSL_get_state(ssl_));
226
11.1k
      const int result = SSL_accept(ssl_);
227
      // SSL_get_error must be the next OpenSSL call after failed I/O. Even a
228
      // state query in between would make its use formally unreliable.
229
11.1k
      const int error = result == 1 ? SSL_ERROR_NONE : SSL_get_error(ssl_, result);
230
11.1k
      const int state_after = static_cast<int>(SSL_get_state(ssl_));
231
11.1k
      if (state_after != state_before) {
232
7.21k
        ++progress;
233
7.21k
      }
234
11.1k
      if (result == 1) {
235
3.58k
        handshake_complete_ = true;
236
3.58k
        if (context_ != nullptr) {
237
3.58k
          context_->RecordHandshake(ssl_);
238
3.58k
        }
239
3.58k
        ++progress;
240
7.53k
      } else {
241
7.53k
        if (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE) {
242
7.53k
          return progress;
243
7.53k
        }
244
0
        failed_ = true;
245
0
        return progress;
246
7.53k
      }
247
11.1k
    }
248
249
    // A WANT result leaves SSL_write_ex in flight. OpenSSL requires the next
250
    // I/O operation to retry that exact write, so do not interpose SSL_read_ex
251
    // merely because the outer driver has yielded back to us.
252
4.03k
    if (pending_write_end_ != 0) {
253
0
      progress += FlushPendingWrites();
254
0
      if (failed_ || pending_write_end_ != 0) {
255
0
        return progress;
256
0
      }
257
0
    }
258
259
4.03k
    unsigned char request[4096];
260
7.75k
    while (true) {
261
7.75k
      std::size_t received = 0;
262
7.75k
      const int result = SSL_read_ex(ssl_, request, sizeof(request), &received);
263
7.75k
      if (result == 1 && received != 0) {
264
3.72k
        progress += received;
265
3.72k
        continue;
266
3.72k
      }
267
4.03k
      if (result != 1) {
268
4.03k
        const int error = SSL_get_error(ssl_, result);
269
4.03k
        if (error != SSL_ERROR_WANT_READ && error != SSL_ERROR_WANT_WRITE && error != SSL_ERROR_ZERO_RETURN) {
270
0
          failed_ = true;
271
0
        }
272
4.03k
      }
273
4.03k
      break;
274
7.75k
    }
275
276
4.03k
    progress += FlushPendingWrites();
277
278
4.03k
    if (!failed_ && shutdown_requested_ && pending_plaintext_.empty() && !write_shutdown_) {
279
3.45k
      const int result = SSL_shutdown(ssl_);
280
3.45k
      if (result >= 0) {
281
        // The first successful call sends close_notify. The fuzzer does not
282
        // need to wait for the client's reciprocal alert before exposing EOF.
283
3.45k
        (void)::shutdown(server_fd(), SHUT_WR);
284
3.45k
        write_shutdown_ = true;
285
3.45k
        ++progress;
286
3.45k
      } else {
287
0
        const int error = SSL_get_error(ssl_, result);
288
0
        if (error != SSL_ERROR_WANT_READ && error != SSL_ERROR_WANT_WRITE) {
289
0
          failed_ = true;
290
0
        }
291
0
      }
292
3.45k
    }
293
4.03k
    return progress;
294
4.03k
  }
295
296
  /// Defer close_notify until every queued plaintext byte has become a record.
297
3.50k
  void ShutdownWrite() override {
298
3.50k
    shutdown_requested_ = true;
299
3.50k
    (void)DrainIncoming();
300
3.50k
  }
301
302
 private:
303
  /// Flush queued response bytes while preserving any OpenSSL retry boundary.
304
  /// @return plaintext bytes OpenSSL accepted during this call.
305
4.03k
  std::size_t FlushPendingWrites() {
306
4.03k
    std::size_t progress = 0;
307
4.75k
    while (!failed_ && pending_offset_ < pending_plaintext_.size()) {
308
720
      if (pending_write_end_ == 0) {
309
        // Freeze one write boundary until OpenSSL accepts it. WriteAll may
310
        // append the next scripted chunk after WANT_READ/WANT_WRITE, but the
311
        // retry contract permits only pointer relocation—not changed bytes or
312
        // length—for the outstanding SSL_write_ex call.
313
720
        pending_write_end_ = pending_plaintext_.size();
314
720
      }
315
720
      std::size_t written = 0;
316
720
      const int result = SSL_write_ex(ssl_, pending_plaintext_.data() + pending_offset_,
317
720
                                      pending_write_end_ - pending_offset_, &written);
318
720
      if (result == 1) {
319
720
        if (written == 0) {
320
0
          failed_ = true;
321
0
          break;
322
0
        }
323
720
        pending_offset_ += written;
324
720
        progress += written;
325
720
        if (pending_offset_ == pending_write_end_) {
326
720
          pending_write_end_ = 0;
327
720
        }
328
720
        continue;
329
720
      }
330
0
      const int error = SSL_get_error(ssl_, result);
331
0
      if (error == SSL_ERROR_WANT_READ || error == SSL_ERROR_WANT_WRITE) {
332
0
        if (context_ != nullptr) {
333
0
          context_->RecordWriteRetry();
334
0
        }
335
0
      } else {
336
0
        failed_ = true;
337
0
      }
338
0
      break;
339
720
    }
340
4.03k
    if (pending_offset_ == pending_plaintext_.size()) {
341
4.03k
      pending_plaintext_.clear();
342
4.03k
      pending_offset_ = 0;
343
4.03k
      pending_write_end_ = 0;
344
4.03k
    }
345
4.03k
    return progress;
346
4.03k
  }
347
348
  SSL* ssl_;
349
  TlsServerContext* context_;
350
  std::string pending_plaintext_;
351
  std::size_t pending_offset_;
352
  /// Exclusive end of a write that OpenSSL may require us to retry exactly.
353
  std::size_t pending_write_end_;
354
  bool handshake_complete_;
355
  bool shutdown_requested_;
356
  bool write_shutdown_;
357
  bool failed_;
358
};
359
360
}  // namespace
361
362
/// Build one reusable in-process server context per fuzz iteration.
363
3.73k
TlsMockServer::TlsMockServer() : context_(std::make_unique<TlsServerContext>()), saw_live_tls_session_(false) {}
364
365
/// Release SSL objects before their owning server context. OpenSSL reference
366
/// counting makes the reverse order legal, but making the ownership order
367
/// explicit keeps future transport state from acquiring a hidden dependency.
368
3.73k
TlsMockServer::~TlsMockServer() { ResetConnections(); }
369
370
/// Add verification to the common socket callbacks. Curl copies the blob
371
/// descriptor during setopt and borrows the inline certificate bytes, whose
372
/// program lifetime safely exceeds every easy handle.
373
3.73k
void TlsMockServer::Install(CURL* easy) {
374
3.73k
  MockServer::Install(easy);
375
3.73k
  struct curl_blob trust_anchor = {const_cast<char*>(tls_test_credentials::kCertificatePem),
376
3.73k
                                   sizeof(tls_test_credentials::kCertificatePem) - 1, CURL_BLOB_NOCOPY};
377
3.73k
  (void)curl_easy_setopt(easy, CURLOPT_CAINFO_BLOB, &trust_anchor);
378
3.73k
  (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L);
379
3.73k
  (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L);
380
3.73k
}
381
382
/// Report whether the drive reached curl's live TLS backend-query path.
383
0
bool TlsMockServer::saw_live_tls_session() const { return saw_live_tls_session_; }
384
385
/// Report the protocol selected by the latest successful server handshake.
386
0
int TlsMockServer::negotiated_tls_version() const {
387
0
  return context_ == nullptr ? 0 : context_->negotiated_tls_version();
388
0
}
389
390
/// Report how many bounded connections finished their TLS handshake.
391
0
std::size_t TlsMockServer::completed_handshake_count() const {
392
0
  return context_ == nullptr ? 0 : context_->completed_handshake_count();
393
0
}
394
395
/// Report how many later connections resumed state from this scenario.
396
0
std::size_t TlsMockServer::reused_session_count() const {
397
0
  return context_ == nullptr ? 0 : context_->reused_session_count();
398
0
}
399
400
/// Report how often response delivery reached OpenSSL's exact-retry path.
401
0
std::size_t TlsMockServer::write_retry_count() const { return context_ == nullptr ? 0 : context_->write_retry_count(); }
402
403
/// Create the polymorphic record-layer connection consumed by MockServer.
404
3.62k
std::unique_ptr<MockConnection> TlsMockServer::CreateConnection() {
405
3.62k
  return std::make_unique<TlsMockConnection>(context_.get());
406
3.62k
}
407
408
/// Query while the connection filters are attached. Stop after the first live
409
/// result so these coverage probes add a bounded handful of calls during the
410
/// handshake rather than recurring throughout every HTTP parser iteration.
411
/// @param easy Active easy handle whose connection filters remain attached.
412
10.9k
void TlsMockServer::ObserveActiveTransfer(CURL* easy) {
413
10.9k
  if (saw_live_tls_session_) {
414
3.63k
    return;
415
3.63k
  }
416
7.36k
  long verify_result = 0;
417
7.36k
  curl_off_t appconnect_time = 0;
418
7.36k
  struct curl_certinfo* certificate_info = nullptr;
419
7.36k
  struct curl_tlssessioninfo* tls_session = nullptr;
420
7.36k
  (void)curl_easy_getinfo(easy, CURLINFO_SSL_VERIFYRESULT, &verify_result);
421
7.36k
  (void)curl_easy_getinfo(easy, CURLINFO_APPCONNECT_TIME_T, &appconnect_time);
422
7.36k
  (void)curl_easy_getinfo(easy, CURLINFO_CERTINFO, &certificate_info);
423
7.36k
  if (curl_easy_getinfo(easy, CURLINFO_TLS_SSL_PTR, &tls_session) == CURLE_OK && tls_session != nullptr &&
424
7.36k
      tls_session->internals != nullptr) {
425
3.58k
    saw_live_tls_session_ = true;
426
3.58k
  }
427
7.36k
}
428
429
}  // namespace proto_fuzzer