curl-fuzzer C++ reference
Loading...
Searching...
No Matches
mqtt_mock_server.cc
Go to the documentation of this file.
1/*
2 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3 *
4 * SPDX-License-Identifier: curl
5 */
6
9
11
12#include <algorithm>
13#include <cstddef>
14#include <cstdint>
15#include <limits>
16#include <memory>
17#include <string>
18#include <string_view>
19
22
23namespace proto_fuzzer {
24
25namespace {
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.
29constexpr unsigned char kMqttConnect = 0x10;
30constexpr unsigned char kMqttPublish = 0x30;
31constexpr unsigned char kMqttPuback = 0x40;
32constexpr unsigned char kMqttSubscribe = 0x80;
33constexpr unsigned char kMqttSuback = 0x90;
34constexpr unsigned char kMqttPingreq = 0xc0;
35constexpr unsigned char kMqttPingresp = 0xd0;
36constexpr unsigned char kMqttDisconnect = 0xe0;
37constexpr std::size_t kMqttProbePublishBytes = 16 * 1024;
38constexpr std::size_t kOversizedMqttCredentialBytes = 0x10000;
39
40unsigned char ByteAt(std::string_view bytes, std::size_t index) { return static_cast<unsigned char>(bytes[index]); }
41
42} // namespace
43
48MqttMockServer::MqttMockServer(curl::fuzzer::proto::MqttProbeMode mode)
49 : mode_(mode),
50 scenario_(nullptr),
51 server_chunk_count_(0),
52 next_server_chunk_(0),
53 packet_count_(0),
54 connect_count_(0),
55 publish_count_(0),
56 subscribe_count_(0),
57 qos1_publish_count_(0),
58 active_pollset_probe_count_(0),
59 release_script_chunks_(false),
60 protocol_error_(false),
61 socket_opened_(false) {}
62
67
69const std::string& MqttMockServer::packet_transcript() const { return packet_transcript_; }
70
72const std::string& MqttMockServer::published_topic() const { return published_topic_; }
73
75const std::string& MqttMockServer::published_payload() const { return published_payload_; }
76
78std::size_t MqttMockServer::packet_count() const { return packet_count_; }
79
81std::size_t MqttMockServer::connect_count() const { return connect_count_; }
82
84std::size_t MqttMockServer::publish_count() const { return publish_count_; }
85
87std::size_t MqttMockServer::subscribe_count() const { return subscribe_count_; }
88
90std::size_t MqttMockServer::qos1_publish_count() const { return qos1_publish_count_; }
91
96std::size_t MqttMockServer::active_pollset_probe_count() const { return active_pollset_probe_count_; }
97
102void MqttMockServer::ResetForScenario(const curl::fuzzer::proto::Scenario& scenario) {
103 connection_.reset();
104 scenario_ = &scenario;
105 server_chunk_count_ = std::min<std::size_t>(scenario_limits::kMaxResponseChunks,
106 static_cast<std::size_t>(scenario.connection().on_readable_size()));
107 next_server_chunk_ = 0;
108 packet_count_ = 0;
109 connect_count_ = 0;
110 publish_count_ = 0;
111 subscribe_count_ = 0;
112 qos1_publish_count_ = 0;
113 active_pollset_probe_count_ = 0;
114 pending_client_bytes_.clear();
115 packet_transcript_.clear();
116 published_topic_.clear();
117 published_payload_.clear();
118 release_script_chunks_ = false;
119 protocol_error_ = false;
120 socket_opened_ = false;
121}
122
128void MqttMockServer::ApplyScriptBackpressure(MockConnection* connection,
129 const curl::fuzzer::proto::Connection& script) {
130 if (connection == nullptr) {
131 return;
132 }
133
134 const std::uint32_t int_max = static_cast<std::uint32_t>(std::numeric_limits<int>::max());
135 const auto& backpressure = script.backpressure();
136 const int receive_buffer = static_cast<int>(std::min(backpressure.recv_buf_bytes(), int_max));
137 connection->ApplyBackpressure(receive_buffer, static_cast<std::size_t>(backpressure.drain_limit()));
138}
139
143std::unique_ptr<MockConnection> MqttMockServer::CreateConnection() { return std::make_unique<MockConnection>(); }
144
150void MqttMockServer::ObserveIncomingData(const unsigned char* data, std::size_t size) {
151 if (data == nullptr || size == 0) {
152 return;
153 }
154 pending_client_bytes_.append(reinterpret_cast<const char*>(data), size);
155}
156
164curl_socket_t MqttMockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* /*address*/) {
165 if (scenario_ == nullptr || purpose != CURLSOCKTYPE_IPCXN || socket_opened_) {
166 return CURL_SOCKET_BAD;
167 }
168
170 if (!connection_ || !connection_->ok()) {
171 connection_.reset();
172 return CURL_SOCKET_BAD;
173 }
174 connection_->SetIncomingDataObserver(this);
175
176 ApplyScriptBackpressure(connection_.get(), scenario_->connection());
177 const std::string& greeting = scenario_->connection().initial_response();
178 if (!greeting.empty() &&
179 !connection_->WriteAll(reinterpret_cast<const unsigned char*>(greeting.data()), greeting.size())) {
180 connection_.reset();
181 return CURL_SOCKET_BAD;
182 }
183
184 socket_opened_ = true;
185 return connection_->take_client_fd();
186}
187
195MqttMockServer::ParseResult MqttMockServer::ParseRemainingLength(std::string_view bytes, std::size_t* header_size,
196 std::size_t* remaining_length) {
197 std::size_t value = 0;
198 std::size_t multiplier = 1;
199 for (std::size_t index = 1; index <= 4; ++index) {
200 if (bytes.size() <= index) {
201 return ParseResult::kNeedMore;
202 }
203 const unsigned char encoded = ByteAt(bytes, index);
204 value += static_cast<std::size_t>(encoded & 0x7fU) * multiplier;
205 if ((encoded & 0x80U) == 0) {
206 *header_size = index + 1;
207 *remaining_length = value;
208 return ParseResult::kComplete;
209 }
210 multiplier *= 128;
211 }
212 return ParseResult::kInvalid;
213}
214
221void MqttMockServer::CapturePrefix(std::string_view source, std::size_t limit, std::string* destination) {
222 if (destination == nullptr || destination->size() >= limit) {
223 return;
224 }
225 const std::size_t available = limit - destination->size();
226 destination->append(source.data(), std::min(source.size(), available));
227}
228
234bool MqttMockServer::WriteServerBytes(std::string_view bytes) {
235 return bytes.empty() || (connection_ != nullptr &&
236 connection_->WriteAll(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size()));
237}
238
244void MqttMockServer::HandleClientPacket(unsigned char first_byte, std::string_view body) {
245 const unsigned char type = static_cast<unsigned char>(first_byte & 0xf0U);
246 switch (type) {
247 case kMqttConnect:
248 ++connect_count_;
249 return;
250
251 case kMqttPublish: {
252 ++publish_count_;
253 if (body.size() < 2) {
254 return;
255 }
256 const std::size_t topic_length =
257 (static_cast<std::size_t>(ByteAt(body, 0)) << 8U) | static_cast<std::size_t>(ByteAt(body, 1));
258 std::size_t cursor = 2;
259 const std::size_t available = body.size() - cursor;
260 const std::size_t captured_topic = std::min(topic_length, available);
261 CapturePrefix(body.substr(cursor, captured_topic), scenario_limits::kMaxMetadataBytes, &published_topic_);
262 cursor += captured_topic;
263 if (captured_topic < topic_length) {
264 return;
265 }
266
267 const unsigned qos = (static_cast<unsigned>(first_byte) >> 1U) & 0x03U;
268 if (qos == 1U) {
269 if (body.size() - cursor < 2) {
270 return;
271 }
272 const unsigned char packet_id_high = ByteAt(body, cursor);
273 const unsigned char packet_id_low = ByteAt(body, cursor + 1);
274 cursor += 2;
275 // PUBACK has no flags and a two-byte remaining length followed by the
276 // packet identifier it acknowledges. Current curl always publishes at
277 // QoS 0; this branch keeps the acknowledgement path correct if a
278 // future revision makes QoS configurable.
279 const unsigned char puback[] = {kMqttPuback, 0x02, packet_id_high, packet_id_low};
280 (void)WriteServerBytes(std::string_view(reinterpret_cast<const char*>(puback), sizeof(puback)));
281 ++qos1_publish_count_;
282 }
283
284 CapturePrefix(body.substr(cursor), kMaxCapturedPayloadBytes, &published_payload_);
285 return;
286 }
287
288 case kMqttSubscribe: {
289 ++subscribe_count_;
290 const unsigned char packet_id_high = body.empty() ? 0 : ByteAt(body, 0);
291 const unsigned char packet_id_low = body.size() < 2 ? 0 : ByteAt(body, 1);
292 // SUBACK echoes the SUBSCRIBE packet id and reports success for the
293 // single requested topic filter. curl's MQTT state machine compares both
294 // identifier bytes and the return code before accepting the packet.
295 const unsigned char suback[] = {kMqttSuback, 0x03, packet_id_high, packet_id_low, 0x00};
296 (void)WriteServerBytes(std::string_view(reinterpret_cast<const char*>(suback), sizeof(suback)));
297 // Only the subscribe flow remains readable after its acknowledgement.
298 // on_readable chunks carry server-initiated packets such as PUBLISH.
299 release_script_chunks_ = true;
300 return;
301 }
302
303 case kMqttPingreq: {
304 const unsigned char pingresp[] = {kMqttPingresp, 0x00};
305 (void)WriteServerBytes(std::string_view(reinterpret_cast<const char*>(pingresp), sizeof(pingresp)));
306 return;
307 }
308
309 case kMqttDisconnect:
310 // curl sends DISCONNECT after PUBLISH. No reply is defined, and the
311 // cleanup half-close below still terminates the socket.
312 return;
313
314 default:
315 // CONNACK/SUBACK/PUBACK and unknown types are server-to-client packets.
316 // Recording them in the transcript is enough; inventing a reply would
317 // only add bytes curl never reads in this direction.
318 return;
319 }
320}
321
327bool MqttMockServer::ReleaseNextServerChunk() {
328 if (!release_script_chunks_ || scenario_ == nullptr || connection_ == nullptr ||
329 next_server_chunk_ >= server_chunk_count_) {
330 return false;
331 }
332
333 const std::string& chunk = scenario_->connection().on_readable(static_cast<int>(next_server_chunk_++));
334 if (chunk.empty()) {
335 connection_->ShutdownWrite();
336 release_script_chunks_ = false;
337 return true;
338 }
339
340 (void)WriteServerBytes(chunk);
341 if (next_server_chunk_ >= server_chunk_count_) {
342 connection_->ShutdownWrite();
343 }
344 return true;
345}
346
355bool MqttMockServer::ServiceClientConnection() {
356 if (connection_ == nullptr || protocol_error_) {
357 return false;
358 }
359
360 const std::size_t size_before_read = pending_client_bytes_.size();
361 const std::size_t transport_progress = connection_->DrainIncoming();
362 bool made_progress = pending_client_bytes_.size() != size_before_read || transport_progress != 0;
363
364 std::size_t consumed = 0;
365 int packets = 0;
366 while (consumed < pending_client_bytes_.size() && packets < kMaxPacketsPerService) {
367 const std::string_view available(pending_client_bytes_.data() + consumed, pending_client_bytes_.size() - consumed);
368 if (available.size() < 2) {
369 break;
370 }
371
372 std::size_t header_size = 0;
373 std::size_t remaining_length = 0;
374 const ParseResult parsed = ParseRemainingLength(available, &header_size, &remaining_length);
375 if (parsed == ParseResult::kNeedMore) {
376 break;
377 }
378 if (parsed == ParseResult::kInvalid || remaining_length > kMaxPacketBytes) {
379 protocol_error_ = true;
380 break;
381 }
382
383 const std::size_t packet_size = header_size + remaining_length;
384 if (available.size() < packet_size) {
385 break;
386 }
387
388 const std::string_view packet = available.substr(0, packet_size);
389 CapturePrefix(packet, kMaxCapturedPacketBytes, &packet_transcript_);
390 ++packet_count_;
391 HandleClientPacket(ByteAt(packet, 0), packet.substr(header_size));
392 consumed += packet_size;
393 ++packets;
394 made_progress = true;
395 }
396
397 if (protocol_error_) {
398 pending_client_bytes_.clear();
399 // curl's malformed-packet paths are bounded, but closing the write half
400 // immediately keeps a bogus remaining length from waiting on the idle
401 // budget for an impossible body.
402 connection_->ShutdownWrite();
403 release_script_chunks_ = false;
404 } else if (consumed != 0) {
405 pending_client_bytes_.erase(0, consumed);
406 }
407 return made_progress;
408}
409
412void MqttMockServer::FinishConnections() {
413 if (connection_ != nullptr) {
414 connection_->ShutdownWrite();
415 }
416}
417
422void MqttMockServer::ApplyProbeOptions(CURL* easy) const {
423 if (easy == nullptr) {
424 return;
425 }
426
427 switch (mode_) {
428 case curl::fuzzer::proto::MQTT_PROBE_SEND_BACKPRESSURE: {
429 static const std::string publish_body(kMqttProbePublishBytes, 'p');
430 (void)curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(publish_body.size()));
431 (void)curl_easy_setopt(easy, CURLOPT_POSTFIELDS, publish_body.c_str());
432 return;
433 }
434
435 case curl::fuzzer::proto::MQTT_PROBE_OVERSIZED_USERNAME: {
436 static const std::string username(kOversizedMqttCredentialBytes, 'u');
437 (void)curl_easy_setopt(easy, CURLOPT_USERNAME, username.c_str());
438 (void)curl_easy_setopt(easy, CURLOPT_PASSWORD, "");
439 return;
440 }
441
442 case curl::fuzzer::proto::MQTT_PROBE_OVERSIZED_PASSWORD: {
443 static const std::string password(kOversizedMqttCredentialBytes, 'p');
444 (void)curl_easy_setopt(easy, CURLOPT_USERNAME, "");
445 (void)curl_easy_setopt(easy, CURLOPT_PASSWORD, password.c_str());
446 return;
447 }
448
449 case curl::fuzzer::proto::MQTT_PROBE_DEFAULT:
450 default:
451 return;
452 }
453}
454
463void MqttMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
464 ApplyProbeOptions(easy);
465 ResetForScenario(scenario);
466
467 int still_running = 1;
468 int idle_iterations = 0;
469 int drive_iterations = 0;
470 CURLMcode result = CURLM_OK;
471 while (still_running && idle_iterations < kMaxIdleIterations && drive_iterations++ < kMaxDriveIterations) {
472 const int running_before = still_running;
473 result = curl_multi_perform(multi, &still_running);
474 if (result != CURLM_OK) {
475 break;
476 }
477
478 // Pollset construction is opt-in coverage work. Waiting until the socket
479 // callback has run and the transfer remains active keeps the default hot
480 // loop syscall-free and asks curl for a live MQTT protocol pollset.
481 if (scenario.probe_multi_pollset() && still_running && socket_opened_) {
482 ProbeMultiPollset(multi);
483 ++active_pollset_probe_count_;
484 }
485
486 bool made_progress = still_running != running_before;
487 made_progress = ServiceClientConnection() || made_progress;
488 made_progress = ReleaseNextServerChunk() || made_progress;
489 if (made_progress) {
490 idle_iterations = 0;
491 } else {
492 ++idle_iterations;
493 }
494 }
495
496 // The final perform may have emitted CONNECT/PUBLISH/DISCONNECT immediately
497 // before marking the transfer done. Drain those bytes before cleanup.
498 (void)ServiceClientConnection();
499 (void)ReleaseNextServerChunk();
500 FinishConnections();
501 scenario_ = nullptr;
502}
503
506#if defined(PROTO_FUZZER_OBSERVE_STATE)
508 const volatile std::size_t mqtt_observation[] = {packet_transcript().size(), packet_count(), publish_count(),
510 (void)mqtt_observation;
511#endif
512}
513
514} // namespace proto_fuzzer
std::unique_ptr< MockConnection > connection_
The per-scenario MockConnection, lazily created by HandleOpenSocket().
static void ProbeMultiPollset(CURLM *multi)
Ask curl to construct and inspect its connection-filter pollset without waiting.
virtual void ObserveState()
Default no-op for peers without protocol-specific observations.
static constexpr int kMaxDriveIterations
Hard operation budget for one scenario.
static constexpr int kMaxIdleIterations
Consecutive no-progress budget for ordinary scenarios.
void RunLoop(CURLM *multi, CURL *easy, const curl::fuzzer::proto::Scenario &scenario) override
Alternate curl transitions with immediate peer service until the transfer completes or deterministic ...
std::size_t packet_count() const
const std::string & published_topic() const
std::size_t qos1_publish_count() const
std::size_t subscribe_count() const
void ObserveIncomingData(const unsigned char *data, std::size_t size) override
Append decoded application bytes to the pending parser buffer.
void ObserveState() override
Retain protocol outcomes in coverage and introspector builds.
std::size_t connect_count() const
virtual std::unique_ptr< MockConnection > CreateConnection()
Create the default plaintext transport for the MQTT packet exchange.
const std::string & packet_transcript() const
curl_socket_t HandleOpenSocket(curlsocktype purpose=CURLSOCKTYPE_IPCXN, struct curl_sockaddr *address=nullptr) override
Create the single MQTT socketpair and queue the scenario's first server bytes.
MqttMockServer(curl::fuzzer::proto::MqttProbeMode mode=curl::fuzzer::proto::MQTT_PROBE_DEFAULT)
Begin without a borrowed protobuf or open socketpair.
std::size_t active_pollset_probe_count() const
Report how many requested zero-timeout multi-poll probes ran during the most recent drive.
std::size_t publish_count() const
const std::string & published_payload() const
~MqttMockServer() override
Release the server-owned socketpair half after curl has returned its client descriptor.
MockConnection and MockServer — the in-process peer that feeds canned response bytes to libcurl over ...
Packet-aware in-process peer for curl's binary MQTT state machine.
Shared complexity limits for structured scenarios.
constexpr std::size_t kMaxResponseChunks
Enough response boundaries to exercise incremental parsers while keeping per-perform delivery work fi...