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/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
36.2k
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
36.0k
void H2Runtime::PrepareMulti(CURLM* multi, bool accept_push) {
39
36.0k
  accept_push_ = accept_push;
40
36.0k
  accepted_push_count_ = 0;
41
36.0k
  pushed_body_bytes_ = 0;
42
36.0k
  (void)curl_multi_setopt(multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
43
36.0k
  (void)curl_multi_setopt(multi, CURLMOPT_PUSHFUNCTION, &H2Runtime::PushCallback);
44
36.0k
  (void)curl_multi_setopt(multi, CURLMOPT_PUSHDATA, this);
45
36.0k
}
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
35.4k
void H2Runtime::AttachConnection(MockConnection* connection) {
51
35.4k
  if (connection != nullptr) {
52
35.4k
    connection->SetIncomingDataObserver(&plan_driver_);
53
35.4k
  }
54
35.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
19.7k
void H2Runtime::ResetPlan(const curl::fuzzer::proto::Http2Plan& plan, std::string scheme, std::string authority) {
61
19.7k
  plan_driver_.Reset(plan, std::move(scheme), std::move(authority));
62
19.7k
}
63
64
/// Alternate curl and one bounded H2 plan boundary. Carrier observation and
65
/// response-resume work run after each successful perform through the hook.
66
/// Idle and operation limits remain selected by the owning driver.
67
/// @param multi Caller-owned multi handle containing the transfer to drive.
68
/// @param server Carrier exposing the currently active connection.
69
/// @param max_idle_iterations Consecutive no-progress turns allowed.
70
/// @param max_drive_iterations Total state-machine turns allowed.
71
/// @param after_perform Carrier hook invoked after each successful perform.
72
void H2Runtime::DrivePlan(CURLM* multi, MockServer& server, int max_idle_iterations, int max_drive_iterations,
73
19.7k
                          const std::function<void()>& after_perform) {
74
19.7k
  int still_running = 1;
75
19.7k
  int idle_iterations = 0;
76
19.7k
  int drive_iterations = 0;
77
385k
  while (still_running && idle_iterations < max_idle_iterations && drive_iterations++ < max_drive_iterations) {
78
374k
    const int running_before = still_running;
79
374k
    if (curl_multi_perform(multi, &still_running) != CURLM_OK) {
80
0
      break;
81
0
    }
82
374k
    after_perform();
83
374k
    if (!still_running) {
84
8.33k
      break;
85
8.33k
    }
86
87
365k
    bool made_progress = still_running != running_before;
88
365k
    if (MockConnection* connection = server.connection(); connection != nullptr) {
89
365k
      made_progress = connection->DrainIncoming() != 0 || made_progress;
90
365k
      std::string chunk;
91
365k
      if (plan_driver_.NextChunk(&chunk)) {
92
260k
        made_progress = true;
93
260k
        if (!chunk.empty()) {
94
126k
          (void)connection->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
95
126k
        }
96
260k
        made_progress = connection->DrainIncoming() != 0 || made_progress;
97
260k
      }
98
365k
    }
99
365k
    if (made_progress) {
100
274k
      idle_iterations = 0;
101
274k
    } else {
102
91.2k
      ++idle_iterations;
103
91.2k
    }
104
365k
  }
105
19.7k
}
106
107
/// Probe upkeep while curl's connection cache and the peer are still live.
108
/// @param easy Caller-owned easy handle whose upkeep API is probed.
109
/// @param connection Peer to drain after upkeep, or nullptr when none opened.
110
36.0k
void H2Runtime::FinishTransfer(CURL* easy, MockConnection* connection) {
111
36.0k
  upkeep_result_ = curl_easy_upkeep(easy);
112
36.0k
  if (connection != nullptr) {
113
35.4k
    (void)connection->DrainIncoming();
114
35.4k
  }
115
116
36.0k
#if defined(PROTO_FUZZER_OBSERVE_STATE)
117
  // The carrier still owns every callback and plan observation here. Retain
118
  // the completed state in profiling builds without changing the H2 exchange
119
  // or adding per-input work to ordinary fuzz targets.
120
36.0k
  const volatile std::size_t observation[] = {push_callback_count(),
121
36.0k
                                              push_header_count(),
122
36.0k
                                              saw_push_path(),
123
36.0k
                                              accepted_push_count(),
124
36.0k
                                              pushed_body_bytes(),
125
36.0k
                                              static_cast<std::size_t>(upkeep_result()),
126
36.0k
                                              observed_request_count(),
127
36.0k
                                              observed_client_settings_count(),
128
36.0k
                                              plan_driver_.has_pending_work()};
129
36.0k
  (void)observation;
130
36.0k
#endif
131
36.0k
}
132
133
/// @return Number of syntactically valid pushes offered to this runtime.
134
36.0k
std::size_t H2Runtime::push_callback_count() const { return push_callback_count_; }
135
136
/// @return Number of PUSH_PROMISE headers advertised to the push callback.
137
36.0k
std::size_t H2Runtime::push_header_count() const { return push_header_count_; }
138
139
/// @return True when the latest push callback found the promised :path.
140
36.0k
bool H2Runtime::saw_push_path() const { return saw_push_path_; }
141
142
/// @return Number of pushed transfers accepted in the most recent drive.
143
36.0k
std::size_t H2Runtime::accepted_push_count() const { return accepted_push_count_; }
144
145
/// @return Body bytes consumed from accepted pushes in the latest drive.
146
36.0k
std::size_t H2Runtime::pushed_body_bytes() const { return pushed_body_bytes_; }
147
148
/// @return Result of the most recent upkeep probe, or CURLE_FAILED_INIT.
149
36.0k
CURLcode H2Runtime::upkeep_result() const { return upkeep_result_; }
150
151
/// @return Number of request streams observed since the latest plan reset.
152
36.0k
std::size_t H2Runtime::observed_request_count() const { return plan_driver_.observed_request_count(); }
153
154
/// @return Non-ACK client SETTINGS frames observed since the latest plan reset.
155
36.0k
std::size_t H2Runtime::observed_client_settings_count() const { return plan_driver_.observed_client_settings_count(); }
156
157
int H2Runtime::PushCallback(CURL* /*parent*/, CURL* pushed, std::size_t header_count, struct curl_pushheaders* headers,
158
8.36k
                            void* userdata) {
159
8.36k
  auto* self = static_cast<H2Runtime*>(userdata);
160
8.36k
  if (self == nullptr) {
161
0
    return CURL_PUSH_DENY;
162
0
  }
163
164
8.36k
  ++self->push_callback_count_;
165
8.36k
  self->push_header_count_ += header_count;
166
8.36k
  const std::size_t inspected = std::min(header_count, kMaxInspectedPushHeaders);
167
49.6k
  for (std::size_t index = 0; index < inspected; ++index) {
168
41.2k
    (void)curl_pushheader_bynum(headers, index);
169
41.2k
  }
170
8.36k
  self->saw_push_path_ = curl_pushheader_byname(headers, ":path") != nullptr;
171
8.36k
  (void)curl_pushheader_byname(headers, "x-fuzzer-missing");
172
173
8.36k
  if (!self->accept_push_ || self->accepted_push_count_ >= kMaxAcceptedPushes) {
174
6.26k
    return CURL_PUSH_DENY;
175
6.26k
  }
176
  // The new pushed handle needs an application-owned sink before acceptance.
177
2.09k
  if (curl_easy_setopt(pushed, CURLOPT_WRITEFUNCTION, &H2Runtime::PushedWriteCallback) != CURLE_OK ||
178
2.09k
      curl_easy_setopt(pushed, CURLOPT_WRITEDATA, self) != CURLE_OK) {
179
0
    return CURL_PUSH_DENY;
180
0
  }
181
2.09k
  ++self->accepted_push_count_;
182
2.09k
  return CURL_PUSH_OK;
183
2.09k
}
184
185
214
std::size_t H2Runtime::PushedWriteCallback(char* /*contents*/, std::size_t size, std::size_t nmemb, void* userdata) {
186
214
  auto* self = static_cast<H2Runtime*>(userdata);
187
214
  const std::size_t maximum = std::numeric_limits<std::size_t>::max();
188
214
  if (self == nullptr || (size != 0 && nmemb > maximum / size)) {
189
0
    return 0;
190
0
  }
191
214
  const std::size_t bytes = size * nmemb;
192
214
  self->pushed_body_bytes_ += std::min(bytes, maximum - self->pushed_body_bytes_);
193
214
  return bytes;
194
214
}
195
196
}  // namespace proto_fuzzer