Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions Bdd/Targets/FreeRtosLwip/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ oracle.

## Scope

UDP for S28.09. The `SwitchingSender` keeps TCP / TLS slots wired to the shared
`NullSender` so `set transport tcp|tls` resolves cleanly (drops on the floor)
rather than crashing; the real LwipRaw TCP / TLS senders land in later E28
stories. `lwipopts.h` runs `NO_SYS=0` (tcpip thread + the contrib FreeRTOS
`sys_arch`); `FreeRTOSConfig.h` mirrors the +TCP networking config.
UDP (S28.09) + TCP (S28.10). The `SwitchingSender` carries a real UDP sender
(`SolidSyslogLwipRawDatagram`) and a real RFC 6587 octet-framed TCP sender
(`SolidSyslogStreamSender` over `SolidSyslogLwipRawTcpStream`); the TLS slot
stays wired to the shared `NullSender` so `set transport tls` resolves cleanly
(drops on the floor) until S28.11 wires the LwipRaw TLS sender. `lwipopts.h`
runs `NO_SYS=0` (tcpip thread + the contrib FreeRTOS `sys_arch`);
`FreeRTOSConfig.h` mirrors the +TCP networking config.

## Build

Expand Down
44 changes: 37 additions & 7 deletions Bdd/Targets/FreeRtosLwip/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
* drains over UDP, and BddTargetInteractive drives `send N` / `set <k> <v>` /
* `quit` over the QEMU -serial stdio UART.
*
* Scope is UDP this story. The SwitchingSender keeps TCP / TLS slots so
* `set transport tcp|tls` resolves cleanly (to the shared NullSender, which
* drops on the floor) rather than crashing; the real LwipRaw TCP / TLS senders
* land in later E28 stories.
* Scope is UDP (S28.09) + TCP (S28.10). The SwitchingSender carries a real UDP
* sender (LwipRawDatagram) and a real octet-framed TCP sender (StreamSender over
* LwipRawTcpStream); the TLS slot still resolves to the shared NullSender (drops
* on the floor) until S28.11 wires the LwipRaw TLS sender.
*
* Static IPv4 (10.0.2.15) on the QEMU slirp network, host reachable at the
* slirp gateway 10.0.2.2 — numeric, because slirp has no route to the docker
Expand Down Expand Up @@ -42,13 +42,15 @@
#include "SolidSyslogLwipRawDatagram.h"
#include "SolidSyslogLwipRawMarshal.h"
#include "SolidSyslogLwipRawResolver.h"
#include "SolidSyslogLwipRawTcpStream.h"
#include "SolidSyslogMetaSd.h"
#include "SolidSyslogMutex.h"
#include "SolidSyslogNullSender.h"
#include "SolidSyslogNullStore.h"
#include "SolidSyslogOriginSd.h"
#include "SolidSyslogPrival.h"
#include "SolidSyslogStdAtomicCounter.h"
#include "SolidSyslogStreamSender.h"
#include "SolidSyslogSwitchingSender.h"
#include "SolidSyslogTimeQuality.h"
#include "SolidSyslogTimeQualitySd.h"
Expand Down Expand Up @@ -131,6 +133,9 @@ static struct SolidSyslogResolver* resolver = NULL;
static struct SolidSyslogDatagram* datagram = NULL;
static struct SolidSyslogAddress* udpAddress = NULL;
static struct SolidSyslogSender* udpSender = NULL;
static struct SolidSyslogStream* tcpStream = NULL;
static struct SolidSyslogAddress* tcpAddress = NULL;
static struct SolidSyslogSender* tcpSender = NULL;
static struct SolidSyslogSender* switchingSender = NULL;
static struct SolidSyslogBuffer* buffer = NULL;
static struct SolidSyslogMutex* bufferMutex = NULL;
Expand Down Expand Up @@ -492,12 +497,34 @@ static void InteractiveTask(void* argument)
};
udpSender = SolidSyslogUdpSender_Create(&udpConfig);

/* Plain TCP path: RFC 6587 octet-framed StreamSender over the LwipRaw TCP
* stream adapter. Shares the resolver and the UDP endpoint callbacks because
* the syslog-ng oracle listens on the same host:port for both transports.
* RtosSleep drives the stream's bounded synchronous-connect spin; the
* connect timeout comes from the SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS tunable
* (GetConnectTimeoutMs NULL). */
struct SolidSyslogLwipRawTcpStreamConfig tcpStreamConfig = {
.GetConnectTimeoutMs = NULL,
.ConnectTimeoutContext = NULL,
.Sleep = RtosSleep,
};
tcpStream = SolidSyslogLwipRawTcpStream_Create(&tcpStreamConfig);
tcpAddress = SolidSyslogLwipRawAddress_Create();
struct SolidSyslogStreamSenderConfig tcpConfig = {
.Resolver = resolver,
.Stream = tcpStream,
.Address = tcpAddress,
.Endpoint = GetEndpoint,
.EndpointVersion = GetEndpointVersion,
};
tcpSender = SolidSyslogStreamSender_Create(&tcpConfig);

/* SwitchingSender lets `set transport <udp|tcp|tls>` flip transport at
* runtime. UDP is wired; TCP / TLS route to the shared NullSender (drop on
* the floor) until later E28 stories wire the LwipRaw TCP / TLS senders. */
* runtime. UDP and TCP are wired; TLS routes to the shared NullSender (drop
* on the floor) until S28.11 wires the LwipRaw TLS sender. */
static struct SolidSyslogSender* inners[BDD_TARGET_SWITCH_COUNT];
inners[BDD_TARGET_SWITCH_UDP] = udpSender;
inners[BDD_TARGET_SWITCH_TCP] = SolidSyslogNullSender_Get();
inners[BDD_TARGET_SWITCH_TCP] = tcpSender;
inners[BDD_TARGET_SWITCH_TLS] = SolidSyslogNullSender_Get();
struct SolidSyslogSwitchingSenderConfig switchConfig = {
.Senders = inners,
Expand Down Expand Up @@ -608,7 +635,10 @@ static void TeardownAll(void)
lifecycleMutex = NULL;
SolidSyslogSwitchingSender_Destroy(switchingSender);
SolidSyslogUdpSender_Destroy(udpSender);
SolidSyslogStreamSender_Destroy(tcpSender);
SolidSyslogLwipRawTcpStream_Destroy(tcpStream);
SolidSyslogLwipRawAddress_Destroy(udpAddress);
SolidSyslogLwipRawAddress_Destroy(tcpAddress);
SolidSyslogLwipRawDatagram_Destroy(datagram);
SolidSyslogLwipRawResolver_Destroy(resolver);
}
Expand Down
51 changes: 51 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12957,6 +12957,57 @@ MISRA rule — different category, doesn't set precedent.
every untriaged style finding becomes a hard gate. Worth E24
discussion.

## 2026-05-29 — S28.10 FreeRtosLwip TCP (octet-framed StreamSender over LwipRawTcpStream)

### Decisions

- **Mostly wiring — but TCP did not come entirely "for free".** The conditional
#473 asked whether TCP fell out of S28.09; it didn't quite. Plain delivery and
runtime UDP→TCP switching were pure wiring (a `SolidSyslogStreamSender` over
`SolidSyslogLwipRawTcpStream` in the SwitchingSender's TCP slot, sharing the
resolver + endpoint callbacks; `RtosSleep` already matched
`SolidSyslogSleepFunction`; the adapter sources were already compiled in). But
`tcp_reconnect` exposed a real Tier-2 gap (below), so S28.10 earned its keep.

- **`LwipRawTcpStream_Send` now fails on a peer close (the reconnect fix).**
Root cause of the `tcp_reconnect` failure: `StreamSender` only reconnects when a
`Send` returns false (or the endpoint version changes) — it never `Read`s. On a
graceful server FIN (syslog-ng config-reload closes the connection), lwIP's
`RecvCallback(NULL)` set `Errored` but left the pcb non-NULL, and `Send` guarded
only on `IsOpen` (pcb != NULL) — so `tcp_write` kept returning `ERR_OK` into a
doomed half-closed connection and the sender never reconnected. Fix: a new
intent-named predicate `LwipRawTcpStream_IsWritable` (`IsOpen && !Errored`) gates
`Send`, so a post-FIN send fails, `StreamSender` closes + reconnects on the next
message, and the recovered message is delivered. `Read` deliberately still runs
while `Errored` (it must drain queued bytes then close on EOF). Driven red→green
by a new `Tests/Lwip` unit test (`SendReturnsFalseAfterPeerFin`); this matches
FreeRTOS-Plus-TCP's send-side closed-socket detection, which is why the +TCP lane
already passed the same scenario.

- **Lane scope: `(@udp or @tcp)`, TLS still excluded.** The lwIP lane filter went
from `@udp and not @tcp …` to `(@udp or @tcp) and not @tls and not @mtls and not
@store`. That enables `tcp_transport`, `tcp_reconnect`, and the UDP→TCP
`switching_transport` scenario; `tcp_singletask` (`@windows_wip`) and the TCP→TLS
switch (`@tls`) stay excluded. TLS lands in S28.11.

- **Local-run gotcha recorded.** Any `cmake` configure regenerates the in-tree
`Bdd/features/steps/solidsyslog_tunables.py`; an intermediate host `debug` build
(default 2048) clobbered the lwIP target's 512, so a stray oracle run *ran* the
`@requires_message_size_1500` `udp_mtu` scenario (which must skip on a 512-byte
target) and failed spuriously. Re-running the cross-lwip configure restored 512
and the lane is green. CI is immune — each lane downloads its own
`bdd-tunables-<target>` artifact.

### Verification

- Oracle (`ci/docker-compose.bdd.yml`, lwIP pair): **11 features / 28 scenarios /
116 steps passed, 0 failed** — `tcp_transport`, `tcp_reconnect`, and UDP→TCP
switch green alongside the full UDP set; TLS/mTLS/store/1500-MTU correctly
skipped. `Tests/Lwip/SolidSyslogLwipRawTcpStreamTest` 56 tests green. Tree
clang-format clean.

---

## 2026-05-29 — S28.09 FreeRtosLwip LAN9118 netif + NO_SYS=0 UDP on QEMU

### Decisions
Expand Down
13 changes: 12 additions & 1 deletion Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ static inline struct SolidSyslogLwipRawTcpStream* LwipRawTcpStream_SelfFromArg(v
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_IsWritable(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);
Expand Down Expand Up @@ -263,7 +264,7 @@ static bool LwipRawTcpStream_Send(struct SolidSyslogStream* base, const void* bu
{
struct SolidSyslogLwipRawTcpStream* self = LwipRawTcpStream_SelfFromBase(base);
bool sent = false;
if (LwipRawTcpStream_IsOpen(self))
if (LwipRawTcpStream_IsWritable(self))
{
struct LwipRawTcpStreamCall call = {.Self = self, .SendBuffer = buffer, .Length = size};
SolidSyslogLwipRaw_Marshal(LwipRawTcpStream_DoSend, &call);
Expand All @@ -272,6 +273,16 @@ static bool LwipRawTcpStream_Send(struct SolidSyslogStream* base, const void* bu
return sent;
}

/* Sendable means the pcb is live AND no peer close / error has been observed.
* A peer FIN (RecvCallback with NULL p) sets Errored but leaves the pcb non-NULL,
* so IsOpen alone would keep writing into a doomed connection; failing the send
* lets StreamSender close and reconnect. Read deliberately still runs while
* Errored — it must drain queued bytes and then close on EOF. */
static inline bool LwipRawTcpStream_IsWritable(const struct SolidSyslogLwipRawTcpStream* self)
{
return LwipRawTcpStream_IsOpen(self) && !self->Errored;
}

static void LwipRawTcpStream_DoSend(void* context)
{
struct LwipRawTcpStreamCall* call = LwipRawTcpStreamCallFromContext(context);
Expand Down
11 changes: 11 additions & 0 deletions Tests/Lwip/SolidSyslogLwipRawTcpStreamTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,17 @@ TEST(SolidSyslogLwipRawTcpStreamConnected, ReadAdvancesPastDrainedPbufToNextInQu
LONGS_EQUAL(2, LwipPbufFake_PbufFreeCallCount());
}

TEST(SolidSyslogLwipRawTcpStreamConnected, SendReturnsFalseAfterPeerFin)
{
pushPeerFin();

/* A peer half-close (FIN) leaves the pcb alive but the connection doomed.
* Send must report failure — without it the StreamSender keeps writing into
* the dead connection and never reconnects after the server recovers. */
CHECK_FALSE(sendBytes());
CALLED_FAKE(LwipTcpFake_TcpWrite, NEVER);
}

TEST(SolidSyslogLwipRawTcpStreamConnected, ReadReturnsMinusOneAfterPeerFinAndDrainsBeforeEof)
{
struct pbuf p = {};
Expand Down
16 changes: 8 additions & 8 deletions ci/docker-compose.bdd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,13 @@ services:
# solidsyslog_user_tunables.h changes.
command: behave --junit --junit-directory Bdd/junit --tags='not @wip and not @freertoswip and not @rtc and not @windows_wip and (@udp or @tcp or @tls or @mtls)' Bdd/features/

# FreeRTOS + lwIP pair (S28.09). Same shape as the freertos (+TCP) pair —
# its own syslog-ng on its own bridge network, behave sharing the netns so
# QEMU's slirp gateway 10.0.2.2 NATs to the pair's loopback where syslog-ng
# listens on 0.0.0.0:5514. The only differences are EXAMPLE_BINARY (the lwIP
# ELF) and the tag filter: this advisory lane runs UDP only for S28.09
# TCP / TLS / mTLS / store land in later E28 stories, and @freertoslwipwip
# is the per-scenario escape hatch while the netif is bedding in.
# FreeRTOS + lwIP pair (S28.09 UDP, S28.10 TCP). Same shape as the freertos
# (+TCP) pair — its own syslog-ng on its own bridge network, behave sharing the
# netns so QEMU's slirp gateway 10.0.2.2 NATs to the pair's loopback where
# syslog-ng listens on 0.0.0.0:5514. The only differences are EXAMPLE_BINARY
# (the lwIP ELF) and the tag filter: this advisory lane runs UDP + TCP
# TLS / mTLS / store land in later E28 stories, and @freertoslwipwip is the
# per-scenario escape hatch while the netif is bedding in.
syslog-ng-freertos-lwip:
image: balabit/syslog-ng:4.8.2
volumes:
Expand Down Expand Up @@ -178,7 +178,7 @@ services:
# ELF rides the existing freertos driver — only the binary path differs.
- BDD_TARGET=freertos
- EXAMPLE_BINARY=build/freertos-cross-lwip/Bdd/Targets/FreeRtosLwip/SolidSyslogBddTargetLwip.elf
command: behave --junit --junit-directory Bdd/junit --tags='not @wip and not @freertoswip and not @freertoslwipwip and not @rtc and not @windows_wip and @udp and not @tcp and not @tls and not @mtls and not @store' Bdd/features/
command: behave --junit --junit-directory Bdd/junit --tags='not @wip and not @freertoswip and not @freertoslwipwip and not @rtc and not @windows_wip and (@udp or @tcp) and not @tls and not @mtls and not @store' Bdd/features/

volumes:
bdd-output-linux:
Expand Down
6 changes: 3 additions & 3 deletions misra_suppressions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ 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:61
misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:129
misra-c2012-11.3:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:130
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
Expand Down Expand Up @@ -76,8 +76,8 @@ 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/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/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:139
misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:147
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
Expand Down
Loading