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/transfer_session.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 Ordered easy-handle and retained-option resource ownership.
9
10
#include "proto_fuzzer/transfer_session.h"
11
12
#include <algorithm>
13
#include <cstddef>
14
#include <cstdint>
15
#include <utility>
16
17
#include "proto_fuzzer/api_lifecycle.h"
18
#include "proto_fuzzer/bounded_anonymous_input_file.h"
19
#include "proto_fuzzer/option_apply.h"
20
#include "proto_fuzzer/request_data.h"
21
#include "proto_fuzzer/scenario_limits.h"
22
23
namespace proto_fuzzer {
24
namespace {
25
26
constexpr char kDevNullPath[] = "/dev/null";
27
constexpr char kAltSvcOrigin[] = "altsvc-origin.test";
28
constexpr char kAltSvcLoopbackResolve[] = "*:80:127.0.1.127";
29
30
/// Fail closed before any resolver backend starts ambient DNS work.
31
23
int AbortUnexpectedAltSvcResolve(void*, void*, void*) { return 1; }
32
33
1.53k
bool HasCanonicalAltSvcAuthority(const std::string& host_path) {
34
1.53k
  const std::size_t host_size = sizeof(kAltSvcOrigin) - 1;
35
1.53k
  return host_path.compare(0, host_size, kAltSvcOrigin) == 0 &&
36
1.53k
         (host_path.size() == host_size || host_path[host_size] == '/' || host_path[host_size] == '?' ||
37
17
          host_path[host_size] == '#');
38
1.53k
}
39
40
/// Repeat fixed-target bounds for direct runtime and compatibility callers.
41
void PrepareInputFile(const std::string& contents, std::size_t* remaining_bytes, BoundedAnonymousInputFile* input_file,
42
183k
                      bool guard_netrc = false) {
43
183k
  const std::size_t size = std::min(contents.size(), std::min(scenario_limits::kMaxFileInputBytes, *remaining_bytes));
44
183k
  if (size == 0) {
45
174k
    return;
46
174k
  }
47
8.72k
  if (guard_netrc) {
48
    // Retain a parser-neutral newline when curl's loader drops comment lines.
49
    // Charge only protobuf bytes to the shared budget, as before.
50
2.14k
    std::string guarded_contents(1, '\n');
51
2.14k
    guarded_contents.append(contents.data(), size);
52
2.14k
    if (input_file->Write(reinterpret_cast<const std::uint8_t*>(guarded_contents.data()), guarded_contents.size())) {
53
2.14k
      *remaining_bytes -= size;
54
2.14k
    }
55
6.58k
  } else if (input_file->Write(reinterpret_cast<const std::uint8_t*>(contents.data()), size)) {
56
6.58k
    *remaining_bytes -= size;
57
6.58k
  }
58
8.72k
}
59
60
}  // namespace
61
62
/// Files are allocated only for parser-input lanes and survive easy cleanup.
63
struct TransferSession::ParserInputs {
64
  BoundedAnonymousInputFile cookie{scenario_limits::kMaxFileInputBytes};
65
  BoundedAnonymousInputFile altsvc{scenario_limits::kMaxFileInputBytes};
66
  BoundedAnonymousInputFile hsts{scenario_limits::kMaxFileInputBytes};
67
  BoundedAnonymousInputFile netrc{scenario_limits::kMaxFileInputBytes + 1};
68
  BoundedAnonymousInputFile crl{scenario_limits::kMaxFileInputBytes};
69
};
70
71
416k
TransferSession::TransferSession() = default;
72
416k
TransferSession::~TransferSession() { Close(); }
73
74
/// Allocate the easy lazily so unused concurrent-transfer slots stay empty.
75
/// @return Whether the session has a usable easy handle.
76
394k
bool TransferSession::Initialize() {
77
394k
  if (!easy_) {
78
394k
    easy_.reset(curl_easy_init());
79
394k
  }
80
394k
  return easy_ != nullptr;
81
394k
}
82
83
/// Borrow the owned easy; callers must not clean it themselves.
84
/// @return Easy handle, or nullptr before successful initialization.
85
965k
CURL* TransferSession::easy() const { return easy_.get(); }
86
87
/// Apply baseline options and retain their CONNECT_TO storage.
88
/// @param scheme Protocol serviced by the installed peer.
89
/// @param trace_ids Whether to enable bounded trace-ID diagnostics.
90
395k
void TransferSession::ApplyBaseline(curl::fuzzer::proto::Scheme scheme, bool trace_ids) {
91
395k
  if (easy_) {
92
    // Apply the new pointer before releasing the previous list.
93
395k
    connect_to_.reset(ApplyBaselineOptions(easy_.get(), scheme, trace_ids));
94
395k
  }
95
395k
}
96
97
/// Reset pre-transfer configuration while preserving prepared parser files.
98
/// @return False if request or API callback state has already been installed.
99
1.89k
bool TransferSession::ResetConfiguration() {
100
1.89k
  if (!easy_ || request_data_ || api_lifecycle_) {
101
0
    return false;
102
0
  }
103
1.89k
  curl_easy_reset(easy_.get());
104
1.89k
  connect_to_.reset();
105
1.89k
  altsvc_resolve_.reset();
106
1.89k
  return true;
107
1.89k
}
108
109
/// Copy bounded parser inputs into lazily allocated anonymous files.
110
/// @param scenario Source of parser-file bytes; not retained.
111
/// @param mode Select deep HTTP or TLS parser inputs.
112
/// @return False after easy initialization, when paths may be retained.
113
325k
bool TransferSession::PrepareInputFiles(const curl::fuzzer::proto::Scenario& scenario, ScenarioRunMode mode) {
114
325k
  if (easy_) {
115
0
    return false;
116
0
  }
117
325k
  if (mode != ScenarioRunMode::kDeepHttpCoverage && mode != ScenarioRunMode::kTlsCoverage) {
118
232k
    return true;
119
232k
  }
120
93.3k
  parser_inputs_ = std::make_unique<ParserInputs>();
121
93.3k
  std::size_t remaining_bytes = scenario_limits::kMaxFileInputTotalBytes;
122
93.3k
  if (mode == ScenarioRunMode::kDeepHttpCoverage) {
123
30.1k
    PrepareInputFile(scenario.cookie_file(), &remaining_bytes, &parser_inputs_->cookie);
124
30.1k
    PrepareInputFile(scenario.altsvc_file(), &remaining_bytes, &parser_inputs_->altsvc);
125
30.1k
    PrepareInputFile(scenario.hsts_file(), &remaining_bytes, &parser_inputs_->hsts);
126
30.1k
    PrepareInputFile(scenario.netrc_file(), &remaining_bytes, &parser_inputs_->netrc, true);
127
63.2k
  } else {
128
63.2k
    PrepareInputFile(scenario.crl_file(), &remaining_bytes, &parser_inputs_->crl);
129
63.2k
  }
130
93.3k
  return true;
131
325k
}
132
133
/// Apply prepared parser paths after baseline and peer installation.
134
326k
void TransferSession::ApplyInputFiles() {
135
326k
  if (!easy_ || !parser_inputs_) {
136
233k
    return;
137
233k
  }
138
93.1k
  if (const char* path = parser_inputs_->cookie.path()) {
139
2.35k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_COOKIEFILE, path);
140
2.35k
  }
141
  // Alt-Svc/HSTS use one option for input and cleanup output. Load first,
142
  // then restore the fixed sink; both loaders retain earlier file entries.
143
93.1k
  if (const char* path = parser_inputs_->altsvc.path()) {
144
1.53k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_ALTSVC, path);
145
1.53k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_ALTSVC, kDevNullPath);
146
1.53k
  }
147
93.1k
  if (const char* path = parser_inputs_->hsts.path()) {
148
1.65k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_HSTS, path);
149
1.65k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_HSTS, kDevNullPath);
150
1.65k
  }
151
93.1k
  if (const char* path = parser_inputs_->netrc.path()) {
152
2.14k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_NETRC_FILE, path);
153
2.14k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_NETRC, CURL_NETRC_REQUIRED);
154
2.14k
  }
155
93.1k
  if (const char* path = parser_inputs_->crl.path()) {
156
1.01k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_CRLFILE, path);
157
1.01k
  }
158
93.1k
}
159
160
/// Enable isolated Alt-Svc routing for the canonical deep-HTTP authority.
161
/// @param host_path Scenario URL authority/path used by this transfer.
162
324k
void TransferSession::ConfigureAltSvcRouting(const std::string& host_path) {
163
324k
  if (!easy_ || !parser_inputs_ || parser_inputs_->altsvc.path() == nullptr ||
164
322k
      !HasCanonicalAltSvcAuthority(host_path)) {
165
322k
    return;
166
322k
  }
167
1.53k
  CurlSlistPtr resolve(curl_slist_append(nullptr, kAltSvcLoopbackResolve));
168
1.53k
  if (!resolve || curl_easy_setopt(easy_.get(), CURLOPT_RESOLVE, resolve.get()) != CURLE_OK) {
169
4
    return;
170
4
  }
171
  // Adopt only after installation; failures leave the previous list alive.
172
1.53k
  altsvc_resolve_ = std::move(resolve);
173
1.53k
  if (curl_easy_setopt(easy_.get(), CURLOPT_RESOLVER_START_FUNCTION, &AbortUnexpectedAltSvcResolve) == CURLE_OK) {
174
    // CONNECT_TO takes precedence over Alt-Svc. Only detach it when the
175
    // wildcard host cache and fail-closed resolver callback are installed.
176
1.53k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_CONNECT_TO, nullptr);
177
1.53k
  }
178
1.53k
}
179
180
/// Install owned request attachments. The Scenario remains borrowed.
181
/// @param scenario Live, unmodified source until session teardown.
182
/// @param apply_resolve_entries Install resolver-lane mappings.
183
/// @return Request owner, or nullptr without an easy or if already installed.
184
ScenarioRequestData* TransferSession::InstallRequestData(const curl::fuzzer::proto::Scenario& scenario,
185
393k
                                                         bool apply_resolve_entries) {
186
393k
  if (!easy_ || request_data_) {
187
0
    return nullptr;
188
0
  }
189
393k
  request_data_.emplace(easy_.get(), scenario, apply_resolve_entries);
190
393k
  return &*request_data_;
191
393k
}
192
193
/// Install API/share state that survives easy cleanup.
194
/// @param plan Live plan until session teardown.
195
/// @param url URL used only during lifecycle construction.
196
/// @return Lifecycle owner, or nullptr if absent/already installed.
197
5.79k
ApiLifecycle* TransferSession::InstallApiLifecycle(const curl::fuzzer::proto::ApiPlan& plan, std::string_view url) {
198
5.79k
  if (!easy_ || api_lifecycle_) {
199
0
    return nullptr;
200
0
  }
201
5.79k
  api_lifecycle_ = std::make_unique<ApiLifecycle>(easy_.get(), plan, url);
202
5.79k
  return api_lifecycle_.get();
203
5.79k
}
204
205
/// Release all resources in their required order. Safe to call repeatedly.
206
485k
void TransferSession::Close() {
207
485k
  request_data_.reset();
208
485k
  easy_.reset();
209
485k
  api_lifecycle_.reset();
210
485k
  altsvc_resolve_.reset();
211
485k
  connect_to_.reset();
212
485k
  parser_inputs_.reset();
213
485k
}
214
215
}  // namespace proto_fuzzer