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/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