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_runtime.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 Shared HTTP/2 origin behavior independent of the transport carrier.
9
10
#include "proto_fuzzer/h2_runtime.h"
11
12
#include <curl/multi.h>
13
14
#include <algorithm>
15
#include <limits>
16
#include <utility>
17
18
#include "proto_fuzzer/mock_server.h"
19
20
namespace proto_fuzzer {
21
22
namespace {
23
24
// Bound header inspection independently of curl's PUSH_PROMISE limits.
25
constexpr std::size_t kMaxInspectedPushHeaders = 16;
26
// One extra easy handle exercises accepted push without multiplying work.
27
constexpr std::size_t kMaxAcceptedPushes = 1;
28
29
}  // namespace
30
31
/// Construct detached protocol state without observing a connection.
32
27.5k
H2Runtime::H2Runtime() = default;
33
34
/// Install the bounded push policy before curl emits its initial SETTINGS.
35
/// The runtime must outlive the multi and any accepted pushed handles.
36
/// @param multi Caller-owned multi handle receiving the push callbacks.
37
/// @param accept_push Whether to accept one push and reject further pushes.
38
27.3k
void H2Runtime::PrepareMulti(CURLM* multi, bool accept_push) {
39
27.3k
  accept_push_ = accept_push;
40
27.3k
  accepted_push_count_ = 0;
41
27.3k
  pushed_body_bytes_ = 0;
42
27.3k
  (void)curl_multi_setopt(multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
43
27.3k
  (void)curl_multi_setopt(multi, CURLMOPT_PUSHFUNCTION, &H2Runtime::PushCallback);
44
27.3k
  (void)curl_multi_setopt(multi, CURLMOPT_PUSHDATA, this);
45
27.3k
}
46
47
/// Observe application bytes after the carrier has decoded its transport.
48
/// A null connection is harmless; an attached connection borrows this runtime.
49
/// @param connection Transport to observe, or nullptr when creation failed.
50
26.4k
void H2Runtime::AttachConnection(MockConnection* connection) {
51
26.4k
  if (connection != nullptr) {
52
26.4k
    connection->SetIncomingDataObserver(&plan_driver_);
53
26.4k
  }
54
26.4k
}
55
56
/// Borrow a structured plan for the ensuing synchronous drive.
57
/// @param plan Stable plan that remains alive and unmodified during the drive.
58
/// @param scheme Scheme used in generated push-request pseudo-headers.
59
/// @param authority Authority used in generated push-request pseudo-headers.
60
16.0k
void H2Runtime::ResetPlan(const curl::fuzzer::proto::Http2Plan& plan, std::string scheme, std::string authority) {
61
16.0k
  plan_driver_.Reset(plan, std::move(scheme), std::move(authority));
62
16.0k
  stream_weight_after_headers_ = plan.stream_weight_after_headers();
63
16.0k
  post_headers_transition_applied_ = false;
64
16.0k
  stream_weight_setopt_result_ = CURLE_FAILED_INIT;
65
16.0k
  stream_weight_egress_result_ = CURLE_FAILED_INIT;
66
16.0k
}
67
68
/// Apply the plan's one state transition only after a real request stream has
69
/// been observed. Changing the public option at this point makes curl compare
70
/// the desired weight against the live stream's effective weight.
71
/// @param easy Active easy handle whose live stream weight is changed.
72
245k
void H2Runtime::ApplyPostHeadersTransition(CURL* easy) {
73
245k
  if (stream_weight_after_headers_ == 0 || post_headers_transition_applied_ ||
74
244k
      plan_driver_.observed_request_count() == 0) {
75
244k
    return;
76
244k
  }
77
1.30k
  post_headers_transition_applied_ = true;
78
1.30k
  stream_weight_setopt_result_ =
79
1.30k
      curl_easy_setopt(easy, CURLOPT_STREAM_WEIGHT, static_cast<long>(stream_weight_after_headers_));
80
  // Unpausing an already-running H2 stream is a documented no-op at the
81
  // transfer layer, but it deliberately asks the live filter to flush its
82
  // updated flow/priority state. This public call provides a deterministic
83
  // egress boundary without sleeping or depending on socket readiness.
84
1.30k
  stream_weight_egress_result_ = curl_easy_pause(easy, CURLPAUSE_CONT);
85
1.30k
}
86
87
/// Alternate curl and one bounded H2 plan boundary. Carrier observation and
88
/// response-resume work run after each successful perform through the hook.
89
/// Idle and operation limits remain selected by the owning driver.
90
/// @param multi Caller-owned multi handle containing the transfer to drive.
91
/// @param server Carrier exposing the currently active connection.
92
/// @param max_idle_iterations Consecutive no-progress turns allowed.
93
/// @param max_drive_iterations Total state-machine turns allowed.
94
/// @param after_perform Carrier hook invoked after each successful perform.
95
void H2Runtime::DrivePlan(CURLM* multi, MockServer& server, int max_idle_iterations, int max_drive_iterations,
96
16.0k
                          const std::function<void()>& after_perform) {
97
16.0k
  int still_running = 1;
98
16.0k
  int idle_iterations = 0;
99
16.0k
  int drive_iterations = 0;
100
253k
  while (still_running && idle_iterations < max_idle_iterations && drive_iterations++ < max_drive_iterations) {
101
245k
    const int running_before = still_running;
102
245k
    if (curl_multi_perform(multi, &still_running) != CURLM_OK) {
103
0
      break;
104
0
    }
105
245k
    after_perform();
106
245k
    if (!still_running) {
107
7.92k
      break;
108
7.92k
    }
109
110
237k
    bool made_progress = still_running != running_before;
111
237k
    if (MockConnection* connection = server.connection(); connection != nullptr) {
112
235k
      made_progress = connection->DrainIncoming() != 0 || made_progress;
113
235k
      std::string chunk;
114
235k
      if (plan_driver_.NextChunk(&chunk)) {
115
155k
        made_progress = true;
116
155k
        if (!chunk.empty()) {
117
80.7k
          (void)connection->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
118
80.7k
        }
119
155k
        made_progress = connection->DrainIncoming() != 0 || made_progress;
120
155k
      }
121
235k
    }
122
237k
    if (made_progress) {
123
172k
      idle_iterations = 0;
124
172k
    } else {
125
65.2k
      ++idle_iterations;
126
65.2k
    }
127
237k
  }
128
16.0k
}
129
130
/// Probe upkeep while curl's connection cache and the peer are still live.
131
/// @param easy Caller-owned easy handle whose upkeep API is probed.
132
/// @param connection Peer to drain after upkeep, or nullptr when none opened.
133
27.3k
void H2Runtime::FinishTransfer(CURL* easy, MockConnection* connection) {
134
27.3k
  upkeep_result_ = curl_easy_upkeep(easy);
135
27.3k
  if (connection != nullptr) {
136
26.4k
    (void)connection->DrainIncoming();
137
26.4k
  }
138
139
27.3k
#if defined(PROTO_FUZZER_OBSERVE_STATE)
140
  // The carrier still owns every callback and plan observation here. Retain
141
  // the completed state in profiling builds without changing the H2 exchange
142
  // or adding per-input work to ordinary fuzz targets.
143
27.3k
  const volatile std::size_t observation[] = {push_callback_count(),
144
27.3k
                                              push_header_count(),
145
27.3k
                                              saw_push_path(),
146
27.3k
                                              accepted_push_count(),
147
27.3k
                                              pushed_body_bytes(),
148
27.3k
                                              static_cast<std::size_t>(upkeep_result()),
149
27.3k
                                              observed_request_count(),
150
27.3k
                                              observed_client_settings_count(),
151
27.3k
                                              plan_driver_.has_pending_work()};
152
27.3k
  (void)observation;
153
27.3k
#endif
154
27.3k
}
155
156
/// @return Number of syntactically valid pushes offered to this runtime.
157
27.3k
std::size_t H2Runtime::push_callback_count() const { return push_callback_count_; }
158
159
/// @return Number of PUSH_PROMISE headers advertised to the push callback.
160
27.3k
std::size_t H2Runtime::push_header_count() const { return push_header_count_; }
161
162
/// @return True when the latest push callback found the promised :path.
163
27.3k
bool H2Runtime::saw_push_path() const { return saw_push_path_; }
164
165
/// @return Number of pushed transfers accepted in the most recent drive.
166
27.3k
std::size_t H2Runtime::accepted_push_count() const { return accepted_push_count_; }
167
168
/// @return Body bytes consumed from accepted pushes in the latest drive.
169
27.3k
std::size_t H2Runtime::pushed_body_bytes() const { return pushed_body_bytes_; }
170
171
/// @return Result of the most recent upkeep probe, or CURLE_FAILED_INIT.
172
27.3k
CURLcode H2Runtime::upkeep_result() const { return upkeep_result_; }
173
174
/// @return Number of request streams observed since the latest plan reset.
175
27.3k
std::size_t H2Runtime::observed_request_count() const { return plan_driver_.observed_request_count(); }
176
177
/// @return Non-ACK client SETTINGS frames observed since the latest plan reset.
178
27.3k
std::size_t H2Runtime::observed_client_settings_count() const { return plan_driver_.observed_client_settings_count(); }
179
180
/// @return PRIORITY frames observed in curl's client byte stream.
181
0
std::size_t H2Runtime::observed_client_priority_count() const { return plan_driver_.observed_client_priority_count(); }
182
183
/// @return Whether request observation triggered the configured transition.
184
0
bool H2Runtime::post_headers_transition_applied() const { return post_headers_transition_applied_; }
185
186
/// @return Result of the live CURLOPT_STREAM_WEIGHT update.
187
0
CURLcode H2Runtime::stream_weight_setopt_result() const { return stream_weight_setopt_result_; }
188
189
/// @return Result of the public resume used as the deterministic egress edge.
190
0
CURLcode H2Runtime::stream_weight_egress_result() const { return stream_weight_egress_result_; }
191
192
int H2Runtime::PushCallback(CURL* /*parent*/, CURL* pushed, std::size_t header_count, struct curl_pushheaders* headers,
193
8.48k
                            void* userdata) {
194
8.48k
  auto* self = static_cast<H2Runtime*>(userdata);
195
8.48k
  if (self == nullptr) {
196
0
    return CURL_PUSH_DENY;
197
0
  }
198
199
8.48k
  ++self->push_callback_count_;
200
8.48k
  self->push_header_count_ += header_count;
201
8.48k
  const std::size_t inspected = std::min(header_count, kMaxInspectedPushHeaders);
202
55.5k
  for (std::size_t index = 0; index < inspected; ++index) {
203
47.0k
    (void)curl_pushheader_bynum(headers, index);
204
47.0k
  }
205
8.48k
  self->saw_push_path_ = curl_pushheader_byname(headers, ":path") != nullptr;
206
8.48k
  (void)curl_pushheader_byname(headers, "x-fuzzer-missing");
207
208
8.48k
  if (!self->accept_push_ || self->accepted_push_count_ >= kMaxAcceptedPushes) {
209
6.63k
    return CURL_PUSH_DENY;
210
6.63k
  }
211
  // The new pushed handle needs an application-owned sink before acceptance.
212
1.84k
  if (curl_easy_setopt(pushed, CURLOPT_WRITEFUNCTION, &H2Runtime::PushedWriteCallback) != CURLE_OK ||
213
1.84k
      curl_easy_setopt(pushed, CURLOPT_WRITEDATA, self) != CURLE_OK) {
214
0
    return CURL_PUSH_DENY;
215
0
  }
216
1.84k
  ++self->accepted_push_count_;
217
1.84k
  return CURL_PUSH_OK;
218
1.84k
}
219
220
164
std::size_t H2Runtime::PushedWriteCallback(char* /*contents*/, std::size_t size, std::size_t nmemb, void* userdata) {
221
164
  auto* self = static_cast<H2Runtime*>(userdata);
222
164
  const std::size_t maximum = std::numeric_limits<std::size_t>::max();
223
164
  if (self == nullptr || (size != 0 && nmemb > maximum / size)) {
224
0
    return 0;
225
0
  }
226
164
  const std::size_t bytes = size * nmemb;
227
164
  self->pushed_body_bytes_ += std::min(bytes, maximum - self->pushed_body_bytes_);
228
164
  return bytes;
229
164
}
230
231
}  // namespace proto_fuzzer