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