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/http3_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 ngtcp2 QUIC transport and nghttp3 peer implementation.
9
10
#include "proto_fuzzer/http3_mock_server.h"
11
12
#include <arpa/inet.h>
13
#include <fcntl.h>
14
#include <nghttp3/nghttp3.h>
15
#include <ngtcp2/ngtcp2.h>
16
#include <ngtcp2/ngtcp2_crypto.h>
17
#include <ngtcp2/ngtcp2_crypto_ossl.h>
18
#include <openssl/err.h>
19
#include <openssl/pem.h>
20
#include <openssl/ssl.h>
21
#include <sys/socket.h>
22
#include <unistd.h>
23
24
#include <algorithm>
25
#include <array>
26
#include <cerrno>
27
#include <chrono>
28
#include <climits>
29
#include <cstddef>
30
#include <cstdint>
31
#include <cstring>
32
#include <memory>
33
#include <string>
34
#include <utility>
35
#include <vector>
36
37
#include "proto_fuzzer/multi_socket_driver.h"
38
#include "proto_fuzzer/scenario_limits.h"
39
#include "proto_fuzzer/tls_test_credentials.h"
40
41
namespace proto_fuzzer {
42
43
namespace {
44
45
constexpr std::int64_t kNoStreamId = -1;
46
constexpr std::size_t kMaxDatagramsPerTurn = 64;
47
constexpr std::size_t kServerConnectionIdBytes = 18;
48
constexpr std::size_t kReceiveBufferBytes = 64 * 1024;
49
// One request plus the six HTTP/3 critical streams leave room for every
50
// bounded action to create a fresh raw unidirectional stream.
51
constexpr std::size_t kMaxStreams = scenario_limits::kMaxHttp3Actions + 8;
52
constexpr std::size_t kMaxWritesPerTurn = 64;
53
constexpr std::size_t kMaxPendingPlaintextBytes = 64 * 1024;
54
constexpr int kHttp3IdleIterations = 32;
55
// QUIC's first two bits select one of these wire widths. The remaining bits
56
// encode the integer itself.
57
constexpr std::array<std::size_t, 4> kQuicVarintByteWidths = {1, 2, 4, 8};
58
constexpr unsigned int kQuicVarintTagBits = 2;
59
constexpr unsigned int kQuicVarintTagShift = CHAR_BIT - kQuicVarintTagBits;
60
static_assert(CHAR_BIT == 8, "QUIC varints require 8-bit bytes");
61
62
enum StreamRoleIndex : std::size_t {
63
  kResponseStream = 0,
64
  kControlStream = 1,
65
  kQpackEncoderStream = 2,
66
  kQpackDecoderStream = 3,
67
  kStreamRoleCount = 4,
68
};
69
70
/// Keep server-side OpenSSL failures from changing error classification in
71
/// curl's OpenSSL client, which executes on the same thread.
72
class OpenSslErrorQueueGuard {
73
 public:
74
104k
  OpenSslErrorQueueGuard() { ERR_clear_error(); }
75
104k
  ~OpenSslErrorQueueGuard() { ERR_clear_error(); }
76
77
  OpenSslErrorQueueGuard(const OpenSslErrorQueueGuard&) = delete;
78
  OpenSslErrorQueueGuard& operator=(const OpenSslErrorQueueGuard&) = delete;
79
};
80
81
/// Configure callback-created descriptors explicitly: curl's requested
82
/// SOCK_NONBLOCK and SOCK_CLOEXEC flags do not constrain an application fd.
83
8.76k
bool ConfigureSocket(int fd) {
84
8.76k
  if (fd < 0) {
85
0
    return false;
86
0
  }
87
8.76k
  const int descriptor_flags = ::fcntl(fd, F_GETFD, 0);
88
8.76k
  if (descriptor_flags < 0 || ::fcntl(fd, F_SETFD, descriptor_flags | FD_CLOEXEC) < 0) {
89
0
    return false;
90
0
  }
91
8.76k
  const int status_flags = ::fcntl(fd, F_GETFL, 0);
92
8.76k
  return status_flags >= 0 && ::fcntl(fd, F_SETFL, status_flags | O_NONBLOCK) == 0;
93
8.76k
}
94
95
/// Bind one private IPv4 UDP endpoint to an ephemeral loopback port.
96
4.38k
int OpenLoopbackSocket(struct sockaddr_in* bound_address) {
97
4.38k
  if (bound_address == nullptr) {
98
0
    return -1;
99
0
  }
100
101
4.38k
  const int fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
102
4.38k
  if (fd < 0 || !ConfigureSocket(fd)) {
103
0
    if (fd >= 0) {
104
0
      (void)::close(fd);
105
0
    }
106
0
    return -1;
107
0
  }
108
109
4.38k
  struct sockaddr_in requested = {};
110
4.38k
  requested.sin_family = AF_INET;
111
4.38k
  requested.sin_port = htons(0);
112
4.38k
  requested.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
113
4.38k
  if (::bind(fd, reinterpret_cast<const struct sockaddr*>(&requested), sizeof(requested)) != 0) {
114
0
    (void)::close(fd);
115
0
    return -1;
116
0
  }
117
118
4.38k
  socklen_t length = sizeof(*bound_address);
119
4.38k
  std::memset(bound_address, 0, sizeof(*bound_address));
120
4.38k
  if (::getsockname(fd, reinterpret_cast<struct sockaddr*>(bound_address), &length) != 0 ||
121
4.38k
      length != sizeof(*bound_address) || bound_address->sin_family != AF_INET) {
122
0
    (void)::close(fd);
123
0
    return -1;
124
0
  }
125
4.38k
  return fd;
126
4.38k
}
127
128
/// Replace all destination metadata curl can retain with the private listener.
129
4.38k
bool RewriteDestination(struct curl_sockaddr* address, const struct sockaddr_in& destination) {
130
4.38k
  if (address == nullptr || sizeof(destination) > sizeof(address->addr)) {
131
0
    return false;
132
0
  }
133
4.38k
  address->family = AF_INET;
134
4.38k
  address->socktype = SOCK_DGRAM;
135
4.38k
  address->protocol = IPPROTO_UDP;
136
4.38k
  address->addrlen = sizeof(destination);
137
4.38k
  std::memset(&address->addr, 0, sizeof(address->addr));
138
4.38k
  std::memcpy(&address->addr, &destination, sizeof(destination));
139
4.38k
  return true;
140
4.38k
}
141
142
/// Select only the final RFC HTTP/3 ALPN identifier.
143
int SelectHttp3Alpn(SSL* /*ssl*/, const unsigned char** selected, unsigned char* selected_length,
144
4.37k
                    const unsigned char* client_protocols, unsigned int client_protocols_length, void* /*userdata*/) {
145
4.37k
  static constexpr unsigned char server_protocols[] = {2, 'h', '3'};
146
4.37k
  unsigned char* match = nullptr;
147
4.37k
  unsigned char match_length = 0;
148
4.37k
  const int result = SSL_select_next_proto(&match, &match_length, server_protocols, sizeof(server_protocols),
149
4.37k
                                           client_protocols, client_protocols_length);
150
4.37k
  if (result != OPENSSL_NPN_NEGOTIATED) {
151
0
    return SSL_TLSEXT_ERR_ALERT_FATAL;
152
0
  }
153
4.37k
  *selected = match;
154
4.37k
  *selected_length = match_length;
155
4.37k
  return SSL_TLSEXT_ERR_OK;
156
4.37k
}
157
158
/// Append one profile-selected certificate without transferring a malformed
159
/// object into the context on parse failure.
160
402
bool AddExtraChainCertificate(SSL_CTX* context, const char* certificate_pem) {
161
402
  BIO* certificate_bio = BIO_new_mem_buf(certificate_pem, -1);
162
402
  if (certificate_bio == nullptr) {
163
0
    return false;
164
0
  }
165
402
  X509* certificate = PEM_read_bio_X509(certificate_bio, nullptr, nullptr, nullptr);
166
402
  BIO_free(certificate_bio);
167
402
  if (certificate == nullptr) {
168
0
    return false;
169
0
  }
170
402
  if (SSL_CTX_add_extra_chain_cert(context, certificate) != 1) {
171
0
    X509_free(certificate);
172
0
    return false;
173
0
  }
174
402
  return true;
175
402
}
176
177
/// Parse the checked-in key and profile-selected chain entirely in memory.
178
4.67k
bool LoadCredentials(SSL_CTX* context, curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain) {
179
4.67k
  BIO* certificate_bio = BIO_new_mem_buf(tls_test_credentials::kCertificatePem, -1);
180
4.67k
  BIO* key_bio = BIO_new_mem_buf(tls_test_credentials::kPrivateKeyPem, -1);
181
4.67k
  if (certificate_bio == nullptr || key_bio == nullptr) {
182
0
    BIO_free(certificate_bio);
183
0
    BIO_free(key_bio);
184
0
    return false;
185
0
  }
186
187
4.67k
  X509* certificate = PEM_read_bio_X509(certificate_bio, nullptr, nullptr, nullptr);
188
4.67k
  EVP_PKEY* key = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, nullptr);
189
4.67k
  BIO_free(certificate_bio);
190
4.67k
  BIO_free(key_bio);
191
4.67k
  if (certificate == nullptr || key == nullptr) {
192
0
    X509_free(certificate);
193
0
    EVP_PKEY_free(key);
194
0
    return false;
195
0
  }
196
197
4.67k
  const bool loaded = SSL_CTX_use_certificate(context, certificate) == 1 && SSL_CTX_use_PrivateKey(context, key) == 1 &&
198
4.67k
                      SSL_CTX_check_private_key(context) == 1;
199
4.67k
  X509_free(certificate);
200
4.67k
  EVP_PKEY_free(key);
201
4.67k
  if (!loaded) {
202
0
    return false;
203
0
  }
204
205
4.67k
  if (certificate_chain == curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_ALL_KEY_TYPES) {
206
134
    return AddExtraChainCertificate(context, tls_test_credentials::kRsaCertificatePem) &&
207
134
           AddExtraChainCertificate(context, tls_test_credentials::kDsaCertificatePem) &&
208
134
           AddExtraChainCertificate(context, tls_test_credentials::kDhCertificatePem);
209
134
  }
210
4.53k
  return true;
211
4.67k
}
212
213
/// Create a TLS 1.3 context for ngtcp2's OpenSSL crypto adapter.
214
4.67k
SSL_CTX* CreateTlsContext(curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain) {
215
4.67k
  OpenSslErrorQueueGuard error_guard;
216
4.67k
  SSL_CTX* context = SSL_CTX_new(TLS_server_method());
217
4.67k
  if (context == nullptr || !LoadCredentials(context, certificate_chain)) {
218
0
    SSL_CTX_free(context);
219
0
    return nullptr;
220
0
  }
221
222
4.67k
  if (SSL_CTX_set_min_proto_version(context, TLS1_3_VERSION) != 1 ||
223
4.67k
      SSL_CTX_set_max_proto_version(context, TLS1_3_VERSION) != 1) {
224
0
    SSL_CTX_free(context);
225
0
    return nullptr;
226
0
  }
227
4.67k
  (void)SSL_CTX_set_options(context, SSL_OP_NO_COMPRESSION);
228
4.67k
  SSL_CTX_set_alpn_select_cb(context, &SelectHttp3Alpn, nullptr);
229
4.67k
  return context;
230
4.67k
}
231
232
/// Encode one QUIC variable-length integer in its shortest representation.
233
2.40k
void AppendQuicVarint(std::string* output, std::uint64_t value) {
234
2.40k
  value &= scenario_limits::kMaxQuicVarint;
235
2.41k
  for (std::size_t encoding = 0; encoding < kQuicVarintByteWidths.size(); ++encoding) {
236
2.41k
    const std::size_t byte_width = kQuicVarintByteWidths[encoding];
237
2.41k
    const unsigned int value_bits = static_cast<unsigned int>(byte_width * CHAR_BIT - kQuicVarintTagBits);
238
2.41k
    if (value >= (std::uint64_t{1} << value_bits)) {
239
14
      continue;
240
14
    }
241
242
4.82k
    for (std::size_t byte = byte_width; byte != 0; --byte) {
243
2.42k
      const unsigned int shift = static_cast<unsigned int>((byte - 1) * CHAR_BIT);
244
2.42k
      unsigned char encoded_byte = static_cast<unsigned char>(value >> shift);
245
2.42k
      if (byte == byte_width) {
246
2.40k
        encoded_byte |= static_cast<unsigned char>(encoding << kQuicVarintTagShift);
247
2.40k
      }
248
2.42k
      output->push_back(static_cast<char>(encoded_byte));
249
2.42k
    }
250
2.40k
    return;
251
2.41k
  }
252
2.40k
}
253
254
/// Normalize a header name again at the runtime boundary. Fixed H3 targets
255
/// already run the postprocessor; this also keeps direct unit-test scenarios
256
/// safe to pass to nghttp3.
257
1.23k
std::string NormalizeHeaderName(const std::string& source) {
258
1.23k
  std::string result = source.substr(0, scenario_limits::kMaxHttp3HeaderNameBytes);
259
1.23k
  if (result.empty()) {
260
0
    return "x-fuzz";
261
0
  }
262
18.7k
  for (char& byte : result) {
263
18.7k
    const unsigned char value = static_cast<unsigned char>(byte);
264
18.7k
    const bool lower = value >= 'a' && value <= 'z';
265
18.7k
    const bool upper = value >= 'A' && value <= 'Z';
266
18.7k
    const bool digit = value >= '0' && value <= '9';
267
18.7k
    const bool punctuation = value == '!' || value == '#' || value == '$' || value == '%' || value == '&' ||
268
15.4k
                             value == '\'' || value == '*' || value == '+' || value == '-' || value == '.' ||
269
9.11k
                             value == '^' || value == '_' || value == '`' || value == '|' || value == '~';
270
18.7k
    if (upper) {
271
0
      byte = static_cast<char>(value - 'A' + 'a');
272
18.7k
    } else if (!lower && !digit && !punctuation) {
273
0
      byte = '-';
274
0
    }
275
18.7k
  }
276
1.23k
  return result;
277
1.23k
}
278
279
/// Remove control bytes that would make a structured field value malformed.
280
1.23k
std::string NormalizeHeaderValue(const std::string& source) {
281
1.23k
  std::string result = source.substr(0, scenario_limits::kMaxHttp3HeaderValueBytes);
282
149k
  for (char& byte : result) {
283
149k
    const unsigned char value = static_cast<unsigned char>(byte);
284
149k
    if ((value < 0x20U && value != '\t') || value == 0x7fU) {
285
0
      byte = ' ';
286
0
    }
287
149k
  }
288
1.23k
  return result;
289
1.23k
}
290
291
/// Return ngtcp2's nanosecond-resolution monotonic timestamp.
292
235k
ngtcp2_tstamp QuicNow() {
293
235k
  const auto now = std::chrono::steady_clock::now().time_since_epoch();
294
235k
  return static_cast<ngtcp2_tstamp>(std::chrono::duration_cast<std::chrono::nanoseconds>(now).count());
295
235k
}
296
297
}  // namespace
298
299
/// Private transport and HTTP/3 state. Keeping ngtcp2, OpenSSL, and nghttp3
300
/// out of the public header lets other proto targets compile without their
301
/// include trees.
302
class Http3MockServerImpl {
303
 public:
304
  explicit Http3MockServerImpl(curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain)
305
4.67k
      : context_(CreateTlsContext(certificate_chain)), crypto_connection_ref_{&GetQuicConnection, this} {
306
4.67k
    role_stream_ids_.fill(kNoStreamId);
307
4.67k
    streams_.reserve(kMaxStreams);
308
4.67k
    retained_writes_.reserve(kMaxWritesPerTurn);
309
4.67k
  }
310
311
4.67k
  ~Http3MockServerImpl() {
312
4.67k
    ResetPeer();
313
4.67k
    OpenSslErrorQueueGuard error_guard;
314
4.67k
    SSL_CTX_free(context_);
315
4.67k
  }
316
317
  Http3MockServerImpl(const Http3MockServerImpl&) = delete;
318
  Http3MockServerImpl& operator=(const Http3MockServerImpl&) = delete;
319
320
4.38k
  curl_socket_t OpenSocket(curlsocktype purpose, struct curl_sockaddr* address) {
321
4.38k
    if (purpose != CURLSOCKTYPE_IPCXN || address == nullptr || socket_opened_ || context_ == nullptr) {
322
0
      return CURL_SOCKET_BAD;
323
0
    }
324
4.38k
    socket_opened_ = true;
325
326
4.38k
    struct sockaddr_in listener_address = {};
327
4.38k
    server_fd_ = OpenLoopbackSocket(&listener_address);
328
4.38k
    if (server_fd_ < 0 || !RewriteDestination(address, listener_address)) {
329
0
      ResetPeer();
330
0
      return CURL_SOCKET_BAD;
331
0
    }
332
4.38k
    server_port_ = ntohs(listener_address.sin_port);
333
4.38k
    local_address_ = listener_address;
334
335
4.38k
    const int client_fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
336
4.38k
    if (client_fd < 0 || !ConfigureSocket(client_fd)) {
337
0
      if (client_fd >= 0) {
338
0
        (void)::close(client_fd);
339
0
      }
340
0
      ResetPeer();
341
0
      return CURL_SOCKET_BAD;
342
0
    }
343
4.38k
    return static_cast<curl_socket_t>(client_fd);
344
4.38k
  }
345
346
4.60k
  void BeginScenario(const curl::fuzzer::proto::Scenario& scenario) {
347
4.60k
    ResetPeer();
348
4.60k
    scenario_ = &scenario;
349
4.60k
    default_response_.Clear();
350
4.60k
    default_response_.set_status_code(200);
351
4.60k
    default_response_.set_finish_stream(true);
352
4.60k
  }
353
354
87.4k
  std::size_t DrivePeerTurn() {
355
87.4k
    if (server_fd_ < 0 || transport_failed_) {
356
1.95k
      return 0;
357
1.95k
    }
358
359
85.5k
    OpenSslErrorQueueGuard error_guard;
360
85.5k
    std::size_t progress = HandleExpiry();
361
85.5k
    if (transport_failed_) {
362
0
      return progress;
363
0
    }
364
85.5k
    progress += ReadDatagrams();
365
85.5k
    if (quic_connection_ == nullptr || transport_failed_) {
366
81
      return progress;
367
81
    }
368
369
85.4k
    if (handshake_complete_ && !close_requested_ && !close_sent_) {
370
75.5k
      progress += CreateServerStreams();
371
75.5k
      progress += DriveApplicationOutput();
372
75.5k
    }
373
85.4k
    progress += FlushTransportPackets();
374
85.4k
    return progress;
375
85.5k
  }
376
377
4.63k
  bool handshake_complete() const { return handshake_complete_; }
378
379
4.63k
  bool request_headers_received() const { return request_headers_received_; }
380
381
4.63k
  std::size_t executed_action_count() const { return next_action_; }
382
383
4.63k
  std::uint16_t server_port() const { return server_port_; }
384
385
 private:
386
  // ngtcp2 acknowledges QUIC writes by stream offset, while nghttp3 accepts
387
  // acknowledgement only for its own serialized bytes. Track those regions
388
  // separately from deliberately raw script writes.
389
  struct StreamState {
390
    struct ManagedWriteRange {
391
      std::uint64_t begin = 0;
392
      std::uint64_t end = 0;
393
    };
394
395
    std::int64_t id = kNoStreamId;
396
    std::uint64_t write_offset = 0;
397
    std::vector<ManagedWriteRange> managed_write_ranges;
398
    std::vector<std::uint64_t> managed_fin_offsets;
399
    std::size_t next_managed_ack_range = 0;
400
    std::size_t next_managed_fin_offset = 0;
401
  };
402
403
  // Queue at most one application write at a time. The byte string is kept in
404
  // retained_writes_ because ngtcp2 may submit only a prefix on each turn.
405
  struct PendingWrite {
406
    std::int64_t stream_id = kNoStreamId;
407
    const std::string* bytes = nullptr;
408
    std::size_t offset = 0;
409
    bool finish_stream = false;
410
    bool nghttp3_managed = false;
411
    bool completes_action = false;
412
413
180k
    bool active() const { return stream_id != kNoStreamId; }
414
415
26.0k
    void Clear() {
416
26.0k
      stream_id = kNoStreamId;
417
26.0k
      bytes = nullptr;
418
26.0k
      offset = 0;
419
26.0k
      finish_stream = false;
420
26.0k
      nghttp3_managed = false;
421
26.0k
      completes_action = false;
422
26.0k
    }
423
  };
424
425
  // nghttp3 pulls response bytes through a callback. This state lets that
426
  // callback preserve the scenario's chunk boundaries and final-stream intent.
427
  struct ResponseBodyState {
428
    std::vector<std::string> chunks;
429
    std::size_t next_chunk = 0;
430
    bool finish_stream = false;
431
    bool has_trailers = false;
432
  };
433
434
  enum class WriteResult {
435
    kComplete,
436
    kBlocked,
437
    kFailed,
438
  };
439
440
  // Tear down in protocol-layer order: HTTP/3 retains references into QUIC,
441
  // and OpenSSL's app data points back to this object until it is cleared.
442
9.28k
  void ResetPeer() {
443
9.28k
    OpenSslErrorQueueGuard error_guard;
444
9.28k
    pending_write_.Clear();
445
9.28k
    nghttp3_conn_del(http3_connection_);
446
9.28k
    http3_connection_ = nullptr;
447
9.28k
    ngtcp2_conn_del(quic_connection_);
448
9.28k
    quic_connection_ = nullptr;
449
9.28k
    if (tls_connection_ != nullptr) {
450
4.37k
      SSL_set_app_data(tls_connection_, nullptr);
451
4.37k
      SSL_free(tls_connection_);
452
4.37k
      tls_connection_ = nullptr;
453
4.37k
    }
454
9.28k
    ngtcp2_crypto_ossl_ctx_del(crypto_context_);
455
9.28k
    crypto_context_ = nullptr;
456
9.28k
    streams_.clear();
457
9.28k
    retained_writes_.clear();
458
9.28k
    if (server_fd_ >= 0) {
459
4.38k
      (void)::close(server_fd_);
460
4.38k
      server_fd_ = -1;
461
4.38k
    }
462
463
9.28k
    scenario_ = nullptr;
464
9.28k
    role_stream_ids_.fill(kNoStreamId);
465
9.28k
    std::memset(&local_address_, 0, sizeof(local_address_));
466
9.28k
    std::memset(&remote_address_, 0, sizeof(remote_address_));
467
9.28k
    ngtcp2_path_storage_zero(&path_storage_);
468
9.28k
    response_body_.chunks.clear();
469
9.28k
    response_body_.next_chunk = 0;
470
9.28k
    response_body_.finish_stream = false;
471
9.28k
    response_body_.has_trailers = false;
472
9.28k
    next_action_ = 0;
473
9.28k
    server_port_ = 0;
474
9.28k
    socket_opened_ = false;
475
9.28k
    handshake_complete_ = false;
476
9.28k
    request_headers_received_ = false;
477
9.28k
    server_streams_bound_ = false;
478
9.28k
    bootstrap_drained_ = false;
479
9.28k
    waiting_for_h3_drain_ = false;
480
9.28k
    waiting_drain_completes_action_ = false;
481
9.28k
    final_response_submitted_ = false;
482
9.28k
    default_response_started_ = false;
483
9.28k
    http3_failed_ = false;
484
9.28k
    transport_failed_ = false;
485
9.28k
    close_requested_ = false;
486
9.28k
    close_sent_ = false;
487
9.28k
    close_error_code_ = 0;
488
9.28k
    random_counter_ = 0;
489
9.28k
  }
490
491
  // ngtcp2 calls into this group while it owns QUIC parsing and encryption.
492
  // Each callback bridges a transport event into the separately-owned nghttp3
493
  // connection or records enough state for a later application turn.
494
196k
  static ngtcp2_conn* GetQuicConnection(ngtcp2_crypto_conn_ref* connection_ref) {
495
196k
    auto* self = static_cast<Http3MockServerImpl*>(connection_ref->user_data);
496
196k
    return self == nullptr ? nullptr : self->quic_connection_;
497
196k
  }
498
499
4.37k
  static int HandshakeCompleted(ngtcp2_conn* /*connection*/, void* user_data) {
500
4.37k
    auto* self = static_cast<Http3MockServerImpl*>(user_data);
501
4.37k
    if (self == nullptr) {
502
0
      return NGTCP2_ERR_CALLBACK_FAILURE;
503
0
    }
504
4.37k
    self->handshake_complete_ = true;
505
4.37k
    return 0;
506
4.37k
  }
507
508
8.75k
  static int ReceiveTransmitKey(ngtcp2_conn* /*connection*/, ngtcp2_encryption_level level, void* user_data) {
509
8.75k
    auto* self = static_cast<Http3MockServerImpl*>(user_data);
510
8.75k
    if (self == nullptr) {
511
0
      return NGTCP2_ERR_CALLBACK_FAILURE;
512
0
    }
513
8.75k
    if (level != NGTCP2_ENCRYPTION_LEVEL_1RTT || self->server_streams_bound_) {
514
4.37k
      return 0;
515
4.37k
    }
516
    // HTTP/3 application streams become valid only after 1-RTT keys exist.
517
    // Bind its critical streams before allowing script output onto the peer.
518
4.37k
    if (!self->CreateHttp3Connection()) {
519
0
      return NGTCP2_ERR_CALLBACK_FAILURE;
520
0
    }
521
4.37k
    (void)self->CreateServerStreams();
522
4.37k
    return self->server_streams_bound_ ? 0 : NGTCP2_ERR_CALLBACK_FAILURE;
523
4.37k
  }
524
525
  static void GenerateRandomBytes(std::uint8_t* destination, std::size_t length,
526
17.5k
                                  const ngtcp2_rand_ctx* random_context) {
527
17.5k
    if (random_context == nullptr) {
528
0
      std::memset(destination, 0, length);
529
0
      return;
530
0
    }
531
17.5k
    auto* self = static_cast<Http3MockServerImpl*>(random_context->native_handle);
532
17.5k
    if (self == nullptr) {
533
0
      std::memset(destination, 0, length);
534
0
      return;
535
0
    }
536
17.5k
    self->FillRandom(destination, length);
537
17.5k
  }
538
539
  static int GetNewConnectionId(ngtcp2_conn* /*connection*/, ngtcp2_cid* connection_id,
540
                                ngtcp2_stateless_reset_token* reset_token, std::size_t connection_id_length,
541
4.37k
                                void* user_data) {
542
4.37k
    auto* self = static_cast<Http3MockServerImpl*>(user_data);
543
4.37k
    if (self == nullptr || connection_id == nullptr || reset_token == nullptr ||
544
4.37k
        connection_id_length > sizeof(connection_id->data)) {
545
0
      return NGTCP2_ERR_CALLBACK_FAILURE;
546
0
    }
547
4.37k
    connection_id->datalen = connection_id_length;
548
4.37k
    self->FillRandom(connection_id->data, connection_id_length);
549
4.37k
    self->FillRandom(reset_token->data, sizeof(reset_token->data));
550
4.37k
    return 0;
551
4.37k
  }
552
553
17.4k
  static int OpenRemoteStream(ngtcp2_conn* /*connection*/, std::int64_t stream_id, void* user_data) {
554
17.4k
    auto* self = static_cast<Http3MockServerImpl*>(user_data);
555
17.4k
    if (self == nullptr || self->streams_.size() >= kMaxStreams) {
556
0
      return NGTCP2_ERR_CALLBACK_FAILURE;
557
0
    }
558
17.4k
    StreamState state;
559
17.4k
    state.id = stream_id;
560
17.4k
    self->streams_.push_back(state);
561
17.4k
    if (self->role_stream_ids_[kResponseStream] == kNoStreamId && ngtcp2_is_bidi_stream(stream_id)) {
562
4.33k
      self->role_stream_ids_[kResponseStream] = stream_id;
563
4.33k
    }
564
17.4k
    return 0;
565
17.4k
  }
566
567
  // ngtcp2 presents decrypted stream bytes; nghttp3 parses the HTTP/3 layer.
568
  // Extending QUIC's flow-control windows by the consumed amount keeps only
569
  // parsed bytes eligible for further client transmission.
570
  static int ReceiveQuicStreamData(ngtcp2_conn* connection, std::uint32_t flags, std::int64_t stream_id,
571
                                   std::uint64_t /*offset*/, const std::uint8_t* data, std::size_t length,
572
19.8k
                                   void* user_data, void* /*stream_user_data*/) {
573
19.8k
    auto* self = static_cast<Http3MockServerImpl*>(user_data);
574
19.8k
    if (self == nullptr || self->http3_connection_ == nullptr || self->http3_failed_) {
575
0
      return NGTCP2_ERR_CALLBACK_FAILURE;
576
0
    }
577
19.8k
    const nghttp3_ssize consumed =
578
19.8k
        nghttp3_conn_read_stream2(self->http3_connection_, stream_id, data, length, flags & NGTCP2_STREAM_DATA_FLAG_FIN,
579
19.8k
                                  ngtcp2_conn_get_timestamp(connection));
580
19.8k
    if (consumed < 0) {
581
6
      self->http3_failed_ = true;
582
6
      return NGTCP2_ERR_CALLBACK_FAILURE;
583
6
    }
584
19.8k
    ngtcp2_conn_extend_max_stream_offset(connection, stream_id, static_cast<std::uint64_t>(consumed));
585
19.8k
    ngtcp2_conn_extend_max_offset(connection, static_cast<std::uint64_t>(consumed));
586
19.8k
    return 0;
587
19.8k
  }
588
589
  // A transport acknowledgement covers both raw fuzz bytes and nghttp3 output.
590
  // Report only intersecting nghttp3-managed ranges back to nghttp3; otherwise
591
  // its write offsets would advance for bytes it never generated.
592
  static int AckedStreamDataOffset(ngtcp2_conn* /*connection*/, std::int64_t stream_id, std::uint64_t offset,
593
13.9k
                                   std::uint64_t length, void* user_data, void* /*stream_user_data*/) {
594
13.9k
    auto* self = static_cast<Http3MockServerImpl*>(user_data);
595
13.9k
    if (self == nullptr || self->http3_connection_ == nullptr || self->http3_failed_) {
596
0
      return 0;
597
0
    }
598
599
13.9k
    StreamState* stream = self->FindStream(stream_id);
600
13.9k
    if (stream == nullptr || offset > UINT64_MAX - length) {
601
0
      return NGTCP2_ERR_CALLBACK_FAILURE;
602
0
    }
603
604
13.9k
    if (length == 0) {
605
21
      if (stream->next_managed_fin_offset < stream->managed_fin_offsets.size() &&
606
0
          stream->managed_fin_offsets[stream->next_managed_fin_offset] == offset) {
607
0
        ++stream->next_managed_fin_offset;
608
0
        if (nghttp3_conn_add_ack_offset(self->http3_connection_, stream_id, 0) != 0) {
609
0
          self->http3_failed_ = true;
610
0
          return NGTCP2_ERR_CALLBACK_FAILURE;
611
0
        }
612
0
      }
613
21
      return 0;
614
21
    }
615
616
13.8k
    const std::uint64_t end = offset + length;
617
13.8k
    std::uint64_t managed_length = 0;
618
27.4k
    for (std::size_t index = stream->next_managed_ack_range; index < stream->managed_write_ranges.size(); ++index) {
619
13.8k
      const StreamState::ManagedWriteRange& range = stream->managed_write_ranges[index];
620
13.8k
      if (range.end <= offset) {
621
0
        stream->next_managed_ack_range = index + 1;
622
0
        continue;
623
0
      }
624
13.8k
      if (range.begin >= end) {
625
309
        break;
626
309
      }
627
13.5k
      managed_length += std::min(range.end, end) - std::max(range.begin, offset);
628
13.5k
      if (range.end <= end) {
629
13.5k
        stream->next_managed_ack_range = index + 1;
630
13.5k
      }
631
13.5k
    }
632
633
13.8k
    if (managed_length != 0 && nghttp3_conn_add_ack_offset(self->http3_connection_, stream_id, managed_length) != 0) {
634
0
      self->http3_failed_ = true;
635
0
      return NGTCP2_ERR_CALLBACK_FAILURE;
636
0
    }
637
13.8k
    return 0;
638
13.8k
  }
639
640
  // Keep nghttp3's stream lifecycle synchronized with QUIC resets, STOP_SENDING
641
  // events, and close error-code flags.
642
  static int CloseQuicStream(ngtcp2_conn* /*connection*/, std::uint32_t flags, std::int64_t stream_id,
643
                             std::uint64_t receive_error_code, std::uint64_t transmit_error_code, void* user_data,
644
166
                             void* /*stream_user_data*/) {
645
166
    auto* self = static_cast<Http3MockServerImpl*>(user_data);
646
166
    if (self == nullptr || self->http3_connection_ == nullptr || self->http3_failed_) {
647
0
      return 0;
648
0
    }
649
166
    std::uint32_t http3_flags = NGHTTP3_STREAM_CLOSE_FLAG_NONE;
650
166
    if ((flags & NGTCP2_STREAM_CLOSE2_FLAG_RX_APP_ERROR_CODE_SET) != 0) {
651
0
      http3_flags |= NGHTTP3_STREAM_CLOSE_FLAG_RX_APP_ERROR_CODE_SET;
652
0
    }
653
166
    if ((flags & NGTCP2_STREAM_CLOSE2_FLAG_TX_APP_ERROR_CODE_SET) != 0) {
654
104
      http3_flags |= NGHTTP3_STREAM_CLOSE_FLAG_TX_APP_ERROR_CODE_SET;
655
104
    }
656
166
    const int result = nghttp3_conn_close_stream2(self->http3_connection_, http3_flags, stream_id, receive_error_code,
657
166
                                                  transmit_error_code);
658
166
    if (result != 0 && result != NGHTTP3_ERR_STREAM_NOT_FOUND) {
659
0
      self->http3_failed_ = true;
660
0
      return NGTCP2_ERR_CALLBACK_FAILURE;
661
0
    }
662
166
    return 0;
663
166
  }
664
665
  static int ResetQuicStream(ngtcp2_conn* /*connection*/, std::int64_t stream_id, std::uint64_t /*final_size*/,
666
0
                             std::uint64_t /*application_error_code*/, void* user_data, void* /*stream_user_data*/) {
667
0
    return ShutdownHttp3StreamRead(stream_id, user_data);
668
0
  }
669
670
  static int StopSendingQuicStream(ngtcp2_conn* /*connection*/, std::int64_t stream_id,
671
                                   std::uint64_t /*application_error_code*/, void* user_data,
672
0
                                   void* /*stream_user_data*/) {
673
0
    return ShutdownHttp3StreamRead(stream_id, user_data);
674
0
  }
675
676
0
  static int ShutdownHttp3StreamRead(std::int64_t stream_id, void* user_data) {
677
0
    auto* self = static_cast<Http3MockServerImpl*>(user_data);
678
0
    if (self == nullptr || self->http3_connection_ == nullptr || self->http3_failed_) {
679
0
      return 0;
680
0
    }
681
0
    const int result = nghttp3_conn_shutdown_stream_read(self->http3_connection_, stream_id);
682
0
    if (result != 0 && result != NGHTTP3_ERR_STREAM_NOT_FOUND) {
683
0
      self->http3_failed_ = true;
684
0
      return NGTCP2_ERR_CALLBACK_FAILURE;
685
0
    }
686
0
    return 0;
687
0
  }
688
689
  // Create HTTP/3 once the QUIC handshake can carry application traffic.
690
  // Dynamic QPACK is intentionally disabled so scenarios cannot retain table
691
  // state beyond the bounded peer lifetime.
692
4.37k
  bool CreateHttp3Connection() {
693
4.37k
    if (http3_connection_ != nullptr) {
694
0
      return true;
695
0
    }
696
4.37k
    nghttp3_callbacks callbacks = {};
697
4.37k
    callbacks.recv_data = &ReceiveRequestData;
698
4.37k
    callbacks.deferred_consume = &DeferredConsumeRequestData;
699
4.37k
    callbacks.end_headers = &EndRequestHeaders;
700
4.37k
    callbacks.end_stream = &EndRequestStream;
701
4.37k
    nghttp3_settings settings;
702
4.37k
    nghttp3_settings_default(&settings);
703
4.37k
    settings.qpack_max_dtable_capacity = 0;
704
4.37k
    settings.qpack_encoder_max_dtable_capacity = 0;
705
4.37k
    settings.qpack_blocked_streams = 0;
706
4.37k
    if (nghttp3_conn_server_new(&http3_connection_, &callbacks, &settings, nullptr, this) != 0) {
707
0
      http3_failed_ = true;
708
0
      return false;
709
0
    }
710
4.37k
    nghttp3_conn_set_max_concurrent_streams(http3_connection_, kMaxStreams);
711
4.37k
    return true;
712
4.37k
  }
713
714
  // QUIC needs unpredictable-looking connection IDs and reset tokens, but
715
  // fuzzing needs deterministic reruns. This local generator is not used for
716
  // cryptographic secrecy.
717
30.6k
  void FillRandom(std::uint8_t* destination, std::size_t length) {
718
30.6k
    std::uint64_t state = ++random_counter_ * UINT64_C(0x9e3779b97f4a7c15);
719
337k
    for (std::size_t index = 0; index < length; ++index) {
720
306k
      state ^= state >> 12U;
721
306k
      state ^= state << 25U;
722
306k
      state ^= state >> 27U;
723
306k
      destination[index] = static_cast<std::uint8_t>((state * UINT64_C(0x2545f4914f6cdd1d)) >> 56U);
724
306k
    }
725
30.6k
  }
726
727
  // The first valid Initial establishes the sole remote endpoint. Construct
728
  // all three protocol layers here because ngtcp2's crypto callbacks need a
729
  // live server-side SSL object while processing this packet.
730
4.37k
  bool InitializeConnection(const ngtcp2_pkt_hd& header, const struct sockaddr_in& remote_address) {
731
4.37k
    if (quic_connection_ != nullptr || header.type != NGTCP2_PKT_INITIAL) {
732
0
      return false;
733
0
    }
734
735
4.37k
    remote_address_ = remote_address;
736
4.37k
    ngtcp2_path_storage_init(&path_storage_, reinterpret_cast<const ngtcp2_sockaddr*>(&local_address_),
737
4.37k
                             sizeof(local_address_), reinterpret_cast<const ngtcp2_sockaddr*>(&remote_address_),
738
4.37k
                             sizeof(remote_address_), nullptr);
739
740
4.37k
    ngtcp2_callbacks callbacks = {};
741
4.37k
    callbacks.recv_client_initial = ngtcp2_crypto_recv_client_initial_cb;
742
4.37k
    callbacks.recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb;
743
4.37k
    callbacks.handshake_completed = &HandshakeCompleted;
744
4.37k
    callbacks.encrypt = ngtcp2_crypto_encrypt_cb;
745
4.37k
    callbacks.decrypt = ngtcp2_crypto_decrypt_cb;
746
4.37k
    callbacks.hp_mask = ngtcp2_crypto_hp_mask_cb;
747
4.37k
    callbacks.recv_stream_data = &ReceiveQuicStreamData;
748
4.37k
    callbacks.acked_stream_data_offset = &AckedStreamDataOffset;
749
4.37k
    callbacks.stream_open = &OpenRemoteStream;
750
4.37k
    callbacks.stream_reset = &ResetQuicStream;
751
4.37k
    callbacks.rand = &GenerateRandomBytes;
752
4.37k
    callbacks.update_key = ngtcp2_crypto_update_key_cb;
753
4.37k
    callbacks.delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb;
754
4.37k
    callbacks.delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb;
755
4.37k
    callbacks.stream_stop_sending = &StopSendingQuicStream;
756
4.37k
    callbacks.version_negotiation = ngtcp2_crypto_version_negotiation_cb;
757
4.37k
    callbacks.recv_tx_key = &ReceiveTransmitKey;
758
4.37k
    callbacks.get_new_connection_id2 = &GetNewConnectionId;
759
4.37k
    callbacks.get_path_challenge_data2 = ngtcp2_crypto_get_path_challenge_data2_cb;
760
4.37k
    callbacks.stream_close2 = &CloseQuicStream;
761
762
4.37k
    ngtcp2_settings settings;
763
4.37k
    ngtcp2_settings_default(&settings);
764
4.37k
    settings.initial_ts = QuicNow();
765
4.37k
    settings.rand_ctx.native_handle = this;
766
4.37k
    settings.no_pmtud = 1;
767
4.37k
    settings.max_tx_udp_payload_size = packet_buffer_.size();
768
769
4.37k
    ngtcp2_transport_params parameters;
770
4.37k
    ngtcp2_transport_params_default(&parameters);
771
4.37k
    parameters.initial_max_stream_data_bidi_local = kMaxPendingPlaintextBytes;
772
4.37k
    parameters.initial_max_stream_data_bidi_remote = kMaxPendingPlaintextBytes;
773
4.37k
    parameters.initial_max_stream_data_uni = kMaxPendingPlaintextBytes;
774
4.37k
    parameters.initial_max_data = kMaxPendingPlaintextBytes * 4U;
775
4.37k
    parameters.initial_max_streams_bidi = kMaxStreams;
776
4.37k
    parameters.initial_max_streams_uni = kMaxStreams;
777
4.37k
    parameters.max_idle_timeout = 5U * NGTCP2_SECONDS;
778
4.37k
    parameters.active_connection_id_limit = 2;
779
4.37k
    parameters.original_dcid = header.dcid;
780
4.37k
    parameters.original_dcid_present = 1;
781
782
4.37k
    ngtcp2_cid server_cid = {};
783
4.37k
    server_cid.datalen = kServerConnectionIdBytes;
784
4.37k
    FillRandom(server_cid.data, server_cid.datalen);
785
4.37k
    if (ngtcp2_conn_server_new(&quic_connection_, &header.scid, &server_cid, &path_storage_.path, header.version,
786
4.37k
                               &callbacks, &settings, &parameters, nullptr, this) != 0) {
787
0
      return false;
788
0
    }
789
790
4.37k
    if (ngtcp2_crypto_ossl_ctx_new(&crypto_context_, nullptr) != 0) {
791
0
      return false;
792
0
    }
793
4.37k
    tls_connection_ = SSL_new(context_);
794
4.37k
    if (tls_connection_ == nullptr) {
795
0
      return false;
796
0
    }
797
4.37k
    ngtcp2_crypto_ossl_ctx_set_ssl(crypto_context_, tls_connection_);
798
4.37k
    if (ngtcp2_crypto_ossl_configure_server_session(tls_connection_) != 0) {
799
0
      return false;
800
0
    }
801
4.37k
    SSL_set_app_data(tls_connection_, &crypto_connection_ref_);
802
4.37k
    SSL_set_accept_state(tls_connection_);
803
4.37k
    ngtcp2_conn_set_tls_native_handle(quic_connection_, crypto_context_);
804
4.37k
    return true;
805
4.37k
  }
806
807
  // Drain a bounded datagram batch. Before connection setup, accept only a
808
  // valid QUIC Initial; afterwards, ignore packets from other UDP endpoints.
809
85.5k
  std::size_t ReadDatagrams() {
810
85.5k
    std::size_t progress = 0;
811
122k
    for (std::size_t packet = 0; packet < kMaxDatagramsPerTurn; ++packet) {
812
122k
      struct sockaddr_in remote_address = {};
813
122k
      socklen_t remote_length = sizeof(remote_address);
814
122k
      const ssize_t received = ::recvfrom(server_fd_, receive_buffer_.data(), receive_buffer_.size(), 0,
815
122k
                                          reinterpret_cast<struct sockaddr*>(&remote_address), &remote_length);
816
122k
      if (received < 0) {
817
85.4k
        if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
818
0
          transport_failed_ = true;
819
0
        }
820
85.4k
        break;
821
85.4k
      }
822
37.3k
      if (received == 0 || remote_length != sizeof(remote_address) || remote_address.sin_family != AF_INET) {
823
0
        continue;
824
0
      }
825
37.3k
      ++progress;
826
827
37.3k
      if (quic_connection_ == nullptr) {
828
4.37k
        ngtcp2_pkt_hd header = {};
829
4.37k
        if (ngtcp2_accept(&header, receive_buffer_.data(), static_cast<std::size_t>(received)) != 0) {
830
0
          continue;
831
0
        }
832
4.37k
        if (!InitializeConnection(header, remote_address)) {
833
0
          transport_failed_ = true;
834
0
          return progress;
835
0
        }
836
32.9k
      } else if (remote_address.sin_port != remote_address_.sin_port ||
837
32.9k
                 remote_address.sin_addr.s_addr != remote_address_.sin_addr.s_addr) {
838
0
        continue;
839
0
      }
840
841
37.3k
      ngtcp2_pkt_info packet_info = {};
842
37.3k
      const int result = ngtcp2_conn_read_pkt(quic_connection_, &path_storage_.path, &packet_info,
843
37.3k
                                              receive_buffer_.data(), static_cast<std::size_t>(received), QuicNow());
844
37.3k
      if (result != 0) {
845
75
        transport_failed_ = true;
846
75
        return progress;
847
75
      }
848
37.3k
    }
849
85.4k
    return progress;
850
85.5k
  }
851
852
  // ngtcp2 timers can make handshake, loss-recovery, or close packets ready
853
  // even when curl has not sent a new UDP datagram this turn.
854
85.5k
  std::size_t HandleExpiry() {
855
85.5k
    if (quic_connection_ == nullptr) {
856
4.38k
      return 0;
857
4.38k
    }
858
81.1k
    const ngtcp2_tstamp now = QuicNow();
859
81.1k
    if (ngtcp2_conn_get_expiry2(quic_connection_) > now) {
860
73.2k
      return 0;
861
73.2k
    }
862
7.93k
    const int result = ngtcp2_conn_handle_expiry(quic_connection_, now);
863
7.93k
    if (result != 0) {
864
0
      transport_failed_ = true;
865
0
    }
866
7.93k
    return 1;
867
81.1k
  }
868
869
  // A nonblocking socket can defer a packet without making the peer invalid.
870
  // Hard send errors are terminal because no later turn can repair the path.
871
29.6k
  bool SendPacket(const ngtcp2_path& path, const std::uint8_t* data, std::size_t length) {
872
29.6k
    const ngtcp2_addr* destination = &path.remote;
873
29.6k
    if (destination->addr == nullptr || destination->addrlen == 0) {
874
0
      destination = &path_storage_.path.remote;
875
0
    }
876
29.6k
    const ssize_t sent = ::sendto(server_fd_, data, length, 0,
877
29.6k
                                  reinterpret_cast<const struct sockaddr*>(destination->addr), destination->addrlen);
878
29.6k
    if (sent == static_cast<ssize_t>(length)) {
879
29.6k
      return true;
880
29.6k
    }
881
0
    if (sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) {
882
0
      return false;
883
0
    }
884
0
    transport_failed_ = true;
885
0
    return false;
886
0
  }
887
888
  // Serialize all QUIC control traffic after application writes. A requested
889
  // CONNECTION_CLOSE takes priority and is emitted at most once.
890
85.4k
  std::size_t FlushTransportPackets() {
891
85.4k
    if (quic_connection_ == nullptr || transport_failed_ || close_sent_) {
892
1.16k
      return 0;
893
1.16k
    }
894
895
84.2k
    if (close_requested_) {
896
1.16k
      ngtcp2_ccerr error;
897
1.16k
      ngtcp2_ccerr_default(&error);
898
1.16k
      ngtcp2_ccerr_set_application_error(&error, close_error_code_, nullptr, 0);
899
1.16k
      ngtcp2_path_storage output_path;
900
1.16k
      ngtcp2_path_storage_zero(&output_path);
901
1.16k
      ngtcp2_pkt_info packet_info = {};
902
1.16k
      const ngtcp2_tstamp now = QuicNow();
903
1.16k
      const ngtcp2_ssize written = ngtcp2_conn_write_connection_close(
904
1.16k
          quic_connection_, &output_path.path, &packet_info, packet_buffer_.data(), packet_buffer_.size(), &error, now);
905
1.16k
      if (written < 0) {
906
0
        transport_failed_ = true;
907
0
        return 0;
908
0
      }
909
1.16k
      close_sent_ = true;
910
1.16k
      if (written == 0) {
911
0
        return 1;
912
0
      }
913
1.16k
      ngtcp2_conn_update_pkt_tx_time(quic_connection_, now);
914
1.16k
      (void)SendPacket(output_path.path, packet_buffer_.data(), static_cast<std::size_t>(written));
915
1.16k
      return 1;
916
1.16k
    }
917
918
83.1k
    std::size_t progress = 0;
919
95.7k
    for (std::size_t packet = 0; packet < kMaxWritesPerTurn; ++packet) {
920
95.7k
      ngtcp2_path_storage output_path;
921
95.7k
      ngtcp2_path_storage_zero(&output_path);
922
95.7k
      ngtcp2_pkt_info packet_info = {};
923
95.7k
      const ngtcp2_tstamp now = QuicNow();
924
95.7k
      const ngtcp2_ssize written = ngtcp2_conn_write_pkt(quic_connection_, &output_path.path, &packet_info,
925
95.7k
                                                         packet_buffer_.data(), packet_buffer_.size(), now);
926
95.7k
      if (written < 0) {
927
0
        transport_failed_ = true;
928
0
        break;
929
0
      }
930
95.7k
      if (written == 0) {
931
83.1k
        break;
932
83.1k
      }
933
12.6k
      ngtcp2_conn_update_pkt_tx_time(quic_connection_, now);
934
12.6k
      if (!SendPacket(output_path.path, packet_buffer_.data(), static_cast<std::size_t>(written))) {
935
0
        break;
936
0
      }
937
12.6k
      ++progress;
938
12.6k
    }
939
83.1k
    return progress;
940
84.2k
  }
941
942
  // HTTP/3 requires one control stream and two QPACK streams before it can
943
  // send response metadata. Their stream IDs are retained by role so script
944
  // actions can target them without exposing transport-chosen IDs.
945
79.8k
  std::size_t CreateServerStreams() {
946
79.8k
    if (server_streams_bound_ || quic_connection_ == nullptr || http3_connection_ == nullptr || http3_failed_) {
947
75.5k
      return 0;
948
75.5k
    }
949
950
4.37k
    std::size_t progress = 0;
951
4.37k
    const std::array<StreamRoleIndex, 3> roles = {kControlStream, kQpackEncoderStream, kQpackDecoderStream};
952
13.1k
    for (const StreamRoleIndex role : roles) {
953
13.1k
      if (role_stream_ids_[role] != kNoStreamId) {
954
0
        continue;
955
0
      }
956
13.1k
      const std::int64_t stream_id = OpenUnidirectionalStream();
957
13.1k
      if (stream_id == kNoStreamId) {
958
0
        return progress;
959
0
      }
960
13.1k
      role_stream_ids_[role] = stream_id;
961
13.1k
      ++progress;
962
13.1k
    }
963
964
4.37k
    if (role_stream_ids_[kControlStream] == kNoStreamId || role_stream_ids_[kQpackEncoderStream] == kNoStreamId ||
965
4.37k
        role_stream_ids_[kQpackDecoderStream] == kNoStreamId) {
966
0
      return progress;
967
0
    }
968
4.37k
    if (nghttp3_conn_bind_qpack_streams(http3_connection_, role_stream_ids_[kQpackEncoderStream],
969
4.37k
                                        role_stream_ids_[kQpackDecoderStream]) != 0 ||
970
4.37k
        nghttp3_conn_bind_control_stream(http3_connection_, role_stream_ids_[kControlStream]) != 0) {
971
0
      http3_failed_ = true;
972
0
      return progress;
973
0
    }
974
4.37k
    server_streams_bound_ = true;
975
4.37k
    return progress + 1;
976
4.37k
  }
977
978
13.8k
  std::int64_t OpenUnidirectionalStream() {
979
13.8k
    if (quic_connection_ == nullptr || streams_.size() >= kMaxStreams) {
980
0
      return kNoStreamId;
981
0
    }
982
13.8k
    std::int64_t stream_id = kNoStreamId;
983
13.8k
    if (ngtcp2_conn_open_uni_stream(quic_connection_, &stream_id, nullptr) != 0) {
984
0
      return kNoStreamId;
985
0
    }
986
987
13.8k
    StreamState state;
988
13.8k
    state.id = stream_id;
989
13.8k
    streams_.push_back(state);
990
13.8k
    return stream_id;
991
13.8k
  }
992
993
  // Advance protocol-generated output before one scenario action. This keeps
994
  // QPACK/control bytes causally ahead of raw writes and makes each turn's
995
  // work bounded even when the client is blocked.
996
75.5k
  std::size_t DriveApplicationOutput() {
997
75.5k
    std::size_t progress = 0;
998
999
75.5k
    if (pending_write_.active()) {
1000
2.31k
      const bool completes_action = pending_write_.completes_action;
1001
2.31k
      const WriteResult result = FlushPendingWrite(&progress);
1002
2.31k
      if (result == WriteResult::kBlocked) {
1003
178
        return progress;
1004
178
      }
1005
2.13k
      if (result == WriteResult::kFailed && transport_failed_) {
1006
0
        return progress;
1007
0
      }
1008
2.13k
      if (completes_action) {
1009
2.11k
        ++next_action_;
1010
2.11k
        return progress + 1;
1011
2.11k
      }
1012
18
      if (result == WriteResult::kFailed && waiting_for_h3_drain_) {
1013
0
        waiting_for_h3_drain_ = false;
1014
0
        if (waiting_drain_completes_action_) {
1015
0
          ++next_action_;
1016
0
        }
1017
0
        return progress + 1;
1018
0
      }
1019
18
    }
1020
1021
73.2k
    if (!server_streams_bound_) {
1022
0
      return progress;
1023
0
    }
1024
73.2k
    if (!bootstrap_drained_) {
1025
4.36k
      if (http3_failed_ || PumpNghttp3(&progress)) {
1026
4.36k
        bootstrap_drained_ = true;
1027
4.36k
        ++progress;
1028
4.36k
      }
1029
4.36k
      return progress;
1030
4.36k
    }
1031
68.8k
    if (waiting_for_h3_drain_) {
1032
1.47k
      if (http3_failed_ || PumpNghttp3(&progress)) {
1033
1.45k
        waiting_for_h3_drain_ = false;
1034
1.45k
        if (waiting_drain_completes_action_) {
1035
1.45k
          ++next_action_;
1036
1.45k
        }
1037
1.45k
        waiting_drain_completes_action_ = false;
1038
1.45k
        ++progress;
1039
1.45k
      }
1040
1.47k
      return progress;
1041
1.47k
    }
1042
1043
    // Reading a request can produce QPACK decoder instructions independently
1044
    // of a response action. Keep those ahead of script-controlled raw writes.
1045
67.3k
    if (!http3_failed_ && !PumpNghttp3(&progress)) {
1046
0
      return progress;
1047
0
    }
1048
67.3k
    if (transport_failed_) {
1049
0
      return progress;
1050
0
    }
1051
1052
67.3k
    const std::size_t action_count =
1053
67.3k
        scenario_ == nullptr
1054
67.3k
            ? 0
1055
67.3k
            : std::min<std::size_t>(scenario_limits::kMaxHttp3Actions, scenario_->http3_plan().actions_size());
1056
67.3k
    if (next_action_ >= action_count) {
1057
      // An empty H3 plan still needs one successful response after a request.
1058
61.9k
      if (action_count == 0 && !default_response_started_ && request_headers_received_) {
1059
0
        default_response_started_ = true;
1060
0
        if (SubmitStructuredResponse(default_response_)) {
1061
0
          waiting_for_h3_drain_ = true;
1062
0
          waiting_drain_completes_action_ = false;
1063
0
          ++progress;
1064
0
        }
1065
0
      }
1066
61.9k
      return progress;
1067
61.9k
    }
1068
1069
5.43k
    const auto& action = scenario_->http3_plan().actions(static_cast<int>(next_action_));
1070
5.43k
    if (action.has_structured_response()) {
1071
1.60k
      if (!request_headers_received_) {
1072
0
        return progress;
1073
0
      }
1074
1.60k
      if (SubmitStructuredResponse(action.structured_response())) {
1075
1.46k
        waiting_for_h3_drain_ = true;
1076
1.46k
        waiting_drain_completes_action_ = true;
1077
1.46k
      } else {
1078
145
        ++next_action_;
1079
145
      }
1080
1.60k
      return progress + 1;
1081
1.60k
    }
1082
1083
3.82k
    if (action.has_stream_write()) {
1084
648
      const auto& write = action.stream_write();
1085
648
      const std::int64_t stream_id = StreamForRole(write.role());
1086
648
      if (stream_id == kNoStreamId) {
1087
0
        return progress;
1088
0
      }
1089
648
      const std::size_t length = std::min<std::size_t>(scenario_limits::kMaxHttp3RawWriteBytes, write.data().size());
1090
648
      QueueWrite(stream_id, write.data().substr(0, length), write.finish_stream(), false, true);
1091
648
      return progress + 1;
1092
648
    }
1093
1094
3.17k
    if (action.has_open_unidirectional_stream()) {
1095
701
      const auto& open = action.open_unidirectional_stream();
1096
701
      const std::int64_t stream_id = OpenUnidirectionalStream();
1097
701
      if (stream_id == kNoStreamId) {
1098
0
        ++next_action_;
1099
0
        return progress + 1;
1100
0
      }
1101
701
      const std::size_t length = std::min<std::size_t>(scenario_limits::kMaxHttp3RawWriteBytes, open.data().size());
1102
701
      QueueWrite(stream_id, open.data().substr(0, length), open.finish_stream(), false, true);
1103
701
      return progress + 1;
1104
701
    }
1105
1106
2.47k
    if (action.has_stream_reset()) {
1107
511
      const auto& reset = action.stream_reset();
1108
511
      const std::int64_t stream_id = StreamForRole(reset.role());
1109
511
      if (stream_id == kNoStreamId) {
1110
0
        return progress;
1111
0
      }
1112
511
      (void)ngtcp2_conn_shutdown_stream_write(quic_connection_, 0, stream_id,
1113
511
                                              reset.application_error_code() & scenario_limits::kMaxQuicVarint);
1114
511
      ++next_action_;
1115
511
      return progress + 1;
1116
511
    }
1117
1118
1.96k
    if (action.has_goaway()) {
1119
800
      const std::int64_t control_stream_id = StreamForRole(curl::fuzzer::proto::HTTP3_STREAM_CONTROL);
1120
800
      if (control_stream_id == kNoStreamId) {
1121
0
        return progress;
1122
0
      }
1123
      // HTTP/3 GOAWAY is a raw control-stream frame so its identifier remains
1124
      // directly mutation-controlled instead of being selected by nghttp3.
1125
800
      std::string payload;
1126
800
      AppendQuicVarint(&payload, action.goaway().id());
1127
800
      std::string frame;
1128
800
      AppendQuicVarint(&frame, 0x07U);
1129
800
      AppendQuicVarint(&frame, payload.size());
1130
800
      frame.append(payload);
1131
800
      QueueWrite(control_stream_id, std::move(frame), false, false, true);
1132
800
      return progress + 1;
1133
800
    }
1134
1135
1.16k
    if (action.has_connection_close()) {
1136
1.16k
      close_error_code_ = action.connection_close().application_error_code() & scenario_limits::kMaxQuicVarint;
1137
1.16k
      close_requested_ = true;
1138
1.16k
      ++next_action_;
1139
1.16k
      return progress + 1;
1140
1.16k
    }
1141
1142
0
    ++next_action_;
1143
0
    return progress + 1;
1144
1.16k
  }
1145
1146
  // Retain script and nghttp3 bytes until ngtcp2 accepts their full prefix.
1147
  // Only one pending write exists, preserving ordering between action types.
1148
  void QueueWrite(std::int64_t stream_id, std::string bytes, bool finish_stream, bool nghttp3_managed,
1149
16.8k
                  bool completes_action) {
1150
16.8k
    retained_writes_.push_back(std::make_unique<std::string>(std::move(bytes)));
1151
16.8k
    pending_write_.stream_id = stream_id;
1152
16.8k
    pending_write_.bytes = retained_writes_.back().get();
1153
16.8k
    pending_write_.offset = 0;
1154
16.8k
    pending_write_.finish_stream = finish_stream;
1155
16.8k
    pending_write_.nghttp3_managed = nghttp3_managed;
1156
16.8k
    pending_write_.completes_action = completes_action;
1157
16.8k
  }
1158
1159
  // Packetize one prefix through ngtcp2. For nghttp3-managed output, record
1160
  // the exact submitted offsets so its acknowledgement accounting can exclude
1161
  // interleaved raw writes on the same QUIC stream.
1162
16.9k
  WriteResult FlushPendingWrite(std::size_t* progress) {
1163
16.9k
    if (!pending_write_.active()) {
1164
0
      return WriteResult::kComplete;
1165
0
    }
1166
1167
16.9k
    if (pending_write_.bytes == nullptr || pending_write_.offset > pending_write_.bytes->size()) {
1168
0
      pending_write_.Clear();
1169
0
      return WriteResult::kFailed;
1170
0
    }
1171
1172
16.9k
    const std::size_t remaining = pending_write_.bytes->size() - pending_write_.offset;
1173
16.9k
    if (remaining == 0 && !pending_write_.finish_stream) {
1174
1.07k
      pending_write_.Clear();
1175
1.07k
      ++*progress;
1176
1.07k
      return WriteResult::kComplete;
1177
1.07k
    }
1178
1179
15.9k
    ngtcp2_vec vector = {};
1180
15.9k
    vector.base =
1181
15.9k
        reinterpret_cast<std::uint8_t*>(const_cast<char*>(pending_write_.bytes->data() + pending_write_.offset));
1182
15.9k
    vector.len = remaining;
1183
15.9k
    const std::size_t vector_count = remaining == 0 ? 0 : 1;
1184
15.9k
    const std::uint32_t flags =
1185
15.9k
        pending_write_.finish_stream ? NGTCP2_WRITE_STREAM_FLAG_FIN : NGTCP2_WRITE_STREAM_FLAG_NONE;
1186
15.9k
    ngtcp2_path_storage output_path;
1187
15.9k
    ngtcp2_path_storage_zero(&output_path);
1188
15.9k
    ngtcp2_pkt_info packet_info = {};
1189
15.9k
    ngtcp2_ssize submitted = -1;
1190
15.9k
    const ngtcp2_tstamp now = QuicNow();
1191
15.9k
    const ngtcp2_ssize packet_length = ngtcp2_conn_writev_stream(
1192
15.9k
        quic_connection_, &output_path.path, &packet_info, packet_buffer_.data(), packet_buffer_.size(), &submitted,
1193
15.9k
        flags, pending_write_.stream_id, vector_count == 0 ? nullptr : &vector, vector_count, now);
1194
15.9k
    if (packet_length == NGTCP2_ERR_STREAM_DATA_BLOCKED) {
1195
0
      return WriteResult::kBlocked;
1196
0
    }
1197
15.9k
    if (packet_length == NGTCP2_ERR_STREAM_SHUT_WR || packet_length == NGTCP2_ERR_STREAM_NOT_FOUND) {
1198
59
      pending_write_.Clear();
1199
59
      return WriteResult::kFailed;
1200
59
    }
1201
15.8k
    if (packet_length < 0) {
1202
0
      transport_failed_ = true;
1203
0
      pending_write_.Clear();
1204
0
      return WriteResult::kFailed;
1205
0
    }
1206
15.8k
    if (packet_length == 0) {
1207
0
      return WriteResult::kBlocked;
1208
0
    }
1209
15.8k
    ngtcp2_conn_update_pkt_tx_time(quic_connection_, now);
1210
15.8k
    (void)SendPacket(output_path.path, packet_buffer_.data(), static_cast<std::size_t>(packet_length));
1211
15.8k
    ++*progress;
1212
15.8k
    if (transport_failed_) {
1213
0
      pending_write_.Clear();
1214
0
      return WriteResult::kFailed;
1215
0
    }
1216
1217
15.8k
    if (submitted >= 0) {
1218
15.8k
      StreamState* stream = FindStream(pending_write_.stream_id);
1219
15.8k
      if (stream == nullptr || static_cast<std::uint64_t>(submitted) > UINT64_MAX - stream->write_offset) {
1220
0
        transport_failed_ = true;
1221
0
        pending_write_.Clear();
1222
0
        return WriteResult::kFailed;
1223
0
      }
1224
15.8k
      const std::uint64_t write_begin = stream->write_offset;
1225
15.8k
      stream->write_offset += static_cast<std::uint64_t>(submitted);
1226
15.8k
      if (pending_write_.nghttp3_managed) {
1227
14.7k
        if (nghttp3_conn_add_write_offset(http3_connection_, pending_write_.stream_id,
1228
14.7k
                                          static_cast<std::uint64_t>(submitted)) != 0) {
1229
0
          http3_failed_ = true;
1230
0
          pending_write_.Clear();
1231
0
          return WriteResult::kFailed;
1232
0
        }
1233
14.7k
        if (submitted != 0) {
1234
14.7k
          stream->managed_write_ranges.push_back({write_begin, stream->write_offset});
1235
14.7k
        } else if (pending_write_.finish_stream && pending_write_.offset == pending_write_.bytes->size()) {
1236
0
          stream->managed_fin_offsets.push_back(write_begin);
1237
0
        }
1238
14.7k
      }
1239
15.8k
      pending_write_.offset += static_cast<std::size_t>(submitted);
1240
15.8k
    }
1241
15.8k
    if (submitted < 0 || pending_write_.offset != pending_write_.bytes->size()) {
1242
197
      return WriteResult::kBlocked;
1243
197
    }
1244
15.6k
    pending_write_.Clear();
1245
15.6k
    return WriteResult::kComplete;
1246
15.8k
  }
1247
1248
  // Let nghttp3 serialize response/control bytes, then feed each generated
1249
  // vector through the same QUIC write path as raw scenario data. Stop when a
1250
  // raw action is pending so it cannot be reordered behind generated output.
1251
73.2k
  bool PumpNghttp3(std::size_t* progress) {
1252
73.2k
    if (http3_connection_ == nullptr || http3_failed_) {
1253
0
      return true;
1254
0
    }
1255
1256
87.8k
    for (std::size_t operation = 0; operation < kMaxWritesPerTurn; ++operation) {
1257
87.8k
      if (pending_write_.active()) {
1258
14.6k
        if (!pending_write_.nghttp3_managed) {
1259
0
          return false;
1260
0
        }
1261
14.6k
        const WriteResult result = FlushPendingWrite(progress);
1262
14.6k
        if (result == WriteResult::kBlocked) {
1263
19
          return false;
1264
19
        }
1265
14.6k
        if (result == WriteResult::kFailed) {
1266
0
          http3_failed_ = true;
1267
0
          return true;
1268
0
        }
1269
14.6k
      }
1270
1271
87.8k
      std::array<nghttp3_vec, 16> vectors;
1272
87.8k
      std::int64_t stream_id = -1;
1273
87.8k
      int finish_stream = 0;
1274
87.8k
      const nghttp3_ssize vector_count =
1275
87.8k
          nghttp3_conn_writev_stream(http3_connection_, &stream_id, &finish_stream, vectors.data(), vectors.size());
1276
87.8k
      if (vector_count < 0) {
1277
0
        http3_failed_ = true;
1278
0
        return true;
1279
0
      }
1280
87.8k
      if (vector_count == 0 && stream_id == -1) {
1281
73.2k
        return true;
1282
73.2k
      }
1283
1284
14.6k
      StreamState* stream = FindStream(stream_id);
1285
14.6k
      if (stream == nullptr) {
1286
0
        http3_failed_ = true;
1287
0
        return true;
1288
0
      }
1289
1290
14.6k
      std::string bytes;
1291
14.6k
      if (vector_count > 0) {
1292
14.6k
        std::size_t total = 0;
1293
29.6k
        for (nghttp3_ssize index = 0; index < vector_count; ++index) {
1294
14.9k
          if (vectors[static_cast<std::size_t>(index)].len > kMaxPendingPlaintextBytes - total) {
1295
0
            http3_failed_ = true;
1296
0
            return true;
1297
0
          }
1298
14.9k
          total += vectors[static_cast<std::size_t>(index)].len;
1299
14.9k
        }
1300
14.6k
        bytes.reserve(total);
1301
29.6k
        for (nghttp3_ssize index = 0; index < vector_count; ++index) {
1302
14.9k
          const nghttp3_vec& vector = vectors[static_cast<std::size_t>(index)];
1303
14.9k
          bytes.append(reinterpret_cast<const char*>(vector.base), vector.len);
1304
14.9k
        }
1305
14.6k
      }
1306
1307
14.6k
      QueueWrite(stream->id, std::move(bytes), finish_stream != 0, true, false);
1308
14.6k
    }
1309
0
    return false;
1310
73.2k
  }
1311
1312
  // Convert a bounded schema response into nghttp3-owned fields and pull-based
1313
  // body state. Interim responses do not consume the single final response.
1314
1.60k
  bool SubmitStructuredResponse(const curl::fuzzer::proto::Http3Response& response) {
1315
1.60k
    if (http3_connection_ == nullptr || http3_failed_ || role_stream_ids_[kResponseStream] == kNoStreamId) {
1316
0
      return false;
1317
0
    }
1318
1319
1.60k
    std::uint32_t status_code = response.status_code();
1320
1.60k
    if (status_code < 100U || status_code > 599U) {
1321
0
      status_code = 100U + status_code % 500U;
1322
0
    }
1323
1.60k
    std::vector<std::pair<std::string, std::string>> fields;
1324
1.60k
    fields.reserve(1 + std::min<std::size_t>(scenario_limits::kMaxHttp3Headers, response.response_headers_size()));
1325
1.60k
    fields.emplace_back(":status", std::to_string(status_code));
1326
1.60k
    AppendHeaders(&fields, response.response_headers(), scenario_limits::kMaxHttp3Headers);
1327
1.60k
    const std::vector<nghttp3_nv> name_values = MakeNameValues(fields);
1328
1.60k
    const std::int64_t response_stream_id = static_cast<std::int64_t>(role_stream_ids_[kResponseStream]);
1329
1330
1.60k
    if (status_code < 200U) {
1331
319
      return nghttp3_conn_submit_info(http3_connection_, response_stream_id, name_values.data(), name_values.size()) ==
1332
319
             0;
1333
319
    }
1334
1.28k
    if (final_response_submitted_) {
1335
143
      return false;
1336
143
    }
1337
1338
1.14k
    response_body_.chunks.clear();
1339
1.14k
    response_body_.next_chunk = 0;
1340
1.14k
    response_body_.finish_stream = response.finish_stream();
1341
1.14k
    response_body_.has_trailers = response.response_trailers_size() != 0;
1342
1.14k
    std::size_t body_bytes = 0;
1343
1.14k
    const std::size_t body_chunks =
1344
1.14k
        std::min<std::size_t>(scenario_limits::kMaxHttp3BodyChunks, response.body_chunks_size());
1345
1.14k
    response_body_.chunks.reserve(body_chunks);
1346
1.35k
    for (std::size_t index = 0; index < body_chunks && body_bytes < scenario_limits::kMaxHttp3BodyBytes; ++index) {
1347
208
      const std::string& source = response.body_chunks(static_cast<int>(index));
1348
208
      const std::size_t length = std::min(source.size(), scenario_limits::kMaxHttp3BodyBytes - body_bytes);
1349
208
      response_body_.chunks.emplace_back(source.data(), length);
1350
208
      body_bytes += length;
1351
208
    }
1352
1353
1.14k
    nghttp3_data_reader reader = {};
1354
1.14k
    reader.read_data = &ReadResponseBody;
1355
1.14k
    if (nghttp3_conn_submit_response(http3_connection_, response_stream_id, name_values.data(), name_values.size(),
1356
1.14k
                                     &reader) != 0) {
1357
0
      return false;
1358
0
    }
1359
1.14k
    final_response_submitted_ = true;
1360
1361
1.14k
    if (response_body_.has_trailers) {
1362
137
      std::vector<std::pair<std::string, std::string>> trailers;
1363
137
      trailers.reserve(std::min<std::size_t>(scenario_limits::kMaxHttp3Trailers, response.response_trailers_size()));
1364
137
      AppendHeaders(&trailers, response.response_trailers(), scenario_limits::kMaxHttp3Trailers);
1365
137
      const std::vector<nghttp3_nv> trailer_values = MakeNameValues(trailers);
1366
137
      if (nghttp3_conn_submit_trailers(http3_connection_, response_stream_id, trailer_values.data(),
1367
137
                                       trailer_values.size()) != 0) {
1368
0
        http3_failed_ = true;
1369
0
      }
1370
137
    }
1371
1.14k
    return true;
1372
1.14k
  }
1373
1374
  template <typename RepeatedHeaders>
1375
  static void AppendHeaders(std::vector<std::pair<std::string, std::string>>* destination,
1376
1.74k
                            const RepeatedHeaders& source, std::size_t count_limit) {
1377
1.74k
    std::size_t remaining = scenario_limits::kMaxHttp3HeaderBytes;
1378
1.74k
    const std::size_t count = std::min<std::size_t>(count_limit, source.size());
1379
2.97k
    for (std::size_t index = 0; index < count && remaining != 0; ++index) {
1380
1.23k
      std::string name = NormalizeHeaderName(source.Get(static_cast<int>(index)).name());
1381
1.23k
      std::string value = NormalizeHeaderValue(source.Get(static_cast<int>(index)).value());
1382
1.23k
      if (name.size() > remaining) {
1383
0
        name.resize(remaining);
1384
0
        value.clear();
1385
1.23k
      } else if (value.size() > remaining - name.size()) {
1386
0
        value.resize(remaining - name.size());
1387
0
      }
1388
1.23k
      remaining -= name.size() + value.size();
1389
1.23k
      destination->emplace_back(std::move(name), std::move(value));
1390
1.23k
    }
1391
1.74k
  }
1392
1393
1.74k
  static std::vector<nghttp3_nv> MakeNameValues(const std::vector<std::pair<std::string, std::string>>& fields) {
1394
1.74k
    std::vector<nghttp3_nv> name_values;
1395
1.74k
    name_values.reserve(fields.size());
1396
2.83k
    for (const auto& field : fields) {
1397
2.83k
      nghttp3_nv name_value = {};
1398
2.83k
      name_value.name = reinterpret_cast<std::uint8_t*>(const_cast<char*>(field.first.data()));
1399
2.83k
      name_value.value = reinterpret_cast<std::uint8_t*>(const_cast<char*>(field.second.data()));
1400
2.83k
      name_value.namelen = field.first.size();
1401
2.83k
      name_value.valuelen = field.second.size();
1402
2.83k
      name_value.flags = NGHTTP3_NV_FLAG_NONE;
1403
2.83k
      name_values.push_back(name_value);
1404
2.83k
    }
1405
1.74k
    return name_values;
1406
1.74k
  }
1407
1408
44.4k
  StreamState* FindStream(std::int64_t stream_id) {
1409
110k
    for (StreamState& stream : streams_) {
1410
110k
      if (stream.id == stream_id) {
1411
44.4k
        return &stream;
1412
44.4k
      }
1413
110k
    }
1414
0
    return nullptr;
1415
44.4k
  }
1416
1417
1.95k
  std::int64_t StreamForRole(curl::fuzzer::proto::Http3StreamRole role) {
1418
1.95k
    const int role_value = static_cast<int>(role);
1419
1.95k
    if (role_value < 0 || role_value >= static_cast<int>(kStreamRoleCount)) {
1420
0
      return kNoStreamId;
1421
0
    }
1422
1.95k
    return role_stream_ids_[static_cast<std::size_t>(role_value)];
1423
1.95k
  }
1424
1425
  // nghttp3 callbacks replenish QUIC flow-control credit only as application
1426
  // bytes are consumed, then discover the first request stream for responses.
1427
  static int ReceiveRequestData(nghttp3_conn* /*connection*/, std::int64_t stream_id, const std::uint8_t* /*data*/,
1428
1.77k
                                std::size_t data_length, void* user_data, void* /*stream_user_data*/) {
1429
1.77k
    auto* self = static_cast<Http3MockServerImpl*>(user_data);
1430
1.77k
    if (self != nullptr && self->quic_connection_ != nullptr) {
1431
1.77k
      ngtcp2_conn_extend_max_stream_offset(self->quic_connection_, stream_id, data_length);
1432
1.77k
      ngtcp2_conn_extend_max_offset(self->quic_connection_, data_length);
1433
1.77k
    }
1434
1.77k
    return 0;
1435
1.77k
  }
1436
1437
  static int DeferredConsumeRequestData(nghttp3_conn* /*connection*/, std::int64_t stream_id, std::size_t consumed,
1438
0
                                        void* user_data, void* /*stream_user_data*/) {
1439
0
    auto* self = static_cast<Http3MockServerImpl*>(user_data);
1440
0
    if (self != nullptr && self->quic_connection_ != nullptr) {
1441
0
      ngtcp2_conn_extend_max_stream_offset(self->quic_connection_, stream_id, consumed);
1442
0
      ngtcp2_conn_extend_max_offset(self->quic_connection_, consumed);
1443
0
    }
1444
0
    return 0;
1445
0
  }
1446
1447
  static int EndRequestHeaders(nghttp3_conn* /*connection*/, std::int64_t stream_id, int /*finish_stream*/,
1448
4.32k
                               void* user_data, void* /*stream_user_data*/) {
1449
4.32k
    auto* self = static_cast<Http3MockServerImpl*>(user_data);
1450
4.32k
    if (self != nullptr) {
1451
4.32k
      if (self->role_stream_ids_[kResponseStream] == kNoStreamId) {
1452
0
        self->role_stream_ids_[kResponseStream] = stream_id;
1453
0
        if (self->FindStream(stream_id) == nullptr && self->streams_.size() < kMaxStreams) {
1454
0
          StreamState state;
1455
0
          state.id = stream_id;
1456
0
          self->streams_.push_back(state);
1457
0
        }
1458
0
      }
1459
4.32k
      if (self->role_stream_ids_[kResponseStream] == stream_id) {
1460
4.32k
        self->request_headers_received_ = true;
1461
4.32k
      }
1462
4.32k
    }
1463
4.32k
    return 0;
1464
4.32k
  }
1465
1466
  static int EndRequestStream(nghttp3_conn* /*connection*/, std::int64_t /*stream_id*/, void* /*user_data*/,
1467
4.25k
                              void* /*stream_user_data*/) {
1468
4.25k
    return 0;
1469
4.25k
  }
1470
1471
  // nghttp3 pulls one schema body chunk per call. EOF may keep the stream open
1472
  // for trailers or a deliberate no-FIN response.
1473
  static nghttp3_ssize ReadResponseBody(nghttp3_conn* /*connection*/, std::int64_t /*stream_id*/, nghttp3_vec* vectors,
1474
                                        std::size_t vector_count, std::uint32_t* flags, void* user_data,
1475
1.26k
                                        void* /*stream_user_data*/) {
1476
1.26k
    auto* self = static_cast<Http3MockServerImpl*>(user_data);
1477
1.26k
    if (self == nullptr || vectors == nullptr || vector_count == 0 || flags == nullptr) {
1478
0
      return NGHTTP3_ERR_CALLBACK_FAILURE;
1479
0
    }
1480
1481
1.29k
    while (self->response_body_.next_chunk < self->response_body_.chunks.size() &&
1482
208
           self->response_body_.chunks[self->response_body_.next_chunk].empty()) {
1483
34
      ++self->response_body_.next_chunk;
1484
34
    }
1485
1.26k
    if (self->response_body_.next_chunk == self->response_body_.chunks.size()) {
1486
1.08k
      *flags = NGHTTP3_DATA_FLAG_EOF;
1487
1.08k
      if (!self->response_body_.finish_stream && !self->response_body_.has_trailers) {
1488
88
        *flags |= NGHTTP3_DATA_FLAG_NO_END_STREAM;
1489
88
      }
1490
1.08k
      return 0;
1491
1.08k
    }
1492
1493
174
    const std::string& chunk = self->response_body_.chunks[self->response_body_.next_chunk++];
1494
174
    vectors[0].base = reinterpret_cast<std::uint8_t*>(const_cast<char*>(chunk.data()));
1495
174
    vectors[0].len = chunk.size();
1496
174
    *flags = NGHTTP3_DATA_FLAG_NONE;
1497
174
    if (self->response_body_.next_chunk == self->response_body_.chunks.size()) {
1498
58
      *flags |= NGHTTP3_DATA_FLAG_EOF;
1499
58
      if (!self->response_body_.finish_stream && !self->response_body_.has_trailers) {
1500
32
        *flags |= NGHTTP3_DATA_FLAG_NO_END_STREAM;
1501
32
      }
1502
58
    }
1503
174
    return 1;
1504
1.26k
  }
1505
1506
  // Layer-owned transport state. The destruction order in ResetPeer mirrors
1507
  // these dependencies: HTTP/3, QUIC, TLS adapter, then TLS context/socket.
1508
  SSL_CTX* context_ = nullptr;
1509
  ngtcp2_conn* quic_connection_ = nullptr;
1510
  SSL* tls_connection_ = nullptr;
1511
  ngtcp2_crypto_ossl_ctx* crypto_context_ = nullptr;
1512
  ngtcp2_crypto_conn_ref crypto_connection_ref_;
1513
  nghttp3_conn* http3_connection_ = nullptr;
1514
  const curl::fuzzer::proto::Scenario* scenario_ = nullptr;
1515
  std::vector<StreamState> streams_;
1516
  std::vector<std::unique_ptr<std::string>> retained_writes_;
1517
  std::array<std::int64_t, kStreamRoleCount> role_stream_ids_;
1518
  PendingWrite pending_write_;
1519
  ResponseBodyState response_body_;
1520
  curl::fuzzer::proto::Http3Response default_response_;
1521
  ngtcp2_path_storage path_storage_ = {};
1522
  struct sockaddr_in local_address_ = {};
1523
  struct sockaddr_in remote_address_ = {};
1524
  std::array<std::uint8_t, kReceiveBufferBytes> receive_buffer_ = {};
1525
  std::array<std::uint8_t, NGTCP2_MAX_UDP_PAYLOAD_SIZE> packet_buffer_ = {};
1526
  std::uint64_t random_counter_ = 0;
1527
  std::uint64_t close_error_code_ = 0;
1528
  std::size_t next_action_ = 0;
1529
  int server_fd_ = -1;
1530
  std::uint16_t server_port_ = 0;
1531
  bool socket_opened_ = false;
1532
  bool handshake_complete_ = false;
1533
  bool request_headers_received_ = false;
1534
  bool server_streams_bound_ = false;
1535
  bool bootstrap_drained_ = false;
1536
  bool waiting_for_h3_drain_ = false;
1537
  bool waiting_drain_completes_action_ = false;
1538
  bool final_response_submitted_ = false;
1539
  bool default_response_started_ = false;
1540
  bool http3_failed_ = false;
1541
  bool transport_failed_ = false;
1542
  bool close_requested_ = false;
1543
  bool close_sent_ = false;
1544
};
1545
1546
/// Construct a peer with the default checked-in EC certificate.
1547
0
Http3MockServer::Http3MockServer() : Http3MockServer(curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_DEFAULT_EC) {}
1548
1549
/// Construct a peer with a bounded, parseable certificate-chain profile.
1550
/// @param certificate_chain Certificate material presented during QUIC TLS.
1551
Http3MockServer::Http3MockServer(curl::fuzzer::proto::TlsCertificateChainProfile certificate_chain)
1552
4.67k
    : MockServerBase(MultiDrivePolicy::kSocketAction), impl_(new Http3MockServerImpl(certificate_chain)) {}
1553
1554
4.67k
Http3MockServer::~Http3MockServer() = default;
1555
1556
/// Install UDP socket callbacks, the local trust anchor, and HTTP/3-only
1557
/// negotiation on an easy handle.
1558
/// @param easy Easy handle that will connect to the in-process QUIC peer.
1559
4.63k
void Http3MockServer::Install(CURL* easy) {
1560
  // Curl trusts the checked-in server certificate directly and must use HTTP/3
1561
  // rather than silently falling back to an HTTP/1 or HTTP/2 code path.
1562
4.63k
  MockServerBase::Install(easy);
1563
4.63k
  struct curl_blob trust_anchor = {const_cast<char*>(tls_test_credentials::kCertificatePem),
1564
4.63k
                                   sizeof(tls_test_credentials::kCertificatePem) - 1, CURL_BLOB_NOCOPY};
1565
4.63k
  (void)curl_easy_setopt(easy, CURLOPT_CAINFO_BLOB, &trust_anchor);
1566
4.63k
  (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 1L);
1567
4.63k
  (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 2L);
1568
4.63k
  (void)curl_easy_setopt(easy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3ONLY);
1569
4.63k
}
1570
1571
/// @return true after the server has completed a QUIC TLS handshake.
1572
4.63k
bool Http3MockServer::handshake_complete() const { return impl_ != nullptr && impl_->handshake_complete(); }
1573
1574
/// @return true after nghttp3 has decoded a complete request field section.
1575
4.63k
bool Http3MockServer::request_headers_received() const { return impl_ != nullptr && impl_->request_headers_received(); }
1576
1577
/// @return number of ordered Http3Action entries accepted by the peer.
1578
4.63k
std::size_t Http3MockServer::executed_action_count() const {
1579
4.63k
  return impl_ == nullptr ? 0 : impl_->executed_action_count();
1580
4.63k
}
1581
1582
/// @return kernel-selected UDP port in host byte order, or zero before use.
1583
4.63k
std::uint16_t Http3MockServer::server_port() const { return impl_ == nullptr ? 0 : impl_->server_port(); }
1584
1585
/// Create curl's real UDP descriptor and rewrite its destination to the
1586
/// private loopback QUIC listener.
1587
/// @param purpose Socket purpose supplied by curl's open-socket callback.
1588
/// @param address Mutable destination description supplied by curl.
1589
/// @return client UDP descriptor, or CURL_SOCKET_BAD on setup failure.
1590
4.38k
curl_socket_t Http3MockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* address) {
1591
4.38k
  return impl_ == nullptr ? CURL_SOCKET_BAD : impl_->OpenSocket(purpose, address);
1592
4.38k
}
1593
1594
/// HTTP/3 returns an unconnected UDP descriptor, so curl must perform its
1595
/// normal socket setup before using the rewritten loopback destination.
1596
/// @param curlfd Descriptor returned by HandleOpenSocket.
1597
/// @param purpose Role curl assigned to the descriptor.
1598
/// @return Whether curl must perform its normal socket setup.
1599
SocketSetupDisposition Http3MockServer::GetSocketSetupDisposition(curl_socket_t /*curlfd*/,
1600
4.38k
                                                                  curlsocktype /*purpose*/) const {
1601
4.38k
  return SocketSetupDisposition::kNeedsSetup;
1602
4.38k
}
1603
1604
/// Alternate nonblocking curl and QUIC server turns under fixed operation
1605
/// and idle budgets.
1606
/// @param multi Multi handle containing the scenario's easy handle.
1607
/// @param easy Easy handle being driven; all work is reached through multi.
1608
/// @param scenario Scenario whose Http3Plan supplies ordered plaintext work.
1609
4.60k
void Http3MockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
1610
4.60k
  (void)easy;
1611
4.60k
  if (impl_ == nullptr) {
1612
0
    return;
1613
0
  }
1614
4.60k
  impl_->BeginScenario(scenario);
1615
1616
4.60k
  int still_running = 1;
1617
4.60k
  int idle_iterations = 0;
1618
4.60k
  if (MultiSocketDriver* driver = multi_socket_driver(); driver != nullptr) {
1619
    // Alternate curl readiness processing with one bounded peer turn. Neither
1620
    // side blocks, so an idle limit prevents a malformed case spinning forever.
1621
4.60k
    CURLMcode result = driver->Start(&still_running);
1622
87.5k
    for (int iteration = 0; result == CURLM_OK && iteration < kMaxDriveIterations; ++iteration) {
1623
87.4k
      std::size_t progress = impl_->DrivePeerTurn();
1624
      // Curl can queue the final ACK or reset while completing the transfer.
1625
      // Give the peer its next bounded turn before stopping, without driving
1626
      // curl again or extending the existing operation budget.
1627
87.4k
      if (still_running == 0) {
1628
2.92k
        break;
1629
2.92k
      }
1630
84.5k
      const MultiSocketDriver::DriveResult drive_result = driver->DriveReady(&still_running);
1631
84.5k
      result = drive_result.code;
1632
84.5k
      progress += drive_result.made_progress ? 1U : 0U;
1633
84.5k
      if (progress != 0) {
1634
23.5k
        idle_iterations = 0;
1635
61.0k
      } else if (++idle_iterations >= kHttp3IdleIterations) {
1636
1.67k
        break;
1637
1.67k
      }
1638
84.5k
    }
1639
4.60k
    return;
1640
4.60k
  }
1641
1642
  // The fallback exercises curl's public multi-perform API while preserving
1643
  // the same peer-first/idle-limit behavior as the socket-action driver.
1644
0
  for (int iteration = 0; iteration < kMaxDriveIterations && still_running != 0; ++iteration) {
1645
0
    const int running_before = still_running;
1646
0
    const CURLMcode result = curl_multi_perform(multi, &still_running);
1647
0
    if (result != CURLM_OK) {
1648
0
      break;
1649
0
    }
1650
1651
0
    std::size_t progress = still_running == running_before ? 0U : 1U;
1652
0
    progress += impl_->DrivePeerTurn();
1653
0
    if (progress != 0) {
1654
0
      idle_iterations = 0;
1655
0
    } else if (++idle_iterations >= kHttp3IdleIterations) {
1656
0
      break;
1657
0
    }
1658
0
  }
1659
0
}
1660
1661
}  // namespace proto_fuzzer