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/smtp_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 command-aware in-process SMTP peer.
9
10
#include "proto_fuzzer/smtp_mock_server.h"
11
12
#include <algorithm>
13
#include <cstddef>
14
#include <memory>
15
#include <string>
16
#include <string_view>
17
18
#include "proto_fuzzer/mock_server.h"
19
#include "proto_fuzzer/scenario_limits.h"
20
21
namespace proto_fuzzer {
22
23
/// Begin without a borrowed protobuf or open socketpair. RunLoop borrows a
24
/// Scenario only for the synchronous DriveScenario call in which socket
25
/// callbacks can run.
26
4.84k
SmtpMockServer::SmtpMockServer() : recipient_count_(0), rcpt_command_count_(0), auth_exchange_active_(false) {}
27
28
/// Release the server-owned socketpair half after curl has returned its client
29
/// descriptor. The out-of-line destructor keeps MockConnection's complete type
30
/// local to this translation unit.
31
4.84k
SmtpMockServer::~SmtpMockServer() = default;
32
33
/// Create the default plaintext transport for the SMTP exchange. The SMTPS
34
/// subclass overrides this factory while retaining the command/reply parser.
35
/// @return A new socketpair-backed connection.
36
2.35k
std::unique_ptr<MockConnection> SmtpMockServer::CreateConnection() { return std::make_unique<MockConnection>(); }
37
38
/// Reset SMTP's recipient bookkeeping for a new drive. The shared base has
39
/// already cleared the transport, reply cursor, transcript, upload capture,
40
/// and any in-flight body phase.
41
4.71k
void SmtpMockServer::ResetProtocolState() {
42
4.71k
  const curl::fuzzer::proto::Scenario* script = scenario();
43
4.71k
  recipient_count_ = script == nullptr ? 0
44
4.71k
                                       : std::min<std::size_t>(scenario_limits::kMaxMailRecipients,
45
4.71k
                                                               static_cast<std::size_t>(script->mail_rcpt_size()));
46
4.71k
  rcpt_command_count_ = 0;
47
4.71k
  auth_exchange_active_ = false;
48
4.71k
}
49
50
/// Return whether the effective option list enables curl's
51
/// CURLOPT_MAIL_RCPT_ALLOWFAILS behavior. Scan backwards because repeated
52
/// setopt calls are legal and libcurl keeps the last value.
53
/// @return true when recipient errors may be followed by another RCPT TO.
54
500
bool SmtpMockServer::AllowsRecipientFailures() const {
55
500
  const curl::fuzzer::proto::Scenario* script = scenario();
56
500
  if (script == nullptr) {
57
0
    return false;
58
0
  }
59
1.07k
  for (int index = script->options_size() - 1; index >= 0; --index) {
60
606
    const auto& option = script->options(index);
61
606
    if (option.option_id() != curl::fuzzer::proto::CURLOPT_MAIL_RCPT_ALLOWFAILS) {
62
577
      continue;
63
577
    }
64
29
    if (option.value_case() == curl::fuzzer::proto::SetOption::kBoolValue) {
65
29
      return option.bool_value();
66
29
    }
67
0
    if (option.value_case() == curl::fuzzer::proto::SetOption::kUintValue) {
68
0
      return option.uint_value() != 0;
69
0
    }
70
0
    return false;
71
0
  }
72
471
  return false;
73
500
}
74
75
/// Scan complete lines because a legal SMTP response fragment may start with
76
/// one or more informational lines. curl ends a response at the first line
77
/// whose first three bytes are digits and whose fourth byte is a space.
78
/// @param response Raw control fragment.
79
/// @return numeric status, or -1 when no terminating line is present.
80
7.38k
int SmtpMockServer::FirstReplyCode(std::string_view response) {
81
7.38k
  std::size_t line_start = 0;
82
83.8k
  while (line_start < response.size()) {
83
82.8k
    const std::size_t newline = response.find('\n', line_start);
84
82.8k
    if (newline == std::string_view::npos) {
85
993
      return -1;
86
993
    }
87
81.8k
    const std::size_t line_size = newline - line_start + 1;
88
81.8k
    if (line_size > 3) {
89
14.6k
      const unsigned char first = static_cast<unsigned char>(response[line_start]);
90
14.6k
      const unsigned char second = static_cast<unsigned char>(response[line_start + 1]);
91
14.6k
      const unsigned char third = static_cast<unsigned char>(response[line_start + 2]);
92
14.6k
      if (first >= '0' && first <= '9' && second >= '0' && second <= '9' && third >= '0' && third <= '9' &&
93
10.4k
          response[line_start + 3] == ' ') {
94
5.38k
        return 100 * (first - '0') + 10 * (second - '0') + (third - '0');
95
5.38k
      }
96
14.6k
    }
97
76.4k
    line_start = newline + 1;
98
76.4k
  }
99
1.00k
  return -1;
100
7.38k
}
101
102
/// Advance one reply for every complete command. A successful DATA reply
103
/// switches the parser into body-draining mode. The reply that follows DATA is
104
/// queued immediately: curl's smtp_done() runs a blocking POSTDATA wait inside
105
/// curl_multi_perform, so the outer peer service loop cannot run after the body
106
/// has been written. Pre-queuing it, as the FTP peer does for its final
107
/// transfer reply, keeps that wait deterministic without changing the order
108
/// curl observes (354 first, then the post-body response).
109
/// @param command One newline-terminated command from curl.
110
995k
void SmtpMockServer::HandleLine(std::string_view command) {
111
995k
  const std::string_view verb = FirstToken(command);
112
995k
  const bool is_auth_continuation_reply = auth_exchange_active_;
113
114
995k
  const std::string* response = NextControlReply();
115
995k
  if (response == nullptr) {
116
987k
    return;
117
987k
  }
118
119
7.38k
  const int response_code = FirstReplyCode(*response);
120
7.38k
  (void)WriteControlBytes(*response);
121
122
7.38k
  if (TokenEquals(verb, "AUTH")) {
123
    // A 334 reply is the only wire-visible distinction between SMTP AUTH and
124
    // a mutation-selected custom command that happens to start with AUTH.
125
182
    auth_exchange_active_ = response_code == 334;
126
7.20k
  } else if (is_auth_continuation_reply) {
127
151
    if (TokenEquals(verb, "*")) {
128
      // Curl accepts the cancellation reply before trying another enabled
129
      // SASL mechanism, so the exchange remains active across this error.
130
40
      auth_exchange_active_ = true;
131
111
    } else if (response_code != 334) {
132
108
      auth_exchange_active_ = false;
133
108
    }
134
151
  }
135
136
7.38k
  if (TokenEquals(verb, "DATA")) {
137
1.07k
    if (response_code == 354) {
138
610
      BeginMarkerBody(kEndOfBody);
139
610
      (void)WriteNextReply();
140
610
    } else if (response_code > 0) {
141
      // curl treats every non-354 DATA reply as terminal. Pre-queue the
142
      // shutdown reply so its blocking cleanup QUIT does not wait one second.
143
153
      (void)WriteControlBytes(kShutdownReply);
144
153
    }
145
1.07k
    return;
146
1.07k
  }
147
148
6.30k
  if (TokenEquals(verb, "RCPT")) {
149
1.65k
    ++rcpt_command_count_;
150
1.65k
  }
151
152
  // A terminal command error is followed by curl's blocking QUIT during
153
  // teardown. EHLO errors fall back to HELO, an early RCPT error may still be
154
  // followed by another recipient, and a failed AUTH exchange can cancel and
155
  // retry another mechanism. Those replies must not consume the shutdown
156
  // reply. Everything else can safely see a queued 221 next.
157
6.30k
  const bool final_rcpt_error =
158
6.30k
      TokenEquals(verb, "RCPT") && (rcpt_command_count_ >= recipient_count_ || !AllowsRecipientFailures());
159
6.30k
  const bool terminal_error = response_code >= 400 && !TokenEquals(verb, "EHLO") && !is_auth_continuation_reply &&
160
32
                              (!TokenEquals(verb, "RCPT") || final_rcpt_error);
161
6.30k
  if (terminal_error) {
162
26
    (void)WriteControlBytes(kShutdownReply);
163
26
  }
164
6.30k
}
165
166
/// Queue a 221 before the shared transport half-closes. SMTP's blocking QUIT
167
/// read has a one-second timeout when no reply is available, so even an
168
/// unfinished transfer gets a liveness reply; any real scripted response
169
/// already consumed by curl still determines the transfer result.
170
4.29k
void SmtpMockServer::QueueShutdownReply() { (void)WriteControlBytes(kShutdownReply); }
171
172
}  // namespace proto_fuzzer