/src/curl_fuzzer/proto_fuzzer/websocket_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 WebSocketMockServer. |
9 | | |
10 | | #include "proto_fuzzer/websocket_mock_server.h" |
11 | | |
12 | | #include <curl/websockets.h> |
13 | | |
14 | | #include <algorithm> |
15 | | #include <cstddef> |
16 | | #include <string> |
17 | | #include <utility> |
18 | | |
19 | | #include "proto_fuzzer/ws_accept_key.h" |
20 | | #include "proto_fuzzer/ws_frame.h" |
21 | | |
22 | | namespace proto_fuzzer { |
23 | | |
24 | | namespace { |
25 | | |
26 | | // Cap per-scenario frame chunks so a mutator that creates thousands of tiny |
27 | | // entries can't dominate runtime. |
28 | | constexpr std::size_t kMaxResponseChunks = 16; |
29 | | |
30 | | // Iteration caps for the post-handshake manual WS drive. Kept small because |
31 | | // every iteration makes a curl_ws_recv / curl_ws_send call. |
32 | | constexpr std::size_t kMaxWsRecvIterations = 128; |
33 | | constexpr std::size_t kMaxWsSendIterations = 32; |
34 | | |
35 | | /// Ordered list of flag combinations fed to curl_ws_send / curl_ws_start_frame |
36 | | /// during the manual-drive tail. Sequencing matters: the TEXT|CONT entry puts |
37 | | /// the encoder into `contfragment=true`, so subsequent CONT / TEXT|CONT / |
38 | | /// BINARY|CONT entries then take the contfragment-aware branches in |
39 | | /// ws_frame_flags2firstbyte that would otherwise be unreachable from the |
40 | | /// fuzzer. A lone CURLWS_CONT runs first (contfragment=false → "No ongoing |
41 | | /// fragmented message" failf), and a bare `0` directly after CURLWS_CONT |
42 | | /// drives the "no flags given; interpreting as continuation" compatibility |
43 | | /// path. (TEXT|BINARY), (CLOSE|CONT), (PING|CONT), (PONG|CONT) cover the |
44 | | /// invalid-combination failf() branches; the trailing lone `0` reaches the |
45 | | /// ordinary "no flags given" rejection with contfragment=false. |
46 | | constexpr unsigned int kWsSendFlagMatrix[] = { |
47 | | CURLWS_CONT, |
48 | | CURLWS_TEXT, |
49 | | CURLWS_BINARY, |
50 | | CURLWS_TEXT | CURLWS_OFFSET, |
51 | | CURLWS_BINARY | CURLWS_OFFSET, |
52 | | CURLWS_TEXT | CURLWS_CONT, |
53 | | CURLWS_CONT, |
54 | | 0, |
55 | | CURLWS_TEXT, |
56 | | CURLWS_BINARY | CURLWS_CONT, |
57 | | CURLWS_BINARY, |
58 | | CURLWS_PING, |
59 | | CURLWS_PONG, |
60 | | CURLWS_CLOSE, |
61 | | 0, |
62 | | CURLWS_CLOSE | CURLWS_CONT, |
63 | | CURLWS_PING | CURLWS_CONT, |
64 | | CURLWS_PONG | CURLWS_CONT, |
65 | | CURLWS_TEXT | CURLWS_BINARY, |
66 | | }; |
67 | | |
68 | | /// Find "Sec-WebSocket-Key:" in the request and return the trimmed value. |
69 | | /// @param request The raw HTTP request bytes buffered from the client. |
70 | | /// @return The header value, or an empty string if not found. |
71 | 718 | std::string ExtractWebSocketKey(const std::string& request) { |
72 | 718 | static const char kHeader[] = "Sec-WebSocket-Key:"; |
73 | 718 | std::size_t pos = request.find(kHeader); |
74 | 718 | if (pos == std::string::npos) { |
75 | 0 | return {}; |
76 | 0 | } |
77 | 718 | pos += sizeof(kHeader) - 1; |
78 | 1.43k | while (pos < request.size() && (request[pos] == ' ' || request[pos] == '\t')) { |
79 | 718 | ++pos; |
80 | 718 | } |
81 | 718 | std::size_t end = request.find("\r\n", pos); |
82 | 718 | if (end == std::string::npos) { |
83 | 0 | return {}; |
84 | 0 | } |
85 | 718 | while (end > pos && (request[end - 1] == ' ' || request[end - 1] == '\t')) { |
86 | 0 | --end; |
87 | 0 | } |
88 | 718 | return request.substr(pos, end - pos); |
89 | 718 | } |
90 | | |
91 | | /// SHA1(key + WS magic guid), base64-encoded — RFC 6455 §4.2.2. |
92 | | /// Delegates to the standalone implementation in ws_accept_key.h so we don't |
93 | | /// need OpenSSL. |
94 | 718 | std::string ComputeWebSocketAccept(const std::string& key) { return proto_fuzzer::ComputeWebSocketAcceptKey(key); } |
95 | | |
96 | | /// Build the ordered list of chunks to deliver once the 101 handshake has |
97 | | /// completed. Mixes raw `on_readable` bytes (fuzzer-controlled) with serialised |
98 | | /// `server_frames` (structured RFC 6455 frames from the proto) under a shared |
99 | | /// kMaxResponseChunks budget. |
100 | 1.05k | std::vector<std::string> BuildFrameChunks(const curl::fuzzer::proto::Connection& conn) { |
101 | 1.05k | std::vector<std::string> chunks; |
102 | 1.05k | chunks.reserve(kMaxResponseChunks); |
103 | 1.05k | const std::size_t raw_budget = std::min<std::size_t>(kMaxResponseChunks, conn.on_readable_size()); |
104 | 1.89k | for (std::size_t i = 0; i < raw_budget; ++i) { |
105 | 842 | chunks.emplace_back(conn.on_readable(i)); |
106 | 842 | } |
107 | 1.05k | const std::size_t frame_budget = kMaxResponseChunks - chunks.size(); |
108 | 1.05k | const std::size_t frame_count = std::min<std::size_t>(frame_budget, conn.server_frames_size()); |
109 | 2.17k | for (std::size_t i = 0; i < frame_count; ++i) { |
110 | 1.11k | chunks.emplace_back(SerializeWebSocketFrame(conn.server_frames(static_cast<int>(i)))); |
111 | 1.11k | } |
112 | 1.05k | return chunks; |
113 | 1.05k | } |
114 | | |
115 | | /// @return true if any scenario option sets CURLOPT_CONNECT_ONLY to 2, which |
116 | | /// is curl's "WebSocket connect-only, drive recv/send manually" mode. |
117 | 1.05k | bool ScenarioRequestsManualWsDrive(const curl::fuzzer::proto::Scenario& scenario) { |
118 | 35.6k | for (const auto& opt : scenario.options()) { |
119 | 35.6k | if (opt.option_id() != curl::fuzzer::proto::CURLOPT_CONNECT_ONLY) { |
120 | 35.1k | continue; |
121 | 35.1k | } |
122 | 455 | if (opt.value_case() == curl::fuzzer::proto::SetOption::ValueCase::kUintValue && opt.uint_value() == 2) { |
123 | 179 | return true; |
124 | 179 | } |
125 | 455 | } |
126 | 876 | return false; |
127 | 1.05k | } |
128 | | |
129 | | /// WRITEFUNCTION / HEADERFUNCTION installed on the easy handle by |
130 | | /// WebSocketMockServer::Install. Pokes curl_ws_meta on every invocation (so |
131 | | /// the Curl_is_in_callback-guarded branch stays covered), and fires a |
132 | | /// one-shot curl_ws_send probe to reach ws_send_raw_blocking — that target |
133 | | /// is unreachable unless CURLWS_RAW_MODE is set AND the caller is inside a |
134 | | /// callback. The probe is sized larger than typical backpressure recv |
135 | | /// buffers so the partial-write / SOCKET_WRITABLE loop engages under a |
136 | | /// tightened SO_RCVBUF. The one-shot gate keeps the per-scenario cost |
137 | | /// bounded when SOCKET_WRITABLE times out. WRITEDATA is the owning |
138 | | /// WebSocketMockServer so callback state lives on the server, not globals. |
139 | 4.30k | size_t WebSocketWriteCallback(void* /*contents*/, size_t size, size_t nmemb, void* userdata) { |
140 | 4.30k | auto* server = static_cast<WebSocketMockServer*>(userdata); |
141 | 4.30k | if (server != nullptr) { |
142 | 718 | CURL* easy = server->easy_handle(); |
143 | 718 | if (easy != nullptr) { |
144 | 718 | (void)curl_ws_meta(easy); |
145 | 718 | if (!server->ws_probe_fired()) { |
146 | 140 | server->MarkWsProbeFired(); |
147 | 140 | static unsigned char kProbe[16384]; |
148 | 140 | std::fill(kProbe, kProbe + sizeof(kProbe), 'P'); |
149 | 140 | std::size_t sent = 0; |
150 | 140 | (void)curl_ws_send(easy, kProbe, sizeof(kProbe), &sent, 0, 0); |
151 | 140 | } |
152 | 718 | } |
153 | 718 | } |
154 | 4.30k | return size * nmemb; |
155 | 4.30k | } |
156 | | |
157 | | /// Drain curl_ws_recv in a tight loop until it returns CURLE_AGAIN / nothing |
158 | | /// pending. Bounded; not expected to do anything on well-formed scenarios. |
159 | 1.06k | void DrainWsRecv(CURL* easy) { |
160 | 2.11k | for (std::size_t i = 0; i < kMaxWsRecvIterations; ++i) { |
161 | 2.11k | unsigned char buffer[4096]; |
162 | 2.11k | std::size_t nread = 0; |
163 | 2.11k | const struct curl_ws_frame* meta = nullptr; |
164 | 2.11k | CURLcode rr = curl_ws_recv(easy, buffer, sizeof(buffer), &nread, &meta); |
165 | 2.11k | if (rr == CURLE_AGAIN) { |
166 | 423 | break; |
167 | 423 | } |
168 | 1.69k | if (rr != CURLE_OK && rr != CURLE_GOT_NOTHING) { |
169 | 642 | break; |
170 | 642 | } |
171 | 1.05k | if (nread == 0 && meta == nullptr) { |
172 | 0 | break; |
173 | 0 | } |
174 | 1.05k | } |
175 | 1.06k | } |
176 | | |
177 | | } // namespace |
178 | | |
179 | | /// Construct an idle WebSocketMockServer with no queued frames. Install() |
180 | | /// on the base class and DriveScenario() configure it from a Scenario proto. |
181 | | WebSocketMockServer::WebSocketMockServer() |
182 | 1.05k | : next_chunk_(0), manual_delivery_(false), handshake_sent_(false), ws_probe_fired_(false), easy_handle_(nullptr) {} |
183 | | |
184 | | /// Default destructor; the owned MockConnection (if any) cleans up its socketpair. |
185 | 1.05k | WebSocketMockServer::~WebSocketMockServer() = default; |
186 | | |
187 | | /// Install the common socket callbacks via the base, then overwrite |
188 | | /// WRITEFUNCTION / HEADERFUNCTION with a ws-aware variant and wire |
189 | | /// WRITEDATA to this server instance so the callback can consult per- |
190 | | /// scenario state (easy handle, one-shot probe flag). |
191 | 1.05k | void WebSocketMockServer::Install(CURL* easy) { |
192 | 1.05k | MockServerBase::Install(easy); |
193 | 1.05k | easy_handle_ = easy; |
194 | 1.05k | ws_probe_fired_ = false; |
195 | 1.05k | curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &WebSocketWriteCallback); |
196 | 1.05k | curl_easy_setopt(easy, CURLOPT_WRITEDATA, this); |
197 | 1.05k | curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &WebSocketWriteCallback); |
198 | 1.05k | } |
199 | | |
200 | | /// @return true once the one-shot WS probe has fired for this scenario. |
201 | 718 | bool WebSocketMockServer::ws_probe_fired() const { return ws_probe_fired_; } |
202 | | |
203 | | /// Flip the one-shot gate closed. Called from the write callback before it |
204 | | /// invokes curl_ws_send, so subsequent callback entries skip the probe. |
205 | 140 | void WebSocketMockServer::MarkWsProbeFired() { ws_probe_fired_ = true; } |
206 | | |
207 | | /// @return the curl easy handle cached by Install() for the write callback. |
208 | 718 | CURL* WebSocketMockServer::easy_handle() const { return easy_handle_; } |
209 | | |
210 | | /// Queue RFC 6455 wire-byte chunks to emit once the handshake has completed. |
211 | | /// Resets the next-chunk cursor. |
212 | | /// @param frames Ordered list of chunk byte strings. |
213 | 1.05k | void WebSocketMockServer::SetFrames(std::vector<std::string> frames) { |
214 | 1.05k | frames_ = std::move(frames); |
215 | 1.05k | next_chunk_ = 0; |
216 | 1.05k | } |
217 | | |
218 | | /// Toggle streaming (false, default) vs manual (true) chunk delivery. |
219 | | /// @param manual Whether to suppress automatic chunk pushing by the |
220 | | /// drive loop. |
221 | 1.05k | void WebSocketMockServer::SetManualDelivery(bool manual) { manual_delivery_ = manual; } |
222 | | |
223 | | /// @return true if chunks are caller-driven rather than drive-loop-driven. |
224 | 62.5k | bool WebSocketMockServer::manual_delivery() const { return manual_delivery_; } |
225 | | |
226 | | /// @return true once a 101 Switching Protocols response has been written. |
227 | 123k | bool WebSocketMockServer::handshake_sent() const { return handshake_sent_; } |
228 | | |
229 | | /// @return true if at least one frame chunk has not yet been sent. |
230 | 57.9k | bool WebSocketMockServer::has_more_chunks() const { return next_chunk_ < frames_.size(); } |
231 | | |
232 | | /// @return the number of queued chunks not yet consumed. |
233 | 1.06k | std::size_t WebSocketMockServer::remaining_chunks() const { |
234 | 1.06k | return next_chunk_ >= frames_.size() ? 0 : frames_.size() - next_chunk_; |
235 | 1.06k | } |
236 | | |
237 | | /// Access a pending chunk without consuming it. |
238 | | /// @param index Offset from the next-pending cursor. |
239 | | /// @return reference to the chunk byte string. |
240 | 899 | const std::string& WebSocketMockServer::PeekChunk(std::size_t index) const { return frames_[next_chunk_ + index]; } |
241 | | |
242 | | /// Advance the pending-chunk cursor by one. No-op when no chunks remain. |
243 | 899 | void WebSocketMockServer::ConsumeChunk() { |
244 | 899 | if (next_chunk_ < frames_.size()) { |
245 | 899 | ++next_chunk_; |
246 | 899 | } |
247 | 899 | } |
248 | | |
249 | | /// Called by the OPENSOCKETFUNCTION trampoline in the base class. Creates the |
250 | | /// MockConnection but does NOT write anything — the handshake is driven later |
251 | | /// by TryAdvanceHandshake(). |
252 | | /// @return the client-side fd to hand to libcurl, or CURL_SOCKET_BAD on |
253 | | /// failure. |
254 | 909 | curl_socket_t WebSocketMockServer::HandleOpenSocket() { |
255 | 909 | if (connection_) { |
256 | 0 | return CURL_SOCKET_BAD; |
257 | 0 | } |
258 | 909 | connection_ = std::make_unique<MockConnection>(); |
259 | 909 | if (!connection_->ok()) { |
260 | 0 | connection_.reset(); |
261 | 0 | return CURL_SOCKET_BAD; |
262 | 0 | } |
263 | 909 | ApplyPendingBackpressure(); |
264 | | // Wait for curl's Upgrade request before we write anything — the drive |
265 | | // loop calls TryAdvanceHandshake() to drive that exchange. |
266 | 909 | return connection_->take_client_fd(); |
267 | 909 | } |
268 | | |
269 | | /// Push raw bytes onto the server fd. Used by the manual-drive path to feed |
270 | | /// frame bytes directly into curl without any mock-side framing. |
271 | | /// @param data Buffer to send. |
272 | | /// @param size Number of bytes in 'data'. |
273 | | /// @return false on short or failed write. |
274 | 802 | bool WebSocketMockServer::PushRawBytes(const unsigned char* data, std::size_t size) { |
275 | 802 | if (!connection_) { |
276 | 0 | return false; |
277 | 0 | } |
278 | 802 | connection_->DrainIncoming(); |
279 | 802 | return connection_->WriteAll(data, size); |
280 | 802 | } |
281 | | |
282 | | /// Push the next queued frame when curl is ready. Used in streaming mode; |
283 | | /// the drive loop calls this after the handshake has been sent. Shuts |
284 | | /// the write side once the last chunk is delivered. |
285 | 656 | void WebSocketMockServer::DeliverNextChunk() { |
286 | 656 | if (!connection_ || next_chunk_ >= frames_.size()) { |
287 | 0 | return; |
288 | 0 | } |
289 | 656 | connection_->DrainIncoming(); |
290 | 656 | const std::string& chunk = frames_[next_chunk_++]; |
291 | 656 | if (!chunk.empty()) { |
292 | 637 | connection_->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size()); |
293 | 637 | } |
294 | 656 | if (next_chunk_ >= frames_.size()) { |
295 | 184 | connection_->ShutdownWrite(); |
296 | 184 | } |
297 | 656 | } |
298 | | |
299 | | /// Drive the WebSocket opening handshake: read whatever curl has written so |
300 | | /// far, and once we've seen the end of the request headers, reply with a |
301 | | /// valid 101 Switching Protocols. |
302 | | /// @return true once the 101 response has been written (idempotent afterwards). |
303 | 3.89k | bool WebSocketMockServer::TryAdvanceHandshake() { |
304 | 3.89k | if (handshake_sent_ || !connection_) { |
305 | 146 | return handshake_sent_; |
306 | 146 | } |
307 | 3.74k | connection_->ReadAvailable(&ws_request_buffer_); |
308 | 3.74k | if (ws_request_buffer_.find("\r\n\r\n") == std::string::npos) { |
309 | 3.03k | return false; |
310 | 3.03k | } |
311 | 718 | std::string key = ExtractWebSocketKey(ws_request_buffer_); |
312 | | // Even if parsing failed, reply with *something* so curl doesn't wedge. |
313 | | // A bad Accept exercises curl's handshake-error path. |
314 | 718 | std::string accept = key.empty() ? std::string("AAAAAAAAAAAAAAAAAAAAAAAAAAA=") : ComputeWebSocketAccept(key); |
315 | 718 | std::string response = |
316 | 718 | "HTTP/1.1 101 Switching Protocols\r\n" |
317 | 718 | "Upgrade: websocket\r\n" |
318 | 718 | "Connection: Upgrade\r\n" |
319 | 718 | "Sec-WebSocket-Accept: " + |
320 | 718 | accept + "\r\n\r\n"; |
321 | 718 | connection_->WriteAll(reinterpret_cast<const unsigned char*>(response.data()), response.size()); |
322 | 718 | handshake_sent_ = true; |
323 | 718 | return true; |
324 | 3.74k | } |
325 | | |
326 | | /// Seed the mock from the scenario, then run the perform loop. Drives the 101 |
327 | | /// handshake on every iteration; in streaming mode also pushes queued frame |
328 | | /// chunks as curl becomes readable. In manual mode (CURLOPT_CONNECT_ONLY=2) |
329 | | /// chunks are left alone during the loop — once the loop returns, this method |
330 | | /// pushes them as raw bytes and exercises curl_ws_recv / curl_ws_send against |
331 | | /// a small flag matrix. |
332 | | /// @param multi caller-owned multi; 'easy' is already added. |
333 | | /// @param easy the curl easy handle attached to this mock. |
334 | | /// @param scenario source of the frame chunks and CONNECT_ONLY setting. |
335 | 1.05k | void WebSocketMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) { |
336 | 1.05k | SetManualDelivery(ScenarioRequestsManualWsDrive(scenario)); |
337 | | // initial_response is unused in WS mode; we synthesise the 101 dynamically |
338 | | // from curl's Upgrade request. |
339 | 1.05k | SetFrames(BuildFrameChunks(scenario.connection())); |
340 | | |
341 | 1.05k | int still_running = 1; |
342 | 1.05k | int idle_iterations = 0; |
343 | 1.05k | CURLMcode rc = CURLM_OK; |
344 | | |
345 | 62.5k | while (still_running && idle_iterations < kMaxIdleIterations) { |
346 | 62.5k | rc = curl_multi_perform(multi, &still_running); |
347 | 62.5k | if (rc != CURLM_OK) { |
348 | 0 | break; |
349 | 0 | } |
350 | | // Drive the 101 handshake on every iteration; no-op once sent. |
351 | 62.5k | if (!handshake_sent()) { |
352 | 3.89k | if (TryAdvanceHandshake()) { |
353 | 718 | idle_iterations = 0; |
354 | 718 | } |
355 | 3.89k | } |
356 | 62.5k | if (!still_running) { |
357 | 1.05k | break; |
358 | 1.05k | } |
359 | | |
360 | 61.5k | int ready = WaitOnMultiFdset(multi, &rc); |
361 | 61.5k | if (rc != CURLM_OK) { |
362 | 0 | break; |
363 | 0 | } |
364 | | |
365 | | // Only push frame chunks in streaming mode — in manual mode the caller |
366 | | // will push them below after the handshake. |
367 | 61.5k | if (!manual_delivery() && handshake_sent() && has_more_chunks()) { |
368 | 656 | DeliverNextChunk(); |
369 | 656 | idle_iterations = 0; |
370 | 60.8k | } else if (ready == 0) { |
371 | 60.3k | ++idle_iterations; |
372 | 60.3k | } else { |
373 | 467 | idle_iterations = 0; |
374 | 467 | } |
375 | 61.5k | } |
376 | | |
377 | 1.05k | if (!manual_delivery() || !handshake_sent()) { |
378 | 889 | return; |
379 | 889 | } |
380 | | |
381 | | // Manual-drive tail: feed every remaining scripted chunk straight onto the |
382 | | // server fd as raw frame bytes, draining curl_ws_recv between each push. |
383 | 1.06k | while (remaining_chunks() > 0) { |
384 | 899 | const std::string& chunk = PeekChunk(0); |
385 | 899 | if (!chunk.empty()) { |
386 | 802 | PushRawBytes(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size()); |
387 | 802 | } |
388 | 899 | ConsumeChunk(); |
389 | 899 | DrainWsRecv(easy); |
390 | 899 | } |
391 | | // Final drain in case frame parsing produced more work after the last push. |
392 | 166 | DrainWsRecv(easy); |
393 | | |
394 | 166 | const auto& probes = scenario.connection().manual_probes(); |
395 | 166 | static const unsigned char kPayload[] = "hello-from-proto-fuzzer"; |
396 | 166 | const std::size_t payload_len = sizeof(kPayload) - 1; |
397 | | |
398 | 166 | if (probes.flag_matrix()) { |
399 | | // Exercise curl_ws_send with a fixed matrix of flags. We don't care whether |
400 | | // the send actually lands on the wire — the point is to reach the encode |
401 | | // paths in ws_enc_add_frame / ws_enc_write_head. |
402 | 3 | std::size_t iteration = 0; |
403 | 57 | for (unsigned int flags : kWsSendFlagMatrix) { |
404 | 57 | if (iteration++ >= kMaxWsSendIterations) { |
405 | 0 | break; |
406 | 0 | } |
407 | | // Announce the frame via the public curl_ws_start_frame entrypoint before |
408 | | // the actual send. In non-raw mode this writes the frame head into the |
409 | | // sendbuf; the follow-up curl_ws_send with the same flags finishes the |
410 | | // exchange or fails cleanly on invalid flag combos. |
411 | 57 | (void)curl_ws_start_frame(easy, flags, static_cast<curl_off_t>(payload_len)); |
412 | 57 | std::size_t sent = 0; |
413 | 57 | curl_off_t fragsize = (flags & CURLWS_OFFSET) ? static_cast<curl_off_t>(payload_len) : 0; |
414 | 57 | (void)curl_ws_send(easy, kPayload, payload_len, &sent, fragsize, flags); |
415 | 57 | if (connection() != nullptr) { |
416 | 57 | connection()->DrainIncoming(); |
417 | 57 | } |
418 | 57 | } |
419 | 3 | } |
420 | | |
421 | 166 | if (probes.unaligned_send()) { |
422 | | // Multi-call mis-sized send probe: declare a fragsize=200 frame, send 23 |
423 | | // bytes, then call curl_ws_send again with a buflen much bigger than the |
424 | | // remaining payload. Hits the "unaligned frame size" failf in ws_enc_send. |
425 | 3 | constexpr curl_off_t kBigFrag = 200; |
426 | 3 | (void)curl_ws_start_frame(easy, CURLWS_TEXT | CURLWS_OFFSET, kBigFrag); |
427 | 3 | std::size_t sent = 0; |
428 | 3 | (void)curl_ws_send(easy, kPayload, payload_len, &sent, kBigFrag, CURLWS_TEXT | CURLWS_OFFSET); |
429 | | // Now enc.payload_remain ≈ kBigFrag - payload_len. A follow-up send with |
430 | | // buflen > remaining trips the guard at ws_enc_send:~1050. |
431 | 3 | std::size_t sent2 = 0; |
432 | 3 | constexpr std::size_t kOverrun = 500; |
433 | 3 | unsigned char overrun[kOverrun]; |
434 | 3 | std::fill(overrun, overrun + kOverrun, 'X'); |
435 | 3 | (void)curl_ws_send(easy, overrun, kOverrun, &sent2, kBigFrag, CURLWS_TEXT | CURLWS_OFFSET); |
436 | 3 | if (connection() != nullptr) { |
437 | 3 | connection()->DrainIncoming(); |
438 | 3 | } |
439 | 3 | } |
440 | | |
441 | 166 | if (probes.raw_send()) { |
442 | | // Raw-mode send path: reachable only when CURLOPT_WS_OPTIONS has |
443 | | // CURLWS_RAW_MODE set. curl_ws_send with flags=0, fragsize=0 takes the |
444 | | // data->set.ws_raw_mode branch → ws_send_raw. No-op for non-raw scenarios |
445 | | // (hits the "no flags given" failure path instead). |
446 | 3 | std::size_t sent = 0; |
447 | 3 | (void)curl_ws_send(easy, kPayload, payload_len, &sent, 0, 0); |
448 | 3 | if (connection() != nullptr) { |
449 | 3 | connection()->DrainIncoming(); |
450 | 3 | } |
451 | 3 | } |
452 | 166 | } |
453 | | |
454 | | } // namespace proto_fuzzer |