Coverage Report

Created: 2026-10-15 06:25

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
8.79k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
24
8.79k
  const std::size_t size = static_cast<std::size_t>(field->size());
25
8.79k
  if (size > limit) {
26
13
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
27
13
  }
28
8.79k
}
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
11.1k
bool BoundTelnetFragment(std::string* fragment, std::size_t* bytes_left, std::size_t* controls_left) {
34
11.1k
  std::size_t retained = 0;
35
1.19M
  while (retained < fragment->size() && retained < *bytes_left) {
36
1.18M
    if (static_cast<unsigned char>((*fragment)[retained]) == 0xff) {
37
11.6k
      if (*controls_left == 0) {
38
42
        break;
39
42
      }
40
11.5k
      --*controls_left;
41
11.5k
    }
42
1.18M
    ++retained;
43
1.18M
  }
44
45
11.1k
  const bool complete = retained == fragment->size();
46
11.1k
  fragment->resize(retained);
47
11.1k
  *bytes_left -= retained;
48
11.1k
  return complete;
49
11.1k
}
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
8.79k
void BoundTelnetResponse(curl::fuzzer::proto::Connection* connection) {
63
8.79k
  if (connection == nullptr) {
64
0
    return;
65
0
  }
66
67
8.79k
  TrimRepeated(connection->mutable_on_readable(), scenario_limits::kMaxTelnetResponseChunks);
68
69
8.79k
  std::size_t bytes_left = scenario_limits::kMaxTelnetResponseBytes;
70
8.79k
  std::size_t controls_left = scenario_limits::kMaxTelnetControlBytes;
71
8.79k
  if (!BoundTelnetFragment(connection->mutable_initial_response(), &bytes_left, &controls_left)) {
72
6
    connection->clear_on_readable();
73
6
    return;
74
6
  }
75
76
8.78k
  int retained_chunks = 0;
77
11.1k
  for (; retained_chunks < connection->on_readable_size(); ++retained_chunks) {
78
2.38k
    std::string* fragment = connection->mutable_on_readable(retained_chunks);
79
2.38k
    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
37
      if (!fragment->empty()) {
84
25
        ++retained_chunks;
85
25
      }
86
37
      break;
87
37
    }
88
2.38k
  }
89
90
8.78k
  auto* chunks = connection->mutable_on_readable();
91
8.78k
  if (retained_chunks < chunks->size()) {
92
23
    chunks->DeleteSubrange(retained_chunks, chunks->size() - retained_chunks);
93
23
  }
94
8.78k
}
95
96
}  // namespace proto_fuzzer