diff --git a/.clang-tidy b/.clang-tidy index aaa03e93..8582ea96 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -76,7 +76,7 @@ CheckOptions: # alternative. New Tier 2 helper structs join this list; Tier 1 # public tags continue to be subject to StructPrefix above. # See docs/NAMING.md Tier 2 "file-scope struct tags". - - { key: readability-identifier-naming.StructIgnoredRegexp, value: '^(SolidSyslog|EscapedContext|BlockPresence|OpenHandle)$' } + - { key: readability-identifier-naming.StructIgnoredRegexp, value: '^(SolidSyslog|EscapedContext|BlockPresence|OpenHandle|LwipRawDatagramCall|LwipRawTcpStreamCall)$' } # Tier 1 — public enum tags: SolidSyslogClass - { key: readability-identifier-naming.EnumCase, value: CamelCase } diff --git a/CLAUDE.md b/CLAUDE.md index 3e9eec66..542c6df0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -363,6 +363,7 @@ live under `Core/Interface/`; platform-specific helpers (the `SolidSyslogPosix*` | `SolidSyslogPlusTcpTcpStreamErrors.h` | Any code installing an error handler that wants to react to PlusTcpTcpStream-specific events (pointer-identity match on `PlusTcpTcpStreamErrorSource`, switch on `enum SolidSyslogPlusTcpTcpStreamErrors`) | `enum SolidSyslogPlusTcpTcpStreamErrors` (`PLUSTCPTCPSTREAM_ERROR_*` codes + `PLUSTCPTCPSTREAM_ERROR_MAX` bookend), `extern const struct SolidSyslogErrorSource PlusTcpTcpStreamErrorSource`. Integrators ignore if not handling errors per source. | | `SolidSyslogLwipRawTcpStream.h` | System setup code on **any target using lwIP Raw API** wiring a TCP sender (or a byte transport for `SolidSyslogTlsStream` / `SolidSyslogMbedTlsStream`) | `SolidSyslogLwipRawTcpStreamConfig` (`GetConnectTimeoutMs` + context + required `Sleep`), `SolidSyslogLwipRawTcpStream_Create(config)`, `_Destroy(base)` (wraps `tcp_new` / `tcp_connect` / `tcp_write` (`TCP_WRITE_FLAG_COPY`) / `tcp_output` / `tcp_recv` / `tcp_recved` / `tcp_close` / `tcp_abort`; integrator-supplied Sleep drives the bounded synchronous-Open spin loop; sets `SOF_KEEPALIVE` on every pcb; bounded RX pbuf queue via `SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE`; encapsulates lwIP's `tcp_close`-after-`tcp_err` use-after-free rule). Instance struct lives in a library-internal static pool (E11); default pool size 2 to support the TLS-over-plain-TCP pair. Pool-exhaustion and bad-config (`NULL` config or `NULL` Sleep) fallback is the shared `SolidSyslogNullStream`. See [`docs/integrating-lwip.md`](docs/integrating-lwip.md) for the full integrator guide. | | `SolidSyslogLwipRawTcpStreamErrors.h` | Any code installing an error handler that wants to react to LwipRawTcpStream-specific events (pointer-identity match on `LwipRawTcpStreamErrorSource`, switch on `enum SolidSyslogLwipRawTcpStreamErrors`) | `enum SolidSyslogLwipRawTcpStreamErrors` (`LWIPRAWTCPSTREAM_ERROR_*` codes + `LWIPRAWTCPSTREAM_ERROR_MAX` bookend), `extern const struct SolidSyslogErrorSource LwipRawTcpStreamErrorSource`. Integrators ignore if not handling errors per source. | +| `SolidSyslogLwipRawMarshal.h` | System setup code on **any target using lwIP Raw API** that needs to pin lwIP calls to the core-owning thread (`NO_SYS=0` integrators) | `SolidSyslogLwipRawCallback`, `SolidSyslogLwipRawMarshalFunction`, `SolidSyslogLwipRaw_SetMarshal(marshal)`. Routes the lwIP-touching Datagram/TcpStream adapter operations through one marshal hop per public operation (the Resolver's numeric `ipaddr_aton` parse touches no lwIP core state and is intentionally not marshalled). Default is a direct-call null marshal (correct for `NO_SYS=1`); `NO_SYS=0` integrators install a `tcpip_callback_with_block` shim or a `LOCK_TCPIP_CORE`/`UNLOCK_TCPIP_CORE` pair. The marshal MUST call its callback synchronously. Setting `marshal = NULL` restores the default. Single global slot — intended for setup-time configuration, not synchronised with concurrent installs. See [`docs/integrating-lwip.md`](docs/integrating-lwip.md). | | `SolidSyslogWinsockTcpStreamErrors.h` | Any code installing an error handler that wants to react to WinsockTcpStream-specific events (pointer-identity match on `WinsockTcpStreamErrorSource`, switch on `enum SolidSyslogWinsockTcpStreamErrors`) | `enum SolidSyslogWinsockTcpStreamErrors` (`WINSOCKTCPSTREAM_ERROR_*` codes + `WINSOCKTCPSTREAM_ERROR_MAX` bookend), `extern const struct SolidSyslogErrorSource WinsockTcpStreamErrorSource`. Integrators ignore if not handling errors per source. | | `SolidSyslogUdpSender.h` | System setup code using UDP transport | `SolidSyslogUdpSenderConfig` (resolver, datagram, address, endpoint, endpointVersion — the integrator supplies one platform Address handle per sender from `SolidSyslog{Posix,Winsock,PlusTcp}Address_Create`), `SolidSyslogUdpSender_Create(config)`, `_Destroy(sender)`. Instance struct lives in a library-internal static pool (E11); `_Destroy` takes the handle returned by `_Create`. Pool-exhaustion and bad-config (including NULL Address) fallback is the shared `SolidSyslogNullSender`. | | `SolidSyslogUdpSenderErrors.h` | Any code installing an error handler that wants to react to UdpSender-specific events (pointer-identity match on `UdpSenderErrorSource`, switch on `enum SolidSyslogUdpSenderErrors`) | `enum SolidSyslogUdpSenderErrors` (`UDPSENDER_ERROR_*` codes + `UDPSENDER_ERROR_MAX` bookend), `extern const struct SolidSyslogErrorSource UdpSenderErrorSource`. Integrators ignore if not handling errors per source. | diff --git a/DEVLOG.md b/DEVLOG.md index 8b8071b8..db14ff52 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,5 +1,61 @@ # Dev Log +## 2026-05-28 — S28.06 LwipRaw marshal injection + +Sixth story under E28. Added `SolidSyslogLwipRaw_SetMarshal` — a +process-global seam that routes every lwIP Raw API call the adapters make +through one hop per public operation. Default is a direct-call null object +(correct for `NO_SYS=1`); `NO_SYS=0` integrators install a +`tcpip_callback_with_block` shim or a `LOCK_TCPIP_CORE` pair to pin the +calls to the lwIP-owning thread. Same single-global-slot shape as +`SolidSyslog_SetErrorHandler`. Motivation: the S28.05 guide's "marshal at +the SolidSyslog API boundary" advice was wrong — it dragged file I/O, +crypto, the buffer mutex, and the formatter work onto the tcpip thread. The +right boundary is the individual lwIP call. + +The public header carries only the integrator setter; the internal +dispatch `SolidSyslogLwipRaw_Marshal` lives in a private header (David's +call when we paired on the API shape). + +### Slicing deviated from the issue, deliberately + +The issue's slice 1 ("arm the rail, existing tests go red, then green via a +tracking marshal — before any routing") can't actually go green: the rail +flag only flips inside the tracking marshal, which only runs once production +*routes* through the marshal. And `LwipPbufFake` is shared by the Datagram +and TcpStream test exes, so guarding it couples both wrappers into one +commit. Reordered to: (1) marshal TU with its own red/green/refactor; (2) +route both wrappers + arm the rail together, rail as the genuine red; (3) +Resolver no-op note; (4) docs. Same end state, every commit green. Agreed +the reorder with David up front. + +### The rail + +`LwipFakeMarshalGuard` arms every faked lwIP function (`udp_*`, `tcp_*`, +`pbuf_*`) to record a breach if called while no marshal is active. Couldn't +fail at the call site directly — the fakes are C and CppUTest aborts by +throwing/longjmp, which is UB through C frames. So the guard *records* the +first breach (with the fake's `__FILE__`/`__LINE__`) and a teardown +`CheckNoBreach()` fails in safe C++ context, still reporting the exact lwIP +call site. Any future refactor that drops a call site out of its marshal +auto-fails the relevant test. + +### TcpStream Open keeps the spin on the caller's thread + +Open marshals setup+`tcp_connect` in one hop, then the bounded connect spin +(`Sleep` between polls) runs on the *caller's* thread — sleeping the tcpip +thread mid-connect would starve RX/timers — then a `tcp_abort`-on-failure is +a second hop. Send/Read/Close take one hop each; the pure would-block Read +and the no-pcb Close take none. The lwIP callbacks (`connected`/`recv`/ +`err`/`sent`) already run on the tcpip thread and touch no lwIP API, so +they're not marshalled. + +### Resolver + +`ipaddr_aton` is a pure parser touching no lwIP state — left a forward- +looking comment marking where the DnsResolver's `dns_gethostbyname` hop will +belong instead. + ## 2026-05-28 — S24.13 IWYU lanes switched to advisory Pivot story under E24. Started out scoped as "replace IWYU with diff --git a/Platform/LwipRaw/Interface/SolidSyslogLwipRawMarshal.h b/Platform/LwipRaw/Interface/SolidSyslogLwipRawMarshal.h new file mode 100644 index 00000000..9b9f28f5 --- /dev/null +++ b/Platform/LwipRaw/Interface/SolidSyslogLwipRawMarshal.h @@ -0,0 +1,36 @@ +#ifndef SOLIDSYSLOGLWIPRAWMARSHAL_H +#define SOLIDSYSLOGLWIPRAWMARSHAL_H + +#include "ExternC.h" + +EXTERN_C_BEGIN + + /* Every lwIP Raw API call the SolidSyslog wrappers make is routed through + a single marshal hop so an integrator can pin those calls to the thread + that owns the lwIP core. + + NO_SYS=1 (bare metal, no RTOS): the default direct-call marshal is + correct — there is one execution context and no core to protect. + + NO_SYS=0 (RTOS with a tcpip thread): the integrator installs a marshal + that hops onto the tcpip thread — e.g. tcpip_callback_with_block, or a + LOCK_TCPIP_CORE / UNLOCK_TCPIP_CORE pair. + + Contract: the marshal MUST invoke its callback synchronously — before + the marshal function returns. The wrapper reads results the callback + writes immediately after the hop, so an asynchronous marshal is caller + error. tcpip_callback_with_block(.., block=1) honours this; a bare + tcpip_callback(..) does not. */ + typedef void (*SolidSyslogLwipRawCallback)(void* context); + typedef void (*SolidSyslogLwipRawMarshalFunction)(SolidSyslogLwipRawCallback callback, void* context); + + /* Installs a process-global marshal. There is one lwIP instance and one + tcpip thread per process, so a single global slot suffices — same shape + as SolidSyslog_SetErrorHandler. Passing NULL restores the direct-call + default. Intended for setup-time configuration; not synchronised with + concurrent installs. */ + void SolidSyslogLwipRaw_SetMarshal(SolidSyslogLwipRawMarshalFunction marshal); + +EXTERN_C_END + +#endif /* SOLIDSYSLOGLWIPRAWMARSHAL_H */ diff --git a/Platform/LwipRaw/Source/SolidSyslogLwipRawDatagram.c b/Platform/LwipRaw/Source/SolidSyslogLwipRawDatagram.c index d007a3f4..aa66cd9a 100644 --- a/Platform/LwipRaw/Source/SolidSyslogLwipRawDatagram.c +++ b/Platform/LwipRaw/Source/SolidSyslogLwipRawDatagram.c @@ -10,11 +10,25 @@ #include "SolidSyslogDatagram.h" #include "SolidSyslogDatagramDefinition.h" #include "SolidSyslogLwipRawAddressPrivate.h" +#include "SolidSyslogLwipRawMarshalPrivate.h" #include "SolidSyslogNullDatagram.h" #include "SolidSyslogUdpPayload.h" struct SolidSyslogAddress; +/* Per-operation parameters carried across one marshal hop. Only the fields + * the in-flight op needs are set (Open/Close set just Self); SendTo fills the + * rest. One struct per class so the void*-context recovery has a single cast + * site (LwipRawDatagramCallFromContext) — see D.002 in docs/misra-deviations.md. */ +struct LwipRawDatagramCall +{ + struct SolidSyslogLwipRawDatagram* Self; + const struct SolidSyslogLwipRawAddress* Dst; + const void* Buffer; + size_t Size; + enum SolidSyslogDatagramSendResult Result; +}; + static bool LwipRawDatagram_Open(struct SolidSyslogDatagram* base); static void LwipRawDatagram_Close(struct SolidSyslogDatagram* base); static enum SolidSyslogDatagramSendResult LwipRawDatagram_SendTo( @@ -26,7 +40,11 @@ static enum SolidSyslogDatagramSendResult LwipRawDatagram_SendTo( static size_t LwipRawDatagram_MaxPayload(struct SolidSyslogDatagram* base); static inline struct SolidSyslogLwipRawDatagram* LwipRawDatagram_SelfFromBase(struct SolidSyslogDatagram* base); +static inline struct LwipRawDatagramCall* LwipRawDatagramCallFromContext(void* context); static inline bool LwipRawDatagram_IsOpen(const struct SolidSyslogLwipRawDatagram* self); +static void LwipRawDatagram_DoOpen(void* context); +static void LwipRawDatagram_DoClose(void* context); +static void LwipRawDatagram_DoSendTo(void* context); void LwipRawDatagram_Initialise(struct SolidSyslogDatagram* base) { @@ -43,6 +61,15 @@ static inline struct SolidSyslogLwipRawDatagram* LwipRawDatagram_SelfFromBase(st return (struct SolidSyslogLwipRawDatagram*) base; } +/* Recovers the per-op call struct from the void* context the marshal passes + * back into each Do* callback. Single named helper so the void→struct cast + * lives in one place — one suppression site per class, not one per callback + * (the marshal-seam analogue of LwipRawTcpStream_SelfFromArg; see D.002). */ +static inline struct LwipRawDatagramCall* LwipRawDatagramCallFromContext(void* context) +{ + return (struct LwipRawDatagramCall*) context; +} + void LwipRawDatagram_Cleanup(struct SolidSyslogDatagram* base) { LwipRawDatagram_Close(base); @@ -56,21 +83,35 @@ static void LwipRawDatagram_Close(struct SolidSyslogDatagram* base) struct SolidSyslogLwipRawDatagram* self = LwipRawDatagram_SelfFromBase(base); if (LwipRawDatagram_IsOpen(self)) { - udp_remove(self->Pcb); - self->Pcb = NULL; + struct LwipRawDatagramCall call = {.Self = self}; + SolidSyslogLwipRaw_Marshal(LwipRawDatagram_DoClose, &call); } } +static void LwipRawDatagram_DoClose(void* context) +{ + struct SolidSyslogLwipRawDatagram* self = LwipRawDatagramCallFromContext(context)->Self; + udp_remove(self->Pcb); + self->Pcb = NULL; +} + static bool LwipRawDatagram_Open(struct SolidSyslogDatagram* base) { struct SolidSyslogLwipRawDatagram* self = LwipRawDatagram_SelfFromBase(base); if (!LwipRawDatagram_IsOpen(self)) { - self->Pcb = udp_new(); + struct LwipRawDatagramCall call = {.Self = self}; + SolidSyslogLwipRaw_Marshal(LwipRawDatagram_DoOpen, &call); } return LwipRawDatagram_IsOpen(self); } +static void LwipRawDatagram_DoOpen(void* context) +{ + struct SolidSyslogLwipRawDatagram* self = LwipRawDatagramCallFromContext(context)->Self; + self->Pcb = udp_new(); +} + static inline bool LwipRawDatagram_IsOpen(const struct SolidSyslogLwipRawDatagram* self) { return self->Pcb != NULL; @@ -87,20 +128,38 @@ static enum SolidSyslogDatagramSendResult LwipRawDatagram_SendTo( enum SolidSyslogDatagramSendResult result = SOLIDSYSLOG_DATAGRAM_SEND_RESULT_FAILED; if (LwipRawDatagram_IsOpen(self)) { - struct pbuf* p = pbuf_alloc(PBUF_TRANSPORT, (u16_t) size, PBUF_REF); - if (p != NULL) + struct LwipRawDatagramCall call = { + .Self = self, + .Dst = SolidSyslogLwipRawAddress_AsConst(addr), + .Buffer = buffer, + .Size = size, + .Result = SOLIDSYSLOG_DATAGRAM_SEND_RESULT_FAILED, + }; + SolidSyslogLwipRaw_Marshal(LwipRawDatagram_DoSendTo, &call); + result = call.Result; + } + return result; +} + +/* Runs the whole send — pbuf alloc, sendto, free — in one marshalled hop so + * a NO_SYS=0 integrator pays a single tcpip-thread context switch per Send + * rather than three. PBUF_REF points lwIP at the caller's buffer; the buffer + * outlives the synchronous hop, so no copy is needed. */ +static void LwipRawDatagram_DoSendTo(void* context) +{ + struct LwipRawDatagramCall* call = LwipRawDatagramCallFromContext(context); + call->Result = SOLIDSYSLOG_DATAGRAM_SEND_RESULT_FAILED; + struct pbuf* p = pbuf_alloc(PBUF_TRANSPORT, (u16_t) call->Size, PBUF_REF); + if (p != NULL) + { + p->payload = (void*) call->Buffer; + err_t err = udp_sendto(call->Self->Pcb, p, &call->Dst->Ip, call->Dst->Port); + (void) pbuf_free(p); + if (err == ERR_OK) { - const struct SolidSyslogLwipRawAddress* dst = SolidSyslogLwipRawAddress_AsConst(addr); - p->payload = (void*) buffer; - err_t err = udp_sendto(self->Pcb, p, &dst->Ip, dst->Port); - (void) pbuf_free(p); - if (err == ERR_OK) - { - result = SOLIDSYSLOG_DATAGRAM_SEND_RESULT_SENT; - } + call->Result = SOLIDSYSLOG_DATAGRAM_SEND_RESULT_SENT; } } - return result; } static size_t LwipRawDatagram_MaxPayload(struct SolidSyslogDatagram* base) diff --git a/Platform/LwipRaw/Source/SolidSyslogLwipRawMarshal.c b/Platform/LwipRaw/Source/SolidSyslogLwipRawMarshal.c new file mode 100644 index 00000000..4321f67e --- /dev/null +++ b/Platform/LwipRaw/Source/SolidSyslogLwipRawMarshal.c @@ -0,0 +1,27 @@ +#include "SolidSyslogLwipRawMarshalPrivate.h" + +#include + +static void Marshal_NullMarshal(SolidSyslogLwipRawCallback callback, void* context) +{ + callback(context); +} + +static SolidSyslogLwipRawMarshalFunction currentMarshal = Marshal_NullMarshal; + +void SolidSyslogLwipRaw_SetMarshal(SolidSyslogLwipRawMarshalFunction marshal) +{ + if (marshal == NULL) + { + currentMarshal = Marshal_NullMarshal; + } + else + { + currentMarshal = marshal; + } +} + +void SolidSyslogLwipRaw_Marshal(SolidSyslogLwipRawCallback callback, void* context) +{ + currentMarshal(callback, context); +} diff --git a/Platform/LwipRaw/Source/SolidSyslogLwipRawMarshalPrivate.h b/Platform/LwipRaw/Source/SolidSyslogLwipRawMarshalPrivate.h new file mode 100644 index 00000000..42003616 --- /dev/null +++ b/Platform/LwipRaw/Source/SolidSyslogLwipRawMarshalPrivate.h @@ -0,0 +1,17 @@ +#ifndef SOLIDSYSLOGLWIPRAWMARSHALPRIVATE_H +#define SOLIDSYSLOGLWIPRAWMARSHALPRIVATE_H + +#include "ExternC.h" +#include "SolidSyslogLwipRawMarshal.h" + +EXTERN_C_BEGIN + + /* Library-internal dispatch call site. The LwipRaw wrapper classes batch + the lwIP API calls for one public operation into a single callback and + hand it here; we run it through the currently-installed marshal. No NULL + check — the null-object default direct-calls. */ + void SolidSyslogLwipRaw_Marshal(SolidSyslogLwipRawCallback callback, void* context); + +EXTERN_C_END + +#endif /* SOLIDSYSLOGLWIPRAWMARSHALPRIVATE_H */ diff --git a/Platform/LwipRaw/Source/SolidSyslogLwipRawResolver.c b/Platform/LwipRaw/Source/SolidSyslogLwipRawResolver.c index 15aae81e..5bba83a9 100644 --- a/Platform/LwipRaw/Source/SolidSyslogLwipRawResolver.c +++ b/Platform/LwipRaw/Source/SolidSyslogLwipRawResolver.c @@ -42,6 +42,11 @@ static bool LwipRawResolver_Resolve( (void) base; (void) transport; struct SolidSyslogLwipRawAddress* self = SolidSyslogLwipRawAddress_As(result); + /* No SolidSyslogLwipRaw_Marshal hop here: ipaddr_aton is a pure string + * parser that touches no lwIP core state, so it is safe on any thread. + * The sibling SolidSyslogLwipRawDnsResolver (S28.08) WILL call lwIP DNS + * APIs (dns_gethostbyname) and must marshal them — that is where the hop + * belongs, not here. */ bool resolved = ipaddr_aton(host, &self->Ip) != 0; if (resolved) { diff --git a/Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c b/Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c index 40d9cb0c..b85da8a3 100644 --- a/Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c +++ b/Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c @@ -13,6 +13,7 @@ #include "lwip/tcp.h" #include "lwip/tcpbase.h" #include "SolidSyslogLwipRawAddressPrivate.h" +#include "SolidSyslogLwipRawMarshalPrivate.h" #include "SolidSyslogNullStream.h" #include "SolidSyslogStream.h" #include "SolidSyslogTunables.h" @@ -20,6 +21,24 @@ struct SolidSyslogAddress; struct SolidSyslogStream; +/* Per-operation parameters carried across one marshal hop. Each public op + * fills only the fields it needs (Close/Abort set just Self); read-back + * results (ConnectErr / SendResult / ReadResult) are returned in the same + * struct. One struct per class so the void*-context recovery has a single + * cast site (LwipRawTcpStreamCallFromContext) — see D.002 in + * docs/misra-deviations.md. Send/Read fields never overlap in one call. */ +struct LwipRawTcpStreamCall +{ + struct SolidSyslogLwipRawTcpStream* Self; + const struct SolidSyslogLwipRawAddress* Dst; + const void* SendBuffer; + void* ReadBuffer; + size_t Length; + err_t ConnectErr; + bool SendResult; + SolidSyslogSsize ReadResult; +}; + static uint32_t LwipRawTcpStream_NullConnectTimeoutGetter(void* context); static bool LwipRawTcpStream_Open(struct SolidSyslogStream* base, const struct SolidSyslogAddress* addr); @@ -29,22 +48,21 @@ static void LwipRawTcpStream_Close(struct SolidSyslogStream* base); static inline struct SolidSyslogLwipRawTcpStream* LwipRawTcpStream_SelfFromBase(struct SolidSyslogStream* base); static inline struct SolidSyslogLwipRawTcpStream* LwipRawTcpStream_SelfFromArg(void* arg); +static inline struct LwipRawTcpStreamCall* LwipRawTcpStreamCallFromContext(void* context); static inline bool LwipRawTcpStream_ConfigProvidesGetter(const struct SolidSyslogLwipRawTcpStreamConfig* config); static inline bool LwipRawTcpStream_IsOpen(const struct SolidSyslogLwipRawTcpStream* self); static inline bool LwipRawTcpStream_HasQueuedRx(const struct SolidSyslogLwipRawTcpStream* self); static inline bool LwipRawTcpStream_RxQueueIsFull(const struct SolidSyslogLwipRawTcpStream* self); +static inline bool LwipRawTcpStream_HasCloseWork(const struct SolidSyslogLwipRawTcpStream* self); +static inline bool LwipRawTcpStream_HasReadWork(const struct SolidSyslogLwipRawTcpStream* self); +static void LwipRawTcpStream_DoOpenAndConnect(void* context); +static void LwipRawTcpStream_DoAbort(void* context); +static void LwipRawTcpStream_DoSend(void* context); +static void LwipRawTcpStream_DoRead(void* context); +static void LwipRawTcpStream_DoClose(void* context); static struct tcp_pcb* LwipRawTcpStream_OpenAndConfigurePcb(struct SolidSyslogLwipRawTcpStream* self); -static bool LwipRawTcpStream_ConnectOrAbortOnFailure( - struct SolidSyslogLwipRawTcpStream* self, - const struct SolidSyslogAddress* addr -); -static bool LwipRawTcpStream_TryConnect( - struct SolidSyslogLwipRawTcpStream* self, - const struct SolidSyslogAddress* addr -); static bool LwipRawTcpStream_WaitForConnectedCallback(struct SolidSyslogLwipRawTcpStream* self); static uint32_t LwipRawTcpStream_ResolveConnectTimeoutMs(struct SolidSyslogLwipRawTcpStream* self); -static void LwipRawTcpStream_AbortAndForgetPcb(struct SolidSyslogLwipRawTcpStream* self); static bool LwipRawTcpStream_SendOrCloseOnFailure( struct SolidSyslogLwipRawTcpStream* self, const void* buffer, @@ -120,6 +138,14 @@ static inline struct SolidSyslogLwipRawTcpStream* LwipRawTcpStream_SelfFromArg(v return (struct SolidSyslogLwipRawTcpStream*) arg; } +/* Recovers the per-op call struct from the void* context the marshal passes + * back into each Do* callback — the marshal-seam analogue of SelfFromArg, + * one cast site for all marshalled ops (see D.002). */ +static inline struct LwipRawTcpStreamCall* LwipRawTcpStreamCallFromContext(void* context) +{ + return (struct LwipRawTcpStreamCall*) context; +} + void LwipRawTcpStream_Cleanup(struct SolidSyslogStream* base) { LwipRawTcpStream_Close(base); @@ -133,15 +159,42 @@ static bool LwipRawTcpStream_Open(struct SolidSyslogStream* base, const struct S struct SolidSyslogLwipRawTcpStream* self = LwipRawTcpStream_SelfFromBase(base); if (!LwipRawTcpStream_IsOpen(self)) { - self->Pcb = LwipRawTcpStream_OpenAndConfigurePcb(self); + struct LwipRawTcpStreamCall call = {.Self = self, .Dst = SolidSyslogLwipRawAddress_AsConst(addr)}; + SolidSyslogLwipRaw_Marshal(LwipRawTcpStream_DoOpenAndConnect, &call); if (LwipRawTcpStream_IsOpen(self)) { - (void) LwipRawTcpStream_ConnectOrAbortOnFailure(self, addr); + bool connected = false; + if (call.ConnectErr == ERR_OK) + { + connected = LwipRawTcpStream_WaitForConnectedCallback(self); + } + if (!connected) + { + SolidSyslogLwipRaw_Marshal(LwipRawTcpStream_DoAbort, &call); + } } } return LwipRawTcpStream_IsOpen(self); } +/* The setup-and-connect hop: tcp_new + pcb configuration + tcp_connect all + * run on the lwIP-owning thread in one marshalled batch. The connected_cb + * may fire here (synchronously, on the same thread) — it only flips volatile + * flags. The bounded spin that waits for those flags stays on the caller's + * thread (sleeping the lwIP thread mid-connect would starve RX/timers). */ +static void LwipRawTcpStream_DoOpenAndConnect(void* context) +{ + struct LwipRawTcpStreamCall* call = LwipRawTcpStreamCallFromContext(context); + struct SolidSyslogLwipRawTcpStream* self = call->Self; + self->Pcb = LwipRawTcpStream_OpenAndConfigurePcb(self); + if (LwipRawTcpStream_IsOpen(self)) + { + self->Connected = false; + self->Errored = false; + call->ConnectErr = tcp_connect(self->Pcb, &call->Dst->Ip, call->Dst->Port, LwipRawTcpStream_ConnectedCallback); + } +} + static inline bool LwipRawTcpStream_IsOpen(const struct SolidSyslogLwipRawTcpStream* self) { return self->Pcb != NULL; @@ -171,38 +224,11 @@ static struct tcp_pcb* LwipRawTcpStream_OpenAndConfigurePcb(struct SolidSyslogLw return pcb; } -static bool LwipRawTcpStream_ConnectOrAbortOnFailure( - struct SolidSyslogLwipRawTcpStream* self, - const struct SolidSyslogAddress* addr -) -{ - bool connected = LwipRawTcpStream_TryConnect(self, addr); - if (!connected) - { - LwipRawTcpStream_AbortAndForgetPcb(self); - } - return connected; -} - -static bool LwipRawTcpStream_TryConnect(struct SolidSyslogLwipRawTcpStream* self, const struct SolidSyslogAddress* addr) -{ - const struct SolidSyslogLwipRawAddress* dst = SolidSyslogLwipRawAddress_AsConst(addr); - self->Connected = false; - self->Errored = false; - err_t connectErr = tcp_connect(self->Pcb, &dst->Ip, dst->Port, LwipRawTcpStream_ConnectedCallback); - bool ok = false; - if (connectErr == ERR_OK) - { - ok = LwipRawTcpStream_WaitForConnectedCallback(self); - } - return ok; -} - /* Bounded synchronous-Open spin: each iteration sleeps via the * integrator-injected Sleep so lwIP's timer / RX paths get cycles to - * advance the SYN/SYN-ACK exchange. Exits on Connected (success), - * Errored (set by connected_cb on non-ERR_OK or by tcp_err), or - * elapsed >= deadline (timeout). */ + * advance the SYN/SYN-ACK exchange. Runs on the caller's thread — never the + * lwIP thread. Exits on Connected (success), Errored (set by connected_cb + * on non-ERR_OK or by tcp_err), or elapsed >= deadline (timeout). */ static bool LwipRawTcpStream_WaitForConnectedCallback(struct SolidSyslogLwipRawTcpStream* self) { const uint32_t pollMs = (uint32_t) SOLIDSYSLOG_LWIP_RAW_TCP_CONNECT_POLL_MS; @@ -224,8 +250,11 @@ static uint32_t LwipRawTcpStream_ResolveConnectTimeoutMs(struct SolidSyslogLwipR return self->Config.GetConnectTimeoutMs(self->Config.ConnectTimeoutContext); } -static void LwipRawTcpStream_AbortAndForgetPcb(struct SolidSyslogLwipRawTcpStream* self) +/* Connect failed or timed out — release the pcb cleanly on the lwIP thread. + * tcp_abort (not tcp_close) because a half-open pcb has no graceful close. */ +static void LwipRawTcpStream_DoAbort(void* context) { + struct SolidSyslogLwipRawTcpStream* self = LwipRawTcpStreamCallFromContext(context)->Self; tcp_abort(self->Pcb); self->Pcb = NULL; } @@ -236,11 +265,19 @@ static bool LwipRawTcpStream_Send(struct SolidSyslogStream* base, const void* bu bool sent = false; if (LwipRawTcpStream_IsOpen(self)) { - sent = LwipRawTcpStream_SendOrCloseOnFailure(self, buffer, size); + struct LwipRawTcpStreamCall call = {.Self = self, .SendBuffer = buffer, .Length = size}; + SolidSyslogLwipRaw_Marshal(LwipRawTcpStream_DoSend, &call); + sent = call.SendResult; } return sent; } +static void LwipRawTcpStream_DoSend(void* context) +{ + struct LwipRawTcpStreamCall* call = LwipRawTcpStreamCallFromContext(context); + call->SendResult = LwipRawTcpStream_SendOrCloseOnFailure(call->Self, call->SendBuffer, call->Length); +} + static bool LwipRawTcpStream_SendOrCloseOnFailure( struct SolidSyslogLwipRawTcpStream* self, const void* buffer, @@ -280,17 +317,16 @@ static SolidSyslogSsize LwipRawTcpStream_Read(struct SolidSyslogStream* base, vo SolidSyslogSsize result = READ_FAILED; if (LwipRawTcpStream_IsOpen(self)) { - if (LwipRawTcpStream_HasQueuedRx(self)) + if (LwipRawTcpStream_HasReadWork(self)) { - size_t copied = LwipRawTcpStream_DrainHeadBytes(self, buffer, size); - tcp_recved(self->Pcb, (u16_t) copied); - result = (SolidSyslogSsize) copied; - } - else if (self->Errored) - { - /* Peer FIN drained → close internally per the Stream contract - * ("< 0 means EOF AND socket closed internally"). */ - LwipRawTcpStream_ClosePcb(self); + struct LwipRawTcpStreamCall call = { + .Self = self, + .ReadBuffer = buffer, + .Length = size, + .ReadResult = READ_FAILED, + }; + SolidSyslogLwipRaw_Marshal(LwipRawTcpStream_DoRead, &call); + result = call.ReadResult; } else { @@ -300,6 +336,33 @@ static SolidSyslogSsize LwipRawTcpStream_Read(struct SolidSyslogStream* base, vo return result; } +/* Read touches lwIP only when there is queued data to drain/ack or a drained + * peer-FIN to close on. The pure would-block case (open, empty queue, not + * errored) does no lwIP work, so it takes no marshal hop. */ +static inline bool LwipRawTcpStream_HasReadWork(const struct SolidSyslogLwipRawTcpStream* self) +{ + return LwipRawTcpStream_HasQueuedRx(self) || self->Errored; +} + +static void LwipRawTcpStream_DoRead(void* context) +{ + struct LwipRawTcpStreamCall* call = LwipRawTcpStreamCallFromContext(context); + struct SolidSyslogLwipRawTcpStream* self = call->Self; + if (LwipRawTcpStream_HasQueuedRx(self)) + { + size_t copied = LwipRawTcpStream_DrainHeadBytes(self, call->ReadBuffer, call->Length); + tcp_recved(self->Pcb, (u16_t) copied); + call->ReadResult = (SolidSyslogSsize) copied; + } + else + { + /* Peer FIN drained → close internally per the Stream contract + * ("< 0 means EOF AND socket closed internally"); ReadResult stays + * READ_FAILED. */ + LwipRawTcpStream_ClosePcb(self); + } +} + /* Copies up to `size` bytes out of the head pbuf, advancing the read * cursor. When the head is fully drained, pbuf_free's it and advances * the queue head — tail entries shift up through the bounded ring via @@ -346,14 +409,32 @@ static void LwipRawTcpStream_DrainAllQueuedPbufs(struct SolidSyslogLwipRawTcpStr static void LwipRawTcpStream_Close(struct SolidSyslogStream* base) { struct SolidSyslogLwipRawTcpStream* self = LwipRawTcpStream_SelfFromBase(base); - LwipRawTcpStream_ClosePcb(self); + if (LwipRawTcpStream_HasCloseWork(self)) + { + struct LwipRawTcpStreamCall call = {.Self = self}; + SolidSyslogLwipRaw_Marshal(LwipRawTcpStream_DoClose, &call); + } +} + +/* Close touches lwIP only if there is a pcb to close or queued pbufs to + * free. Close-before-open and close-after-tcp_err (Pcb already nulled, queue + * empty) do no lwIP work and take no marshal hop. */ +static inline bool LwipRawTcpStream_HasCloseWork(const struct SolidSyslogLwipRawTcpStream* self) +{ + return LwipRawTcpStream_IsOpen(self) || LwipRawTcpStream_HasQueuedRx(self); +} + +static void LwipRawTcpStream_DoClose(void* context) +{ + LwipRawTcpStream_ClosePcb(LwipRawTcpStreamCallFromContext(context)->Self); } /* tcp_close must NOT be called on a pcb that has already been released by * tcp_err — that's a use-after-free in lwIP. The Pcb != NULL guard works * because LwipRawTcpStream_ErrCallback nulls Pcb when lwIP releases the * pcb on its side. The queue drain runs unconditionally — pbufs we - * accepted via tcp_recv are ours to free regardless of pcb state. */ + * accepted via tcp_recv are ours to free regardless of pcb state. Always + * called from inside a marshalled hop (DoSend / DoRead / DoClose). */ static void LwipRawTcpStream_ClosePcb(struct SolidSyslogLwipRawTcpStream* self) { LwipRawTcpStream_DrainAllQueuedPbufs(self); @@ -382,7 +463,8 @@ static err_t LwipRawTcpStream_ConnectedCallback(void* arg, struct tcp_pcb* pcb, /* tcp_recv fires when lwIP has bytes for us — non-NULL p means a pbuf * arrived; NULL p means peer half-closed (FIN). Backpressure on a full * queue by returning non-ERR_OK; lwIP holds the pbuf and replays the - * callback when the queue drains. */ + * callback when the queue drains. Runs on the lwIP thread (lwIP invokes it + * directly), touches no lwIP API, so it needs no marshalling. */ static err_t LwipRawTcpStream_RecvCallback(void* arg, struct tcp_pcb* tpcb, struct pbuf* p, err_t err) { (void) tpcb; @@ -417,7 +499,8 @@ static err_t LwipRawTcpStream_SentCallback(void* arg, struct tcp_pcb* tpcb, u16_ /* lwIP fires tcp_err for fatal events (RST, OOM, ABRT) AFTER releasing the * pcb upstream — we must null our Pcb pointer and NOT call tcp_close. - * Subsequent Stream_Close sees Pcb == NULL and is a safe no-op. */ + * Subsequent Stream_Close sees Pcb == NULL and is a safe no-op. Runs on the + * lwIP thread, touches no lwIP API. */ static void LwipRawTcpStream_ErrCallback(void* arg, err_t err) { (void) err; diff --git a/Tests/Lwip/CMakeLists.txt b/Tests/Lwip/CMakeLists.txt index fda4909d..528310f2 100644 --- a/Tests/Lwip/CMakeLists.txt +++ b/Tests/Lwip/CMakeLists.txt @@ -31,6 +31,7 @@ set(LWIP_TEST_INCLUDE_DIRS # the FreeRtosFakes pattern (sources live under Tests/Support/LwipFakes/Source/, # pulled inline rather than wrapped as a static lib). set(LWIP_FAKE_SOURCES + ${CMAKE_SOURCE_DIR}/Tests/Support/LwipFakes/Source/LwipFakeMarshalGuard.cpp ${CMAKE_SOURCE_DIR}/Tests/Support/LwipFakes/Source/LwipPbufFake.c ${CMAKE_SOURCE_DIR}/Tests/Support/LwipFakes/Source/LwipTcpFake.c ${CMAKE_SOURCE_DIR}/Tests/Support/LwipFakes/Source/LwipUdpFake.c @@ -122,6 +123,7 @@ add_executable(SolidSyslogLwipRawDatagramTest ${CMAKE_SOURCE_DIR}/Platform/LwipRaw/Source/SolidSyslogLwipRawDatagram.c ${CMAKE_SOURCE_DIR}/Platform/LwipRaw/Source/SolidSyslogLwipRawDatagramMessages.c ${CMAKE_SOURCE_DIR}/Platform/LwipRaw/Source/SolidSyslogLwipRawDatagramStatic.c + ${CMAKE_SOURCE_DIR}/Platform/LwipRaw/Source/SolidSyslogLwipRawMarshal.c ) target_link_libraries(SolidSyslogLwipRawDatagramTest PRIVATE @@ -145,11 +147,13 @@ add_test(NAME SolidSyslogLwipRawDatagramTest COMMAND SolidSyslogLwipRawDatagramT add_executable(SolidSyslogLwipRawTcpStreamTest SolidSyslogLwipRawTcpStreamTest.cpp main.cpp + ${CMAKE_SOURCE_DIR}/Tests/Support/LwipFakes/Source/LwipFakeMarshalGuard.cpp ${CMAKE_SOURCE_DIR}/Tests/Support/LwipFakes/Source/LwipPbufFake.c ${CMAKE_SOURCE_DIR}/Tests/Support/LwipFakes/Source/LwipTcpFake.c ${CMAKE_SOURCE_DIR}/Platform/LwipRaw/Source/SolidSyslogLwipRawAddress.c ${CMAKE_SOURCE_DIR}/Platform/LwipRaw/Source/SolidSyslogLwipRawAddressMessages.c ${CMAKE_SOURCE_DIR}/Platform/LwipRaw/Source/SolidSyslogLwipRawAddressStatic.c + ${CMAKE_SOURCE_DIR}/Platform/LwipRaw/Source/SolidSyslogLwipRawMarshal.c ${CMAKE_SOURCE_DIR}/Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c ${CMAKE_SOURCE_DIR}/Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStreamMessages.c ${CMAKE_SOURCE_DIR}/Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStreamStatic.c @@ -170,3 +174,25 @@ set_target_properties(SolidSyslogLwipRawTcpStreamTest PROPERTIES ) add_test(NAME SolidSyslogLwipRawTcpStreamTest COMMAND SolidSyslogLwipRawTcpStreamTest) + +# ---- SolidSyslogLwipRawMarshalTest ---- + +add_executable(SolidSyslogLwipRawMarshalTest + SolidSyslogLwipRawMarshalTest.cpp + main.cpp + ${CMAKE_SOURCE_DIR}/Platform/LwipRaw/Source/SolidSyslogLwipRawMarshal.c +) + +target_link_libraries(SolidSyslogLwipRawMarshalTest PRIVATE + ${PROJECT_NAME} + CppUTest + CppUTestExt +) + +target_include_directories(SolidSyslogLwipRawMarshalTest PRIVATE ${LWIP_TEST_INCLUDE_DIRS}) + +set_target_properties(SolidSyslogLwipRawMarshalTest PROPERTIES + CXX_CPPCHECK "" +) + +add_test(NAME SolidSyslogLwipRawMarshalTest COMMAND SolidSyslogLwipRawMarshalTest) diff --git a/Tests/Lwip/SolidSyslogLwipRawDatagramTest.cpp b/Tests/Lwip/SolidSyslogLwipRawDatagramTest.cpp index 6c5ec5aa..0f08fd6d 100644 --- a/Tests/Lwip/SolidSyslogLwipRawDatagramTest.cpp +++ b/Tests/Lwip/SolidSyslogLwipRawDatagramTest.cpp @@ -8,6 +8,7 @@ using namespace CososoTesting; #include "ConfigLockFake.h" #include "ErrorHandlerFake.h" +#include "LwipFakeMarshalGuard.h" #include "LwipPbufFake.h" #include "LwipUdpFake.h" #include "SolidSyslogDatagram.h" @@ -69,6 +70,8 @@ TEST_BASE(LwipRawDatagramTestBase) { LwipUdpFake_Reset(); LwipPbufFake_Reset(); + LwipFakeMarshalGuard_Reset(); + SolidSyslogLwipRaw_SetMarshal(LwipFakeMarshalGuard_TrackingMarshal); datagram = SolidSyslogLwipRawDatagram_Create(); address = SolidSyslogLwipRawAddress_Create(); struct SolidSyslogLwipRawAddress* lwipAddress = SolidSyslogLwipRawAddress_As(address); @@ -82,6 +85,8 @@ TEST_BASE(LwipRawDatagramTestBase) SolidSyslogLwipRawDatagram_Destroy(datagram); LONGS_EQUAL_TEXT(0, LwipUdpFake_OutstandingPcbCount(), "leaked udp_pcb past teardown"); LONGS_EQUAL_TEXT(0, LwipPbufFake_OutstandingPbufCount(), "leaked pbuf past teardown"); + LwipFakeMarshalGuard_CheckNoBreach(); + SolidSyslogLwipRaw_SetMarshal(nullptr); } /* SendTo against the shared buffer + address. Default length 1 — most diff --git a/Tests/Lwip/SolidSyslogLwipRawMarshalTest.cpp b/Tests/Lwip/SolidSyslogLwipRawMarshalTest.cpp new file mode 100644 index 00000000..7050c9fa --- /dev/null +++ b/Tests/Lwip/SolidSyslogLwipRawMarshalTest.cpp @@ -0,0 +1,94 @@ +#include "CppUTest/TestHarness.h" + +#include "SolidSyslogLwipRawMarshal.h" +#include "SolidSyslogLwipRawMarshalPrivate.h" + +static void Increment(void* context) +{ + ++*static_cast(context); +} + +static int customMarshalCalls = 0; +static SolidSyslogLwipRawCallback lastMarshalledCallback = nullptr; +static void* lastMarshalledContext = nullptr; + +static void CustomMarshal(SolidSyslogLwipRawCallback callback, void* context) +{ + ++customMarshalCalls; + lastMarshalledCallback = callback; + lastMarshalledContext = context; + callback(context); +} + +// clang-format off +TEST_GROUP(SolidSyslogLwipRawMarshal) +{ + void setup() override + { + customMarshalCalls = 0; + lastMarshalledCallback = nullptr; + lastMarshalledContext = nullptr; + } + + void teardown() override + { + SolidSyslogLwipRaw_SetMarshal(nullptr); + } +}; +// clang-format on + +TEST(SolidSyslogLwipRawMarshal, DefaultMarshalInvokesCallbackOnce) +{ + int calls = 0; + + SolidSyslogLwipRaw_Marshal(Increment, &calls); + + LONGS_EQUAL(1, calls); +} + +TEST(SolidSyslogLwipRawMarshal, InstalledMarshalReceivesTheDispatch) +{ + int calls = 0; + SolidSyslogLwipRaw_SetMarshal(CustomMarshal); + + SolidSyslogLwipRaw_Marshal(Increment, &calls); + + LONGS_EQUAL(1, customMarshalCalls); + LONGS_EQUAL(1, calls); +} + +TEST(SolidSyslogLwipRawMarshal, SetMarshalNullRestoresDirectCallDefault) +{ + int calls = 0; + SolidSyslogLwipRaw_SetMarshal(CustomMarshal); + + SolidSyslogLwipRaw_SetMarshal(nullptr); + SolidSyslogLwipRaw_Marshal(Increment, &calls); + + LONGS_EQUAL(0, customMarshalCalls); + LONGS_EQUAL(1, calls); +} + +TEST(SolidSyslogLwipRawMarshal, EachDispatchIsOneMarshalHop) +{ + int calls = 0; + SolidSyslogLwipRaw_SetMarshal(CustomMarshal); + + SolidSyslogLwipRaw_Marshal(Increment, &calls); + SolidSyslogLwipRaw_Marshal(Increment, &calls); + SolidSyslogLwipRaw_Marshal(Increment, &calls); + + LONGS_EQUAL(3, customMarshalCalls); + LONGS_EQUAL(3, calls); +} + +TEST(SolidSyslogLwipRawMarshal, MarshalForwardsCallbackAndContextUnchanged) +{ + int calls = 0; + SolidSyslogLwipRaw_SetMarshal(CustomMarshal); + + SolidSyslogLwipRaw_Marshal(Increment, &calls); + + FUNCTIONPOINTERS_EQUAL(Increment, lastMarshalledCallback); + POINTERS_EQUAL(&calls, lastMarshalledContext); +} diff --git a/Tests/Lwip/SolidSyslogLwipRawTcpStreamTest.cpp b/Tests/Lwip/SolidSyslogLwipRawTcpStreamTest.cpp index 90f41c5d..910465f4 100644 --- a/Tests/Lwip/SolidSyslogLwipRawTcpStreamTest.cpp +++ b/Tests/Lwip/SolidSyslogLwipRawTcpStreamTest.cpp @@ -10,6 +10,7 @@ using namespace CososoTesting; #include "ConfigLockFake.h" #include "ErrorHandlerFake.h" +#include "LwipFakeMarshalGuard.h" #include "LwipPbufFake.h" #include "LwipTcpFake.h" #include "SolidSyslogLwipRawAddress.h" @@ -117,6 +118,8 @@ TEST_BASE(LwipRawTcpStreamTestBase) LwipPbufFake_Reset(); FakeSleep_Reset(); FakeGetConnectTimeoutMs_Reset(); + LwipFakeMarshalGuard_Reset(); + SolidSyslogLwipRaw_SetMarshal(LwipFakeMarshalGuard_TrackingMarshal); config = {}; config.Sleep = FakeSleep; stream = SolidSyslogLwipRawTcpStream_Create(&config); @@ -132,6 +135,8 @@ TEST_BASE(LwipRawTcpStreamTestBase) SolidSyslogLwipRawTcpStream_Destroy(stream); LONGS_EQUAL_TEXT(0, LwipTcpFake_OutstandingPcbCount(), "leaked tcp_pcb past teardown"); LONGS_EQUAL_TEXT(0, LwipPbufFake_OutstandingPbufCount(), "leaked pbuf past teardown"); + LwipFakeMarshalGuard_CheckNoBreach(); + SolidSyslogLwipRaw_SetMarshal(nullptr); } /* Send through the abstract base against the shared sendBuffer. Default diff --git a/Tests/Support/LwipFakes/Interface/LwipFakeMarshalGuard.h b/Tests/Support/LwipFakes/Interface/LwipFakeMarshalGuard.h new file mode 100644 index 00000000..e2471f45 --- /dev/null +++ b/Tests/Support/LwipFakes/Interface/LwipFakeMarshalGuard.h @@ -0,0 +1,43 @@ +#ifndef LWIPFAKEMARSHALGUARD_H +#define LWIPFAKEMARSHALGUARD_H + +#include + +#include "ExternC.h" +#include "SolidSyslogLwipRawMarshal.h" + +EXTERN_C_BEGIN + + /* Invariant rail proving every lwIP Raw API call the production wrappers + * make happens inside an installed marshal. Each faked lwIP function calls + * LWIP_REQUIRE_MARSHAL_ACTIVE() as its first statement; if the marshal is + * not active the first breach is recorded (with the fake's call site) and + * surfaced by LwipFakeMarshalGuard_CheckNoBreach() at teardown. + * + * Why record-then-check rather than fail at the call site: the fakes are + * C, and CppUTest aborts a test by throwing / longjmp — unwinding through + * C frames is undefined. Recording the breach and failing in the C++ + * teardown is robust, and the captured file/line still points the reader + * straight at the offending lwIP call. */ + extern bool LwipFakeMarshalGuard_Active; + + void LwipFakeMarshalGuard_RequireActive(const char* file, int line); + + /* Resets the rail: clears any recorded breach and disarms the active flag. + * Call from fixture setup before installing the tracking marshal. */ + void LwipFakeMarshalGuard_Reset(void); + + /* Fails the current test (at the recorded fake call site) if any lwIP API + * was called outside a marshalled section. Call from fixture teardown. */ + void LwipFakeMarshalGuard_CheckNoBreach(void); + + /* Marshal that flips the rail flag around its callback, so production lwIP + * calls routed through SolidSyslogLwipRaw_Marshal run with the flag active. + * Fixtures install this via SolidSyslogLwipRaw_SetMarshal. */ + void LwipFakeMarshalGuard_TrackingMarshal(SolidSyslogLwipRawCallback callback, void* context); + +EXTERN_C_END + +#define LWIP_REQUIRE_MARSHAL_ACTIVE() LwipFakeMarshalGuard_RequireActive(__FILE__, __LINE__) + +#endif /* LWIPFAKEMARSHALGUARD_H */ diff --git a/Tests/Support/LwipFakes/Source/LwipFakeMarshalGuard.cpp b/Tests/Support/LwipFakes/Source/LwipFakeMarshalGuard.cpp new file mode 100644 index 00000000..a87d9e27 --- /dev/null +++ b/Tests/Support/LwipFakes/Source/LwipFakeMarshalGuard.cpp @@ -0,0 +1,52 @@ +#include "LwipFakeMarshalGuard.h" + +#include + +#include "CppUTest/TestHarness.h" + +bool LwipFakeMarshalGuard_Active = false; + +static bool guardBreached = false; +static const char* guardBreachFile = nullptr; +static int guardBreachLine = 0; + +void LwipFakeMarshalGuard_RequireActive(const char* file, int line) +{ + if (!LwipFakeMarshalGuard_Active && !guardBreached) + { + guardBreached = true; + guardBreachFile = file; + guardBreachLine = line; + } +} + +void LwipFakeMarshalGuard_Reset(void) +{ + guardBreached = false; + guardBreachFile = nullptr; + guardBreachLine = 0; + LwipFakeMarshalGuard_Active = false; +} + +void LwipFakeMarshalGuard_CheckNoBreach(void) +{ + if (guardBreached) + { + UtestShell::getCurrent()->fail( + "lwIP API called outside the marshal — marshalling rail breach", + guardBreachFile, + (size_t) guardBreachLine + ); + } +} + +void LwipFakeMarshalGuard_TrackingMarshal(SolidSyslogLwipRawCallback callback, void* context) +{ + /* Save/restore rather than unconditionally clearing, so a nested marshal + * (callback re-entering the marshal) leaves the outer scope still active + * on return instead of a false breach. */ + bool wasActive = LwipFakeMarshalGuard_Active; + LwipFakeMarshalGuard_Active = true; + callback(context); + LwipFakeMarshalGuard_Active = wasActive; +} diff --git a/Tests/Support/LwipFakes/Source/LwipPbufFake.c b/Tests/Support/LwipFakes/Source/LwipPbufFake.c index 6f0258a1..df4c781a 100644 --- a/Tests/Support/LwipFakes/Source/LwipPbufFake.c +++ b/Tests/Support/LwipFakes/Source/LwipPbufFake.c @@ -2,6 +2,7 @@ #include +#include "LwipFakeMarshalGuard.h" #include "lwip/arch.h" #include "lwip/pbuf.h" @@ -76,6 +77,7 @@ void LwipPbufFake_NoteIncomingPbuf(void) struct pbuf* pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type) { + LWIP_REQUIRE_MARSHAL_ACTIVE(); ++pbufAllocCallCount; lastAllocLayer = layer; lastAllocLength = length; @@ -97,6 +99,7 @@ struct pbuf* pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type) u8_t pbuf_free(struct pbuf* p) { + LWIP_REQUIRE_MARSHAL_ACTIVE(); (void) p; ++pbufFreeCallCount; --outstandingPbufCount; diff --git a/Tests/Support/LwipFakes/Source/LwipTcpFake.c b/Tests/Support/LwipFakes/Source/LwipTcpFake.c index 7f503b06..32daea92 100644 --- a/Tests/Support/LwipFakes/Source/LwipTcpFake.c +++ b/Tests/Support/LwipFakes/Source/LwipTcpFake.c @@ -2,6 +2,7 @@ #include +#include "LwipFakeMarshalGuard.h" #include "lwip/arch.h" #include "lwip/err.h" #include "lwip/ip_addr.h" @@ -304,6 +305,7 @@ void LwipTcpFake_NotePcbReleasedByErr(void) struct tcp_pcb* tcp_new(void) { + LWIP_REQUIRE_MARSHAL_ACTIVE(); ++tcpNewCallCount; if (tcpNewFails) { @@ -319,6 +321,7 @@ struct tcp_pcb* tcp_new(void) void tcp_arg(struct tcp_pcb* pcb, void* arg) { + LWIP_REQUIRE_MARSHAL_ACTIVE(); (void) pcb; ++tcpArgCallCount; lastCallbackArg = arg; @@ -326,6 +329,7 @@ void tcp_arg(struct tcp_pcb* pcb, void* arg) void tcp_recv(struct tcp_pcb* pcb, tcp_recv_fn recv) { + LWIP_REQUIRE_MARSHAL_ACTIVE(); (void) pcb; ++tcpRecvCallCount; lastRecvFn = recv; @@ -333,6 +337,7 @@ void tcp_recv(struct tcp_pcb* pcb, tcp_recv_fn recv) void tcp_err(struct tcp_pcb* pcb, tcp_err_fn err) { + LWIP_REQUIRE_MARSHAL_ACTIVE(); (void) pcb; ++tcpErrCallCount; lastErrFn = err; @@ -340,6 +345,7 @@ void tcp_err(struct tcp_pcb* pcb, tcp_err_fn err) void tcp_sent(struct tcp_pcb* pcb, tcp_sent_fn sent) { + LWIP_REQUIRE_MARSHAL_ACTIVE(); (void) pcb; ++tcpSentCallCount; lastSentFn = sent; @@ -355,11 +361,15 @@ static inline bool ShouldFireConnectCallback(tcp_connected_fn connected) err_t tcp_connect(struct tcp_pcb* pcb, const ip_addr_t* ipaddr, u16_t port, tcp_connected_fn connected) { + LWIP_REQUIRE_MARSHAL_ACTIVE(); ++tcpConnectCallCount; lastConnectPcb = pcb; lastConnectIpaddr = ipaddr; lastConnectPort = port; lastConnectedFn = connected; + /* The fake fires the connected callback synchronously here, inside the + * marshalled tcp_connect, mirroring production routing the whole + * setup-and-connect through one marshal hop. */ if (ShouldFireConnectCallback(connected)) { (void) connected(lastCallbackArg, pcb, connectCallbackResult); @@ -369,6 +379,7 @@ err_t tcp_connect(struct tcp_pcb* pcb, const ip_addr_t* ipaddr, u16_t port, tcp_ err_t tcp_close(struct tcp_pcb* pcb) { + LWIP_REQUIRE_MARSHAL_ACTIVE(); ++tcpCloseCallCount; lastClosePcb = pcb; if (tcpCloseError == ERR_OK) @@ -380,6 +391,7 @@ err_t tcp_close(struct tcp_pcb* pcb) void tcp_abort(struct tcp_pcb* pcb) { + LWIP_REQUIRE_MARSHAL_ACTIVE(); ++tcpAbortCallCount; lastAbortPcb = pcb; --outstandingPcbCount; @@ -387,6 +399,7 @@ void tcp_abort(struct tcp_pcb* pcb) err_t tcp_write(struct tcp_pcb* pcb, const void* dataptr, u16_t len, u8_t apiflags) { + LWIP_REQUIRE_MARSHAL_ACTIVE(); ++tcpWriteCallCount; lastWritePcb = pcb; lastWriteDataptr = dataptr; @@ -397,6 +410,7 @@ err_t tcp_write(struct tcp_pcb* pcb, const void* dataptr, u16_t len, u8_t apifla err_t tcp_output(struct tcp_pcb* pcb) { + LWIP_REQUIRE_MARSHAL_ACTIVE(); ++tcpOutputCallCount; lastOutputPcb = pcb; return tcpOutputError; @@ -404,6 +418,7 @@ err_t tcp_output(struct tcp_pcb* pcb) void tcp_recved(struct tcp_pcb* pcb, u16_t len) { + LWIP_REQUIRE_MARSHAL_ACTIVE(); ++tcpRecvedCallCount; lastRecvedPcb = pcb; lastRecvedLen = len; diff --git a/Tests/Support/LwipFakes/Source/LwipUdpFake.c b/Tests/Support/LwipFakes/Source/LwipUdpFake.c index fac23e49..4f4bdcf7 100644 --- a/Tests/Support/LwipFakes/Source/LwipUdpFake.c +++ b/Tests/Support/LwipFakes/Source/LwipUdpFake.c @@ -2,6 +2,7 @@ #include +#include "LwipFakeMarshalGuard.h" #include "lwip/arch.h" #include "lwip/err.h" #include "lwip/udp.h" @@ -101,6 +102,7 @@ int LwipUdpFake_OutstandingPcbCount(void) struct udp_pcb* udp_new(void) { + LWIP_REQUIRE_MARSHAL_ACTIVE(); ++udpNewCallCount; if (udpNewFails) { @@ -116,6 +118,7 @@ struct udp_pcb* udp_new(void) void udp_remove(struct udp_pcb* pcb) { + LWIP_REQUIRE_MARSHAL_ACTIVE(); ++udpRemoveCallCount; lastUdpRemovePcb = pcb; --outstandingPcbCount; @@ -123,6 +126,7 @@ void udp_remove(struct udp_pcb* pcb) err_t udp_sendto(struct udp_pcb* pcb, struct pbuf* p, const ip_addr_t* dst_ip, u16_t dst_port) { + LWIP_REQUIRE_MARSHAL_ACTIVE(); ++udpSendtoCallCount; lastSendtoPcb = pcb; lastSendtoPbuf = p; diff --git a/docs/integrating-lwip.md b/docs/integrating-lwip.md index ff822e6c..80299595 100644 --- a/docs/integrating-lwip.md +++ b/docs/integrating-lwip.md @@ -26,7 +26,7 @@ re-teach lwIP — for that, see the | Class | Wraps | Purpose | |---|---|---| | `SolidSyslogLwipRawAddress` | `ip_addr_t` + `u16_t` port | Destination handle the Resolver writes into and the Datagram/TcpStream read from. | -| `SolidSyslogLwipRawResolver` | `ipaddr_aton` | Synchronous numeric IPv4 parsing. Rejects DNS names — they need the future `SolidSyslogLwipRawDnsResolver` (S28.07). | +| `SolidSyslogLwipRawResolver` | `ipaddr_aton` | Synchronous numeric IPv4 parsing. Rejects DNS names — they need the future `SolidSyslogLwipRawDnsResolver` (S28.08). | | `SolidSyslogLwipRawDatagram` | `udp_new` / `udp_sendto` / `udp_remove` | UDP sender. Zero-copy `PBUF_REF` send. | | `SolidSyslogLwipRawTcpStream` | `tcp_new` / `tcp_connect` / `tcp_write` / `tcp_output` / `tcp_recv` / `tcp_recved` / `tcp_close` / `tcp_abort` | TCP byte transport. Bounded synchronous Open. Bounded RX pbuf queue. | @@ -48,10 +48,46 @@ for the TLS side. lwIP supports both threading models. SolidSyslog supports both — the adapter code is the same; the difference is entirely in how *you* -drive lwIP forward. +drive lwIP forward, and which **marshal** you install. + +### The marshal seam + +Every Raw API call the adapters make — `udp_sendto`, `tcp_write`, +`pbuf_alloc`, `tcp_close`, … — is routed through a single hop: + +```c +#include "SolidSyslogLwipRawMarshal.h" + +SolidSyslogLwipRaw_SetMarshal(MyMarshal); /* once, at boot */ +``` + +The default (nothing installed, or `SolidSyslogLwipRaw_SetMarshal(NULL)`) +is a **direct call** — the adapter calls lwIP on the calling thread. That +is exactly right for `NO_SYS=1`. For `NO_SYS=0` you install a marshal that +hops onto the thread owning the lwIP core. + +> **Earlier guidance was wrong.** Prior versions of this guide told you to +> "marshal at the SolidSyslog API boundary" — wrap `SolidSyslog_Service()` +> in `tcpip_callback()`. Don't. That puts file I/O, mbedTLS crypto, the +> CircularBuffer mutex, the StreamSender's formatter work, and the Resolver +> parse — none of which touch lwIP — on the tcpip thread, starving lwIP's +> timer / RX path under load. The correct boundary is the **individual lwIP +> Raw API call**, which is what the marshal seam gives you. + +**Contract.** The marshal MUST invoke its callback *synchronously* — before +the marshal function returns. The adapter reads results the callback wrote +immediately after the hop returns. `tcpip_callback_with_block(.., block=1)` +honours this; a bare `tcpip_callback(..)` does not (it queues and returns). + +One global slot serves the whole process — there is one lwIP instance and +one tcpip thread, so per-instance marshals would be flexibility without use. ### `NO_SYS=1` (bare-metal main-loop) +Install nothing — the default direct-call marshal is correct. There is one +execution context and no core to protect. + + Your `main()` is a forever-loop that, on each pass, calls `sys_check_timeouts()` and drives the RX path (`netif->input()` / `ethernetif_input()` / whichever your BSP wires). @@ -78,38 +114,80 @@ void MyLwipSleep(int milliseconds) Without this, `tcp_connect`'s `connected_cb` never fires (lwIP can't advance its state machine while you sleep), and Open times out. -### `NO_SYS=0` (tcpip thread) +### `NO_SYS=0` (tcpip thread) — option A: `tcpip_callback` -lwIP runs a dedicated `tcpip` thread that owns its state machine; you -post work to it via `tcpip_callback()` or use the BSD Sockets / -NETCONN APIs. +lwIP runs a dedicated `tcpip` thread that owns its state machine. Install a +marshal that posts each adapter callback to it and **blocks until it runs**: -**Threading rule for the Raw API**: every Raw API call from outside -the tcpip thread must be marshalled via `tcpip_callback()` (or via -the `LWIP_TCPIP_CORE_LOCKING_INPUT` lock if you've compiled with core -locking). This applies to every call SolidSyslog's adapters make into -lwIP — Datagram's `udp_sendto`, TcpStream's `tcp_write`, etc. +```c +#include "lwip/tcpip.h" +#include "SolidSyslogLwipRawMarshal.h" -**SolidSyslog does not marshal internally.** That would force every -integrator to compile `tcpip.c` even when they're on `NO_SYS=1`. The -expectation is that *you* call SolidSyslog APIs on the tcpip thread, -either by running your Service loop on it, or by marshalling each -call at the SolidSyslog API boundary: +struct MarshalHop +{ + SolidSyslogLwipRawCallback callback; + void* context; +}; -```c -static void DoService(void* ctx) +static void RunHop(void* ctx) +{ + struct MarshalHop* hop = (struct MarshalHop*) ctx; + hop->callback(hop->context); +} + +void MyTcpipMarshal(SolidSyslogLwipRawCallback callback, void* context) { - SolidSyslog_Service((struct SolidSyslog*) ctx); + /* Re-entry guard: if we are ALREADY on the tcpip thread (e.g. the adapter + * was called from inside a SolidSyslog callback that lwIP itself invoked), + * posting-and-blocking would deadlock. Run directly instead. */ + if (sys_current_task_is_tcpip_thread()) /* your port's predicate */ + { + callback(context); + return; + } + + struct MarshalHop hop = {callback, context}; + /* block=1 makes this synchronous — required by the marshal contract. */ + (void) tcpip_callback_with_block(RunHop, &hop, 1); } -void MyServiceTick(struct SolidSyslog* handle) +/* ... at boot ... */ +SolidSyslogLwipRaw_SetMarshal(MyTcpipMarshal); +``` + +`tcpip_callback_with_block` needs an OS mailbox sized for the blocking post — +ensure `TCPIP_MBOX_SIZE` is adequate. lwIP does not expose a portable +"am I on the tcpip thread?" predicate; most ports compare the current task +handle against the one passed to `tcpip_init`. + +### `NO_SYS=0` (tcpip thread) — option B: core locking + +If you compiled lwIP with `LWIP_TCPIP_CORE_LOCKING=1`, take the core lock +around the hop instead of posting to the mailbox — lower latency, no context +switch: + +```c +#include "lwip/tcpip.h" +#include "SolidSyslogLwipRawMarshal.h" + +void MyCoreLockMarshal(SolidSyslogLwipRawCallback callback, void* context) { - (void) tcpip_callback(DoService, handle); + LOCK_TCPIP_CORE(); + callback(context); + UNLOCK_TCPIP_CORE(); } + +/* ... at boot ... */ +SolidSyslogLwipRaw_SetMarshal(MyCoreLockMarshal); ``` -Your injected `Sleep` callback under `NO_SYS=0` is just a yield — -typically `vTaskDelay(pdMS_TO_TICKS(milliseconds))` on FreeRTOS: +`LOCK_TCPIP_CORE` is a recursive lock on most ports, so this is safe even if +the adapter is reached from a context that already holds it. + +### The `Sleep` callback under `NO_SYS=0` + +TcpStream's bounded-Open spin runs on *your* thread (never the tcpip thread), +so its `Sleep` is just a yield — typically `vTaskDelay` on FreeRTOS: ```c void MyLwipSleep(int milliseconds) @@ -118,8 +196,9 @@ void MyLwipSleep(int milliseconds) } ``` -The tcpip thread runs concurrently and processes the SYN/SYN-ACK -exchange while you yield. +The tcpip thread runs concurrently and processes the SYN/SYN-ACK exchange +while you yield. A worked `NO_SYS=0` + `tcpip_callback` integration ships as +the FreeRtosLwip BDD target (S28.07). --- @@ -134,7 +213,8 @@ features you must enable are flagged. | `LWIP_RAW=1` | **Yes** | The whole point — Raw API. | | `LWIP_UDP=1` | **Yes (Datagram)** | Wraps `udp_*`. | | `LWIP_TCP=1` | **Yes (TcpStream)** | Wraps `tcp_*`. | -| `LWIP_DNS` | No | The current Resolver only parses numeric IPv4 via `ipaddr_aton`. DNS lands in S28.07. | +| `LWIP_DNS` | No | The current Resolver only parses numeric IPv4 via `ipaddr_aton`. DNS lands in S28.08. | +| `LWIP_TCPIP_CORE_LOCKING` | Marshal-dependent | Only needed if you install the core-locking marshal (option B above). The default `tcpip_callback` marshal (option A) does not require it. `NO_SYS=1` never needs it. | | `ARP_QUEUEING=1` | **Recommended** | lwIP default. With it, the first datagram to an unresolved peer is `pbuf_clone`d into PBUF_RAM and queued behind the ARP request — when the reply lands, the packet ships. With `ARP_QUEUEING=0` the first datagram is silently dropped at the IP layer; cold-start logging loses messages. | | `LWIP_TCP_KEEPALIVE=1` | **Recommended** | Without this, the `SOF_KEEPALIVE` bit the adapter sets on every pcb is a no-op. Tune `TCP_KEEPIDLE_DEFAULT` / `TCP_KEEPINTVL_DEFAULT` / `TCP_KEEPCNT_DEFAULT` for your deadline budget. | | `TCP_MSS` | Per-platform | Default `536` (RFC-conservative). Bump to `1460` on Ethernet links if your MTU is 1500 and you want fewer segments per syslog record. | @@ -328,11 +408,11 @@ the `SOLIDSYSLOG_USER_TUNABLES_FILE` CMake variable. - **DNS** — currently out of scope. `SolidSyslogLwipRawResolver` only parses numeric IPv4. `SolidSyslogLwipRawDnsResolver` lands in - S28.07. + S28.08. - **IPv6** — the current Address / Resolver are IPv4-only. - **Multi-`netif` routing** — neither Datagram nor TcpStream selects an output interface; lwIP's routing table decides. - **Jumbo-frame MTU discovery** — `Datagram_MaxPayload` returns `SOLIDSYSLOG_UDP_IPV6_SAFE_PAYLOAD` (1232 bytes) unconditionally. - **BDD coverage** — the FreeRTOS-on-lwIP BDD target arrives in - S28.06. + S28.07. diff --git a/misra_suppressions.txt b/misra_suppressions.txt index ca264641..eca76ef0 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -41,8 +41,8 @@ misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawAddressPrivate.h:24 misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawAddressPrivate.h:31 misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawAddressStatic.c:34 misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawAddressStatic.c:54 -misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawDatagram.c:43 -misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:111 +misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawDatagram.c:61 +misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:129 misra-c2012-11.3:Platform/PlusTcp/Source/SolidSyslogPlusTcpAddress.c:10 misra-c2012-11.3:Platform/PlusTcp/Source/SolidSyslogPlusTcpAddressPrivate.h:19 misra-c2012-11.3:Platform/PlusTcp/Source/SolidSyslogPlusTcpAddressPrivate.h:26 @@ -75,7 +75,9 @@ misra-c2012-11.3:Platform/Windows/Source/SolidSyslogWindowsMutex.c:34 misra-c2012-11.3:Platform/Windows/Source/SolidSyslogWinsockDatagram.c:94 misra-c2012-11.3:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:181 misra-c2012-11.5:Core/Source/SolidSyslogUdpSender.c:205 -misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:120 +misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawDatagram.c:70 +misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:138 +misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:146 misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:271 misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:283 misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockDatagram.c:138 @@ -102,7 +104,7 @@ misra-c2012-11.8:Core/Source/SolidSyslog.c:99 misra-c2012-11.8:Core/Source/SolidSyslog.c:100 misra-c2012-11.8:Core/Source/SolidSyslog.c:101 misra-c2012-11.8:Core/Source/SolidSyslogBlockStoreStatic.c:40 -misra-c2012-11.8:Platform/LwipRaw/Source/SolidSyslogLwipRawDatagram.c:94 +misra-c2012-11.8:Platform/LwipRaw/Source/SolidSyslogLwipRawDatagram.c:155 misra-c2012-11.8:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:93 # D.007 — Rule 21.10: transitive via