/src/curl_fuzzer/proto_fuzzer/ldap_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 BER/ASN.1-aware in-process LDAP peer. |
9 | | |
10 | | #include "proto_fuzzer/ldap_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 | | // RFC 4511 protocol operation tags used by this peer. |
28 | | constexpr unsigned char kSequenceTag = 0x30; |
29 | | constexpr unsigned char kBooleanTag = 0x01; |
30 | | constexpr unsigned char kIntegerTag = 0x02; |
31 | | constexpr unsigned char kEnumeratedTag = 0x0a; |
32 | | constexpr unsigned char kOctetStringTag = 0x04; |
33 | | constexpr unsigned char kUnbindRequest = 0x42; |
34 | | constexpr unsigned char kBindRequest = 0x60; |
35 | | constexpr unsigned char kBindResponse = 0x61; |
36 | | constexpr unsigned char kSearchRequest = 0x63; |
37 | | constexpr unsigned char kSearchResultEntry = 0x64; |
38 | | constexpr unsigned char kSearchResultDone = 0x65; |
39 | | constexpr unsigned char kAbandonRequest = 0x50; |
40 | | constexpr unsigned char kExtendedRequest = 0x77; |
41 | | constexpr unsigned char kExtendedResponse = 0x78; |
42 | | constexpr unsigned char kSetTag = 0x31; |
43 | | constexpr unsigned char kPresentFilter = 0x87; |
44 | | constexpr int kLdapSuccess = 0; |
45 | | constexpr std::string_view kObjectClass = "objectclass"; |
46 | | constexpr std::string_view kSupportedSaslMechanisms = "supportedSASLMechanisms"; |
47 | | constexpr std::string_view kPlainMechanism = "PLAIN"; |
48 | | |
49 | 219k | unsigned char ByteAt(std::string_view bytes, std::size_t index) { return static_cast<unsigned char>(bytes[index]); } |
50 | | |
51 | | } // namespace |
52 | | |
53 | | /// Begin without a borrowed protobuf or open socketpair. RunLoop borrows a |
54 | | /// Scenario only for the synchronous DriveScenario call in which socket |
55 | | /// callbacks can run. |
56 | | LdapMockServer::LdapMockServer() |
57 | 5.76k | : scenario_(nullptr), |
58 | 5.76k | server_chunk_count_(0), |
59 | 5.76k | next_server_chunk_(0), |
60 | 5.76k | bind_request_count_(0), |
61 | 5.76k | search_request_count_(0), |
62 | 5.76k | mechanism_search_request_count_(0), |
63 | 5.76k | starttls_request_count_(0), |
64 | 5.76k | unbind_request_count_(0), |
65 | 5.76k | server_message_count_(0), |
66 | 5.76k | scripted_message_count_(0), |
67 | 5.76k | synthesized_done_count_(0), |
68 | 5.76k | scripted_bind_response_count_(0), |
69 | 5.76k | search_message_id_(0), |
70 | 5.76k | search_active_(false), |
71 | 5.76k | search_done_sent_(false), |
72 | 5.76k | scripted_bind_consumed_(false), |
73 | 5.76k | release_script_chunks_(false), |
74 | 5.76k | peer_closed_(false), |
75 | 5.76k | protocol_error_(false), |
76 | 5.76k | socket_opened_(false) {} |
77 | | |
78 | | /// Release the server-owned socketpair half after curl has returned its client |
79 | | /// descriptor. The out-of-line destructor keeps MockConnection's complete type |
80 | | /// local to this translation unit. |
81 | 5.76k | LdapMockServer::~LdapMockServer() = default; |
82 | | |
83 | | /// @return A prefix of complete client LDAPMessages from the most recent drive. |
84 | 5.73k | const std::string& LdapMockServer::packet_transcript() const { return packet_transcript_; } |
85 | | |
86 | | /// Append one fragment after the transport has decoded it. Plaintext peers |
87 | | /// report socket bytes directly; the LDAPS peer reports decrypted application |
88 | | /// bytes, so the BER parser is independent of the carrier. |
89 | | /// @param data First byte of the fragment. |
90 | | /// @param size Number of bytes available at `data`. |
91 | 16.5k | void LdapMockServer::ObserveIncomingData(const unsigned char* data, std::size_t size) { |
92 | 16.5k | if (data == nullptr || size == 0) { |
93 | 0 | return; |
94 | 0 | } |
95 | 16.5k | pending_client_bytes_.append(reinterpret_cast<const char*>(data), size); |
96 | 16.5k | } |
97 | | |
98 | | /// @return Number of bindRequest messages observed. |
99 | 5.73k | std::size_t LdapMockServer::bind_request_count() const { return bind_request_count_; } |
100 | | |
101 | | /// @return Number of searchRequest messages observed. |
102 | 5.73k | std::size_t LdapMockServer::search_request_count() const { return search_request_count_; } |
103 | | |
104 | | /// @return Number of root-DSE supportedSASLMechanisms searches observed. |
105 | 0 | std::size_t LdapMockServer::mechanism_search_request_count() const { return mechanism_search_request_count_; } |
106 | | |
107 | | /// @return Number of StartTLS ExtendedRequest messages observed. |
108 | 5.73k | std::size_t LdapMockServer::starttls_request_count() const { return starttls_request_count_; } |
109 | | |
110 | | /// @return Number of unbindRequest messages observed. |
111 | 0 | std::size_t LdapMockServer::unbind_request_count() const { return unbind_request_count_; } |
112 | | |
113 | | /// @return Number of server messages written in the most recent drive. |
114 | 0 | std::size_t LdapMockServer::server_message_count() const { return server_message_count_; } |
115 | | |
116 | | /// @return Number of scripted search-result chunks forwarded. |
117 | 5.73k | std::size_t LdapMockServer::scripted_message_count() const { return scripted_message_count_; } |
118 | | |
119 | | /// @return Number of searchResDone messages generated by the peer. |
120 | 5.73k | std::size_t LdapMockServer::synthesized_done_count() const { return synthesized_done_count_; } |
121 | | |
122 | | /// @return Number of bind replies taken from initial_response. |
123 | 0 | std::size_t LdapMockServer::scripted_bind_response_count() const { return scripted_bind_response_count_; } |
124 | | |
125 | | /// Decode one definite-length BER TLV. The decoder accepts a single short-form |
126 | | /// length byte or long form with at most four length bytes; indefinite lengths |
127 | | /// and lengths beyond the peer's message budget are protocol errors. A |
128 | | /// truncated header or body reports kNeedMore so the caller can retain the |
129 | | /// partial bytes for the next socket read. |
130 | | /// @param bytes Buffer beginning at the TLV start. |
131 | | /// @param offset Offset of the tag byte within `bytes`. |
132 | | /// @param view Receives the decoded tag and bounded value extent. |
133 | | /// @return kComplete, kNeedMore, or kInvalid. |
134 | 98.9k | LdapMockServer::ParseResult LdapMockServer::ParseTlv(std::string_view bytes, std::size_t offset, TlvView* view) { |
135 | 98.9k | if (view == nullptr) { |
136 | 0 | return ParseResult::kInvalid; |
137 | 0 | } |
138 | 98.9k | if (offset >= bytes.size()) { |
139 | 2.30k | return ParseResult::kNeedMore; |
140 | 2.30k | } |
141 | | |
142 | 96.6k | const unsigned char tag = ByteAt(bytes, offset); |
143 | 96.6k | if (tag == 0) { |
144 | | // End-of-contents is only valid for indefinite lengths, which LDAP forbids |
145 | | // in the commands this peer accepts. |
146 | 1.45k | return ParseResult::kInvalid; |
147 | 1.45k | } |
148 | | |
149 | 95.1k | std::size_t cursor = offset + 1; |
150 | 95.1k | if (cursor >= bytes.size()) { |
151 | 7.21k | return ParseResult::kNeedMore; |
152 | 7.21k | } |
153 | | |
154 | 87.9k | const unsigned char length_byte = ByteAt(bytes, cursor++); |
155 | 87.9k | std::size_t length = 0; |
156 | 87.9k | if ((length_byte & 0x80U) == 0) { |
157 | 84.5k | length = static_cast<std::size_t>(length_byte); |
158 | 84.5k | } else { |
159 | 3.37k | const std::size_t length_bytes = static_cast<std::size_t>(length_byte & 0x7fU); |
160 | 3.37k | if (length_bytes == 0 || length_bytes > 4) { |
161 | 1.04k | return ParseResult::kInvalid; |
162 | 1.04k | } |
163 | 2.32k | if (bytes.size() - cursor < length_bytes) { |
164 | 296 | return ParseResult::kNeedMore; |
165 | 296 | } |
166 | 6.45k | for (std::size_t index = 0; index < length_bytes; ++index) { |
167 | 4.42k | length = (length << 8U) | static_cast<std::size_t>(ByteAt(bytes, cursor + index)); |
168 | 4.42k | } |
169 | 2.03k | cursor += length_bytes; |
170 | 2.03k | } |
171 | | |
172 | 86.6k | if (length > kMaxClientMessageBytes) { |
173 | 386 | return ParseResult::kInvalid; |
174 | 386 | } |
175 | 86.2k | if (bytes.size() - cursor < length) { |
176 | 2.24k | return ParseResult::kNeedMore; |
177 | 2.24k | } |
178 | | |
179 | 83.9k | view->tag = tag; |
180 | 83.9k | view->value_offset = cursor; |
181 | 83.9k | view->value_length = length; |
182 | 83.9k | view->total_length = cursor - offset + length; |
183 | 83.9k | return ParseResult::kComplete; |
184 | 86.2k | } |
185 | | |
186 | | /// Recognize the exact root-DSE query emitted by oldap_perform_mechs. A plain |
187 | | /// substring check would let a mutation-controlled data search for the same |
188 | | /// attribute consume the peer-owned setup response, so every BER field that |
189 | | /// distinguishes the fixed discovery request is checked fail closed. Routing |
190 | | /// also requires the pre-bind state because a later data search can be byte |
191 | | /// identical to this query. |
192 | | /// @param value Value bytes of a searchRequest protocol operation. |
193 | | /// @return true only for the supportedSASLMechanisms root-DSE query. |
194 | 4.94k | bool LdapMockServer::IsSaslMechanismSearch(std::string_view value) { |
195 | 4.94k | std::size_t cursor = 0; |
196 | 4.94k | TlvView field; |
197 | 24.7k | const auto parse_field = [&value, &cursor](unsigned char tag, TlvView* parsed) { |
198 | 24.7k | if (ParseTlv(value, cursor, parsed) != ParseResult::kComplete || parsed->tag != tag) { |
199 | 505 | return false; |
200 | 505 | } |
201 | 24.2k | cursor = parsed->value_offset + parsed->value_length; |
202 | 24.2k | return true; |
203 | 24.7k | }; |
204 | 14.2k | const auto is_zero = [&value](const TlvView& parsed) { |
205 | 14.2k | return parsed.value_length == 1 && ByteAt(value, parsed.value_offset) == 0; |
206 | 14.2k | }; |
207 | | |
208 | 4.94k | if (!parse_field(kOctetStringTag, &field) || field.value_length != 0 || !parse_field(kEnumeratedTag, &field) || |
209 | 3.16k | !is_zero(field) || !parse_field(kEnumeratedTag, &field) || !is_zero(field) || !parse_field(kIntegerTag, &field) || |
210 | 2.77k | !is_zero(field) || !parse_field(kIntegerTag, &field) || !is_zero(field) || !parse_field(kBooleanTag, &field) || |
211 | 2.67k | !is_zero(field) || !parse_field(kPresentFilter, &field) || |
212 | 2.61k | value.substr(field.value_offset, field.value_length) != kObjectClass || !parse_field(kSequenceTag, &field) || |
213 | 2.60k | cursor != value.size()) { |
214 | 2.34k | return false; |
215 | 2.34k | } |
216 | | |
217 | 2.60k | const std::size_t attributes_end = field.value_offset + field.value_length; |
218 | 2.60k | TlvView attribute; |
219 | 2.60k | if (ParseTlv(value, field.value_offset, &attribute) != ParseResult::kComplete || attribute.tag != kOctetStringTag || |
220 | 2.33k | attribute.value_offset + attribute.value_length != attributes_end) { |
221 | 2.33k | return false; |
222 | 2.33k | } |
223 | 273 | return value.substr(attribute.value_offset, attribute.value_length) == kSupportedSaslMechanisms; |
224 | 2.60k | } |
225 | | |
226 | | /// Decode an LDAPMessage envelope: SEQUENCE { messageID INTEGER, protocolOp }. |
227 | | /// The peer needs the integer id and outer operation tag. Search bodies remain |
228 | | /// opaque except for exact classification of OpenLDAP's fixed mechanism query, |
229 | | /// so mutation-controlled data searches still reach OpenLDAP's BER parser. |
230 | | /// @param bytes Buffer beginning at the message's first tag byte. |
231 | | /// @param message Receives the decoded envelope fields. |
232 | | /// @return kComplete, kNeedMore, or kInvalid. |
233 | 23.8k | LdapMockServer::ParseResult LdapMockServer::ParseMessage(std::string_view bytes, MessageView* message) { |
234 | 23.8k | if (message == nullptr) { |
235 | 0 | return ParseResult::kInvalid; |
236 | 0 | } |
237 | | |
238 | 23.8k | TlvView outer; |
239 | 23.8k | ParseResult parsed = ParseTlv(bytes, 0, &outer); |
240 | 23.8k | if (parsed != ParseResult::kComplete) { |
241 | 5.41k | return parsed; |
242 | 5.41k | } |
243 | 18.3k | if (outer.tag != kSequenceTag || outer.value_length < 2) { |
244 | 723 | return ParseResult::kInvalid; |
245 | 723 | } |
246 | | |
247 | 17.6k | const std::size_t outer_end = outer.value_offset + outer.value_length; |
248 | 17.6k | TlvView integer; |
249 | 17.6k | parsed = ParseTlv(bytes, outer.value_offset, &integer); |
250 | 17.6k | if (parsed != ParseResult::kComplete) { |
251 | 615 | return parsed; |
252 | 615 | } |
253 | 17.0k | if (integer.tag != kIntegerTag || integer.value_length == 0 || integer.value_length > 4 || |
254 | 16.5k | integer.value_offset + integer.value_length > outer_end) { |
255 | 523 | return ParseResult::kInvalid; |
256 | 523 | } |
257 | | |
258 | 16.5k | std::uint32_t message_id = 0; |
259 | 33.1k | for (std::size_t index = 0; index < integer.value_length; ++index) { |
260 | 16.6k | message_id = (message_id << 8U) | static_cast<std::uint32_t>(ByteAt(bytes, integer.value_offset + index)); |
261 | 16.6k | } |
262 | | |
263 | 16.5k | const std::size_t operation_offset = integer.value_offset + integer.value_length; |
264 | 16.5k | if (operation_offset >= outer_end) { |
265 | 22 | return ParseResult::kInvalid; |
266 | 22 | } |
267 | 16.5k | TlvView operation; |
268 | 16.5k | parsed = ParseTlv(bytes, operation_offset, &operation); |
269 | 16.5k | if (parsed != ParseResult::kComplete) { |
270 | 187 | return parsed; |
271 | 187 | } |
272 | 16.3k | if (operation.value_offset + operation.value_length > outer_end) { |
273 | 17 | return ParseResult::kInvalid; |
274 | 17 | } |
275 | | |
276 | 16.3k | message->op_tag = operation.tag; |
277 | 16.3k | message->message_id = message_id; |
278 | 16.3k | message->total_length = outer.total_length; |
279 | 16.3k | message->sasl_mechanism_query = operation.tag == kSearchRequest && |
280 | 4.94k | IsSaslMechanismSearch(bytes.substr(operation.value_offset, operation.value_length)); |
281 | 16.3k | return ParseResult::kComplete; |
282 | 16.3k | } |
283 | | |
284 | | /// Append a definite BER length. LDAP fuzz inputs remain far below the |
285 | | /// long-form boundary in ordinary traffic, but server chunks are |
286 | | /// mutation-controlled, so the encoder handles the full 32-bit size_t range |
287 | | /// with the shortest representation. |
288 | | /// @param out Destination string. |
289 | | /// @param length Number of value bytes that follow. |
290 | 58.1k | void LdapMockServer::AppendBerLength(std::string* out, std::size_t length) { |
291 | 58.1k | if (out == nullptr) { |
292 | 0 | return; |
293 | 0 | } |
294 | 58.1k | if (length < 128) { |
295 | 58.1k | out->push_back(static_cast<char>(length)); |
296 | 58.1k | return; |
297 | 58.1k | } |
298 | | |
299 | 0 | unsigned char encoded[sizeof(std::size_t)]; |
300 | 0 | std::size_t count = 0; |
301 | 0 | while (length != 0) { |
302 | 0 | encoded[count++] = static_cast<unsigned char>(length & 0xffU); |
303 | 0 | length >>= 8U; |
304 | 0 | } |
305 | 0 | out->push_back(static_cast<char>(0x80U | count)); |
306 | 0 | while (count != 0) { |
307 | 0 | out->push_back(static_cast<char>(encoded[--count])); |
308 | 0 | } |
309 | 0 | } |
310 | | |
311 | | /// Append one TLV with a definite length. |
312 | | /// @param out Destination string. |
313 | | /// @param tag BER identifier octet. |
314 | | /// @param value Value bytes. |
315 | 58.1k | void LdapMockServer::AppendTlv(std::string* out, unsigned char tag, std::string_view value) { |
316 | 58.1k | if (out == nullptr) { |
317 | 0 | return; |
318 | 0 | } |
319 | 58.1k | out->push_back(static_cast<char>(tag)); |
320 | 58.1k | AppendBerLength(out, value.size()); |
321 | 58.1k | out->append(value.data(), value.size()); |
322 | 58.1k | } |
323 | | |
324 | | /// Append a minimally encoded non-negative BER INTEGER. |
325 | | /// @param out Destination string. |
326 | | /// @param value Integer to encode. |
327 | 11.1k | void LdapMockServer::AppendBerInteger(std::string* out, std::uint32_t value) { |
328 | 11.1k | unsigned char encoded[5]; |
329 | 11.1k | std::size_t count = 0; |
330 | 11.1k | do { |
331 | 11.1k | encoded[count++] = static_cast<unsigned char>(value & 0xffU); |
332 | 11.1k | value >>= 8U; |
333 | 11.1k | } while (value != 0); |
334 | | |
335 | 11.1k | std::string bytes; |
336 | 11.1k | if ((encoded[count - 1] & 0x80U) != 0) { |
337 | 0 | bytes.push_back('\0'); |
338 | 0 | } |
339 | 22.3k | while (count != 0) { |
340 | 11.1k | bytes.push_back(static_cast<char>(encoded[--count])); |
341 | 11.1k | } |
342 | 11.1k | AppendTlv(out, kIntegerTag, bytes); |
343 | 11.1k | } |
344 | | |
345 | | /// Encode a bindResponse or searchResDone carrying a single result code and |
346 | | /// empty matchedDN/diagnosticMessage fields. The peer echoes the request's |
347 | | /// message id so OpenLDAP associates the reply with curl's outstanding call. |
348 | | /// @param message_id Request message id. |
349 | | /// @param op_tag kBindResponse or kSearchResultDone. |
350 | | /// @param result_code LDAP resultCode (0 is success). |
351 | | /// @param out Receives the complete LDAPMessage. |
352 | | void LdapMockServer::BuildResultMessage(std::uint32_t message_id, unsigned char op_tag, int result_code, |
353 | 8.74k | std::string* out) { |
354 | 8.74k | if (out == nullptr) { |
355 | 0 | return; |
356 | 0 | } |
357 | | |
358 | 8.74k | std::string result; |
359 | 8.74k | const int bounded_code = std::max(0, std::min(result_code, 255)); |
360 | 8.74k | const char code_byte = static_cast<char>(bounded_code); |
361 | 8.74k | AppendTlv(&result, kEnumeratedTag, std::string_view(&code_byte, 1)); |
362 | 8.74k | AppendTlv(&result, kOctetStringTag, std::string_view()); |
363 | 8.74k | AppendTlv(&result, kOctetStringTag, std::string_view()); |
364 | | |
365 | 8.74k | std::string content; |
366 | 8.74k | AppendBerInteger(&content, message_id); |
367 | 8.74k | AppendTlv(&content, op_tag, result); |
368 | | |
369 | 8.74k | out->clear(); |
370 | 8.74k | AppendTlv(out, kSequenceTag, content); |
371 | 8.74k | } |
372 | | |
373 | | /// Encode a root-DSE searchResEntry advertising the one deterministic SASL |
374 | | /// mechanism that the in-process peer can complete without external state. |
375 | | /// @param message_id Mechanism-search request id to echo. |
376 | | /// @param out Receives the complete LDAPMessage. |
377 | 111 | void LdapMockServer::BuildSaslMechanismsEntry(std::uint32_t message_id, std::string* out) { |
378 | 111 | if (out == nullptr) { |
379 | 0 | return; |
380 | 0 | } |
381 | | |
382 | 111 | std::string values; |
383 | 111 | AppendTlv(&values, kOctetStringTag, kPlainMechanism); |
384 | | |
385 | 111 | std::string attribute; |
386 | 111 | AppendTlv(&attribute, kOctetStringTag, kSupportedSaslMechanisms); |
387 | 111 | AppendTlv(&attribute, kSetTag, values); |
388 | | |
389 | 111 | std::string attributes; |
390 | 111 | AppendTlv(&attributes, kSequenceTag, attribute); |
391 | | |
392 | 111 | std::string entry; |
393 | 111 | AppendTlv(&entry, kOctetStringTag, std::string_view()); |
394 | 111 | AppendTlv(&entry, kSequenceTag, attributes); |
395 | | |
396 | 111 | std::string content; |
397 | 111 | AppendBerInteger(&content, message_id); |
398 | 111 | AppendTlv(&content, kSearchResultEntry, entry); |
399 | | |
400 | 111 | out->clear(); |
401 | 111 | AppendTlv(out, kSequenceTag, content); |
402 | 111 | } |
403 | | |
404 | | /// Replace the messageID of a complete LDAPMessage. Scripted server bytes are |
405 | | /// mutation-controlled and may not know OpenLDAP's per-connection counter, so |
406 | | /// this keeps a valid seed meaningful without rewriting the operation body. |
407 | | /// @param message Complete LDAPMessage bytes. |
408 | | /// @param message_id New envelope integer. |
409 | | /// @param out Receives the rewritten message. |
410 | | /// @return true when the envelope parsed and was rewritten; false leaves `out` |
411 | | /// unspecified and the caller should relay the original bytes. |
412 | 10.1k | bool LdapMockServer::RewriteMessageId(std::string_view message, std::uint32_t message_id, std::string* out) { |
413 | 10.1k | if (out == nullptr) { |
414 | 0 | return false; |
415 | 0 | } |
416 | | |
417 | 10.1k | TlvView outer; |
418 | 10.1k | if (ParseTlv(message, 0, &outer) != ParseResult::kComplete || outer.tag != kSequenceTag || |
419 | 6.80k | outer.total_length != message.size()) { |
420 | 6.80k | return false; |
421 | 6.80k | } |
422 | | |
423 | 3.38k | TlvView integer; |
424 | 3.38k | if (ParseTlv(message, outer.value_offset, &integer) != ParseResult::kComplete || integer.tag != kIntegerTag || |
425 | 2.32k | integer.value_offset + integer.value_length > outer.value_offset + outer.value_length) { |
426 | 1.06k | return false; |
427 | 1.06k | } |
428 | | |
429 | 2.32k | std::string content; |
430 | 2.32k | AppendBerInteger(&content, message_id); |
431 | 2.32k | const std::size_t remainder_offset = integer.value_offset + integer.value_length; |
432 | 2.32k | const std::size_t outer_end = outer.value_offset + outer.value_length; |
433 | 2.32k | content.append(message.data() + remainder_offset, outer_end - remainder_offset); |
434 | | |
435 | 2.32k | out->clear(); |
436 | 2.32k | AppendTlv(out, kSequenceTag, content); |
437 | 2.32k | return true; |
438 | 3.38k | } |
439 | | |
440 | | /// Extract only the protocol operation tag from a complete server message. |
441 | | /// @param message Candidate LDAPMessage bytes. |
442 | | /// @param op_tag Receives the operation tag. |
443 | | /// @return true when the envelope parsed completely. |
444 | 9.79k | bool LdapMockServer::MessageOpTag(std::string_view message, unsigned char* op_tag) { |
445 | 9.79k | if (op_tag == nullptr) { |
446 | 0 | return false; |
447 | 0 | } |
448 | 9.79k | MessageView decoded; |
449 | 9.79k | if (ParseMessage(message, &decoded) != ParseResult::kComplete || decoded.total_length != message.size()) { |
450 | 7.71k | return false; |
451 | 7.71k | } |
452 | 2.08k | *op_tag = decoded.op_tag; |
453 | 2.08k | return true; |
454 | 9.79k | } |
455 | | |
456 | | /// Reset every borrowed pointer, parser cursor, and observable capture before |
457 | | /// borrowing the next Scenario. |
458 | | /// @param scenario Scenario whose Connection supplies the bind reply and |
459 | | /// search-result messages. |
460 | 5.66k | void LdapMockServer::ResetForScenario(const curl::fuzzer::proto::Scenario& scenario) { |
461 | 5.66k | connection_.reset(); |
462 | 5.66k | scenario_ = &scenario; |
463 | 5.66k | server_chunk_count_ = std::min<std::size_t>(scenario_limits::kMaxResponseChunks, |
464 | 5.66k | static_cast<std::size_t>(scenario.connection().on_readable_size())); |
465 | 5.66k | next_server_chunk_ = 0; |
466 | 5.66k | bind_request_count_ = 0; |
467 | 5.66k | search_request_count_ = 0; |
468 | 5.66k | mechanism_search_request_count_ = 0; |
469 | 5.66k | starttls_request_count_ = 0; |
470 | 5.66k | unbind_request_count_ = 0; |
471 | 5.66k | server_message_count_ = 0; |
472 | 5.66k | scripted_message_count_ = 0; |
473 | 5.66k | synthesized_done_count_ = 0; |
474 | 5.66k | scripted_bind_response_count_ = 0; |
475 | 5.66k | pending_client_bytes_.clear(); |
476 | 5.66k | packet_transcript_.clear(); |
477 | 5.66k | search_message_id_ = 0; |
478 | 5.66k | search_active_ = false; |
479 | 5.66k | search_done_sent_ = false; |
480 | 5.66k | scripted_bind_consumed_ = false; |
481 | 5.66k | release_script_chunks_ = false; |
482 | 5.66k | peer_closed_ = false; |
483 | 5.66k | protocol_error_ = false; |
484 | 5.66k | socket_opened_ = false; |
485 | 5.66k | } |
486 | | |
487 | | /// Apply bounded transport knobs when creating the scripted socket. Target |
488 | | /// policies normally clear LDAP backpressure, but direct corpus replay must |
489 | | /// retain deterministic, well-defined behavior. |
490 | | /// @param connection Peer receiving the socket settings. |
491 | | /// @param script Connection script carrying optional backpressure values. |
492 | | void LdapMockServer::ApplyScriptBackpressure(MockConnection* connection, |
493 | 5.11k | const curl::fuzzer::proto::Connection& script) { |
494 | 5.11k | if (connection == nullptr) { |
495 | 0 | return; |
496 | 0 | } |
497 | | |
498 | 5.11k | const std::uint32_t int_max = static_cast<std::uint32_t>(std::numeric_limits<int>::max()); |
499 | 5.11k | const auto& backpressure = script.backpressure(); |
500 | 5.11k | const int receive_buffer = static_cast<int>(std::min(backpressure.recv_buf_bytes(), int_max)); |
501 | 5.11k | connection->ApplyBackpressure(receive_buffer, static_cast<std::size_t>(backpressure.drain_limit())); |
502 | 5.11k | } |
503 | | |
504 | | /// Create the default plaintext transport. The LDAPS subclass overrides this |
505 | | /// factory, which keeps every BER handling path identical across carriers. |
506 | | /// @return Newly allocated in-process socketpair peer. |
507 | 2.46k | std::unique_ptr<MockConnection> LdapMockServer::CreateConnection() { return std::make_unique<MockConnection>(); } |
508 | | |
509 | | /// Create the single LDAP socketpair. The bind reply is deliberately not |
510 | | /// queued here: LDAP is client-first, and releasing the scripted reply only |
511 | | /// after the bindRequest lets the peer rewrite its messageID to OpenLDAP's |
512 | | /// current counter. |
513 | | /// @param purpose Socket role requested by curl. |
514 | | /// @param address Unused stream destination metadata required by the callback |
515 | | /// signature. |
516 | | /// @return Client descriptor for the LDAP channel, or CURL_SOCKET_BAD. |
517 | 5.11k | curl_socket_t LdapMockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* /*address*/) { |
518 | 5.11k | if (scenario_ == nullptr || purpose != CURLSOCKTYPE_IPCXN || socket_opened_) { |
519 | 0 | return CURL_SOCKET_BAD; |
520 | 0 | } |
521 | | |
522 | 5.11k | connection_ = CreateConnection(); |
523 | 5.11k | if (!connection_ || !connection_->ok()) { |
524 | 0 | connection_.reset(); |
525 | 0 | return CURL_SOCKET_BAD; |
526 | 0 | } |
527 | 5.11k | connection_->SetIncomingDataObserver(this); |
528 | | |
529 | 5.11k | ApplyScriptBackpressure(connection_.get(), scenario_->connection()); |
530 | 5.11k | socket_opened_ = true; |
531 | 5.11k | return connection_->take_client_fd(); |
532 | 5.11k | } |
533 | | |
534 | | /// Retain only a bounded prefix for assertions. Parsing and draining continue |
535 | | /// against the full transport bytes, so reaching the cap cannot change curl's |
536 | | /// behavior or manufacture socket backpressure. |
537 | | /// @param source Newly observed bytes. |
538 | | /// @param limit Maximum retained destination size. |
539 | | /// @param destination Capture buffer receiving the available prefix. |
540 | 13.9k | void LdapMockServer::CapturePrefix(std::string_view source, std::size_t limit, std::string* destination) { |
541 | 13.9k | if (destination == nullptr || destination->size() >= limit) { |
542 | 3 | return; |
543 | 3 | } |
544 | 13.9k | const std::size_t available = limit - destination->size(); |
545 | 13.9k | destination->append(source.data(), std::min(source.size(), available)); |
546 | 13.9k | } |
547 | | |
548 | | /// Keep failed peer writes local to the mock. Response cursors must still |
549 | | /// advance after curl closes early, otherwise a replacement socket could see a |
550 | | /// reply intended for an earlier request. |
551 | | /// @param bytes Response bytes to queue. |
552 | | /// @return true when the complete fragment was queued. |
553 | 19.0k | bool LdapMockServer::WriteServerBytes(std::string_view bytes) { |
554 | 19.0k | if (bytes.empty()) { |
555 | 0 | return true; |
556 | 0 | } |
557 | 19.0k | const bool written = connection_ != nullptr && |
558 | 19.0k | connection_->WriteAll(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size()); |
559 | 19.0k | if (written) { |
560 | 18.3k | ++server_message_count_; |
561 | 18.3k | } |
562 | 19.0k | return written; |
563 | 19.0k | } |
564 | | |
565 | | /// Answer one bindRequest. A non-empty initial_response is the scenario's bind |
566 | | /// reply and is renumbered to the request id; otherwise a success response is |
567 | | /// synthesized. Reusing the script only once preserves OpenLDAP's protocol-v2 |
568 | | /// fallback path, which sends a second bind after a protocol-error reply. |
569 | | /// @param message_id Client bind message id. |
570 | 5.00k | void LdapMockServer::HandleBindRequest(std::uint32_t message_id) { |
571 | 5.00k | ++bind_request_count_; |
572 | | |
573 | 5.00k | std::string response; |
574 | 5.00k | if (!scripted_bind_consumed_ && scenario_ != nullptr && !scenario_->connection().initial_response().empty()) { |
575 | 395 | const std::string& scripted = scenario_->connection().initial_response(); |
576 | 395 | if (!RewriteMessageId(scripted, message_id, &response)) { |
577 | 248 | response = scripted; |
578 | 248 | } |
579 | 395 | scripted_bind_consumed_ = true; |
580 | 395 | ++scripted_bind_response_count_; |
581 | 4.61k | } else { |
582 | 4.61k | BuildResultMessage(message_id, kBindResponse, kLdapSuccess, &response); |
583 | 4.61k | } |
584 | 5.00k | (void)WriteServerBytes(response); |
585 | 5.00k | } |
586 | | |
587 | | /// Begin (or restart) the search script. Result chunks are released one per |
588 | | /// drive turn so curl consumes each LDAPMessage before the next arrives; the |
589 | | /// terminal done follows once the cursor is exhausted. |
590 | | /// @param message_id Client search message id. |
591 | 4.13k | void LdapMockServer::HandleSearchRequest(std::uint32_t message_id) { |
592 | 4.13k | ++search_request_count_; |
593 | 4.13k | search_message_id_ = message_id; |
594 | 4.13k | search_active_ = true; |
595 | 4.13k | search_done_sent_ = false; |
596 | 4.13k | release_script_chunks_ = true; |
597 | 4.13k | next_server_chunk_ = 0; |
598 | 4.13k | } |
599 | | |
600 | | /// Answer OpenLDAP's root-DSE SASL discovery search before the ordinary bind |
601 | | /// and URL-derived search. Keeping this response peer-owned prevents mutable |
602 | | /// search-result scripts from being consumed by the setup query. |
603 | | /// @param message_id Mechanism-search request id to echo. |
604 | 111 | void LdapMockServer::HandleMechanismSearchRequest(std::uint32_t message_id) { |
605 | 111 | ++search_request_count_; |
606 | 111 | ++mechanism_search_request_count_; |
607 | | |
608 | 111 | std::string entry; |
609 | 111 | BuildSaslMechanismsEntry(message_id, &entry); |
610 | 111 | (void)WriteServerBytes(entry); |
611 | | |
612 | 111 | std::string done; |
613 | 111 | BuildResultMessage(message_id, kSearchResultDone, kLdapSuccess, &done); |
614 | 111 | ++synthesized_done_count_; |
615 | 111 | (void)WriteServerBytes(done); |
616 | 111 | } |
617 | | |
618 | | /// Accept the RFC 4511 StartTLS ExtendedRequest. The successful correlated |
619 | | /// response makes OpenLDAP return control to curl, which installs its TLS |
620 | | /// connection filter on the existing confined socketpair. This plaintext peer |
621 | | /// deliberately does not impersonate a TLS server after the upgrade: the |
622 | | /// subsequent handshake observes a bounded EOF/error path while still |
623 | | /// exercising the production filter transition. |
624 | | /// @param message_id Client request identifier echoed in ExtendedResponse. |
625 | 50 | void LdapMockServer::HandleStartTlsRequest(std::uint32_t message_id) { |
626 | 50 | ++starttls_request_count_; |
627 | 50 | std::string response; |
628 | 50 | BuildResultMessage(message_id, kExtendedResponse, kLdapSuccess, &response); |
629 | 50 | (void)WriteServerBytes(response); |
630 | 50 | } |
631 | | |
632 | | /// Route one complete client message. Unknown protocol operations are consumed |
633 | | /// without a reply, matching LDAP's asymmetry: bind, search, and StartTLS |
634 | | /// expect responses, while unbind/abandon are fire-and-forget. |
635 | | /// @param message Decoded client envelope. |
636 | 13.9k | void LdapMockServer::HandleClientMessage(const MessageView& message) { |
637 | 13.9k | switch (message.op_tag) { |
638 | 5.00k | case kBindRequest: |
639 | 5.00k | HandleBindRequest(message.message_id); |
640 | 5.00k | return; |
641 | | |
642 | 4.24k | case kSearchRequest: |
643 | 4.24k | if (message.sasl_mechanism_query && bind_request_count_ == 0 && mechanism_search_request_count_ == 0) { |
644 | 111 | HandleMechanismSearchRequest(message.message_id); |
645 | 4.13k | } else { |
646 | 4.13k | HandleSearchRequest(message.message_id); |
647 | 4.13k | } |
648 | 4.24k | return; |
649 | | |
650 | 50 | case kExtendedRequest: |
651 | 50 | HandleStartTlsRequest(message.message_id); |
652 | 50 | return; |
653 | | |
654 | 3.36k | case kUnbindRequest: |
655 | 3.36k | ++unbind_request_count_; |
656 | 3.36k | return; |
657 | | |
658 | 1.26k | case kAbandonRequest: |
659 | 1.26k | return; |
660 | | |
661 | 0 | default: |
662 | 0 | return; |
663 | 13.9k | } |
664 | 13.9k | } |
665 | | |
666 | | /// Release one scenario-provided search-result message. Chunks are opaque BER |
667 | | /// LDAPMessages rather than command replies: each is forwarded with its |
668 | | /// envelope messageID rewritten to the active search id. A scripted |
669 | | /// searchResDone ends the search so no duplicate completion is synthesized; an |
670 | | /// explicitly empty chunk half-closes the peer for a truncated-stream path. |
671 | | /// @return true when a script slot was consumed. |
672 | 40.6k | bool LdapMockServer::ReleaseNextServerChunk() { |
673 | 40.6k | if (!release_script_chunks_ || scenario_ == nullptr || connection_ == nullptr || |
674 | 30.7k | next_server_chunk_ >= server_chunk_count_) { |
675 | 30.7k | return false; |
676 | 30.7k | } |
677 | | |
678 | 9.89k | const std::string& chunk = scenario_->connection().on_readable(static_cast<int>(next_server_chunk_)); |
679 | 9.89k | ++next_server_chunk_; |
680 | | |
681 | 9.89k | if (chunk.empty()) { |
682 | 99 | connection_->ShutdownWrite(); |
683 | 99 | peer_closed_ = true; |
684 | 99 | release_script_chunks_ = false; |
685 | 99 | return true; |
686 | 99 | } |
687 | | |
688 | 9.79k | unsigned char op_tag = 0; |
689 | 9.79k | const bool has_op_tag = MessageOpTag(chunk, &op_tag); |
690 | 9.79k | std::string rewritten; |
691 | 9.79k | const bool renumbered = RewriteMessageId(chunk, search_message_id_, &rewritten); |
692 | 9.79k | const std::string_view bytes = renumbered ? std::string_view(rewritten) : std::string_view(chunk); |
693 | | |
694 | 9.79k | ++scripted_message_count_; |
695 | 9.79k | (void)WriteServerBytes(bytes); |
696 | 9.79k | if (has_op_tag && op_tag == kSearchResultDone) { |
697 | 18 | search_done_sent_ = true; |
698 | 18 | release_script_chunks_ = false; |
699 | 18 | } |
700 | 9.79k | return true; |
701 | 9.89k | } |
702 | | |
703 | | /// Complete an outstanding search once its script cursor is exhausted. A |
704 | | /// scripted done (or an explicit truncation) suppresses this synthesized |
705 | | /// message, so the peer never invents completion after a deliberate error. |
706 | | /// @return true when a searchResDone was written. |
707 | 40.6k | bool LdapMockServer::FinishSearchIfReady() { |
708 | 40.6k | if (!search_active_ || search_done_sent_ || peer_closed_ || connection_ == nullptr) { |
709 | 28.9k | return false; |
710 | 28.9k | } |
711 | 11.7k | if (release_script_chunks_ && next_server_chunk_ < server_chunk_count_) { |
712 | 7.78k | return false; |
713 | 7.78k | } |
714 | | |
715 | 3.97k | std::string done; |
716 | 3.97k | BuildResultMessage(search_message_id_, kSearchResultDone, kLdapSuccess, &done); |
717 | 3.97k | ++synthesized_done_count_; |
718 | 3.97k | search_done_sent_ = true; |
719 | 3.97k | release_script_chunks_ = false; |
720 | 3.97k | return WriteServerBytes(done); |
721 | 11.7k | } |
722 | | |
723 | | /// Read without waiting and process every complete LDAPMessage currently |
724 | | /// buffered. A message is delivered only after its outer TLV has fully |
725 | | /// arrived, so fragmentation cannot shift the parser. DrainIncoming() also |
726 | | /// advances a layered TLS carrier, and reports its handshake progress so the |
727 | | /// outer loop does not mistake useful negotiation for an idle peer. |
728 | | /// @return true when bytes, a message, or a transport state advanced. |
729 | 40.6k | bool LdapMockServer::ServiceClientConnection() { |
730 | 40.6k | if (connection_ == nullptr || protocol_error_ || peer_closed_) { |
731 | 2.22k | return false; |
732 | 2.22k | } |
733 | | |
734 | 38.4k | const std::size_t size_before_read = pending_client_bytes_.size(); |
735 | 38.4k | const std::size_t transport_progress = connection_->DrainIncoming(); |
736 | 38.4k | bool made_progress = pending_client_bytes_.size() != size_before_read || transport_progress != 0; |
737 | | |
738 | 38.4k | std::size_t consumed = 0; |
739 | 38.4k | int messages = 0; |
740 | 52.3k | while (consumed < pending_client_bytes_.size() && messages < kMaxMessagesPerService) { |
741 | 14.0k | const std::string_view available(pending_client_bytes_.data() + consumed, pending_client_bytes_.size() - consumed); |
742 | 14.0k | MessageView message; |
743 | 14.0k | const ParseResult parsed = ParseMessage(available, &message); |
744 | 14.0k | if (parsed == ParseResult::kNeedMore) { |
745 | 0 | break; |
746 | 0 | } |
747 | 14.0k | if (parsed == ParseResult::kInvalid) { |
748 | 89 | protocol_error_ = true; |
749 | 89 | break; |
750 | 89 | } |
751 | | |
752 | 13.9k | const std::string_view complete = available.substr(0, message.total_length); |
753 | 13.9k | CapturePrefix(complete, kMaxCapturedMessageBytes, &packet_transcript_); |
754 | 13.9k | HandleClientMessage(message); |
755 | 13.9k | consumed += message.total_length; |
756 | 13.9k | ++messages; |
757 | 13.9k | made_progress = true; |
758 | 13.9k | } |
759 | | |
760 | 38.4k | if (protocol_error_) { |
761 | 89 | pending_client_bytes_.clear(); |
762 | | // Close the write half immediately so a bogus length cannot wait on the |
763 | | // idle budget for an impossible body. |
764 | 89 | connection_->ShutdownWrite(); |
765 | 89 | release_script_chunks_ = false; |
766 | 38.3k | } else if (consumed != 0) { |
767 | 13.4k | pending_client_bytes_.erase(0, consumed); |
768 | 13.4k | } |
769 | 38.4k | return made_progress; |
770 | 40.6k | } |
771 | | |
772 | | /// Half-close the peer so curl observes a deterministic end of server messages |
773 | | /// after the transfer has stopped or the idle budget expired. |
774 | 5.66k | void LdapMockServer::FinishConnections() { |
775 | 5.66k | if (connection_ != nullptr) { |
776 | 5.11k | connection_->ShutdownWrite(); |
777 | 5.11k | } |
778 | 5.66k | } |
779 | | |
780 | | /// Alternate curl transitions with immediate peer service until the transfer |
781 | | /// completes or deterministic idle/operation caps win. No select(), poll(), or |
782 | | /// sleep is needed: OpenLDAP reads through a duplicate of the same in-process |
783 | | /// socketpair endpoint that curl opened, so the peer can answer requests |
784 | | /// synchronously from this thread. |
785 | | /// @param multi Multi handle containing `easy`. |
786 | | /// @param easy Easy handle already installed on this mock. |
787 | | /// @param scenario Connection script to borrow. |
788 | 5.66k | void LdapMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) { |
789 | 5.66k | (void)easy; |
790 | 5.66k | ResetForScenario(scenario); |
791 | | |
792 | 5.66k | int still_running = 1; |
793 | 5.66k | int idle_iterations = 0; |
794 | 5.66k | int drive_iterations = 0; |
795 | 5.66k | CURLMcode result = CURLM_OK; |
796 | 40.6k | while (still_running && idle_iterations < kMaxIdleIterations && drive_iterations++ < kMaxDriveIterations) { |
797 | 35.0k | const int running_before = still_running; |
798 | 35.0k | result = curl_multi_perform(multi, &still_running); |
799 | 35.0k | if (result != CURLM_OK) { |
800 | 0 | break; |
801 | 0 | } |
802 | | |
803 | 35.0k | bool made_progress = still_running != running_before; |
804 | 35.0k | made_progress = ServiceClientConnection() || made_progress; |
805 | 35.0k | made_progress = ReleaseNextServerChunk() || made_progress; |
806 | 35.0k | made_progress = FinishSearchIfReady() || made_progress; |
807 | 35.0k | if (made_progress) { |
808 | 28.7k | idle_iterations = 0; |
809 | 28.7k | } else { |
810 | 6.23k | ++idle_iterations; |
811 | 6.23k | } |
812 | 35.0k | } |
813 | | |
814 | | // The final perform may have emitted a request immediately before marking |
815 | | // the transfer done. Drain those bytes before cleanup. |
816 | 5.66k | (void)ServiceClientConnection(); |
817 | 5.66k | (void)ReleaseNextServerChunk(); |
818 | 5.66k | (void)FinishSearchIfReady(); |
819 | 5.66k | FinishConnections(); |
820 | 5.66k | scenario_ = nullptr; |
821 | 5.66k | } |
822 | | |
823 | | } // namespace proto_fuzzer |