Coverage Report

Created: 2026-10-22 06:56

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
22
int AbortUnexpectedAltSvcResolve(void*, void*, void*) { return 1; }
32
33
1.62k
bool HasCanonicalAltSvcAuthority(const std::string& host_path) {
34
1.62k
  const std::size_t host_size = sizeof(kAltSvcOrigin) - 1;
35
1.62k
  return host_path.compare(0, host_size, kAltSvcOrigin) == 0 &&
36
1.62k
         (host_path.size() == host_size || host_path[host_size] == '/' || host_path[host_size] == '?' ||
37
17
          host_path[host_size] == '#');
38
1.62k
}
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
186k
                      bool guard_netrc = false) {
43
186k
  const std::size_t size = std::min(contents.size(), std::min(scenario_limits::kMaxFileInputBytes, *remaining_bytes));
44
186k
  if (size == 0) {
45
176k
    return;
46
176k
  }
47
9.65k
  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.36k
    std::string guarded_contents(1, '\n');
51
2.36k
    guarded_contents.append(contents.data(), size);
52
2.36k
    if (input_file->Write(reinterpret_cast<const std::uint8_t*>(guarded_contents.data()), guarded_contents.size())) {
53
2.36k
      *remaining_bytes -= size;
54
2.36k
    }
55
7.28k
  } else if (input_file->Write(reinterpret_cast<const std::uint8_t*>(contents.data()), size)) {
56
7.28k
    *remaining_bytes -= size;
57
7.28k
  }
58
9.65k
}
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
422k
TransferSession::TransferSession() = default;
72
422k
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
400k
bool TransferSession::Initialize() {
77
400k
  if (!easy_) {
78
400k
    easy_.reset(curl_easy_init());
79
400k
  }
80
400k
  return easy_ != nullptr;
81
400k
}
82
83
/// Borrow the owned easy; callers must not clean it themselves.
84
/// @return Easy handle, or nullptr before successful initialization.
85
971k
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
400k
void TransferSession::ApplyBaseline(curl::fuzzer::proto::Scheme scheme, bool trace_ids) {
91
400k
  if (easy_) {
92
    // Apply the new pointer before releasing the previous list.
93
400k
    connect_to_.reset(ApplyBaselineOptions(easy_.get(), scheme, trace_ids));
94
400k
  }
95
400k
}
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.92k
bool TransferSession::ResetConfiguration() {
100
1.92k
  if (!easy_ || request_data_ || api_lifecycle_) {
101
0
    return false;
102
0
  }
103
1.92k
  curl_easy_reset(easy_.get());
104
1.92k
  connect_to_.reset();
105
1.92k
  altsvc_resolve_.reset();
106
1.92k
  return true;
107
1.92k
}
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
330k
bool TransferSession::PrepareInputFiles(const curl::fuzzer::proto::Scenario& scenario, ScenarioRunMode mode) {
114
330k
  if (easy_) {
115
0
    return false;
116
0
  }
117
330k
  if (mode != ScenarioRunMode::kDeepHttpCoverage && mode != ScenarioRunMode::kTlsCoverage) {
118
236k
    return true;
119
236k
  }
120
94.4k
  parser_inputs_ = std::make_unique<ParserInputs>();
121
94.4k
  std::size_t remaining_bytes = scenario_limits::kMaxFileInputTotalBytes;
122
94.4k
  if (mode == ScenarioRunMode::kDeepHttpCoverage) {
123
30.6k
    PrepareInputFile(scenario.cookie_file(), &remaining_bytes, &parser_inputs_->cookie);
124
30.6k
    PrepareInputFile(scenario.altsvc_file(), &remaining_bytes, &parser_inputs_->altsvc);
125
30.6k
    PrepareInputFile(scenario.hsts_file(), &remaining_bytes, &parser_inputs_->hsts);
126
30.6k
    PrepareInputFile(scenario.netrc_file(), &remaining_bytes, &parser_inputs_->netrc, true);
127
63.7k
  } else {
128
63.7k
    PrepareInputFile(scenario.crl_file(), &remaining_bytes, &parser_inputs_->crl);
129
63.7k
  }
130
94.4k
  return true;
131
330k
}
132
133
/// Apply prepared parser paths after baseline and peer installation.
134
331k
void TransferSession::ApplyInputFiles() {
135
331k
  if (!easy_ || !parser_inputs_) {
136
237k
    return;
137
237k
  }
138
94.0k
  if (const char* path = parser_inputs_->cookie.path()) {
139
2.70k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_COOKIEFILE, path);
140
2.70k
  }
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
94.0k
  if (const char* path = parser_inputs_->altsvc.path()) {
144
1.62k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_ALTSVC, path);
145
1.62k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_ALTSVC, kDevNullPath);
146
1.62k
  }
147
94.0k
  if (const char* path = parser_inputs_->hsts.path()) {
148
1.80k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_HSTS, path);
149
1.80k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_HSTS, kDevNullPath);
150
1.80k
  }
151
94.0k
  if (const char* path = parser_inputs_->netrc.path()) {
152
2.36k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_NETRC_FILE, path);
153
2.36k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_NETRC, CURL_NETRC_REQUIRED);
154
2.36k
  }
155
94.0k
  if (const char* path = parser_inputs_->crl.path()) {
156
1.10k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_CRLFILE, path);
157
1.10k
  }
158
94.0k
}
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
329k
void TransferSession::ConfigureAltSvcRouting(const std::string& host_path) {
163
329k
  if (!easy_ || !parser_inputs_ || parser_inputs_->altsvc.path() == nullptr ||
164
327k
      !HasCanonicalAltSvcAuthority(host_path)) {
165
327k
    return;
166
327k
  }
167
1.62k
  CurlSlistPtr resolve(curl_slist_append(nullptr, kAltSvcLoopbackResolve));
168
1.62k
  if (!resolve || curl_easy_setopt(easy_.get(), CURLOPT_RESOLVE, resolve.get()) != CURLE_OK) {
169
3
    return;
170
3
  }
171
  // Adopt only after installation; failures leave the previous list alive.
172
1.62k
  altsvc_resolve_ = std::move(resolve);
173
1.62k
  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.62k
    (void)curl_easy_setopt(easy_.get(), CURLOPT_CONNECT_TO, nullptr);
177
1.62k
  }
178
1.62k
}
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
398k
                                                         bool apply_resolve_entries) {
186
398k
  if (!easy_ || request_data_) {
187
0
    return nullptr;
188
0
  }
189
398k
  request_data_.emplace(easy_.get(), scenario, apply_resolve_entries);
190
398k
  return &*request_data_;
191
398k
}
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.97k
ApiLifecycle* TransferSession::InstallApiLifecycle(const curl::fuzzer::proto::ApiPlan& plan, std::string_view url) {
198
5.97k
  if (!easy_ || api_lifecycle_) {
199
0
    return nullptr;
200
0
  }
201
5.97k
  api_lifecycle_ = std::make_unique<ApiLifecycle>(easy_.get(), plan, url);
202
5.97k
  return api_lifecycle_.get();
203
5.97k
}
204
205
/// Release all resources in their required order. Safe to call repeatedly.
206
491k
void TransferSession::Close() {
207
491k
  request_data_.reset();
208
491k
  easy_.reset();
209
491k
  api_lifecycle_.reset();
210
491k
  altsvc_resolve_.reset();
211
491k
  connect_to_.reset();
212
491k
  parser_inputs_.reset();
213
491k
}
214
215
}  // namespace proto_fuzzer