Branch data Line data Source code
1 : : /* SPDX-FileCopyrightText: Copyright 2026 Cozens Software Solutions Limited
2 : : * SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 OR LicenseRef-PolyForm-Internal-Use-1.0.0 OR LicenseRef-COSOSO-Commercial
3 : : */
4 : :
5 : : #include "lwip/opt.h"
6 : :
7 : : /* This component requires lwIP built with TCP. */
8 : : #if LWIP_TCP
9 : :
10 : : #include "SolidSyslogLwipRawTcpStream.h"
11 : : #include "SolidSyslogLwipRawTcpStreamPrivate.h"
12 : :
13 : : #include <stdbool.h>
14 : : #include <stddef.h>
15 : : #include <stdint.h>
16 : :
17 : : #include "SolidSyslogError.h"
18 : : #include "SolidSyslogErrorCategory.h"
19 : : #include "SolidSyslogLwipRawAddressPrivate.h"
20 : : #include "SolidSyslogLwipRawMarshalPrivate.h"
21 : : #include "SolidSyslogLwipRawTcpStreamErrors.h"
22 : : #include "SolidSyslogNullStream.h"
23 : : #include "SolidSyslogStream.h"
24 : : #include "SolidSyslogStreamCategories.h"
25 : : #include "SolidSyslogTunables.h"
26 : : #include "lwip/arch.h"
27 : : #include "lwip/err.h"
28 : : #include "lwip/ip.h"
29 : : #include "lwip/pbuf.h"
30 : : #include "lwip/tcp.h"
31 : : #include "lwip/tcpbase.h"
32 : :
33 : : const struct SolidSyslogErrorSource SolidSyslogLwipRawTcpStreamErrorSource = {
34 : : "LwipRawTcpStream",
35 : : SolidSyslogTcpStreamErrors_Name
36 : : };
37 : :
38 : : struct SolidSyslogAddress;
39 : : struct SolidSyslogStream;
40 : :
41 : : /* Per-operation parameters carried across one marshal hop. Each public op
42 : : * fills only the fields it needs (Close sets just Self); what the hop
43 : : * observed comes back to the caller in the same struct. One struct per class
44 : : * so the void*-context recovery has a single cast site
45 : : * (LwipRawTcpStreamCallFromContext) - see D.002 in docs/misra-deviations.md.
46 : : * Send/Read fields never overlap in one call. */
47 : : struct LwipRawTcpStreamCall
48 : : {
49 : : struct SolidSyslogLwipRawTcpStream* Self;
50 : : const struct SolidSyslogLwipRawAddress* Dst;
51 : : const void* SendBuffer;
52 : : void* ReadBuffer;
53 : : size_t Length;
54 : : /* Whether the setup hop got a pcb, read on the caller's thread. Pcb itself
55 : : is not: once the hop releases the stack, the tcpip thread may run the
56 : : error callback that nulls it before the caller looks. */
57 : : bool PcbCreated;
58 : : err_t ConnectErr;
59 : : /* What ended a failed attempt, read on the lwIP thread by the abort hop:
60 : : the callbacks write Errored and LastError there, and the hop is what
61 : : carries both to the caller's thread together. */
62 : : bool EndedByPeer;
63 : : err_t PeerError;
64 : : bool SendResult;
65 : : SolidSyslogSsize ReadResult;
66 : : };
67 : :
68 : : static uint32_t LwipRawTcpStream_NullConnectTimeoutGetter(void* context);
69 : :
70 : : static bool LwipRawTcpStream_Open(struct SolidSyslogStream* base, const struct SolidSyslogAddress* addr);
71 : : static bool LwipRawTcpStream_Send(struct SolidSyslogStream* base, const void* buffer, size_t size);
72 : : static SolidSyslogSsize LwipRawTcpStream_Read(struct SolidSyslogStream* base, void* buffer, size_t size);
73 : : static void LwipRawTcpStream_Close(struct SolidSyslogStream* base);
74 : : static uint32_t LwipRawTcpStream_Version(struct SolidSyslogStream* base);
75 : :
76 : : static inline struct SolidSyslogLwipRawTcpStream* LwipRawTcpStream_SelfFromBase(struct SolidSyslogStream* base);
77 : : static inline struct SolidSyslogLwipRawTcpStream* LwipRawTcpStream_SelfFromArg(void* arg);
78 : : static inline struct LwipRawTcpStreamCall* LwipRawTcpStreamCallFromContext(void* context);
79 : : static inline bool LwipRawTcpStream_ConfigProvidesGetter(const struct SolidSyslogLwipRawTcpStreamConfig* config);
80 : : static inline bool LwipRawTcpStream_IsOpen(const struct SolidSyslogLwipRawTcpStream* self);
81 : : static inline bool LwipRawTcpStream_IsWritable(const struct SolidSyslogLwipRawTcpStream* self);
82 : : static inline bool LwipRawTcpStream_HasQueuedRx(const struct SolidSyslogLwipRawTcpStream* self);
83 : : static inline bool LwipRawTcpStream_RxQueueIsFull(const struct SolidSyslogLwipRawTcpStream* self);
84 : : static inline bool LwipRawTcpStream_HasCloseWork(const struct SolidSyslogLwipRawTcpStream* self);
85 : : static inline bool LwipRawTcpStream_HasReadWork(const struct SolidSyslogLwipRawTcpStream* self);
86 : : static void LwipRawTcpStream_ReportConnectFailure(
87 : : struct SolidSyslogLwipRawTcpStream* self,
88 : : enum SolidSyslogSeverity severity,
89 : : enum SolidSyslogTcpStreamErrors detail
90 : : );
91 : : static void LwipRawTcpStream_ReportConnectFailureWithCode(
92 : : struct SolidSyslogLwipRawTcpStream* self,
93 : : enum SolidSyslogSeverity severity,
94 : : enum SolidSyslogTcpStreamErrors detail,
95 : : err_t nativeCode
96 : : );
97 : : static void LwipRawTcpStream_ReportFailedAttempt(
98 : : struct SolidSyslogLwipRawTcpStream* self,
99 : : const struct LwipRawTcpStreamCall* call
100 : : );
101 : : static void LwipRawTcpStream_DoOpenAndConnect(void* context);
102 : : static void LwipRawTcpStream_DoAbort(void* context);
103 : : static void LwipRawTcpStream_DoSend(void* context);
104 : : static void LwipRawTcpStream_DoRead(void* context);
105 : : static void LwipRawTcpStream_DoClose(void* context);
106 : : static struct tcp_pcb* LwipRawTcpStream_OpenAndConfigurePcb(struct SolidSyslogLwipRawTcpStream* self);
107 : : static bool LwipRawTcpStream_WaitForConnectedCallback(struct SolidSyslogLwipRawTcpStream* self);
108 : : static uint32_t LwipRawTcpStream_ResolveConnectTimeoutMs(struct SolidSyslogLwipRawTcpStream* self);
109 : : static bool LwipRawTcpStream_SendOrCloseOnFailure(
110 : : struct SolidSyslogLwipRawTcpStream* self,
111 : : const void* buffer,
112 : : size_t size
113 : : );
114 : : static bool LwipRawTcpStream_OutputResultIsAcceptable(err_t outputErr);
115 : : static size_t LwipRawTcpStream_DrainHeadBytes(struct SolidSyslogLwipRawTcpStream* self, void* buffer, size_t size);
116 : : static void LwipRawTcpStream_EnqueueRxPbuf(struct SolidSyslogLwipRawTcpStream* self, struct pbuf* p);
117 : : static void LwipRawTcpStream_DrainAllQueuedPbufs(struct SolidSyslogLwipRawTcpStream* self);
118 : : static void LwipRawTcpStream_ClosePcb(struct SolidSyslogLwipRawTcpStream* self);
119 : : static void LwipRawTcpStream_DetachPcb(struct tcp_pcb* pcb);
120 : :
121 : : static err_t LwipRawTcpStream_ConnectedCallback(void* arg, struct tcp_pcb* pcb, err_t err);
122 : : static err_t LwipRawTcpStream_RecvCallback(void* arg, struct tcp_pcb* tpcb, struct pbuf* p, err_t err);
123 : : static err_t LwipRawTcpStream_SentCallback(void* arg, struct tcp_pcb* tpcb, u16_t len);
124 : : static void LwipRawTcpStream_ErrCallback(void* arg, err_t err);
125 : :
126 : 84 : void SolidSyslogLwipRawTcpStream_Initialise(
127 : : struct SolidSyslogStream* base,
128 : : const struct SolidSyslogLwipRawTcpStreamConfig* config
129 : : )
130 : : {
131 : : static const struct SolidSyslogLwipRawTcpStream DefaultLwipRawTcpStream = {
132 : : .Base =
133 : : {.Open = LwipRawTcpStream_Open,
134 : : .Send = LwipRawTcpStream_Send,
135 : : .Read = LwipRawTcpStream_Read,
136 : : .Close = LwipRawTcpStream_Close,
137 : : .Version = LwipRawTcpStream_Version},
138 : : .Config =
139 : : {.GetConnectTimeoutMs = LwipRawTcpStream_NullConnectTimeoutGetter,
140 : : .ConnectTimeoutContext = NULL,
141 : : .Sleep = NULL},
142 : : .Pcb = NULL,
143 : : .Connected = false,
144 : : .Errored = false,
145 : : .LastError = ERR_OK,
146 : : .RxQueue = {0},
147 : : .RxQueueHead = 0,
148 : : .RxQueueCount = 0,
149 : : .RxHeadOffset = 0,
150 : : };
151 : :
152 : 84 : struct SolidSyslogLwipRawTcpStream* self = LwipRawTcpStream_SelfFromBase(base);
153 : 84 : *self = DefaultLwipRawTcpStream;
154 : 84 : self->Config.Sleep = config->Sleep;
155 : 84 : self->Config.ConnectTimeoutContext = config->ConnectTimeoutContext;
156 [ + + ]: 84 : if (LwipRawTcpStream_ConfigProvidesGetter(config))
157 : : {
158 : 2 : self->Config.GetConnectTimeoutMs = config->GetConnectTimeoutMs;
159 : : }
160 : 84 : }
161 : :
162 : 84 : static inline bool LwipRawTcpStream_ConfigProvidesGetter(const struct SolidSyslogLwipRawTcpStreamConfig* config)
163 : : {
164 : 84 : return config->GetConnectTimeoutMs != NULL;
165 : : }
166 : :
167 : : /* Null Object substituted when the integrator does not install a getter -
168 : : * returns the compile-time tunable so the bounded-wait path has a single
169 : : * code path regardless of whether the integrator wired runtime tuning. */
170 : 57 : static uint32_t LwipRawTcpStream_NullConnectTimeoutGetter(void* context)
171 : : {
172 : : (void) context;
173 : 57 : return (uint32_t) SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS;
174 : : }
175 : :
176 : 272 : static inline struct SolidSyslogLwipRawTcpStream* LwipRawTcpStream_SelfFromBase(struct SolidSyslogStream* base)
177 : : {
178 : 272 : return (struct SolidSyslogLwipRawTcpStream*) base;
179 : : }
180 : :
181 : : /* Recovers our self pointer from the void* argument lwIP passes back into
182 : : * every callback we registered via tcp_arg(pcb, self). Single named helper
183 : : * so the void->struct cast lives in one place - and MISRA 11.5 has one
184 : : * suppression site, not one per callback. */
185 : 84 : static inline struct SolidSyslogLwipRawTcpStream* LwipRawTcpStream_SelfFromArg(void* arg)
186 : : {
187 : 84 : return (struct SolidSyslogLwipRawTcpStream*) arg;
188 : : }
189 : :
190 : : /* Recovers the per-op call struct from the void* context the marshal passes
191 : : * back into each Do* callback - the marshal-seam analogue of SelfFromArg,
192 : : * one cast site for all marshalled ops (see D.002). */
193 : 138 : static inline struct LwipRawTcpStreamCall* LwipRawTcpStreamCallFromContext(void* context)
194 : : {
195 : 138 : return (struct LwipRawTcpStreamCall*) context;
196 : : }
197 : :
198 : 84 : void SolidSyslogLwipRawTcpStream_Cleanup(struct SolidSyslogStream* base)
199 : : {
200 : 84 : LwipRawTcpStream_Close(base);
201 : : /* Overwrite the abstract base with the shared NullStream vtable so
202 : : * use-after-destroy is a safe no-op rather than a NULL-fn-pointer crash. */
203 : 84 : *base = *SolidSyslogNullStream_Get();
204 : 84 : }
205 : :
206 : 67 : static bool LwipRawTcpStream_Open(struct SolidSyslogStream* base, const struct SolidSyslogAddress* addr)
207 : : {
208 : 67 : struct SolidSyslogLwipRawTcpStream* self = LwipRawTcpStream_SelfFromBase(base);
209 : 67 : bool opened = false;
210 [ + + ]: 67 : if (LwipRawTcpStream_IsOpen(self))
211 : : {
212 : : /* Refused by contract, not reconnected: what is held stays as it is. */
213 : 2 : LwipRawTcpStream_Report(
214 : : SOLIDSYSLOG_BAD_ARGUMENT_SEVERITY,
215 : : SOLIDSYSLOG_CAT_BAD_ARGUMENT,
216 : : SOLIDSYSLOG_TCP_STREAM_ERROR_ALREADY_OPEN
217 : : );
218 : : }
219 : : else
220 : : {
221 : 65 : struct LwipRawTcpStreamCall call = {.Self = self, .Dst = SolidSyslogLwipRawAddress_AsConst(addr)};
222 : 65 : SolidSyslogLwipRaw_Marshal(LwipRawTcpStream_DoOpenAndConnect, &call);
223 [ + + ]: 65 : if (call.PcbCreated)
224 : : {
225 [ + + ]: 63 : if (call.ConnectErr == ERR_OK)
226 : : {
227 : 59 : opened = LwipRawTcpStream_WaitForConnectedCallback(self);
228 : : }
229 [ + + ]: 63 : if (!opened)
230 : : {
231 : : /* Abort first: the hop also reads what ended the attempt. */
232 : 18 : SolidSyslogLwipRaw_Marshal(LwipRawTcpStream_DoAbort, &call);
233 : 18 : LwipRawTcpStream_ReportFailedAttempt(self, &call);
234 : : }
235 : : }
236 : : else
237 : : {
238 : 2 : LwipRawTcpStream_ReportConnectFailure(
239 : : self,
240 : : SOLIDSYSLOG_STREAM_CONNECT_LOCAL_SEVERITY,
241 : : SOLIDSYSLOG_TCP_STREAM_ERROR_ENDPOINT_UNAVAILABLE
242 : : );
243 : : }
244 : : /* Decided by what the hop and the wait saw, not by Pcb: a connection
245 : : * the peer resets straight after accepting it was still made, and
246 : : * its loss is the next Send's to report. */
247 [ + + ]: 65 : if (opened)
248 : : {
249 : 45 : SolidSyslogStreamOpenHealth_Opened(&self->OpenHealth);
250 : : }
251 : : }
252 : 67 : return opened;
253 : : }
254 : :
255 : : /* Every connect failure is one category with the detail naming which step
256 : : * failed, so a portable handler reacts to "no connection" without knowing
257 : : * lwIP. Runs on the caller's thread - never inside a marshalled hop, since
258 : : * the handler is integrator code. Reported once per run of the same detail,
259 : : * as the Open contract requires: the sender retries every service pass. */
260 : 7 : static void LwipRawTcpStream_ReportConnectFailure(
261 : : struct SolidSyslogLwipRawTcpStream* self,
262 : : enum SolidSyslogSeverity severity,
263 : : enum SolidSyslogTcpStreamErrors detail
264 : : )
265 : : {
266 : 7 : SolidSyslogStreamOpenHealth_ReportFailure(
267 : : &self->OpenHealth,
268 : : &SolidSyslogLwipRawTcpStreamErrorSource,
269 : : severity,
270 : : SOLIDSYSLOG_CAT_STREAM_CONNECT_FAILED,
271 : : (int32_t) detail
272 : : );
273 : 7 : }
274 : :
275 : : /* The fault, with the err_t behind it. Every caller has one: tcp_connect
276 : : * declined with it, or a callback was handed it, and each callback that sets
277 : : * Errored records one. */
278 : 13 : static void LwipRawTcpStream_ReportConnectFailureWithCode(
279 : : struct SolidSyslogLwipRawTcpStream* self,
280 : : enum SolidSyslogSeverity severity,
281 : : enum SolidSyslogTcpStreamErrors detail,
282 : : err_t nativeCode
283 : : )
284 : : {
285 : 13 : SolidSyslogStreamOpenHealth_ReportFailureWithNativeCode(
286 : : &self->OpenHealth,
287 : : &SolidSyslogLwipRawTcpStreamErrorSource,
288 : : severity,
289 : : SOLIDSYSLOG_CAT_STREAM_CONNECT_FAILED,
290 : : (int32_t) detail,
291 : : (int32_t) nativeCode
292 : : );
293 : 13 : }
294 : :
295 : : /* Names which step of a failed attempt gave up. A non-ERR_OK from tcp_connect
296 : : * is the local stack declining to start - no memory for the SYN, no route -
297 : : * so no packet was ever sent. Otherwise the bounded wait ended, and the abort
298 : : * hop's reading separates a reset that arrived from a budget that simply ran
299 : : * out. The code is the one tcp_connect answered, or the one the callback that
300 : : * ended the attempt was handed; a budget that ran out has none. */
301 : 18 : static void LwipRawTcpStream_ReportFailedAttempt(
302 : : struct SolidSyslogLwipRawTcpStream* self,
303 : : const struct LwipRawTcpStreamCall* call
304 : : )
305 : : {
306 [ + + ]: 18 : if (call->ConnectErr != ERR_OK)
307 : : {
308 : 4 : LwipRawTcpStream_ReportConnectFailureWithCode(
309 : : self,
310 : : SOLIDSYSLOG_STREAM_CONNECT_LOCAL_SEVERITY,
311 : : SOLIDSYSLOG_TCP_STREAM_ERROR_CONNECT_NOT_STARTED,
312 : 4 : call->ConnectErr
313 : : );
314 : : }
315 [ + + ]: 14 : else if (call->EndedByPeer)
316 : : {
317 : 9 : LwipRawTcpStream_ReportConnectFailureWithCode(
318 : : self,
319 : : SOLIDSYSLOG_STREAM_CONNECT_REMOTE_SEVERITY,
320 : : SOLIDSYSLOG_TCP_STREAM_ERROR_CONNECT_REFUSED,
321 : 9 : call->PeerError
322 : : );
323 : : }
324 : : else
325 : : {
326 : 5 : LwipRawTcpStream_ReportConnectFailure(
327 : : self,
328 : : SOLIDSYSLOG_STREAM_CONNECT_REMOTE_SEVERITY,
329 : : SOLIDSYSLOG_TCP_STREAM_ERROR_CONNECT_TIMED_OUT
330 : : );
331 : : }
332 : 18 : }
333 : :
334 : : /* The setup-and-connect hop: tcp_new + pcb configuration + tcp_connect all
335 : : * run on the lwIP-owning thread in one marshalled batch. The connected_cb
336 : : * may fire here (synchronously, on the same thread) - it only sets the
337 : : * Connected / Errored flags. The bounded spin that polls those flags stays on
338 : : * the caller's thread (sleeping the lwIP thread mid-connect would starve
339 : : * RX/timers); each poll sleeps through an opaque call, so the flags are
340 : : * re-read rather than cached. */
341 : 65 : static void LwipRawTcpStream_DoOpenAndConnect(void* context)
342 : : {
343 : 65 : struct LwipRawTcpStreamCall* call = LwipRawTcpStreamCallFromContext(context);
344 : 65 : struct SolidSyslogLwipRawTcpStream* self = call->Self;
345 : 65 : self->Pcb = LwipRawTcpStream_OpenAndConfigurePcb(self);
346 : 65 : call->PcbCreated = LwipRawTcpStream_IsOpen(self);
347 [ + + ]: 65 : if (call->PcbCreated)
348 : : {
349 : 63 : self->Connected = false;
350 : 63 : self->Errored = false;
351 : 63 : self->LastError = ERR_OK;
352 : 63 : call->ConnectErr = tcp_connect(self->Pcb, &call->Dst->Ip, call->Dst->Port, LwipRawTcpStream_ConnectedCallback);
353 : : }
354 : 65 : }
355 : :
356 : 311 : static inline bool LwipRawTcpStream_IsOpen(const struct SolidSyslogLwipRawTcpStream* self)
357 : : {
358 : 311 : return self->Pcb != NULL;
359 : : }
360 : :
361 : 138 : static inline bool LwipRawTcpStream_HasQueuedRx(const struct SolidSyslogLwipRawTcpStream* self)
362 : : {
363 : 138 : return self->RxQueueCount > 0U;
364 : : }
365 : :
366 : 22 : static inline bool LwipRawTcpStream_RxQueueIsFull(const struct SolidSyslogLwipRawTcpStream* self)
367 : : {
368 : 22 : return self->RxQueueCount >= SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE;
369 : : }
370 : :
371 : 65 : static struct tcp_pcb* LwipRawTcpStream_OpenAndConfigurePcb(struct SolidSyslogLwipRawTcpStream* self)
372 : : {
373 : 65 : struct tcp_pcb* pcb = tcp_new();
374 [ + + ]: 65 : if (pcb != NULL)
375 : : {
376 : 63 : ip_set_option(pcb, SOF_KEEPALIVE);
377 : 63 : SolidSyslogLwipRawTcpStream_ApplyKeepalive(pcb);
378 : : /* Disable Nagle. The syslog client writes small, latency-sensitive
379 : : * records (octet-framed messages, and - when an upper TLS layer is
380 : : * stacked on this stream - multi-segment handshake flights) and never
381 : : * pipelines a second write behind an unacked one. With Nagle on, lwIP
382 : : * holds a sub-MSS segment until the previous one is ACKed; a TLS
383 : : * handshake flight (e.g. a client certificate) then stalls mid-exchange
384 : : * waiting for an ACK that the peer only sends after it has the whole
385 : : * flight - a deadlock observed against syslog-ng over mutual TLS.
386 : : * TCP_NODELAY is the right default for this request/response workload. */
387 : 63 : tcp_nagle_disable(pcb);
388 : 63 : tcp_arg(pcb, self);
389 : 63 : tcp_recv(pcb, LwipRawTcpStream_RecvCallback);
390 : 63 : tcp_sent(pcb, LwipRawTcpStream_SentCallback);
391 : 63 : tcp_err(pcb, LwipRawTcpStream_ErrCallback);
392 : : }
393 : 65 : return pcb;
394 : : }
395 : :
396 : : /* Bounded synchronous-Open spin: each iteration sleeps via the
397 : : * integrator-injected Sleep so lwIP's timer / RX paths get cycles to
398 : : * advance the SYN/SYN-ACK exchange. Runs on the caller's thread - never the
399 : : * lwIP thread. Exits on Connected (success), Errored (set by connected_cb
400 : : * on non-ERR_OK or by tcp_err), or elapsed >= deadline (timeout). */
401 : 59 : static bool LwipRawTcpStream_WaitForConnectedCallback(struct SolidSyslogLwipRawTcpStream* self)
402 : : {
403 : 59 : const uint32_t pollMs = (uint32_t) SOLIDSYSLOG_LWIP_RAW_TCP_CONNECT_POLL_MS;
404 : 59 : const uint32_t deadlineMs = LwipRawTcpStream_ResolveConnectTimeoutMs(self);
405 : 59 : uint32_t elapsedMs = 0U;
406 [ + + + + : 30121 : while (!self->Connected && !self->Errored && (elapsedMs < deadlineMs))
+ + ]
407 : : {
408 : 30062 : self->Config.Sleep(pollMs);
409 : 30062 : elapsedMs += pollMs;
410 : : }
411 : 59 : return self->Connected;
412 : : }
413 : :
414 : : /* Bridges the integrator-installed getter (or the Null Object substituted
415 : : * in Initialise) to the bounded spin deadline. Invoked on every connect
416 : : * attempt so a runtime-tunable value takes effect on the next reconnect. */
417 : 59 : static uint32_t LwipRawTcpStream_ResolveConnectTimeoutMs(struct SolidSyslogLwipRawTcpStream* self)
418 : : {
419 : 59 : uint32_t ms = self->Config.GetConnectTimeoutMs(self->Config.ConnectTimeoutContext);
420 : 59 : return (ms > SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS_MAX) ? SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS_MAX : ms;
421 : : }
422 : :
423 : : /* Connect failed or timed out. Errored means tcp_err fired or the connected
424 : : * callback reported a failure - the attempt was answered, just not with a
425 : : * connection - and both it and the code behind it are read on the lwIP thread
426 : : * that wrote them. A half-open pcb has no graceful close, so what is still
427 : : * held is released with tcp_abort, detached first so the abort's own tcp_err
428 : : * cannot reach the flags. A reset has already cost the pcb: lwIP frees it as
429 : : * soon as tcp_err returns, and then there is nothing left to abort. */
430 : 18 : static void LwipRawTcpStream_DoAbort(void* context)
431 : : {
432 : 18 : struct LwipRawTcpStreamCall* call = LwipRawTcpStreamCallFromContext(context);
433 : 18 : struct SolidSyslogLwipRawTcpStream* self = call->Self;
434 : 18 : call->EndedByPeer = self->Errored;
435 : 18 : call->PeerError = self->LastError;
436 [ + + ]: 18 : if (LwipRawTcpStream_IsOpen(self))
437 : : {
438 : 16 : LwipRawTcpStream_DetachPcb(self->Pcb);
439 : 16 : tcp_abort(self->Pcb);
440 : 16 : self->Pcb = NULL;
441 : : }
442 : 18 : }
443 : :
444 : 10 : static bool LwipRawTcpStream_Send(struct SolidSyslogStream* base, const void* buffer, size_t size)
445 : : {
446 : 10 : struct SolidSyslogLwipRawTcpStream* self = LwipRawTcpStream_SelfFromBase(base);
447 : 10 : bool sent = false;
448 [ + + ]: 10 : if (LwipRawTcpStream_IsWritable(self))
449 : : {
450 : 6 : struct LwipRawTcpStreamCall call = {.Self = self, .SendBuffer = buffer, .Length = size};
451 : 6 : SolidSyslogLwipRaw_Marshal(LwipRawTcpStream_DoSend, &call);
452 : 6 : sent = call.SendResult;
453 : : }
454 : 10 : return sent;
455 : : }
456 : :
457 : : /* Sendable means the pcb is live AND no peer close / error has been observed.
458 : : * A peer FIN (RecvCallback with NULL p) sets Errored but leaves the pcb non-NULL,
459 : : * so IsOpen alone would keep writing into a doomed connection; failing the send
460 : : * lets StreamSender close and reconnect. Read deliberately still runs while
461 : : * Errored - it must drain queued bytes and then close on EOF. */
462 : 10 : static inline bool LwipRawTcpStream_IsWritable(const struct SolidSyslogLwipRawTcpStream* self)
463 : : {
464 [ + + + + ]: 10 : return LwipRawTcpStream_IsOpen(self) && !self->Errored;
465 : : }
466 : :
467 : 6 : static void LwipRawTcpStream_DoSend(void* context)
468 : : {
469 : 6 : struct LwipRawTcpStreamCall* call = LwipRawTcpStreamCallFromContext(context);
470 : 6 : call->SendResult = LwipRawTcpStream_SendOrCloseOnFailure(call->Self, call->SendBuffer, call->Length);
471 : 6 : }
472 : :
473 : 6 : static bool LwipRawTcpStream_SendOrCloseOnFailure(
474 : : struct SolidSyslogLwipRawTcpStream* self,
475 : : const void* buffer,
476 : : size_t size
477 : : )
478 : : {
479 : : /* TCP_WRITE_FLAG_COPY hands the caller's buffer to lwIP-owned pbufs
480 : : * before tcp_write returns - caller buffer lifetime ends here.
481 : : * tcp_output nudges transmission; ERR_MEM there is "queued, lwIP
482 : : * retries" so we still report success (lwIP owns the bytes). */
483 : 6 : err_t writeErr = tcp_write(self->Pcb, buffer, (u16_t) size, TCP_WRITE_FLAG_COPY);
484 : 6 : bool ok = (writeErr == ERR_OK);
485 [ + + ]: 6 : if (ok)
486 : : {
487 : 5 : err_t outputErr = tcp_output(self->Pcb);
488 : 5 : ok = LwipRawTcpStream_OutputResultIsAcceptable(outputErr);
489 : : }
490 [ + + ]: 6 : if (!ok)
491 : : {
492 : 2 : LwipRawTcpStream_ClosePcb(self);
493 : : }
494 : 6 : return ok;
495 : : }
496 : :
497 : 5 : static bool LwipRawTcpStream_OutputResultIsAcceptable(err_t outputErr)
498 : : {
499 [ + + + + ]: 5 : return (outputErr == ERR_OK) || (outputErr == ERR_MEM);
500 : : }
501 : :
502 : 15 : static SolidSyslogSsize LwipRawTcpStream_Read(struct SolidSyslogStream* base, void* buffer, size_t size)
503 : : {
504 : : /* SolidSyslogStream_Read returns < 0 to signal EOF/error (socket closed
505 : : * internally); -1 is the in-tree convention across the Stream adapters. */
506 : : static const SolidSyslogSsize READ_FAILED = -1;
507 : :
508 : 15 : struct SolidSyslogLwipRawTcpStream* self = LwipRawTcpStream_SelfFromBase(base);
509 : 15 : SolidSyslogSsize result = READ_FAILED;
510 [ + + ]: 15 : if (LwipRawTcpStream_IsOpen(self))
511 : : {
512 [ + + ]: 13 : if (LwipRawTcpStream_HasReadWork(self))
513 : : {
514 : 12 : struct LwipRawTcpStreamCall call = {
515 : : .Self = self,
516 : : .ReadBuffer = buffer,
517 : : .Length = size,
518 : : .ReadResult = READ_FAILED,
519 : : };
520 : 12 : SolidSyslogLwipRaw_Marshal(LwipRawTcpStream_DoRead, &call);
521 : 12 : result = call.ReadResult;
522 : : }
523 : : else
524 : : {
525 : 1 : result = 0; /* would-block */
526 : : }
527 : : }
528 : 15 : return result;
529 : : }
530 : :
531 : : /* Read touches lwIP only when there is queued data to drain/ack or a drained
532 : : * peer-FIN to close on. The pure would-block case (open, empty queue, not
533 : : * errored) does no lwIP work, so it takes no marshal hop. */
534 : 13 : static inline bool LwipRawTcpStream_HasReadWork(const struct SolidSyslogLwipRawTcpStream* self)
535 : : {
536 [ + + + + ]: 13 : return LwipRawTcpStream_HasQueuedRx(self) || self->Errored;
537 : : }
538 : :
539 : 12 : static void LwipRawTcpStream_DoRead(void* context)
540 : : {
541 : 12 : struct LwipRawTcpStreamCall* call = LwipRawTcpStreamCallFromContext(context);
542 : 12 : struct SolidSyslogLwipRawTcpStream* self = call->Self;
543 [ + + ]: 12 : if (LwipRawTcpStream_HasQueuedRx(self))
544 : : {
545 : 11 : size_t copied = LwipRawTcpStream_DrainHeadBytes(self, call->ReadBuffer, call->Length);
546 : 11 : tcp_recved(self->Pcb, (u16_t) copied);
547 : 11 : call->ReadResult = (SolidSyslogSsize) copied;
548 : : }
549 : : else
550 : : {
551 : : /* Peer FIN drained -> close internally per the Stream contract
552 : : * ("< 0 means EOF AND socket closed internally"); ReadResult stays
553 : : * READ_FAILED. */
554 : 1 : LwipRawTcpStream_ClosePcb(self);
555 : : }
556 : 12 : }
557 : :
558 : : /* Copies up to `size` bytes out of the head pbuf chain, advancing the read
559 : : * cursor. The cursor (RxHeadOffset) and the fully-drained test are keyed off
560 : : * tot_len, not the first link's len - lwIP hands us a pbuf chain whenever a
561 : : * segment spans more than one pool pbuf, and pbuf_copy_partial walks head->next
562 : : * for us so the tail links are not lost. The cursor advances by the count
563 : : * pbuf_copy_partial actually copied, not the requested amount: under lwIP's
564 : : * tot_len == sum of link->len invariant the two are equal, but keying off the real
565 : : * copy keeps a malformed/overstated tot_len from advancing past un-copied
566 : : * bytes and reporting stale buffer content as received. When the whole chain
567 : : * is drained, pbuf_free's it (frees every link) and advances the queue head -
568 : : * tail entries shift up through the bounded ring via modular arithmetic, no
569 : : * compaction. */
570 : 11 : static size_t LwipRawTcpStream_DrainHeadBytes(struct SolidSyslogLwipRawTcpStream* self, void* buffer, size_t size)
571 : : {
572 : 11 : struct pbuf* head = self->RxQueue[self->RxQueueHead];
573 : 11 : size_t available = (size_t) head->tot_len - self->RxHeadOffset;
574 : 11 : size_t toCopy = (size < available) ? size : available;
575 : 11 : size_t copied = (size_t) pbuf_copy_partial(head, buffer, (u16_t) toCopy, (u16_t) self->RxHeadOffset);
576 : 11 : self->RxHeadOffset += copied;
577 [ + + ]: 11 : if (self->RxHeadOffset >= (size_t) head->tot_len)
578 : : {
579 : 8 : (void) pbuf_free(head);
580 : 8 : self->RxQueueHead = (self->RxQueueHead + 1U) % SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE;
581 : 8 : self->RxQueueCount--;
582 : 8 : self->RxHeadOffset = 0;
583 : : }
584 : 11 : return copied;
585 : : }
586 : :
587 : 21 : static void LwipRawTcpStream_EnqueueRxPbuf(struct SolidSyslogLwipRawTcpStream* self, struct pbuf* p)
588 : : {
589 : 21 : size_t tail = (self->RxQueueHead + self->RxQueueCount) % SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE;
590 : 21 : self->RxQueue[tail] = p;
591 : 21 : self->RxQueueCount++;
592 : 21 : }
593 : :
594 : : /* Drains every queued pbuf via pbuf_free. Used by ClosePcb so an explicit
595 : : * Close, Send-failure-induced Close, or Destroy never leaks the pbufs lwIP
596 : : * handed us via tcp_recv. After tcp_err nulls Pcb the queue may still hold
597 : : * pbufs we accepted before the error - those need freeing too. */
598 : 40 : static void LwipRawTcpStream_DrainAllQueuedPbufs(struct SolidSyslogLwipRawTcpStream* self)
599 : : {
600 [ + + ]: 53 : while (LwipRawTcpStream_HasQueuedRx(self))
601 : : {
602 : 13 : (void) pbuf_free(self->RxQueue[self->RxQueueHead]);
603 : 13 : self->RxQueueHead = (self->RxQueueHead + 1U) % SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE;
604 : 13 : self->RxQueueCount--;
605 : : }
606 : 40 : self->RxHeadOffset = 0;
607 : 40 : }
608 : :
609 : 96 : static void LwipRawTcpStream_Close(struct SolidSyslogStream* base)
610 : : {
611 : 96 : struct SolidSyslogLwipRawTcpStream* self = LwipRawTcpStream_SelfFromBase(base);
612 [ + + ]: 96 : if (LwipRawTcpStream_HasCloseWork(self))
613 : : {
614 : 37 : struct LwipRawTcpStreamCall call = {.Self = self};
615 : 37 : SolidSyslogLwipRaw_Marshal(LwipRawTcpStream_DoClose, &call);
616 : : }
617 : 96 : }
618 : :
619 : 1 : static uint32_t LwipRawTcpStream_Version(struct SolidSyslogStream* base)
620 : : {
621 : : (void) base;
622 : 1 : return 0U;
623 : : }
624 : :
625 : : /* Close touches lwIP only if there is a pcb to close or queued pbufs to
626 : : * free. Close-before-open and close-after-tcp_err (Pcb already nulled, queue
627 : : * empty) do no lwIP work and take no marshal hop. */
628 : 96 : static inline bool LwipRawTcpStream_HasCloseWork(const struct SolidSyslogLwipRawTcpStream* self)
629 : : {
630 [ + + + + ]: 96 : return LwipRawTcpStream_IsOpen(self) || LwipRawTcpStream_HasQueuedRx(self);
631 : : }
632 : :
633 : 37 : static void LwipRawTcpStream_DoClose(void* context)
634 : : {
635 : 37 : LwipRawTcpStream_ClosePcb(LwipRawTcpStreamCallFromContext(context)->Self);
636 : 37 : }
637 : :
638 : : /* tcp_close must NOT be called on a pcb that has already been released by
639 : : * tcp_err - that's a use-after-free in lwIP. The Pcb != NULL guard works
640 : : * because LwipRawTcpStream_ErrCallback nulls Pcb when lwIP releases the
641 : : * pcb on its side. The queue drain runs unconditionally - pbufs we
642 : : * accepted via tcp_recv are ours to free regardless of pcb state. Always
643 : : * called from inside a marshalled hop (DoSend / DoRead / DoClose). */
644 : 40 : static void LwipRawTcpStream_ClosePcb(struct SolidSyslogLwipRawTcpStream* self)
645 : : {
646 : 40 : LwipRawTcpStream_DrainAllQueuedPbufs(self);
647 [ + + ]: 40 : if (LwipRawTcpStream_IsOpen(self))
648 : : {
649 : 39 : struct tcp_pcb* pcb = self->Pcb;
650 : 39 : self->Pcb = NULL;
651 : 39 : LwipRawTcpStream_DetachPcb(pcb);
652 [ + + ]: 39 : if (tcp_close(pcb) != ERR_OK)
653 : : {
654 : : /* Close could not be queued (ERR_MEM). Abort frees the pcb now -
655 : : leaving it would strand it, since nothing holds it any more. */
656 : 1 : tcp_abort(pcb);
657 : : }
658 : : }
659 : 40 : }
660 : :
661 : : /* lwIP keeps a closed pcb until its close completes, and while it lives it
662 : : calls whatever tcp_arg holds - which, on a stream that has since reconnected,
663 : : is the state of a different connection. Detaching before the close is what
664 : : makes that impossible: the pcb we are finished with holds no way back to us. */
665 : 55 : static void LwipRawTcpStream_DetachPcb(struct tcp_pcb* pcb)
666 : : {
667 : 55 : tcp_arg(pcb, NULL);
668 : 55 : tcp_recv(pcb, NULL);
669 : 55 : tcp_sent(pcb, NULL);
670 : 55 : tcp_err(pcb, NULL);
671 : 55 : }
672 : :
673 : 52 : static err_t LwipRawTcpStream_ConnectedCallback(void* arg, struct tcp_pcb* pcb, err_t err)
674 : : {
675 : : (void) pcb;
676 : 52 : struct SolidSyslogLwipRawTcpStream* self = LwipRawTcpStream_SelfFromArg(arg);
677 [ + + ]: 52 : if (err == ERR_OK)
678 : : {
679 : 45 : self->Connected = true;
680 : : }
681 : : else
682 : : {
683 : 7 : self->LastError = err;
684 : 7 : self->Errored = true;
685 : : }
686 : 52 : return ERR_OK;
687 : : }
688 : :
689 : : /* tcp_recv fires when lwIP has bytes for us - non-NULL p means a pbuf
690 : : * arrived; NULL p means peer half-closed (FIN). Backpressure on a full
691 : : * queue by returning non-ERR_OK; lwIP holds the pbuf and replays the
692 : : * callback when the queue drains. Runs on the lwIP thread (lwIP invokes it
693 : : * directly), touches no lwIP API, so it needs no marshalling. */
694 : 24 : static err_t LwipRawTcpStream_RecvCallback(void* arg, struct tcp_pcb* tpcb, struct pbuf* p, err_t err)
695 : : {
696 : : (void) tpcb;
697 : : (void) err;
698 : 24 : struct SolidSyslogLwipRawTcpStream* self = LwipRawTcpStream_SelfFromArg(arg);
699 : 24 : err_t result = ERR_OK;
700 [ + + ]: 24 : if (p == NULL)
701 : : {
702 : : /* lwIP signals the close with a NULL pbuf rather than an err_t;
703 : : * ERR_CLSD is its name for that state. */
704 : 2 : self->LastError = ERR_CLSD;
705 : 2 : self->Errored = true;
706 : : }
707 [ + + ]: 22 : else if (LwipRawTcpStream_RxQueueIsFull(self))
708 : : {
709 : 1 : result = ERR_MEM;
710 : : }
711 : : else
712 : : {
713 : 21 : LwipRawTcpStream_EnqueueRxPbuf(self, p);
714 : : }
715 : 24 : return result;
716 : : }
717 : :
718 : : /* Real tcp_sent handling is unused under TCP_WRITE_FLAG_COPY - caller
719 : : * buffers are released at Send return, not at peer-ACK time. The slot
720 : : * exists because lwIP requires the callback set when the pcb is wired. */
721 : 1 : static err_t LwipRawTcpStream_SentCallback(void* arg, struct tcp_pcb* tpcb, u16_t len)
722 : : {
723 : : (void) arg;
724 : : (void) tpcb;
725 : : (void) len;
726 : 1 : return ERR_OK;
727 : : }
728 : :
729 : : /* lwIP fires tcp_err for fatal events (RST, OOM, ABRT) and frees the pcb
730 : : * itself - just before this call for an abort, as soon as it returns for a
731 : : * reset - so we null our Pcb pointer and do NOT call tcp_close. Subsequent
732 : : * Stream_Close sees Pcb == NULL and is a safe no-op. Runs on the lwIP
733 : : * thread, touches no lwIP API. */
734 : 8 : static void LwipRawTcpStream_ErrCallback(void* arg, err_t err)
735 : : {
736 : 8 : struct SolidSyslogLwipRawTcpStream* self = LwipRawTcpStream_SelfFromArg(arg);
737 : 8 : self->Pcb = NULL;
738 : 8 : self->LastError = err;
739 : 8 : self->Errored = true;
740 : 8 : }
741 : :
742 : : #else
743 : :
744 : : /* ISO C forbids an empty translation unit. */
745 : : typedef int LwipRawTcpStream_EmptyTranslationUnit;
746 : :
747 : : #endif /* LWIP_TCP */
|