23namespace proto_fuzzer {
28constexpr unsigned char kSequenceTag = 0x30;
29constexpr unsigned char kBooleanTag = 0x01;
30constexpr unsigned char kIntegerTag = 0x02;
31constexpr unsigned char kEnumeratedTag = 0x0a;
32constexpr unsigned char kOctetStringTag = 0x04;
33constexpr unsigned char kUnbindRequest = 0x42;
34constexpr unsigned char kBindRequest = 0x60;
35constexpr unsigned char kBindResponse = 0x61;
36constexpr unsigned char kSearchRequest = 0x63;
37constexpr unsigned char kSearchResultEntry = 0x64;
38constexpr unsigned char kSearchResultDone = 0x65;
39constexpr unsigned char kAbandonRequest = 0x50;
40constexpr unsigned char kExtendedRequest = 0x77;
41constexpr unsigned char kExtendedResponse = 0x78;
42constexpr unsigned char kSetTag = 0x31;
43constexpr unsigned char kPresentFilter = 0x87;
44constexpr int kLdapSuccess = 0;
45constexpr std::string_view kObjectClass =
"objectclass";
46constexpr std::string_view kSupportedSaslMechanisms =
"supportedSASLMechanisms";
47constexpr std::string_view kPlainMechanism =
"PLAIN";
49unsigned char ByteAt(std::string_view bytes, std::size_t index) {
return static_cast<unsigned char>(bytes[index]); }
58 server_chunk_count_(0),
59 next_server_chunk_(0),
60 bind_request_count_(0),
61 search_request_count_(0),
62 mechanism_search_request_count_(0),
63 starttls_request_count_(0),
64 unbind_request_count_(0),
65 server_message_count_(0),
66 scripted_message_count_(0),
67 synthesized_done_count_(0),
68 scripted_bind_response_count_(0),
69 search_message_id_(0),
70 search_active_(false),
71 search_done_sent_(false),
72 scripted_bind_consumed_(false),
73 release_script_chunks_(false),
75 protocol_error_(false),
76 socket_opened_(false) {}
92 if (data ==
nullptr || size == 0) {
95 pending_client_bytes_.append(
reinterpret_cast<const char*
>(data), size);
134LdapMockServer::ParseResult LdapMockServer::ParseTlv(std::string_view bytes, std::size_t offset, TlvView* view) {
135 if (view ==
nullptr) {
136 return ParseResult::kInvalid;
138 if (offset >= bytes.size()) {
139 return ParseResult::kNeedMore;
142 const unsigned char tag = ByteAt(bytes, offset);
146 return ParseResult::kInvalid;
149 std::size_t cursor = offset + 1;
150 if (cursor >= bytes.size()) {
151 return ParseResult::kNeedMore;
154 const unsigned char length_byte = ByteAt(bytes, cursor++);
155 std::size_t length = 0;
156 if ((length_byte & 0x80U) == 0) {
157 length =
static_cast<std::size_t
>(length_byte);
159 const std::size_t length_bytes =
static_cast<std::size_t
>(length_byte & 0x7fU);
160 if (length_bytes == 0 || length_bytes > 4) {
161 return ParseResult::kInvalid;
163 if (bytes.size() - cursor < length_bytes) {
164 return ParseResult::kNeedMore;
166 for (std::size_t index = 0; index < length_bytes; ++index) {
167 length = (length << 8U) | static_cast<std::size_t>(ByteAt(bytes, cursor + index));
169 cursor += length_bytes;
172 if (length > kMaxClientMessageBytes) {
173 return ParseResult::kInvalid;
175 if (bytes.size() - cursor < length) {
176 return ParseResult::kNeedMore;
180 view->value_offset = cursor;
181 view->value_length = length;
182 view->total_length = cursor - offset + length;
183 return ParseResult::kComplete;
194bool LdapMockServer::IsSaslMechanismSearch(std::string_view value) {
195 std::size_t cursor = 0;
197 const auto parse_field = [&value, &cursor](
unsigned char tag, TlvView* parsed) {
198 if (ParseTlv(value, cursor, parsed) != ParseResult::kComplete || parsed->tag != tag) {
201 cursor = parsed->value_offset + parsed->value_length;
204 const auto is_zero = [&value](
const TlvView& parsed) {
205 return parsed.value_length == 1 && ByteAt(value, parsed.value_offset) == 0;
208 if (!parse_field(kOctetStringTag, &field) || field.value_length != 0 || !parse_field(kEnumeratedTag, &field) ||
209 !is_zero(field) || !parse_field(kEnumeratedTag, &field) || !is_zero(field) || !parse_field(kIntegerTag, &field) ||
210 !is_zero(field) || !parse_field(kIntegerTag, &field) || !is_zero(field) || !parse_field(kBooleanTag, &field) ||
211 !is_zero(field) || !parse_field(kPresentFilter, &field) ||
212 value.substr(field.value_offset, field.value_length) != kObjectClass || !parse_field(kSequenceTag, &field) ||
213 cursor != value.size()) {
217 const std::size_t attributes_end = field.value_offset + field.value_length;
219 if (ParseTlv(value, field.value_offset, &attribute) != ParseResult::kComplete || attribute.tag != kOctetStringTag ||
220 attribute.value_offset + attribute.value_length != attributes_end) {
223 return value.substr(attribute.value_offset, attribute.value_length) == kSupportedSaslMechanisms;
233LdapMockServer::ParseResult LdapMockServer::ParseMessage(std::string_view bytes, MessageView* message) {
234 if (message ==
nullptr) {
235 return ParseResult::kInvalid;
239 ParseResult parsed = ParseTlv(bytes, 0, &outer);
240 if (parsed != ParseResult::kComplete) {
243 if (outer.tag != kSequenceTag || outer.value_length < 2) {
244 return ParseResult::kInvalid;
247 const std::size_t outer_end = outer.value_offset + outer.value_length;
249 parsed = ParseTlv(bytes, outer.value_offset, &integer);
250 if (parsed != ParseResult::kComplete) {
253 if (integer.tag != kIntegerTag || integer.value_length == 0 || integer.value_length > 4 ||
254 integer.value_offset + integer.value_length > outer_end) {
255 return ParseResult::kInvalid;
258 std::uint32_t message_id = 0;
259 for (std::size_t index = 0; index < integer.value_length; ++index) {
260 message_id = (message_id << 8U) | static_cast<std::uint32_t>(ByteAt(bytes, integer.value_offset + index));
263 const std::size_t operation_offset = integer.value_offset + integer.value_length;
264 if (operation_offset >= outer_end) {
265 return ParseResult::kInvalid;
268 parsed = ParseTlv(bytes, operation_offset, &operation);
269 if (parsed != ParseResult::kComplete) {
272 if (operation.value_offset + operation.value_length > outer_end) {
273 return ParseResult::kInvalid;
276 message->op_tag = operation.tag;
277 message->message_id = message_id;
278 message->total_length = outer.total_length;
279 message->sasl_mechanism_query = operation.tag == kSearchRequest &&
280 IsSaslMechanismSearch(bytes.substr(operation.value_offset, operation.value_length));
281 return ParseResult::kComplete;
290void LdapMockServer::AppendBerLength(std::string* out, std::size_t length) {
291 if (out ==
nullptr) {
295 out->push_back(
static_cast<char>(length));
299 unsigned char encoded[
sizeof(std::size_t)];
300 std::size_t count = 0;
301 while (length != 0) {
302 encoded[count++] =
static_cast<unsigned char>(length & 0xffU);
305 out->push_back(
static_cast<char>(0x80U | count));
307 out->push_back(
static_cast<char>(encoded[--count]));
315void LdapMockServer::AppendTlv(std::string* out,
unsigned char tag, std::string_view value) {
316 if (out ==
nullptr) {
319 out->push_back(
static_cast<char>(tag));
320 AppendBerLength(out, value.size());
321 out->append(value.data(), value.size());
327void LdapMockServer::AppendBerInteger(std::string* out, std::uint32_t value) {
328 unsigned char encoded[5];
329 std::size_t count = 0;
331 encoded[count++] =
static_cast<unsigned char>(value & 0xffU);
333 }
while (value != 0);
336 if ((encoded[count - 1] & 0x80U) != 0) {
337 bytes.push_back(
'\0');
340 bytes.push_back(
static_cast<char>(encoded[--count]));
342 AppendTlv(out, kIntegerTag, bytes);
352void LdapMockServer::BuildResultMessage(std::uint32_t message_id,
unsigned char op_tag,
int result_code,
354 if (out ==
nullptr) {
359 const int bounded_code = std::max(0, std::min(result_code, 255));
360 const char code_byte =
static_cast<char>(bounded_code);
361 AppendTlv(&result, kEnumeratedTag, std::string_view(&code_byte, 1));
362 AppendTlv(&result, kOctetStringTag, std::string_view());
363 AppendTlv(&result, kOctetStringTag, std::string_view());
366 AppendBerInteger(&content, message_id);
367 AppendTlv(&content, op_tag, result);
370 AppendTlv(out, kSequenceTag, content);
377void LdapMockServer::BuildSaslMechanismsEntry(std::uint32_t message_id, std::string* out) {
378 if (out ==
nullptr) {
383 AppendTlv(&values, kOctetStringTag, kPlainMechanism);
385 std::string attribute;
386 AppendTlv(&attribute, kOctetStringTag, kSupportedSaslMechanisms);
387 AppendTlv(&attribute, kSetTag, values);
389 std::string attributes;
390 AppendTlv(&attributes, kSequenceTag, attribute);
393 AppendTlv(&entry, kOctetStringTag, std::string_view());
394 AppendTlv(&entry, kSequenceTag, attributes);
397 AppendBerInteger(&content, message_id);
398 AppendTlv(&content, kSearchResultEntry, entry);
401 AppendTlv(out, kSequenceTag, content);
412bool LdapMockServer::RewriteMessageId(std::string_view message, std::uint32_t message_id, std::string* out) {
413 if (out ==
nullptr) {
418 if (ParseTlv(message, 0, &outer) != ParseResult::kComplete || outer.tag != kSequenceTag ||
419 outer.total_length != message.size()) {
424 if (ParseTlv(message, outer.value_offset, &integer) != ParseResult::kComplete || integer.tag != kIntegerTag ||
425 integer.value_offset + integer.value_length > outer.value_offset + outer.value_length) {
430 AppendBerInteger(&content, message_id);
431 const std::size_t remainder_offset = integer.value_offset + integer.value_length;
432 const std::size_t outer_end = outer.value_offset + outer.value_length;
433 content.append(message.data() + remainder_offset, outer_end - remainder_offset);
436 AppendTlv(out, kSequenceTag, content);
444bool LdapMockServer::MessageOpTag(std::string_view message,
unsigned char* op_tag) {
445 if (op_tag ==
nullptr) {
449 if (ParseMessage(message, &decoded) != ParseResult::kComplete || decoded.total_length != message.size()) {
452 *op_tag = decoded.op_tag;
460void LdapMockServer::ResetForScenario(
const curl::fuzzer::proto::Scenario& scenario) {
462 scenario_ = &scenario;
463 server_chunk_count_ = std::min<std::size_t>(scenario_limits::kMaxResponseChunks,
464 static_cast<std::size_t
>(scenario.connection().on_readable_size()));
465 next_server_chunk_ = 0;
466 bind_request_count_ = 0;
467 search_request_count_ = 0;
468 mechanism_search_request_count_ = 0;
469 starttls_request_count_ = 0;
470 unbind_request_count_ = 0;
471 server_message_count_ = 0;
472 scripted_message_count_ = 0;
473 synthesized_done_count_ = 0;
474 scripted_bind_response_count_ = 0;
475 pending_client_bytes_.clear();
476 packet_transcript_.clear();
477 search_message_id_ = 0;
478 search_active_ =
false;
479 search_done_sent_ =
false;
480 scripted_bind_consumed_ =
false;
481 release_script_chunks_ =
false;
482 peer_closed_ =
false;
483 protocol_error_ =
false;
484 socket_opened_ =
false;
492void LdapMockServer::ApplyScriptBackpressure(
MockConnection* connection,
493 const curl::fuzzer::proto::Connection& script) {
498 const std::uint32_t int_max =
static_cast<std::uint32_t
>(std::numeric_limits<int>::max());
499 const auto& backpressure = script.backpressure();
500 const int receive_buffer =
static_cast<int>(std::min(backpressure.recv_buf_bytes(), int_max));
501 connection->ApplyBackpressure(receive_buffer,
static_cast<std::size_t
>(backpressure.drain_limit()));
518 if (scenario_ ==
nullptr || purpose != CURLSOCKTYPE_IPCXN || socket_opened_) {
519 return CURL_SOCKET_BAD;
525 return CURL_SOCKET_BAD;
529 ApplyScriptBackpressure(
connection_.get(), scenario_->connection());
530 socket_opened_ =
true;
540void LdapMockServer::CapturePrefix(std::string_view source, std::size_t limit, std::string* destination) {
541 if (destination ==
nullptr || destination->size() >= limit) {
544 const std::size_t available = limit - destination->size();
545 destination->append(source.data(), std::min(source.size(), available));
553bool LdapMockServer::WriteServerBytes(std::string_view bytes) {
558 connection_->WriteAll(
reinterpret_cast<const unsigned char*
>(bytes.data()), bytes.size());
560 ++server_message_count_;
570void LdapMockServer::HandleBindRequest(std::uint32_t message_id) {
571 ++bind_request_count_;
573 std::string response;
574 if (!scripted_bind_consumed_ && scenario_ !=
nullptr && !scenario_->connection().initial_response().empty()) {
575 const std::string& scripted = scenario_->connection().initial_response();
576 if (!RewriteMessageId(scripted, message_id, &response)) {
579 scripted_bind_consumed_ =
true;
580 ++scripted_bind_response_count_;
582 BuildResultMessage(message_id, kBindResponse, kLdapSuccess, &response);
584 (void)WriteServerBytes(response);
591void LdapMockServer::HandleSearchRequest(std::uint32_t message_id) {
592 ++search_request_count_;
593 search_message_id_ = message_id;
594 search_active_ =
true;
595 search_done_sent_ =
false;
596 release_script_chunks_ =
true;
597 next_server_chunk_ = 0;
604void LdapMockServer::HandleMechanismSearchRequest(std::uint32_t message_id) {
605 ++search_request_count_;
606 ++mechanism_search_request_count_;
609 BuildSaslMechanismsEntry(message_id, &entry);
610 (void)WriteServerBytes(entry);
613 BuildResultMessage(message_id, kSearchResultDone, kLdapSuccess, &done);
614 ++synthesized_done_count_;
615 (void)WriteServerBytes(done);
625void LdapMockServer::HandleStartTlsRequest(std::uint32_t message_id) {
626 ++starttls_request_count_;
627 std::string response;
628 BuildResultMessage(message_id, kExtendedResponse, kLdapSuccess, &response);
629 (void)WriteServerBytes(response);
636void LdapMockServer::HandleClientMessage(
const MessageView& message) {
637 switch (message.op_tag) {
639 HandleBindRequest(message.message_id);
643 if (message.sasl_mechanism_query && bind_request_count_ == 0 && mechanism_search_request_count_ == 0) {
644 HandleMechanismSearchRequest(message.message_id);
646 HandleSearchRequest(message.message_id);
650 case kExtendedRequest:
651 HandleStartTlsRequest(message.message_id);
655 ++unbind_request_count_;
658 case kAbandonRequest:
672bool LdapMockServer::ReleaseNextServerChunk() {
673 if (!release_script_chunks_ || scenario_ ==
nullptr ||
connection_ ==
nullptr ||
674 next_server_chunk_ >= server_chunk_count_) {
678 const std::string& chunk = scenario_->connection().on_readable(
static_cast<int>(next_server_chunk_));
679 ++next_server_chunk_;
684 release_script_chunks_ =
false;
688 unsigned char op_tag = 0;
689 const bool has_op_tag = MessageOpTag(chunk, &op_tag);
690 std::string rewritten;
691 const bool renumbered = RewriteMessageId(chunk, search_message_id_, &rewritten);
692 const std::string_view bytes = renumbered ? std::string_view(rewritten) : std::string_view(chunk);
694 ++scripted_message_count_;
695 (void)WriteServerBytes(bytes);
696 if (has_op_tag && op_tag == kSearchResultDone) {
697 search_done_sent_ =
true;
698 release_script_chunks_ =
false;
707bool LdapMockServer::FinishSearchIfReady() {
708 if (!search_active_ || search_done_sent_ || peer_closed_ ||
connection_ ==
nullptr) {
711 if (release_script_chunks_ && next_server_chunk_ < server_chunk_count_) {
716 BuildResultMessage(search_message_id_, kSearchResultDone, kLdapSuccess, &done);
717 ++synthesized_done_count_;
718 search_done_sent_ =
true;
719 release_script_chunks_ =
false;
720 return WriteServerBytes(done);
729bool LdapMockServer::ServiceClientConnection() {
730 if (
connection_ ==
nullptr || protocol_error_ || peer_closed_) {
734 const std::size_t size_before_read = pending_client_bytes_.size();
735 const std::size_t transport_progress =
connection_->DrainIncoming();
736 bool made_progress = pending_client_bytes_.size() != size_before_read || transport_progress != 0;
738 std::size_t consumed = 0;
740 while (consumed < pending_client_bytes_.size() && messages < kMaxMessagesPerService) {
741 const std::string_view available(pending_client_bytes_.data() + consumed, pending_client_bytes_.size() - consumed);
743 const ParseResult parsed = ParseMessage(available, &message);
744 if (parsed == ParseResult::kNeedMore) {
747 if (parsed == ParseResult::kInvalid) {
748 protocol_error_ =
true;
752 const std::string_view complete = available.substr(0, message.total_length);
753 CapturePrefix(complete, kMaxCapturedMessageBytes, &packet_transcript_);
754 HandleClientMessage(message);
755 consumed += message.total_length;
757 made_progress =
true;
760 if (protocol_error_) {
761 pending_client_bytes_.clear();
765 release_script_chunks_ =
false;
766 }
else if (consumed != 0) {
767 pending_client_bytes_.erase(0, consumed);
769 return made_progress;
774void LdapMockServer::FinishConnections() {
790 ResetForScenario(scenario);
792 int still_running = 1;
793 int idle_iterations = 0;
794 int drive_iterations = 0;
795 CURLMcode result = CURLM_OK;
797 const int running_before = still_running;
798 result = curl_multi_perform(multi, &still_running);
799 if (result != CURLM_OK) {
803 bool made_progress = still_running != running_before;
804 made_progress = ServiceClientConnection() || made_progress;
805 made_progress = ReleaseNextServerChunk() || made_progress;
806 made_progress = FinishSearchIfReady() || made_progress;
816 (void)ServiceClientConnection();
817 (void)ReleaseNextServerChunk();
818 (void)FinishSearchIfReady();
825#if defined(PROTO_FUZZER_OBSERVE_STATE)
830 (void)ldap_observation;
void ObserveState() override
Retain protocol outcomes in coverage and introspector builds.
LdapMockServer()
Begin without a borrowed protobuf or open socketpair.
std::size_t scripted_bind_response_count() const
std::size_t synthesized_done_count() const
std::size_t starttls_request_count() const
std::size_t search_request_count() const
std::size_t mechanism_search_request_count() const
std::size_t unbind_request_count() const
curl_socket_t HandleOpenSocket(curlsocktype purpose=CURLSOCKTYPE_IPCXN, struct curl_sockaddr *address=nullptr) override
Create the single LDAP socketpair.
~LdapMockServer() override
Release the server-owned socketpair half after curl has returned its client descriptor.
std::size_t scripted_message_count() const
void ObserveIncomingData(const unsigned char *data, std::size_t size) override
Append one fragment after the transport has decoded it.
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 server_message_count() const
virtual std::unique_ptr< MockConnection > CreateConnection()
Create the default plaintext transport.
std::size_t bind_request_count() const
const std::string & packet_transcript() const
Owns one half of a socketpair used to feed canned responses to libcurl.
std::unique_ptr< MockConnection > connection_
The per-scenario MockConnection, lazily created by HandleOpenSocket().
MockConnection * connection()
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.
BER/ASN.1-aware in-process peer for curl's OpenLDAP-backed LDAP client.
MockConnection and MockServer — the in-process peer that feeds canned response bytes to libcurl over ...
Shared complexity limits for structured scenarios.