/src/curl_fuzzer/proto_fuzzer/mqtt_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 packet-aware in-process MQTT peer. |
9 | | |
10 | | #include "proto_fuzzer/mqtt_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 | | #include "proto_fuzzer/scenario_limits.h" |
22 | | |
23 | | namespace proto_fuzzer { |
24 | | |
25 | | namespace { |
26 | | |
27 | | // MQTT 3.1.1 control packet types, already shifted into the fixed header's |
28 | | // high nibble. The low nibble carries type-specific flags such as PUBLISH QoS. |
29 | | constexpr unsigned char kMqttConnect = 0x10; |
30 | | constexpr unsigned char kMqttPublish = 0x30; |
31 | | constexpr unsigned char kMqttPuback = 0x40; |
32 | | constexpr unsigned char kMqttSubscribe = 0x80; |
33 | | constexpr unsigned char kMqttSuback = 0x90; |
34 | | constexpr unsigned char kMqttPingreq = 0xc0; |
35 | | constexpr unsigned char kMqttPingresp = 0xd0; |
36 | | constexpr unsigned char kMqttDisconnect = 0xe0; |
37 | | |
38 | 12.7k | unsigned char ByteAt(std::string_view bytes, std::size_t index) { return static_cast<unsigned char>(bytes[index]); } |
39 | | |
40 | | } // namespace |
41 | | |
42 | | /// Begin without a borrowed protobuf or open socketpair. RunLoop borrows a |
43 | | /// Scenario only for the synchronous DriveScenario call in which socket |
44 | | /// callbacks can run. |
45 | | MqttMockServer::MqttMockServer() |
46 | 4.04k | : scenario_(nullptr), |
47 | 4.04k | server_chunk_count_(0), |
48 | 4.04k | next_server_chunk_(0), |
49 | 4.04k | packet_count_(0), |
50 | 4.04k | connect_count_(0), |
51 | 4.04k | publish_count_(0), |
52 | 4.04k | subscribe_count_(0), |
53 | 4.04k | qos1_publish_count_(0), |
54 | 4.04k | active_pollset_probe_count_(0), |
55 | 4.04k | release_script_chunks_(false), |
56 | 4.04k | protocol_error_(false), |
57 | 4.04k | socket_opened_(false) {} |
58 | | |
59 | | /// Release the server-owned socketpair half after curl has returned its client |
60 | | /// descriptor. The out-of-line destructor keeps MockConnection's complete type |
61 | | /// local to this translation unit. |
62 | 4.04k | MqttMockServer::~MqttMockServer() = default; |
63 | | |
64 | | /// @return A prefix of complete client packets from the most recent drive. |
65 | 4.02k | const std::string& MqttMockServer::packet_transcript() const { return packet_transcript_; } |
66 | | |
67 | | /// @return A prefix of the first PUBLISH packet's topic. |
68 | 0 | const std::string& MqttMockServer::published_topic() const { return published_topic_; } |
69 | | |
70 | | /// @return A prefix of the first PUBLISH packet's application payload. |
71 | 4.02k | const std::string& MqttMockServer::published_payload() const { return published_payload_; } |
72 | | |
73 | | /// @return Number of complete client packets observed in the most recent drive. |
74 | 7.03k | std::size_t MqttMockServer::packet_count() const { return packet_count_; } |
75 | | |
76 | | /// @return Number of CONNECT packets observed. |
77 | 0 | std::size_t MqttMockServer::connect_count() const { return connect_count_; } |
78 | | |
79 | | /// @return Number of PUBLISH packets observed. |
80 | 4.02k | std::size_t MqttMockServer::publish_count() const { return publish_count_; } |
81 | | |
82 | | /// @return Number of SUBSCRIBE packets observed. |
83 | 4.02k | std::size_t MqttMockServer::subscribe_count() const { return subscribe_count_; } |
84 | | |
85 | | /// @return Number of PUBLISH packets that requested QoS 1. |
86 | 0 | std::size_t MqttMockServer::qos1_publish_count() const { return qos1_publish_count_; } |
87 | | |
88 | | /// Report how many requested zero-timeout multi-poll probes ran during the |
89 | | /// most recent drive. RunLoop increments this only after a successful perform |
90 | | /// turn while curl still owns an active transfer and the MQTT socket is open. |
91 | | /// @return Number of active, connected pollset probes in the previous drive. |
92 | 0 | std::size_t MqttMockServer::active_pollset_probe_count() const { return active_pollset_probe_count_; } |
93 | | |
94 | | /// Reset every borrowed pointer, parser cursor, and observable capture before |
95 | | /// borrowing the next Scenario. |
96 | | /// @param scenario Scenario whose Connection supplies the CONNACK and any |
97 | | /// server-initiated packet chunks. |
98 | 3.88k | void MqttMockServer::ResetForScenario(const curl::fuzzer::proto::Scenario& scenario) { |
99 | 3.88k | connection_.reset(); |
100 | 3.88k | scenario_ = &scenario; |
101 | 3.88k | server_chunk_count_ = std::min<std::size_t>(scenario_limits::kMaxResponseChunks, |
102 | 3.88k | static_cast<std::size_t>(scenario.connection().on_readable_size())); |
103 | 3.88k | next_server_chunk_ = 0; |
104 | 3.88k | packet_count_ = 0; |
105 | 3.88k | connect_count_ = 0; |
106 | 3.88k | publish_count_ = 0; |
107 | 3.88k | subscribe_count_ = 0; |
108 | 3.88k | qos1_publish_count_ = 0; |
109 | 3.88k | active_pollset_probe_count_ = 0; |
110 | 3.88k | pending_client_bytes_.clear(); |
111 | 3.88k | packet_transcript_.clear(); |
112 | 3.88k | published_topic_.clear(); |
113 | 3.88k | published_payload_.clear(); |
114 | 3.88k | release_script_chunks_ = false; |
115 | 3.88k | protocol_error_ = false; |
116 | 3.88k | socket_opened_ = false; |
117 | 3.88k | } |
118 | | |
119 | | /// Apply bounded transport knobs when creating the scripted socket. Target |
120 | | /// policies normally clear MQTT backpressure, but direct corpus replay must |
121 | | /// retain deterministic, well-defined behavior. |
122 | | /// @param connection Peer receiving the socket settings. |
123 | | /// @param script Connection script carrying optional backpressure values. |
124 | | void MqttMockServer::ApplyScriptBackpressure(MockConnection* connection, |
125 | 3.52k | const curl::fuzzer::proto::Connection& script) { |
126 | 3.52k | if (connection == nullptr) { |
127 | 0 | return; |
128 | 0 | } |
129 | | |
130 | 3.52k | const std::uint32_t int_max = static_cast<std::uint32_t>(std::numeric_limits<int>::max()); |
131 | 3.52k | const auto& backpressure = script.backpressure(); |
132 | 3.52k | const int receive_buffer = static_cast<int>(std::min(backpressure.recv_buf_bytes(), int_max)); |
133 | 3.52k | connection->ApplyBackpressure(receive_buffer, static_cast<std::size_t>(backpressure.drain_limit())); |
134 | 3.52k | } |
135 | | |
136 | | /// Create the default plaintext transport for the MQTT packet exchange. The |
137 | | /// MQTTS subclass overrides this factory while retaining the binary parser. |
138 | | /// @return A new socketpair-backed connection. |
139 | 826 | std::unique_ptr<MockConnection> MqttMockServer::CreateConnection() { return std::make_unique<MockConnection>(); } |
140 | | |
141 | | /// Append decoded application bytes to the pending parser buffer. The outer |
142 | | /// service loop drains this buffer after every transport call, so it remains |
143 | | /// bounded by the scenario's packet budget. |
144 | | /// @param data First decoded byte. |
145 | | /// @param size Number of bytes available at `data`. |
146 | 6.46k | void MqttMockServer::ObserveIncomingData(const unsigned char* data, std::size_t size) { |
147 | 6.46k | if (data == nullptr || size == 0) { |
148 | 0 | return; |
149 | 0 | } |
150 | 6.46k | pending_client_bytes_.append(reinterpret_cast<const char*>(data), size); |
151 | 6.46k | } |
152 | | |
153 | | /// Create the single MQTT socketpair and queue the scenario's first server |
154 | | /// bytes. curl sends CONNECT immediately, so a normal scenario preloads the |
155 | | /// CONNACK here; an empty or malformed greeting remains a reachable error path. |
156 | | /// @param purpose Socket role requested by curl. |
157 | | /// @param address Unused stream destination metadata required by the callback |
158 | | /// signature. |
159 | | /// @return Client descriptor for the MQTT channel, or CURL_SOCKET_BAD. |
160 | 3.52k | curl_socket_t MqttMockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* /*address*/) { |
161 | 3.52k | if (scenario_ == nullptr || purpose != CURLSOCKTYPE_IPCXN || socket_opened_) { |
162 | 0 | return CURL_SOCKET_BAD; |
163 | 0 | } |
164 | | |
165 | 3.52k | connection_ = CreateConnection(); |
166 | 3.52k | if (!connection_ || !connection_->ok()) { |
167 | 0 | connection_.reset(); |
168 | 0 | return CURL_SOCKET_BAD; |
169 | 0 | } |
170 | 3.52k | connection_->SetIncomingDataObserver(this); |
171 | | |
172 | 3.52k | ApplyScriptBackpressure(connection_.get(), scenario_->connection()); |
173 | 3.52k | const std::string& greeting = scenario_->connection().initial_response(); |
174 | 3.52k | if (!greeting.empty() && |
175 | 1.62k | !connection_->WriteAll(reinterpret_cast<const unsigned char*>(greeting.data()), greeting.size())) { |
176 | 2 | connection_.reset(); |
177 | 2 | return CURL_SOCKET_BAD; |
178 | 2 | } |
179 | | |
180 | 3.52k | socket_opened_ = true; |
181 | 3.52k | return connection_->take_client_fd(); |
182 | 3.52k | } |
183 | | |
184 | | /// Decode MQTT's base-128 remaining-length field. MQTT 3.1.1 permits at most |
185 | | /// four length bytes; a fifth continuation is a malformed fixed header rather |
186 | | /// than an invitation to wait for more input. |
187 | | /// @param bytes Fixed header and whatever length bytes have arrived. |
188 | | /// @param header_size Receives the fixed-header size when complete. |
189 | | /// @param remaining_length Receives the declared body size when complete. |
190 | | /// @return kComplete, kNeedMore, or kInvalid. |
191 | | MqttMockServer::ParseResult MqttMockServer::ParseRemainingLength(std::string_view bytes, std::size_t* header_size, |
192 | 4.90k | std::size_t* remaining_length) { |
193 | 4.90k | std::size_t value = 0; |
194 | 4.90k | std::size_t multiplier = 1; |
195 | 5.40k | for (std::size_t index = 1; index <= 4; ++index) { |
196 | 5.40k | if (bytes.size() <= index) { |
197 | 0 | return ParseResult::kNeedMore; |
198 | 0 | } |
199 | 5.40k | const unsigned char encoded = ByteAt(bytes, index); |
200 | 5.40k | value += static_cast<std::size_t>(encoded & 0x7fU) * multiplier; |
201 | 5.40k | if ((encoded & 0x80U) == 0) { |
202 | 4.90k | *header_size = index + 1; |
203 | 4.90k | *remaining_length = value; |
204 | 4.90k | return ParseResult::kComplete; |
205 | 4.90k | } |
206 | 496 | multiplier *= 128; |
207 | 496 | } |
208 | 0 | return ParseResult::kInvalid; |
209 | 4.90k | } |
210 | | |
211 | | /// Retain only a bounded prefix for assertions. Parsing and draining continue |
212 | | /// against the full transport bytes, so reaching the cap cannot change curl's |
213 | | /// behavior or manufacture socket backpressure. |
214 | | /// @param source Newly observed bytes. |
215 | | /// @param limit Maximum retained destination size. |
216 | | /// @param destination Capture buffer receiving the available prefix. |
217 | 5.27k | void MqttMockServer::CapturePrefix(std::string_view source, std::size_t limit, std::string* destination) { |
218 | 5.27k | if (destination == nullptr || destination->size() >= limit) { |
219 | 8 | return; |
220 | 8 | } |
221 | 5.26k | const std::size_t available = limit - destination->size(); |
222 | 5.26k | destination->append(source.data(), std::min(source.size(), available)); |
223 | 5.26k | } |
224 | | |
225 | | /// Keep failed peer writes local to the mock. Response cursors must still |
226 | | /// advance after curl closes early, otherwise a replacement socket could see a |
227 | | /// reply intended for an earlier packet. |
228 | | /// @param bytes Response bytes to queue. |
229 | | /// @return true when the complete fragment was queued. |
230 | 6.39k | bool MqttMockServer::WriteServerBytes(std::string_view bytes) { |
231 | 6.39k | return bytes.empty() || (connection_ != nullptr && |
232 | 6.39k | connection_->WriteAll(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size())); |
233 | 6.39k | } |
234 | | |
235 | | /// Interpret one complete client packet. Packet classification uses only the |
236 | | /// fixed header, so a malformed variable header cannot desynchronize the |
237 | | /// remaining-length parser that already found the next boundary. |
238 | | /// @param first_byte Fixed header byte carrying the type and flags. |
239 | | /// @param body Variable header plus payload, exactly `remaining_length` bytes. |
240 | 4.89k | void MqttMockServer::HandleClientPacket(unsigned char first_byte, std::string_view body) { |
241 | 4.89k | const unsigned char type = static_cast<unsigned char>(first_byte & 0xf0U); |
242 | 4.89k | switch (type) { |
243 | 3.46k | case kMqttConnect: |
244 | 3.46k | ++connect_count_; |
245 | 3.46k | return; |
246 | | |
247 | 186 | case kMqttPublish: { |
248 | 186 | ++publish_count_; |
249 | 186 | if (body.size() < 2) { |
250 | 0 | return; |
251 | 0 | } |
252 | 186 | const std::size_t topic_length = |
253 | 186 | (static_cast<std::size_t>(ByteAt(body, 0)) << 8U) | static_cast<std::size_t>(ByteAt(body, 1)); |
254 | 186 | std::size_t cursor = 2; |
255 | 186 | const std::size_t available = body.size() - cursor; |
256 | 186 | const std::size_t captured_topic = std::min(topic_length, available); |
257 | 186 | CapturePrefix(body.substr(cursor, captured_topic), scenario_limits::kMaxMetadataBytes, &published_topic_); |
258 | 186 | cursor += captured_topic; |
259 | 186 | if (captured_topic < topic_length) { |
260 | 0 | return; |
261 | 0 | } |
262 | | |
263 | 186 | const unsigned qos = (static_cast<unsigned>(first_byte) >> 1U) & 0x03U; |
264 | 186 | if (qos == 1U) { |
265 | 0 | if (body.size() - cursor < 2) { |
266 | 0 | return; |
267 | 0 | } |
268 | 0 | const unsigned char packet_id_high = ByteAt(body, cursor); |
269 | 0 | const unsigned char packet_id_low = ByteAt(body, cursor + 1); |
270 | 0 | cursor += 2; |
271 | | // PUBACK has no flags and a two-byte remaining length followed by the |
272 | | // packet identifier it acknowledges. Current curl always publishes at |
273 | | // QoS 0; this branch keeps the acknowledgement path correct if a |
274 | | // future revision makes QoS configurable. |
275 | 0 | const unsigned char puback[] = {kMqttPuback, 0x02, packet_id_high, packet_id_low}; |
276 | 0 | (void)WriteServerBytes(std::string_view(reinterpret_cast<const char*>(puback), sizeof(puback))); |
277 | 0 | ++qos1_publish_count_; |
278 | 0 | } |
279 | | |
280 | 186 | CapturePrefix(body.substr(cursor), kMaxCapturedPayloadBytes, &published_payload_); |
281 | 186 | return; |
282 | 186 | } |
283 | | |
284 | 1.06k | case kMqttSubscribe: { |
285 | 1.06k | ++subscribe_count_; |
286 | 1.06k | const unsigned char packet_id_high = body.empty() ? 0 : ByteAt(body, 0); |
287 | 1.06k | const unsigned char packet_id_low = body.size() < 2 ? 0 : ByteAt(body, 1); |
288 | | // SUBACK echoes the SUBSCRIBE packet id and reports success for the |
289 | | // single requested topic filter. curl's MQTT state machine compares both |
290 | | // identifier bytes and the return code before accepting the packet. |
291 | 1.06k | const unsigned char suback[] = {kMqttSuback, 0x03, packet_id_high, packet_id_low, 0x00}; |
292 | 1.06k | (void)WriteServerBytes(std::string_view(reinterpret_cast<const char*>(suback), sizeof(suback))); |
293 | | // Only the subscribe flow remains readable after its acknowledgement. |
294 | | // on_readable chunks carry server-initiated packets such as PUBLISH. |
295 | 1.06k | release_script_chunks_ = true; |
296 | 1.06k | return; |
297 | 186 | } |
298 | | |
299 | 0 | case kMqttPingreq: { |
300 | 0 | const unsigned char pingresp[] = {kMqttPingresp, 0x00}; |
301 | 0 | (void)WriteServerBytes(std::string_view(reinterpret_cast<const char*>(pingresp), sizeof(pingresp))); |
302 | 0 | return; |
303 | 186 | } |
304 | | |
305 | 186 | case kMqttDisconnect: |
306 | | // curl sends DISCONNECT after PUBLISH. No reply is defined, and the |
307 | | // cleanup half-close below still terminates the socket. |
308 | 186 | return; |
309 | | |
310 | 0 | default: |
311 | | // CONNACK/SUBACK/PUBACK and unknown types are server-to-client packets. |
312 | | // Recording them in the transcript is enough; inventing a reply would |
313 | | // only add bytes curl never reads in this direction. |
314 | 0 | return; |
315 | 4.89k | } |
316 | 4.89k | } |
317 | | |
318 | | /// Release one scenario-provided server chunk after SUBACK. Chunks are opaque |
319 | | /// bytes, not newline commands: each one is expected to be a complete MQTT |
320 | | /// packet (for example PUBLISH followed by DISCONNECT). An explicitly empty |
321 | | /// chunk half-closes the peer, retaining a scripted truncation/EOF path. |
322 | | /// @return true when a script slot was consumed. |
323 | 40.3k | bool MqttMockServer::ReleaseNextServerChunk() { |
324 | 40.3k | if (!release_script_chunks_ || scenario_ == nullptr || connection_ == nullptr || |
325 | 34.9k | next_server_chunk_ >= server_chunk_count_) { |
326 | 34.9k | return false; |
327 | 34.9k | } |
328 | | |
329 | 5.40k | const std::string& chunk = scenario_->connection().on_readable(static_cast<int>(next_server_chunk_++)); |
330 | 5.40k | if (chunk.empty()) { |
331 | 68 | connection_->ShutdownWrite(); |
332 | 68 | release_script_chunks_ = false; |
333 | 68 | return true; |
334 | 68 | } |
335 | | |
336 | 5.33k | (void)WriteServerBytes(chunk); |
337 | 5.33k | if (next_server_chunk_ >= server_chunk_count_) { |
338 | 659 | connection_->ShutdownWrite(); |
339 | 659 | } |
340 | 5.33k | return true; |
341 | 5.40k | } |
342 | | |
343 | | /// Advance the carrier without waiting, then process every complete packet |
344 | | /// already decoded into the pending buffer. A plaintext connection yields raw |
345 | | /// socket bytes; a TLS carrier completes its handshake and yields decrypted |
346 | | /// application bytes through ObserveIncomingData. A packet is delivered only |
347 | | /// after its fixed header and declared body have both arrived, so fragmentation |
348 | | /// cannot shift the parser. |
349 | | /// @return true when bytes, transport state, a packet, or a response cursor |
350 | | /// advanced. |
351 | 40.3k | bool MqttMockServer::ServiceClientConnection() { |
352 | 40.3k | if (connection_ == nullptr || protocol_error_) { |
353 | 1.28k | return false; |
354 | 1.28k | } |
355 | | |
356 | 39.1k | const std::size_t size_before_read = pending_client_bytes_.size(); |
357 | 39.1k | const std::size_t transport_progress = connection_->DrainIncoming(); |
358 | 39.1k | bool made_progress = pending_client_bytes_.size() != size_before_read || transport_progress != 0; |
359 | | |
360 | 39.1k | std::size_t consumed = 0; |
361 | 39.1k | int packets = 0; |
362 | 44.0k | while (consumed < pending_client_bytes_.size() && packets < kMaxPacketsPerService) { |
363 | 4.90k | const std::string_view available(pending_client_bytes_.data() + consumed, pending_client_bytes_.size() - consumed); |
364 | 4.90k | if (available.size() < 2) { |
365 | 0 | break; |
366 | 0 | } |
367 | | |
368 | 4.90k | std::size_t header_size = 0; |
369 | 4.90k | std::size_t remaining_length = 0; |
370 | 4.90k | const ParseResult parsed = ParseRemainingLength(available, &header_size, &remaining_length); |
371 | 4.90k | if (parsed == ParseResult::kNeedMore) { |
372 | 0 | break; |
373 | 0 | } |
374 | 4.90k | if (parsed == ParseResult::kInvalid || remaining_length > kMaxPacketBytes) { |
375 | 7 | protocol_error_ = true; |
376 | 7 | break; |
377 | 7 | } |
378 | | |
379 | 4.89k | const std::size_t packet_size = header_size + remaining_length; |
380 | 4.89k | if (available.size() < packet_size) { |
381 | 0 | break; |
382 | 0 | } |
383 | | |
384 | 4.89k | const std::string_view packet = available.substr(0, packet_size); |
385 | 4.89k | CapturePrefix(packet, kMaxCapturedPacketBytes, &packet_transcript_); |
386 | 4.89k | ++packet_count_; |
387 | 4.89k | HandleClientPacket(ByteAt(packet, 0), packet.substr(header_size)); |
388 | 4.89k | consumed += packet_size; |
389 | 4.89k | ++packets; |
390 | 4.89k | made_progress = true; |
391 | 4.89k | } |
392 | | |
393 | 39.1k | if (protocol_error_) { |
394 | 7 | pending_client_bytes_.clear(); |
395 | | // curl's malformed-packet paths are bounded, but closing the write half |
396 | | // immediately keeps a bogus remaining length from waiting on the idle |
397 | | // budget for an impossible body. |
398 | 7 | connection_->ShutdownWrite(); |
399 | 7 | release_script_chunks_ = false; |
400 | 39.0k | } else if (consumed != 0) { |
401 | 4.71k | pending_client_bytes_.erase(0, consumed); |
402 | 4.71k | } |
403 | 39.1k | return made_progress; |
404 | 40.3k | } |
405 | | |
406 | | /// Half-close the peer so curl observes a deterministic end of server packets |
407 | | /// after the transfer has stopped or the idle budget expired. |
408 | 3.88k | void MqttMockServer::FinishConnections() { |
409 | 3.88k | if (connection_ != nullptr) { |
410 | 3.52k | connection_->ShutdownWrite(); |
411 | 3.52k | } |
412 | 3.88k | } |
413 | | |
414 | | /// Alternate curl transitions with immediate peer service until the transfer |
415 | | /// completes or deterministic idle/operation caps win. No select(), poll(), or |
416 | | /// sleep is needed: the CONNACK is preloaded, synthesized acknowledgements are |
417 | | /// written from the same thread, and scripted server packets are released one |
418 | | /// per turn so curl consumes the preceding acknowledgement first. |
419 | | /// @param multi Multi handle containing `easy`. |
420 | | /// @param easy Easy handle already installed on this mock. |
421 | | /// @param scenario Connection script to borrow. |
422 | 3.88k | void MqttMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) { |
423 | 3.88k | (void)easy; |
424 | 3.88k | ResetForScenario(scenario); |
425 | | |
426 | 3.88k | int still_running = 1; |
427 | 3.88k | int idle_iterations = 0; |
428 | 3.88k | int drive_iterations = 0; |
429 | 3.88k | CURLMcode result = CURLM_OK; |
430 | 40.3k | while (still_running && idle_iterations < kMaxIdleIterations && drive_iterations++ < kMaxDriveIterations) { |
431 | 36.5k | const int running_before = still_running; |
432 | 36.5k | result = curl_multi_perform(multi, &still_running); |
433 | 36.5k | if (result != CURLM_OK) { |
434 | 0 | break; |
435 | 0 | } |
436 | | |
437 | | // Pollset construction is opt-in coverage work. Waiting until the socket |
438 | | // callback has run and the transfer remains active keeps the default hot |
439 | | // loop syscall-free and asks curl for a live MQTT protocol pollset. |
440 | 36.5k | if (scenario.probe_multi_pollset() && still_running && socket_opened_) { |
441 | 2.34k | ProbeMultiPollset(multi); |
442 | 2.34k | ++active_pollset_probe_count_; |
443 | 2.34k | } |
444 | | |
445 | 36.5k | bool made_progress = still_running != running_before; |
446 | 36.5k | made_progress = ServiceClientConnection() || made_progress; |
447 | 36.5k | made_progress = ReleaseNextServerChunk() || made_progress; |
448 | 36.5k | if (made_progress) { |
449 | 13.5k | idle_iterations = 0; |
450 | 22.9k | } else { |
451 | 22.9k | ++idle_iterations; |
452 | 22.9k | } |
453 | 36.5k | } |
454 | | |
455 | | // The final perform may have emitted CONNECT/PUBLISH/DISCONNECT immediately |
456 | | // before marking the transfer done. Drain those bytes before cleanup. |
457 | 3.88k | (void)ServiceClientConnection(); |
458 | 3.88k | (void)ReleaseNextServerChunk(); |
459 | 3.88k | FinishConnections(); |
460 | 3.88k | scenario_ = nullptr; |
461 | 3.88k | } |
462 | | |
463 | | } // namespace proto_fuzzer |