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/telnet_scenario.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 shared TELNET response normalization.
9
10
#include "proto_fuzzer/telnet_scenario.h"
11
12
#include <cstddef>
13
#include <string>
14
15
#include "proto_fuzzer/scenario_limits.h"
16
17
namespace proto_fuzzer {
18
19
namespace {
20
21
/// Remove a repeated-field suffix that the TELNET runtime cannot observe.
22
template <typename RepeatedField>
23
12.6k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
24
12.6k
  const std::size_t size = static_cast<std::size_t>(field->size());
25
12.6k
  if (size > limit) {
26
12
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
27
12
  }
28
12.6k
}
29
30
/// Retain one contiguous fragment without crossing either shared budget.
31
/// Counting IAC bytes conservatively covers negotiation and subnegotiation
32
/// replies without duplicating curl's state machine in the harness.
33
16.4k
bool BoundTelnetFragment(std::string* fragment, std::size_t* bytes_left, std::size_t* controls_left) {
34
16.4k
  std::size_t retained = 0;
35
1.96M
  while (retained < fragment->size() && retained < *bytes_left) {
36
1.95M
    if (static_cast<unsigned char>((*fragment)[retained]) == 0xff) {
37
17.1k
      if (*controls_left == 0) {
38
63
        break;
39
63
      }
40
17.1k
      --*controls_left;
41
17.1k
    }
42
1.95M
    ++retained;
43
1.95M
  }
44
45
16.4k
  const bool complete = retained == fragment->size();
46
16.4k
  fragment->resize(retained);
47
16.4k
  *bytes_left -= retained;
48
16.4k
  return complete;
49
16.4k
}
50
51
}  // namespace
52
53
/// Retain the response prefix that TelnetMockServer can preload safely.
54
///
55
/// TELNET runs synchronously inside curl_multi_perform(), so the harness
56
/// cannot deliver later fragments or drain an arbitrary number of replies
57
/// between parser transitions. This function applies both the byte budget and
58
/// the stricter IAC budget shared by the LPM postprocessor and runtime. Once a
59
/// fragment is truncated, every later fragment is removed because it can no
60
/// longer affect curl.
61
/// @param connection Response script to normalize in place.
62
12.6k
void BoundTelnetResponse(curl::fuzzer::proto::Connection* connection) {
63
12.6k
  if (connection == nullptr) {
64
0
    return;
65
0
  }
66
67
12.6k
  TrimRepeated(connection->mutable_on_readable(), scenario_limits::kMaxTelnetResponseChunks);
68
69
12.6k
  std::size_t bytes_left = scenario_limits::kMaxTelnetResponseBytes;
70
12.6k
  std::size_t controls_left = scenario_limits::kMaxTelnetControlBytes;
71
12.6k
  if (!BoundTelnetFragment(connection->mutable_initial_response(), &bytes_left, &controls_left)) {
72
14
    connection->clear_on_readable();
73
14
    return;
74
14
  }
75
76
12.6k
  int retained_chunks = 0;
77
16.3k
  for (; retained_chunks < connection->on_readable_size(); ++retained_chunks) {
78
3.76k
    std::string* fragment = connection->mutable_on_readable(retained_chunks);
79
3.76k
    if (!BoundTelnetFragment(fragment, &bytes_left, &controls_left)) {
80
      // A genuinely partial prefix is observable and worth retaining. When a
81
      // previous fragment exhausted the budget, however, keeping the empty
82
      // truncation would give LPM a field curl can never distinguish.
83
53
      if (!fragment->empty()) {
84
37
        ++retained_chunks;
85
37
      }
86
53
      break;
87
53
    }
88
3.76k
  }
89
90
12.6k
  auto* chunks = connection->mutable_on_readable();
91
12.6k
  if (retained_chunks < chunks->size()) {
92
34
    chunks->DeleteSubrange(retained_chunks, chunks->size() - retained_chunks);
93
34
  }
94
12.6k
}
95
96
}  // namespace proto_fuzzer