Coverage Report

Created: 2026-10-28 07:17

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/curl_fuzzer/proto_fuzzer/h2_plan.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 HTTP/2 structured-action serialization and client frame tracking.
9
10
#include "proto_fuzzer/h2_plan.h"
11
12
#include <nghttp2/nghttp2.h>
13
14
#include <algorithm>
15
#include <array>
16
#include <cstddef>
17
#include <cstdint>
18
#include <string>
19
#include <string_view>
20
#include <utility>
21
#include <vector>
22
23
#include "proto_fuzzer/scenario_limits.h"
24
25
namespace proto_fuzzer {
26
27
namespace {
28
29
constexpr std::string_view kClientPreface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
30
constexpr std::size_t kFrameHeaderSize = 9;
31
constexpr std::size_t kMaxFramePayload = 16384;
32
33
constexpr std::uint8_t kDataFrame = 0;
34
constexpr std::uint8_t kHeadersFrame = 1;
35
constexpr std::uint8_t kPriorityFrame = 2;
36
constexpr std::uint8_t kRstStreamFrame = 3;
37
constexpr std::uint8_t kSettingsFrame = 4;
38
constexpr std::uint8_t kPushPromiseFrame = 5;
39
constexpr std::uint8_t kPingFrame = 6;
40
constexpr std::uint8_t kGoawayFrame = 7;
41
constexpr std::uint8_t kWindowUpdateFrame = 8;
42
constexpr std::uint8_t kContinuationFrame = 9;
43
44
constexpr std::uint8_t kFlagAck = 0x1;
45
constexpr std::uint8_t kFlagEndStream = 0x1;
46
constexpr std::uint8_t kFlagEndHeaders = 0x4;
47
48
113k
void AppendUint32(std::string* output, std::uint32_t value) {
49
113k
  output->push_back(static_cast<char>((value >> 24U) & 0xffU));
50
113k
  output->push_back(static_cast<char>((value >> 16U) & 0xffU));
51
113k
  output->push_back(static_cast<char>((value >> 8U) & 0xffU));
52
113k
  output->push_back(static_cast<char>(value & 0xffU));
53
113k
}
54
55
224k
std::uint32_t ReadUint32(const char* input) {
56
224k
  const auto* bytes = reinterpret_cast<const unsigned char*>(input);
57
224k
  return (static_cast<std::uint32_t>(bytes[0]) << 24U) | (static_cast<std::uint32_t>(bytes[1]) << 16U) |
58
224k
         (static_cast<std::uint32_t>(bytes[2]) << 8U) | static_cast<std::uint32_t>(bytes[3]);
59
224k
}
60
61
void AppendFrame(std::string* output, std::uint32_t declared_length, std::uint8_t type, std::uint8_t flags,
62
79.3k
                 std::uint32_t stream_id, std::string_view payload) {
63
79.3k
  const std::uint32_t length = declared_length & 0x00ffffffU;
64
79.3k
  output->push_back(static_cast<char>((length >> 16U) & 0xffU));
65
79.3k
  output->push_back(static_cast<char>((length >> 8U) & 0xffU));
66
79.3k
  output->push_back(static_cast<char>(length & 0xffU));
67
79.3k
  output->push_back(static_cast<char>(type));
68
79.3k
  output->push_back(static_cast<char>(flags));
69
79.3k
  AppendUint32(output, stream_id & 0x7fffffffU);
70
79.3k
  output->append(payload.data(), payload.size());
71
79.3k
}
72
73
14.5k
std::uint32_t CanonicalStatus(std::uint32_t status) {
74
14.5k
  if (status >= 100U && status <= 599U) {
75
14.5k
    return status;
76
14.5k
  }
77
0
  return status == 0U ? 200U : 100U + (status % 500U);
78
14.5k
}
79
80
89.4k
std::string BoundedString(const std::string& input, std::size_t limit) { return input.substr(0, limit); }
81
82
38.8k
std::string CanonicalHeaderName(const std::string& input) {
83
38.8k
  std::string name = BoundedString(input, scenario_limits::kMaxHttp3HeaderNameBytes);
84
38.8k
  if (name.empty()) {
85
0
    return "x-fuzz";
86
0
  }
87
348k
  for (char& byte : name) {
88
348k
    const unsigned char value = static_cast<unsigned char>(byte);
89
348k
    const bool alpha = (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z');
90
348k
    const bool digit = value >= '0' && value <= '9';
91
348k
    const bool punctuation = value == '!' || value == '#' || value == '$' || value == '%' || value == '&' ||
92
339k
                             value == '\'' || value == '*' || value == '+' || value == '-' || value == '.' ||
93
177k
                             value == '^' || value == '_' || value == '`' || value == '|' || value == '~';
94
348k
    if (value >= 'A' && value <= 'Z') {
95
0
      byte = static_cast<char>(value - 'A' + 'a');
96
348k
    } else if (!alpha && !digit && !punctuation) {
97
0
      byte = '-';
98
0
    }
99
348k
  }
100
38.8k
  return name;
101
38.8k
}
102
103
38.8k
std::string CanonicalHeaderValue(const std::string& input) {
104
38.8k
  std::string value = BoundedString(input, scenario_limits::kMaxMetadataBytes);
105
3.25M
  for (char& byte : value) {
106
3.25M
    const unsigned char character = static_cast<unsigned char>(byte);
107
3.25M
    if ((character < 0x20U && character != '\t') || character == 0x7fU) {
108
0
      byte = ' ';
109
0
    }
110
3.25M
  }
111
38.8k
  return value;
112
38.8k
}
113
114
}  // namespace
115
116
H2PlanDriver::H2PlanDriver()
117
27.5k
    : plan_(nullptr),
118
27.5k
      deflater_(nullptr),
119
27.5k
      next_action_(0),
120
27.5k
      remaining_yields_(0),
121
27.5k
      initial_settings_sent_(false),
122
27.5k
      client_settings_ack_sent_(false),
123
27.5k
      expect_client_preface_(true),
124
27.5k
      observed_client_settings_(0),
125
27.5k
      observed_client_priority_(0) {
126
27.5k
  (void)nghttp2_hd_deflate_new(&deflater_, 4096);
127
27.5k
}
128
129
27.5k
H2PlanDriver::~H2PlanDriver() { nghttp2_hd_deflate_del(deflater_); }
130
131
/// Reset HPACK and stream state for one connection.
132
/// @param plan Ordered H2 actions and initial peer settings to execute.
133
/// @param scheme Scheme used in generated push-request pseudo-headers.
134
/// @param authority Authority used in generated push-request pseudo-headers.
135
16.0k
void H2PlanDriver::Reset(const curl::fuzzer::proto::Http2Plan& plan, std::string scheme, std::string authority) {
136
16.0k
  plan_ = &plan;
137
16.0k
  next_action_ = 0;
138
16.0k
  remaining_yields_ = 0;
139
16.0k
  initial_settings_sent_ = false;
140
16.0k
  client_settings_ack_sent_ = false;
141
16.0k
  expect_client_preface_ = true;
142
16.0k
  observed_client_settings_ = 0;
143
16.0k
  observed_client_priority_ = 0;
144
16.0k
  client_bytes_.clear();
145
16.0k
  request_streams_.clear();
146
16.0k
  pushed_streams_.clear();
147
16.0k
  client_streams_.clear();
148
16.0k
  scheme_ = std::move(scheme);
149
16.0k
  authority_ = std::move(authority);
150
16.0k
  nghttp2_hd_deflate_del(deflater_);
151
16.0k
  deflater_ = nullptr;
152
16.0k
  (void)nghttp2_hd_deflate_new(&deflater_, 4096);
153
16.0k
}
154
155
/// Parse client frame headers after transport decoding.
156
/// @param data Contiguous bytes received from curl.
157
/// @param size Number of bytes available at `data`.
158
122k
void H2PlanDriver::ObserveIncomingData(const unsigned char* data, std::size_t size) {
159
122k
  if (data == nullptr || size == 0) {
160
0
    return;
161
0
  }
162
122k
  if (size > scenario_limits::kMaxHttp2ObservedBytes ||
163
122k
      client_bytes_.size() > scenario_limits::kMaxHttp2ObservedBytes - size) {
164
0
    client_bytes_.clear();
165
0
    expect_client_preface_ = false;
166
0
    return;
167
0
  }
168
122k
  client_bytes_.append(reinterpret_cast<const char*>(data), size);
169
122k
  ParseClientFrames();
170
122k
}
171
172
122k
void H2PlanDriver::ParseClientFrames() {
173
122k
  if (expect_client_preface_) {
174
25.5k
    if (client_bytes_.size() < kClientPreface.size()) {
175
0
      return;
176
0
    }
177
25.5k
    if (std::string_view(client_bytes_).substr(0, kClientPreface.size()) == kClientPreface) {
178
25.5k
      client_bytes_.erase(0, kClientPreface.size());
179
25.5k
    }
180
25.5k
    expect_client_preface_ = false;
181
25.5k
  }
182
183
122k
  std::size_t consumed = 0;
184
347k
  while (client_bytes_.size() - consumed >= kFrameHeaderSize) {
185
244k
    const auto* header = reinterpret_cast<const unsigned char*>(client_bytes_.data() + consumed);
186
244k
    const std::size_t payload_size = (static_cast<std::size_t>(header[0]) << 16U) |
187
244k
                                     (static_cast<std::size_t>(header[1]) << 8U) | static_cast<std::size_t>(header[2]);
188
244k
    if (payload_size > scenario_limits::kMaxHttp2ObservedBytes ||
189
244k
        client_bytes_.size() - consumed < kFrameHeaderSize + payload_size) {
190
19.2k
      break;
191
19.2k
    }
192
224k
    const std::uint8_t type = header[3];
193
224k
    const std::uint8_t flags = header[4];
194
224k
    const std::uint32_t stream_id = ReadUint32(client_bytes_.data() + consumed + 5) & 0x7fffffffU;
195
224k
    RecordClientFrame(type, flags, stream_id);
196
224k
    consumed += kFrameHeaderSize + payload_size;
197
224k
  }
198
122k
  client_bytes_.erase(0, consumed);
199
122k
}
200
201
224k
void H2PlanDriver::RecordClientFrame(std::uint8_t type, std::uint8_t flags, std::uint32_t stream_id) {
202
224k
  if (type == kSettingsFrame && stream_id == 0 && (flags & kFlagAck) == 0) {
203
25.5k
    ++observed_client_settings_;
204
25.5k
    return;
205
25.5k
  }
206
199k
  if (stream_id == 0) {
207
53.5k
    return;
208
53.5k
  }
209
210
145k
  if (type == kPriorityFrame) {
211
0
    ++observed_client_priority_;
212
0
  }
213
214
145k
  auto& stream = client_streams_[stream_id];
215
145k
  if (type == kHeadersFrame) {
216
31.2k
    ++stream.headers;
217
31.2k
    if ((stream_id & 1U) != 0 &&
218
31.2k
        std::find(request_streams_.begin(), request_streams_.end(), stream_id) == request_streams_.end()) {
219
31.2k
      request_streams_.push_back(stream_id);
220
31.2k
    }
221
114k
  } else if (type == kDataFrame) {
222
35.7k
    ++stream.data;
223
35.7k
  }
224
145k
  if ((type == kHeadersFrame || type == kDataFrame) && (flags & kFlagEndStream) != 0) {
225
29.7k
    ++stream.end_stream;
226
29.7k
  }
227
145k
}
228
229
std::uint32_t H2PlanDriver::ResolveStream(const curl::fuzzer::proto::Http2StreamRef& stream,
230
39.6k
                                          bool allow_connection) const {
231
39.6k
  using StreamRef = curl::fuzzer::proto::Http2StreamRef;
232
39.6k
  switch (stream.target_case()) {
233
1.37k
    case StreamRef::kConnection:
234
1.37k
      return allow_connection ? 0U : 1U;
235
2.57k
    case StreamRef::kRequestIndex: {
236
2.57k
      const std::size_t index = static_cast<std::size_t>(stream.request_index());
237
2.57k
      if (index < request_streams_.size()) {
238
1.63k
        return request_streams_[index];
239
1.63k
      }
240
934
      return 1U + 2U * static_cast<std::uint32_t>(index % 0x3fffffffU);
241
2.57k
    }
242
2.44k
    case StreamRef::kPushIndex: {
243
2.44k
      const std::size_t index = static_cast<std::size_t>(stream.push_index());
244
2.44k
      if (index < pushed_streams_.size()) {
245
1.34k
        return pushed_streams_[index];
246
1.34k
      }
247
1.10k
      return 2U + 2U * static_cast<std::uint32_t>(index % 0x3fffffffU);
248
2.44k
    }
249
3.22k
    case StreamRef::kExplicitId:
250
3.22k
      return stream.explicit_id() & 0x7fffffffU;
251
518
    case StreamRef::kLatestRequest:
252
30.0k
    case StreamRef::TARGET_NOT_SET:
253
30.0k
    default:
254
30.0k
      return request_streams_.empty() ? 1U : request_streams_.back();
255
39.6k
  }
256
39.6k
}
257
258
9.85k
std::uint32_t H2PlanDriver::ResolvePromisedStream(const curl::fuzzer::proto::Http2StreamRef& stream) {
259
9.85k
  std::uint32_t stream_id = 0;
260
9.85k
  if (stream.target_case() == curl::fuzzer::proto::Http2StreamRef::TARGET_NOT_SET ||
261
1.81k
      stream.target_case() == curl::fuzzer::proto::Http2StreamRef::kLatestRequest ||
262
8.50k
      stream.target_case() == curl::fuzzer::proto::Http2StreamRef::kConnection) {
263
8.50k
    stream_id = 2U + 2U * static_cast<std::uint32_t>(pushed_streams_.size());
264
8.50k
  } else {
265
1.35k
    stream_id = ResolveStream(stream, false);
266
1.35k
  }
267
9.85k
  if (std::find(pushed_streams_.begin(), pushed_streams_.end(), stream_id) == pushed_streams_.end()) {
268
9.71k
    pushed_streams_.push_back(stream_id);
269
9.71k
  }
270
9.85k
  return stream_id;
271
9.85k
}
272
273
6.21k
bool H2PlanDriver::WaitSatisfied(const curl::fuzzer::proto::Http2Wait& wait) const {
274
6.21k
  const std::size_t required = std::max<std::size_t>(1, std::min<std::size_t>(wait.count(), 16));
275
6.21k
  if (wait.event() == curl::fuzzer::proto::HTTP2_CLIENT_EVENT_SETTINGS) {
276
1.73k
    return observed_client_settings_ >= required;
277
1.73k
  }
278
4.48k
  const std::uint32_t stream_id = ResolveStream(wait.stream(), false);
279
4.48k
  const auto found = client_streams_.find(stream_id);
280
4.48k
  if (found == client_streams_.end()) {
281
1.89k
    return false;
282
1.89k
  }
283
2.59k
  switch (wait.event()) {
284
968
    case curl::fuzzer::proto::HTTP2_CLIENT_EVENT_HEADERS:
285
968
      return found->second.headers >= required;
286
386
    case curl::fuzzer::proto::HTTP2_CLIENT_EVENT_DATA:
287
386
      return found->second.data >= required;
288
1.23k
    case curl::fuzzer::proto::HTTP2_CLIENT_EVENT_END_STREAM:
289
1.23k
      return found->second.end_stream >= required;
290
0
    case curl::fuzzer::proto::HTTP2_CLIENT_EVENT_SETTINGS:
291
0
    default:
292
0
      return false;
293
2.59k
  }
294
2.59k
}
295
296
24.9k
bool H2PlanDriver::EncodeHeaders(const std::vector<std::pair<std::string, std::string>>& headers, std::string* output) {
297
24.9k
  if (deflater_ == nullptr || output == nullptr) {
298
0
    return false;
299
0
  }
300
24.9k
  std::vector<nghttp2_nv> name_values;
301
24.9k
  name_values.reserve(headers.size());
302
92.8k
  for (const auto& header : headers) {
303
92.8k
    nghttp2_nv nv{};
304
92.8k
    nv.name = reinterpret_cast<std::uint8_t*>(const_cast<char*>(header.first.data()));
305
92.8k
    nv.value = reinterpret_cast<std::uint8_t*>(const_cast<char*>(header.second.data()));
306
92.8k
    nv.namelen = header.first.size();
307
92.8k
    nv.valuelen = header.second.size();
308
92.8k
    nv.flags = NGHTTP2_NV_FLAG_NONE;
309
92.8k
    name_values.push_back(nv);
310
92.8k
  }
311
24.9k
  const std::size_t bound = nghttp2_hd_deflate_bound(deflater_, name_values.data(), name_values.size());
312
24.9k
  if (bound > scenario_limits::kMaxHttp2HeaderBytes * 2U) {
313
0
    return false;
314
0
  }
315
24.9k
  output->resize(bound);
316
24.9k
  const nghttp2_ssize encoded = nghttp2_hd_deflate_hd2(deflater_, reinterpret_cast<std::uint8_t*>(output->data()),
317
24.9k
                                                       output->size(), name_values.data(), name_values.size());
318
24.9k
  if (encoded < 0) {
319
0
    output->clear();
320
0
    return false;
321
0
  }
322
24.9k
  output->resize(static_cast<std::size_t>(encoded));
323
24.9k
  return true;
324
24.9k
}
325
326
65.5k
bool H2PlanDriver::SerializeAction(const curl::fuzzer::proto::Http2Action& action, std::string* output) {
327
65.5k
  using Action = curl::fuzzer::proto::Http2Action;
328
65.5k
  output->clear();
329
65.5k
  switch (action.action_case()) {
330
21.0k
    case Action::kSettings: {
331
21.0k
      std::string payload;
332
21.0k
      const auto& settings = action.settings();
333
21.0k
      if (!settings.ack()) {
334
20.8k
        const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxHttp2Settings,
335
20.8k
                                                        static_cast<std::size_t>(settings.entries_size()));
336
20.8k
        payload.reserve(count * 6U);
337
34.8k
        for (std::size_t index = 0; index < count; ++index) {
338
14.0k
          const auto& entry = settings.entries(static_cast<int>(index));
339
14.0k
          const std::uint16_t identifier = static_cast<std::uint16_t>(entry.identifier());
340
14.0k
          payload.push_back(static_cast<char>((identifier >> 8U) & 0xffU));
341
14.0k
          payload.push_back(static_cast<char>(identifier & 0xffU));
342
14.0k
          AppendUint32(&payload, entry.value());
343
14.0k
        }
344
20.8k
      }
345
21.0k
      AppendFrame(output, payload.size(), kSettingsFrame, settings.ack() ? kFlagAck : 0, 0, payload);
346
21.0k
      return true;
347
0
    }
348
15.1k
    case Action::kHeaders: {
349
15.1k
      const auto& headers = action.headers();
350
15.1k
      std::vector<std::pair<std::string, std::string>> fields;
351
15.1k
      const std::size_t count =
352
15.1k
          std::min<std::size_t>(scenario_limits::kMaxHttp2Headers, static_cast<std::size_t>(headers.fields_size()));
353
15.1k
      fields.reserve(count + 1U);
354
15.1k
      if (!headers.trailers()) {
355
14.5k
        fields.emplace_back(":status", std::to_string(CanonicalStatus(headers.status_code())));
356
14.5k
      }
357
15.1k
      std::size_t bytes = 0;
358
37.9k
      for (std::size_t index = 0; index < count && bytes < scenario_limits::kMaxHttp2HeaderBytes; ++index) {
359
22.8k
        const auto& field = headers.fields(static_cast<int>(index));
360
22.8k
        std::string name = CanonicalHeaderName(field.name());
361
22.8k
        std::string value = CanonicalHeaderValue(field.value());
362
22.8k
        const std::size_t remaining = scenario_limits::kMaxHttp2HeaderBytes - bytes;
363
22.8k
        if (name.size() + value.size() > remaining) {
364
0
          value.resize(remaining > name.size() ? remaining - name.size() : 0);
365
0
          name.resize(std::min(name.size(), remaining));
366
0
        }
367
22.8k
        bytes += name.size() + value.size();
368
22.8k
        fields.emplace_back(std::move(name), std::move(value));
369
22.8k
      }
370
15.1k
      std::string block;
371
15.1k
      if (!EncodeHeaders(fields, &block)) {
372
0
        return false;
373
0
      }
374
15.1k
      const std::uint32_t stream_id = ResolveStream(headers.stream(), false);
375
15.1k
      std::size_t offset = 0;
376
15.1k
      do {
377
15.1k
        const std::size_t chunk_size = std::min(kMaxFramePayload, block.size() - offset);
378
15.1k
        const bool last = offset + chunk_size == block.size();
379
15.1k
        std::uint8_t flags = last ? kFlagEndHeaders : 0;
380
15.1k
        if (offset == 0 && headers.end_stream()) {
381
9.38k
          flags |= kFlagEndStream;
382
9.38k
        }
383
15.1k
        AppendFrame(output, chunk_size, offset == 0 ? kHeadersFrame : kContinuationFrame, flags, stream_id,
384
15.1k
                    std::string_view(block).substr(offset, chunk_size));
385
15.1k
        offset += chunk_size;
386
15.1k
      } while (offset < block.size());
387
15.1k
      return true;
388
15.1k
    }
389
1.57k
    case Action::kData: {
390
1.57k
      const auto& data = action.data();
391
1.57k
      const std::string payload = BoundedString(data.data(), scenario_limits::kMaxHttp2DataBytes);
392
1.57k
      const std::uint32_t stream_id = ResolveStream(data.stream(), false);
393
1.57k
      std::size_t offset = 0;
394
1.58k
      do {
395
1.58k
        const std::size_t chunk_size = std::min(kMaxFramePayload, payload.size() - offset);
396
1.58k
        const bool last = offset + chunk_size == payload.size();
397
1.58k
        AppendFrame(output, chunk_size, kDataFrame, last && data.end_stream() ? kFlagEndStream : 0, stream_id,
398
1.58k
                    std::string_view(payload).substr(offset, chunk_size));
399
1.58k
        offset += chunk_size;
400
1.58k
      } while (offset < payload.size());
401
1.57k
      return true;
402
15.1k
    }
403
2.29k
    case Action::kWindowUpdate: {
404
2.29k
      const auto& update = action.window_update();
405
2.29k
      std::string payload;
406
2.29k
      AppendUint32(&payload, std::max(1U, update.increment() & 0x7fffffffU));
407
2.29k
      AppendFrame(output, payload.size(), kWindowUpdateFrame, 0, ResolveStream(update.stream(), true), payload);
408
2.29k
      return true;
409
15.1k
    }
410
1.87k
    case Action::kRstStream: {
411
1.87k
      const auto& reset = action.rst_stream();
412
1.87k
      std::string payload;
413
1.87k
      AppendUint32(&payload, reset.error_code());
414
1.87k
      AppendFrame(output, payload.size(), kRstStreamFrame, 0, ResolveStream(reset.stream(), false), payload);
415
1.87k
      return true;
416
15.1k
    }
417
3.68k
    case Action::kPing: {
418
3.68k
      std::array<char, 8> opaque{};
419
3.68k
      const std::string& source = action.ping().opaque_data();
420
3.68k
      std::copy_n(source.begin(), std::min(source.size(), opaque.size()), opaque.begin());
421
3.68k
      AppendFrame(output, opaque.size(), kPingFrame, action.ping().ack() ? kFlagAck : 0, 0,
422
3.68k
                  std::string_view(opaque.data(), opaque.size()));
423
3.68k
      return true;
424
15.1k
    }
425
3.08k
    case Action::kGoaway: {
426
3.08k
      const auto& goaway = action.goaway();
427
3.08k
      std::string payload;
428
3.08k
      AppendUint32(&payload, ResolveStream(goaway.last_stream(), true));
429
3.08k
      AppendUint32(&payload, goaway.error_code());
430
3.08k
      payload.append(BoundedString(goaway.debug_data(), scenario_limits::kMaxHttp2RawFrameBytes));
431
3.08k
      AppendFrame(output, payload.size(), kGoawayFrame, 0, 0, payload);
432
3.08k
      return true;
433
15.1k
    }
434
9.85k
    case Action::kPushPromise: {
435
9.85k
      const auto& promise = action.push_promise();
436
9.85k
      std::vector<std::pair<std::string, std::string>> fields = {
437
9.85k
          {":method", "GET"}, {":scheme", scheme_}, {":authority", authority_}, {":path", "/push"}};
438
9.85k
      const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxHttp2Headers,
439
9.85k
                                                      static_cast<std::size_t>(promise.request_headers_size()));
440
25.9k
      for (std::size_t index = 0; index < count; ++index) {
441
16.0k
        const auto& field = promise.request_headers(static_cast<int>(index));
442
16.0k
        fields.emplace_back(CanonicalHeaderName(field.name()), CanonicalHeaderValue(field.value()));
443
16.0k
      }
444
9.85k
      std::string block;
445
9.85k
      if (!EncodeHeaders(fields, &block)) {
446
0
        return false;
447
0
      }
448
9.85k
      const std::uint32_t parent = ResolveStream(promise.parent_stream(), false);
449
9.85k
      const std::uint32_t promised = ResolvePromisedStream(promise.promised_stream());
450
9.85k
      std::string first_payload;
451
9.85k
      AppendUint32(&first_payload, promised);
452
9.85k
      const std::size_t first_size = std::min(kMaxFramePayload - 4U, block.size());
453
9.85k
      first_payload.append(block.data(), first_size);
454
9.85k
      AppendFrame(output, first_payload.size(), kPushPromiseFrame, first_size == block.size() ? kFlagEndHeaders : 0,
455
9.85k
                  parent, first_payload);
456
9.85k
      std::size_t offset = first_size;
457
9.87k
      while (offset < block.size()) {
458
15
        const std::size_t chunk_size = std::min(kMaxFramePayload, block.size() - offset);
459
15
        offset += chunk_size;
460
15
        AppendFrame(output, chunk_size, kContinuationFrame, offset == block.size() ? kFlagEndHeaders : 0, parent,
461
15
                    std::string_view(block).substr(offset - chunk_size, chunk_size));
462
15
      }
463
9.85k
      return true;
464
9.85k
    }
465
5.24k
    case Action::kWireFrame: {
466
5.24k
      const auto& frame = action.wire_frame();
467
5.24k
      const std::string payload = BoundedString(frame.payload(), scenario_limits::kMaxHttp2RawFrameBytes);
468
5.24k
      const std::uint32_t declared = frame.use_declared_length() ? frame.declared_length() : payload.size();
469
5.24k
      AppendFrame(output, declared, static_cast<std::uint8_t>(frame.type()), static_cast<std::uint8_t>(frame.flags()),
470
5.24k
                  frame.stream_id(), payload);
471
5.24k
      return true;
472
9.85k
    }
473
1.78k
    case Action::kRawBytes:
474
1.78k
      *output = BoundedString(action.raw_bytes(), scenario_limits::kMaxHttp2RawFrameBytes);
475
1.78k
      return true;
476
0
    case Action::kWait:
477
0
    case Action::kYieldTurns:
478
0
    case Action::ACTION_NOT_SET:
479
0
    default:
480
0
      return false;
481
65.5k
  }
482
65.5k
}
483
484
/// Advance one scripted boundary. `output` receives zero or more complete
485
/// HTTP/2 frames. A true result also represents a consumed wait or yield;
486
/// false means the next barrier is not ready or the plan is complete.
487
/// @param output Destination for the serialized frame bytes.
488
/// @return true when a frame, wait, or yield boundary was consumed.
489
235k
bool H2PlanDriver::NextChunk(std::string* output) {
490
235k
  if (output == nullptr || plan_ == nullptr) {
491
0
    return false;
492
0
  }
493
235k
  output->clear();
494
235k
  if (!initial_settings_sent_) {
495
15.7k
    curl::fuzzer::proto::Http2Action initial;
496
15.7k
    *initial.mutable_settings() = plan_->initial_settings();
497
15.7k
    initial.mutable_settings()->set_ack(false);
498
15.7k
    (void)SerializeAction(initial, output);
499
15.7k
    initial_settings_sent_ = true;
500
15.7k
    return true;
501
15.7k
  }
502
220k
  if (!client_settings_ack_sent_) {
503
15.6k
    if (observed_client_settings_ == 0) {
504
0
      return false;
505
0
    }
506
15.6k
    AppendFrame(output, 0, kSettingsFrame, kFlagAck, 0, {});
507
15.6k
    client_settings_ack_sent_ = true;
508
15.6k
    return true;
509
15.6k
  }
510
511
204k
  const std::size_t action_count =
512
204k
      std::min<std::size_t>(scenario_limits::kMaxHttp2Actions, static_cast<std::size_t>(plan_->actions_size()));
513
204k
  if (next_action_ >= action_count) {
514
75.1k
    return false;
515
75.1k
  }
516
129k
  const auto& action = plan_->actions(static_cast<int>(next_action_));
517
129k
  if (action.action_case() == curl::fuzzer::proto::Http2Action::kWait) {
518
6.21k
    if (!WaitSatisfied(action.wait())) {
519
4.72k
      return false;
520
4.72k
    }
521
1.48k
    ++next_action_;
522
1.48k
    return true;
523
6.21k
  }
524
123k
  if (action.action_case() == curl::fuzzer::proto::Http2Action::kYieldTurns) {
525
73.2k
    if (remaining_yields_ == 0) {
526
6.02k
      remaining_yields_ = std::min<std::size_t>(scenario_limits::kMaxHttp2YieldTurns, action.yield_turns());
527
6.02k
      if (remaining_yields_ == 0) {
528
655
        ++next_action_;
529
655
        return true;
530
655
      }
531
6.02k
    }
532
72.5k
    --remaining_yields_;
533
72.5k
    if (remaining_yields_ == 0) {
534
5.25k
      ++next_action_;
535
5.25k
    }
536
72.5k
    return true;
537
73.2k
  }
538
539
49.7k
  (void)SerializeAction(action, output);
540
49.7k
  ++next_action_;
541
49.7k
  return true;
542
123k
}
543
544
/// @return true while startup or a bounded action remains to be processed.
545
27.3k
bool H2PlanDriver::has_pending_work() const {
546
27.3k
  if (plan_ == nullptr || !initial_settings_sent_ || !client_settings_ack_sent_) {
547
11.7k
    return plan_ != nullptr;
548
11.7k
  }
549
15.6k
  return next_action_ <
550
15.6k
         std::min<std::size_t>(scenario_limits::kMaxHttp2Actions, static_cast<std::size_t>(plan_->actions_size()));
551
27.3k
}
552
553
/// @return number of distinct client request streams observed.
554
30.5k
std::size_t H2PlanDriver::observed_request_count() const { return request_streams_.size(); }
555
556
/// @return number of non-ACK client SETTINGS frames observed.
557
27.3k
std::size_t H2PlanDriver::observed_client_settings_count() const { return observed_client_settings_; }
558
559
/// @return PRIORITY frames observed from curl since the latest plan reset.
560
0
std::size_t H2PlanDriver::observed_client_priority_count() const { return observed_client_priority_; }
561
562
}  // namespace proto_fuzzer