/src/curl_fuzzer/proto_fuzzer/pingpong_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 Implementation of the shared line-oriented pingpong peer. |
9 | | |
10 | | #include "proto_fuzzer/pingpong_mock_server.h" |
11 | | |
12 | | #include <algorithm> |
13 | | #include <cstddef> |
14 | | #include <cstdint> |
15 | | #include <limits> |
16 | | #include <memory> |
17 | | #include <string> |
18 | | #include <string_view> |
19 | | |
20 | | #include "proto_fuzzer/mock_server.h" |
21 | | |
22 | | namespace proto_fuzzer { |
23 | | |
24 | | /// Construct the shared transport state without a borrowed protobuf or open |
25 | | /// socketpair. RunLoop borrows a Scenario only for the synchronous |
26 | | /// DriveScenario call in which socket callbacks can run. |
27 | | /// @param drive_policy Multi-loop policy inherited from MockServerBase. |
28 | | PingpongMockServer::PingpongMockServer(MultiDrivePolicy drive_policy) |
29 | 23.3k | : MockServerBase(drive_policy), |
30 | 23.3k | scenario_(nullptr), |
31 | 23.3k | control_reply_count_(0), |
32 | 23.3k | next_control_reply_(0), |
33 | 23.3k | active_pollset_probe_count_(0), |
34 | 23.3k | body_length_remaining_(0), |
35 | 23.3k | body_terminator_remaining_(0), |
36 | 23.3k | body_mode_(BodyMode::kCommand), |
37 | 23.3k | control_opened_(false) {} |
38 | | |
39 | | /// Release the server-owned socketpair half after curl has returned its client |
40 | | /// descriptor. The out-of-line destructor keeps MockConnection's complete type |
41 | | /// local to this translation unit. |
42 | 23.3k | PingpongMockServer::~PingpongMockServer() = default; |
43 | | |
44 | | /// Expose the bounded command capture from the most recent drive so tests can |
45 | | /// verify protocol states without coupling assertions to curl debug output. |
46 | | /// @return A prefix of complete control commands, including line endings. |
47 | 23.1k | const std::string& PingpongMockServer::control_transcript() const { return control_transcript_; } |
48 | | |
49 | | /// Expose a bounded prefix of bytes drained during the most recent body phase |
50 | | /// while continuing to drain every client byte, so test observability cannot |
51 | | /// create backpressure. |
52 | | /// @return Body bytes curl sent, or an empty string when the peer has no |
53 | | /// upload direction. |
54 | 16.9k | const std::string& PingpongMockServer::uploaded_data() const { return uploaded_data_; } |
55 | | |
56 | | /// Report how many requested zero-timeout multi-poll probes ran during the |
57 | | /// most recent drive. RunLoop increments this only after a successful perform |
58 | | /// turn while curl still owns an active transfer and the control socket is |
59 | | /// open, making the count a focused test assertion rather than a syscall |
60 | | /// counter for default scenarios. |
61 | | /// @return Number of active, connected pollset probes in the previous drive. |
62 | 0 | std::size_t PingpongMockServer::active_pollset_probe_count() const { return active_pollset_probe_count_; } |
63 | | |
64 | | /// Create the default plaintext transport. The SMTPS subclass overrides this |
65 | | /// factory while retaining the command/reply parser. |
66 | | /// @return A new socketpair-backed connection. |
67 | 11.1k | std::unique_ptr<MockConnection> PingpongMockServer::CreateConnection() { return std::make_unique<MockConnection>(); } |
68 | | |
69 | | /// Enter marker-terminated body mode. The marker and any body bytes before it |
70 | | /// are captured together, matching SMTP's inclusion of its end-of-body CRLF. |
71 | | /// @param terminator Byte sequence that ends the body phase. |
72 | 610 | void PingpongMockServer::BeginMarkerBody(std::string_view terminator) { |
73 | 610 | body_mode_ = BodyMode::kMarker; |
74 | 610 | body_marker_.assign(terminator.data(), terminator.size()); |
75 | 610 | body_length_remaining_ = 0; |
76 | 610 | body_terminator_remaining_ = 0; |
77 | 610 | } |
78 | | |
79 | | /// Enter length-announced literal body mode. Exactly `length` bytes are |
80 | | /// captured; the CRLF that follows the literal is consumed but not captured. |
81 | | /// A zero length is valid and waits directly for that terminator. |
82 | | /// @param length Announced literal byte count. |
83 | 840 | void PingpongMockServer::BeginLiteralBody(std::size_t length) { |
84 | 840 | body_mode_ = BodyMode::kLiteral; |
85 | 840 | body_marker_.clear(); |
86 | 840 | body_length_remaining_ = length; |
87 | 840 | body_terminator_remaining_ = 2; |
88 | 840 | } |
89 | | |
90 | | /// Enter exact-length body mode. Exactly `length` bytes are captured and no |
91 | | /// terminator is consumed, so the next byte after the body starts the next |
92 | | /// command. A zero length completes immediately. |
93 | | /// @param length Announced body byte count. |
94 | 1.53k | void PingpongMockServer::BeginExactBody(std::size_t length) { |
95 | 1.53k | body_mode_ = BodyMode::kExact; |
96 | 1.53k | body_marker_.clear(); |
97 | 1.53k | body_length_remaining_ = length; |
98 | 1.53k | body_terminator_remaining_ = 0; |
99 | 1.53k | } |
100 | | |
101 | | /// Append decoded application bytes to the pending parser buffer. The outer |
102 | | /// service loop drains this buffer after every transport call, so it remains |
103 | | /// bounded by the scenario's command and upload budgets. |
104 | | /// @param data First decoded byte. |
105 | | /// @param size Number of bytes available at `data`. |
106 | 96.5k | void PingpongMockServer::ObserveIncomingData(const unsigned char* data, std::size_t size) { |
107 | 96.5k | if (data == nullptr || size == 0) { |
108 | 0 | return; |
109 | 0 | } |
110 | 96.5k | pending_control_bytes_.append(reinterpret_cast<const char*>(data), size); |
111 | 96.5k | } |
112 | | |
113 | | /// Create the single control socketpair and queue the server greeting. These |
114 | | /// peers never open a second application socket, so any later request is |
115 | | /// rejected rather than silently replacing the live peer. |
116 | | /// @param purpose Socket role requested by curl. |
117 | | /// @param address Unused stream destination metadata required by the callback |
118 | | /// signature. |
119 | | /// @return Client descriptor for the control channel, or CURL_SOCKET_BAD. |
120 | 20.3k | curl_socket_t PingpongMockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* /*address*/) { |
121 | 20.3k | if (scenario_ == nullptr || purpose != CURLSOCKTYPE_IPCXN || control_opened_) { |
122 | 0 | return CURL_SOCKET_BAD; |
123 | 0 | } |
124 | | |
125 | 20.3k | connection_ = CreateConnection(); |
126 | 20.3k | if (!connection_ || !connection_->ok()) { |
127 | 0 | connection_.reset(); |
128 | 0 | return CURL_SOCKET_BAD; |
129 | 0 | } |
130 | 20.3k | connection_->SetIncomingDataObserver(this); |
131 | | |
132 | 20.3k | ApplyScriptBackpressure(connection_.get(), scenario_->connection()); |
133 | 20.3k | const std::string& greeting = scenario_->connection().initial_response(); |
134 | 20.3k | if (!greeting.empty() && |
135 | 7.58k | !connection_->WriteAll(reinterpret_cast<const unsigned char*>(greeting.data()), greeting.size())) { |
136 | 9 | connection_.reset(); |
137 | 9 | return CURL_SOCKET_BAD; |
138 | 9 | } |
139 | | |
140 | 20.3k | control_opened_ = true; |
141 | 20.3k | return connection_->take_client_fd(); |
142 | 20.3k | } |
143 | | |
144 | | /// Reset every borrowed pointer, descriptor, parser cursor, and observable |
145 | | /// capture before borrowing the next Scenario, then let the subclass clear its |
146 | | /// own protocol state. |
147 | | /// @param scenario Scenario whose Connection supplies the greeting and replies. |
148 | 22.7k | void PingpongMockServer::ResetForScenario(const curl::fuzzer::proto::Scenario& scenario) { |
149 | 22.7k | connection_.reset(); |
150 | 22.7k | scenario_ = &scenario; |
151 | 22.7k | control_reply_count_ = std::min<std::size_t>(scenario_limits::kMaxResponseChunks, |
152 | 22.7k | static_cast<std::size_t>(scenario.connection().on_readable_size())); |
153 | 22.7k | next_control_reply_ = 0; |
154 | 22.7k | pending_control_bytes_.clear(); |
155 | 22.7k | control_transcript_.clear(); |
156 | 22.7k | uploaded_data_.clear(); |
157 | 22.7k | active_pollset_probe_count_ = 0; |
158 | 22.7k | body_marker_.clear(); |
159 | 22.7k | body_length_remaining_ = 0; |
160 | 22.7k | body_terminator_remaining_ = 0; |
161 | 22.7k | body_mode_ = BodyMode::kCommand; |
162 | 22.7k | control_opened_ = false; |
163 | 22.7k | ResetProtocolState(); |
164 | 22.7k | } |
165 | | |
166 | | /// Apply bounded transport knobs when creating the scripted socket. Target |
167 | | /// policies normally clear mail backpressure, but direct corpus replay must |
168 | | /// retain deterministic, well-defined behavior. |
169 | | /// @param connection Peer receiving the socket settings. |
170 | | /// @param script Connection script carrying optional backpressure values. |
171 | | void PingpongMockServer::ApplyScriptBackpressure(MockConnection* connection, |
172 | 20.3k | const curl::fuzzer::proto::Connection& script) { |
173 | 20.3k | if (connection == nullptr) { |
174 | 0 | return; |
175 | 0 | } |
176 | | |
177 | 20.3k | const std::uint32_t int_max = static_cast<std::uint32_t>(std::numeric_limits<int>::max()); |
178 | 20.3k | const auto& backpressure = script.backpressure(); |
179 | 20.3k | const int receive_buffer = static_cast<int>(std::min(backpressure.recv_buf_bytes(), int_max)); |
180 | 20.3k | connection->ApplyBackpressure(receive_buffer, static_cast<std::size_t>(backpressure.drain_limit())); |
181 | 20.3k | } |
182 | | |
183 | | /// Return the next command-aligned response within the shared response-count |
184 | | /// budget. Empty strings still advance the cursor: they are meaningful |
185 | | /// truncation mutations for ordinary command states. |
186 | | /// @return Scenario-owned reply, or nullptr after the bounded prefix. |
187 | 1.10M | const std::string* PingpongMockServer::NextControlReply() { |
188 | 1.10M | if (scenario_ == nullptr || next_control_reply_ >= control_reply_count_) { |
189 | 1.08M | return nullptr; |
190 | 1.08M | } |
191 | 22.8k | return &scenario_->connection().on_readable(static_cast<int>(next_control_reply_++)); |
192 | 1.10M | } |
193 | | |
194 | | /// Keep failed peer writes local to the mock. Response cursors must still |
195 | | /// advance after curl closes early, otherwise a replacement socket could see |
196 | | /// a reply intended for an earlier command. |
197 | | /// @param bytes Response fragment to queue. |
198 | | /// @return true when the complete fragment was queued. |
199 | 35.1k | bool PingpongMockServer::WriteControlBytes(std::string_view bytes) { |
200 | 35.1k | return bytes.empty() || (connection_ != nullptr && |
201 | 34.8k | connection_->WriteAll(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size())); |
202 | 35.1k | } |
203 | | |
204 | | /// Consume the next scripted reply. Protocol peers use this when the reply |
205 | | /// that completes an upload must be queued at command-acceptance time because |
206 | | /// curl performs a blocking completion wait. |
207 | | /// @return true when a scripted reply existed and was successfully written. |
208 | 19.2k | bool PingpongMockServer::WriteNextReply() { |
209 | 19.2k | const std::string* response = NextControlReply(); |
210 | 19.2k | return response == nullptr ? false : WriteControlBytes(*response); |
211 | 19.2k | } |
212 | | |
213 | | /// Remove optional leading horizontal whitespace and return the first token |
214 | | /// without allocating. Protocol arguments are never interpreted here. |
215 | | /// @param line Complete command line. |
216 | | /// @return View of its first non-whitespace token. |
217 | 1.10M | std::string_view PingpongMockServer::FirstToken(std::string_view line) { |
218 | 1.10M | std::size_t begin = 0; |
219 | 1.20M | while (begin < line.size() && (line[begin] == ' ' || line[begin] == '\t')) { |
220 | 100k | ++begin; |
221 | 100k | } |
222 | | |
223 | 1.10M | std::size_t end = begin; |
224 | 69.1M | while (end < line.size() && line[end] != ' ' && line[end] != '\t' && line[end] != '\r' && line[end] != '\n') { |
225 | 68.0M | ++end; |
226 | 68.0M | } |
227 | 1.10M | return line.substr(begin, end - begin); |
228 | 1.10M | } |
229 | | |
230 | | /// Skip the first token and return the second whitespace-delimited token |
231 | | /// without allocating. IMAP uses this to read the verb after the command tag. |
232 | | /// @param line Complete command line. |
233 | | /// @return View of its second whitespace-delimited token, or an empty view. |
234 | 23.9k | std::string_view PingpongMockServer::SecondToken(std::string_view line) { |
235 | 23.9k | std::size_t position = 0; |
236 | 30.7k | while (position < line.size() && (line[position] == ' ' || line[position] == '\t')) { |
237 | 6.75k | ++position; |
238 | 6.75k | } |
239 | 851k | while (position < line.size() && line[position] != ' ' && line[position] != '\t' && line[position] != '\r' && |
240 | 827k | line[position] != '\n') { |
241 | 827k | ++position; |
242 | 827k | } |
243 | 45.6k | while (position < line.size() && (line[position] == ' ' || line[position] == '\t')) { |
244 | 21.7k | ++position; |
245 | 21.7k | } |
246 | | |
247 | 23.9k | std::size_t end = position; |
248 | 166k | while (end < line.size() && line[end] != ' ' && line[end] != '\t' && line[end] != '\r' && line[end] != '\n') { |
249 | 142k | ++end; |
250 | 142k | } |
251 | 23.9k | return line.substr(position, end - position); |
252 | 23.9k | } |
253 | | |
254 | | /// Fold only ASCII lowercase command letters. Protocol verbs are ASCII tokens, |
255 | | /// so locale-aware case conversion would add state and undefined signed-char |
256 | | /// behavior without accepting anything curl can legitimately send. |
257 | | /// @param token Parsed command token. |
258 | | /// @param expected_uppercase Literal uppercase command name. |
259 | | /// @return true when the tokens are equal ignoring ASCII case. |
260 | 40.0k | bool PingpongMockServer::TokenEquals(std::string_view token, std::string_view expected_uppercase) { |
261 | 40.0k | if (token.size() != expected_uppercase.size()) { |
262 | 9.91k | return false; |
263 | 9.91k | } |
264 | 78.6k | for (std::size_t index = 0; index < token.size(); ++index) { |
265 | 69.0k | unsigned char actual = static_cast<unsigned char>(token[index]); |
266 | 69.0k | if (actual >= 'a' && actual <= 'z') { |
267 | 88 | actual = static_cast<unsigned char>(actual - 'a' + 'A'); |
268 | 88 | } |
269 | 69.0k | if (actual != static_cast<unsigned char>(expected_uppercase[index])) { |
270 | 20.5k | return false; |
271 | 20.5k | } |
272 | 69.0k | } |
273 | 9.53k | return true; |
274 | 30.0k | } |
275 | | |
276 | | /// Retain only a bounded prefix for assertions. Parsing and draining continue |
277 | | /// against the full transport bytes, so reaching the cap cannot change curl's |
278 | | /// behavior or manufacture socket backpressure. |
279 | | /// @param source Newly observed bytes. |
280 | | /// @param limit Maximum retained destination size. |
281 | | /// @param destination Capture buffer receiving the available prefix. |
282 | 1.18M | void PingpongMockServer::CapturePrefix(std::string_view source, std::size_t limit, std::string* destination) { |
283 | 1.18M | if (destination == nullptr || destination->size() >= limit) { |
284 | 677k | return; |
285 | 677k | } |
286 | 503k | const std::size_t available = limit - destination->size(); |
287 | 503k | destination->append(source.data(), std::min(source.size(), available)); |
288 | 503k | } |
289 | | |
290 | | /// Advance the carrier without waiting, then process every complete command or |
291 | | /// body segment already decoded into the pending buffer. A plaintext |
292 | | /// connection yields raw socket bytes; a TLS carrier completes its handshake |
293 | | /// and yields decrypted application bytes through ObserveIncomingData. |
294 | | /// @return true when bytes, transport state, or a command/reply cursor advanced. |
295 | 243k | bool PingpongMockServer::ServiceControlConnection() { |
296 | 243k | if (connection_ == nullptr) { |
297 | 10.9k | return false; |
298 | 10.9k | } |
299 | | |
300 | 232k | const std::size_t size_before_read = pending_control_bytes_.size(); |
301 | 232k | const std::size_t transport_progress = connection_->DrainIncoming(); |
302 | 232k | bool made_progress = pending_control_bytes_.size() != size_before_read || transport_progress != 0; |
303 | | |
304 | 232k | std::size_t consumed = 0; |
305 | 1.41M | while (consumed < pending_control_bytes_.size()) { |
306 | 1.20M | if (body_mode_ == BodyMode::kMarker) { |
307 | 8.36k | const std::size_t marker = pending_control_bytes_.find(body_marker_, consumed); |
308 | 8.36k | if (marker == std::string::npos) { |
309 | 7.88k | break; |
310 | 7.88k | } |
311 | 480 | const std::size_t body_end = marker + body_marker_.size(); |
312 | 480 | CapturePrefix(std::string_view(pending_control_bytes_.data() + consumed, body_end - consumed), |
313 | 480 | kMaxCapturedUploadBytes, &uploaded_data_); |
314 | 480 | consumed = body_end; |
315 | 480 | body_mode_ = BodyMode::kCommand; |
316 | 480 | made_progress = true; |
317 | 480 | HandleBodyComplete(); |
318 | 480 | continue; |
319 | 8.36k | } |
320 | | |
321 | 1.20M | if (body_mode_ == BodyMode::kExact) { |
322 | 1.94k | const std::size_t available = pending_control_bytes_.size() - consumed; |
323 | 1.94k | const std::size_t take = std::min(available, body_length_remaining_); |
324 | 1.94k | CapturePrefix(std::string_view(pending_control_bytes_.data() + consumed, take), kMaxCapturedUploadBytes, |
325 | 1.94k | &uploaded_data_); |
326 | 1.94k | consumed += take; |
327 | 1.94k | body_length_remaining_ -= take; |
328 | 1.94k | if (take != 0) { |
329 | 1.94k | made_progress = true; |
330 | 1.94k | } |
331 | 1.94k | if (body_length_remaining_ == 0) { |
332 | 1.27k | body_mode_ = BodyMode::kCommand; |
333 | 1.27k | made_progress = true; |
334 | 1.27k | HandleBodyComplete(); |
335 | 1.27k | continue; |
336 | 1.27k | } |
337 | 671 | break; |
338 | 1.94k | } |
339 | | |
340 | 1.19M | if (body_mode_ == BodyMode::kLiteral) { |
341 | 13.0k | if (body_length_remaining_ != 0) { |
342 | 12.5k | const std::size_t available = pending_control_bytes_.size() - consumed; |
343 | 12.5k | const std::size_t take = std::min(available, body_length_remaining_); |
344 | 12.5k | CapturePrefix(std::string_view(pending_control_bytes_.data() + consumed, take), kMaxCapturedUploadBytes, |
345 | 12.5k | &uploaded_data_); |
346 | 12.5k | consumed += take; |
347 | 12.5k | body_length_remaining_ -= take; |
348 | 12.5k | made_progress = true; |
349 | 12.5k | continue; |
350 | 12.5k | } |
351 | | |
352 | 499 | const std::size_t available = pending_control_bytes_.size() - consumed; |
353 | 499 | if (body_terminator_remaining_ == 2 && available >= 2 && pending_control_bytes_[consumed] == '\r' && |
354 | 495 | pending_control_bytes_[consumed + 1] == '\n') { |
355 | 495 | consumed += 2; |
356 | 495 | body_terminator_remaining_ = 0; |
357 | 495 | body_mode_ = BodyMode::kCommand; |
358 | 495 | made_progress = true; |
359 | 495 | HandleBodyComplete(); |
360 | 495 | continue; |
361 | 495 | } |
362 | 4 | if (available >= 1) { |
363 | 4 | const char byte = pending_control_bytes_[consumed]; |
364 | 4 | if (byte == '\r' || byte == '\n') { |
365 | 1 | ++consumed; |
366 | 1 | if (byte == '\n' || body_terminator_remaining_ == 0) { |
367 | 1 | body_terminator_remaining_ = 0; |
368 | 1 | body_mode_ = BodyMode::kCommand; |
369 | 1 | HandleBodyComplete(); |
370 | 1 | } else { |
371 | 0 | body_terminator_remaining_ = 1; |
372 | 0 | } |
373 | 1 | made_progress = true; |
374 | 1 | continue; |
375 | 1 | } |
376 | | // No terminating empty line is recognisable. Resume command parsing so |
377 | | // malformed uploads still make bounded progress. |
378 | 3 | body_terminator_remaining_ = 0; |
379 | 3 | body_mode_ = BodyMode::kCommand; |
380 | 3 | continue; |
381 | 4 | } |
382 | 0 | break; |
383 | 4 | } |
384 | | |
385 | 1.18M | const std::size_t newline = pending_control_bytes_.find('\n', consumed); |
386 | 1.18M | if (newline == std::string::npos) { |
387 | 20.8k | break; |
388 | 20.8k | } |
389 | 1.16M | const std::size_t command_size = newline - consumed + 1; |
390 | 1.16M | const std::string_view command(pending_control_bytes_.data() + consumed, command_size); |
391 | 1.16M | CapturePrefix(command, kMaxCapturedControlBytes, &control_transcript_); |
392 | 1.16M | HandleLine(command); |
393 | 1.16M | consumed += command_size; |
394 | 1.16M | made_progress = true; |
395 | 1.16M | } |
396 | | |
397 | 232k | if (consumed != 0) { |
398 | 53.0k | pending_control_bytes_.erase(0, consumed); |
399 | 53.0k | } |
400 | 232k | return made_progress; |
401 | 243k | } |
402 | | |
403 | | /// Prepare cleanup while the mock can still write to curl's control socket. |
404 | | /// Let the peer queue its protocol-specific liveness reply, then half-close so |
405 | | /// an unfinished transfer cannot wait on a reply that will never arrive. |
406 | 22.7k | void PingpongMockServer::FinishConnections() { |
407 | 22.7k | if (connection_ != nullptr) { |
408 | 20.3k | QueueShutdownReply(); |
409 | 20.3k | connection_->ShutdownWrite(); |
410 | 20.3k | } |
411 | 22.7k | } |
412 | | |
413 | | /// Give curl's graceful shutdown one last chance to emit its teardown command |
414 | | /// while the peer still owns its read half, then release the multi as the base |
415 | | /// class would. The matching liveness reply was already queued by |
416 | | /// FinishConnections. |
417 | | /// @param multi Detached multi handle whose connection cache may be shutting |
418 | | /// the protocol connection down. |
419 | 22.7k | void PingpongMockServer::HandleDetachedMulti(CurlMultiPtr multi) { |
420 | 22.7k | multi.reset(); |
421 | 22.7k | (void)ServiceControlConnection(); |
422 | 22.7k | } |
423 | | |
424 | | /// Alternate curl transitions with immediate peer service until the transfer |
425 | | /// completes or deterministic idle/operation caps win. No select(), poll(), or |
426 | | /// sleep is needed: every transport is in-process, and malformed scripts are |
427 | | /// terminated by half-close after the bounded loop. |
428 | | /// @param multi Multi handle containing `easy`. |
429 | | /// @param easy Easy handle already installed on this mock. |
430 | | /// @param scenario Control script to borrow. |
431 | 22.7k | void PingpongMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) { |
432 | 22.7k | (void)easy; |
433 | 22.7k | ResetForScenario(scenario); |
434 | | |
435 | 22.7k | int still_running = 1; |
436 | 22.7k | int idle_iterations = 0; |
437 | 22.7k | int drive_iterations = 0; |
438 | 22.7k | CURLMcode result = CURLM_OK; |
439 | 220k | while (still_running && idle_iterations < kMaxIdleIterations && drive_iterations++ < kMaxDriveIterations) { |
440 | 197k | const int running_before = still_running; |
441 | 197k | result = curl_multi_perform(multi, &still_running); |
442 | 197k | if (result != CURLM_OK) { |
443 | 0 | break; |
444 | 0 | } |
445 | | |
446 | | // Pollset construction is coverage work, not part of the default driver: |
447 | | // keep the extra public-API call behind the scenario opt-in. Waiting until |
448 | | // the socket callback has run and the transfer remains active ensures the |
449 | | // pollset belongs to a live protocol state rather than setup or cleanup. |
450 | 197k | if (scenario.probe_multi_pollset() && still_running && control_opened_) { |
451 | 24.8k | ProbeMultiPollset(multi); |
452 | 24.8k | ++active_pollset_probe_count_; |
453 | 24.8k | } |
454 | | |
455 | 197k | bool made_progress = still_running != running_before; |
456 | 197k | made_progress = ServiceControlConnection() || made_progress; |
457 | 197k | if (made_progress) { |
458 | 94.5k | idle_iterations = 0; |
459 | 103k | } else { |
460 | 103k | ++idle_iterations; |
461 | 103k | } |
462 | 197k | } |
463 | | |
464 | | // The final perform may have emitted the last command or body bytes |
465 | | // immediately before marking the transfer done. Drain them before cleanup. |
466 | 22.7k | (void)ServiceControlConnection(); |
467 | 22.7k | FinishConnections(); |
468 | 22.7k | scenario_ = nullptr; |
469 | 22.7k | } |
470 | | |
471 | | } // namespace proto_fuzzer |