diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4a2f481..ae56cb95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -772,6 +772,16 @@ jobs: with: persist-credentials: false + # Platforms are declared in SOLIDSYSLOG_PLATFORM_REGISTRY and roles by + # their Core/Interface/SolidSyslogDefinition.h header. This asserts + # the docs enumerate exactly what the code declares — a platform's pages, + # nav entry, description and matrix row; a role's link from the porting + # guide, the roles index and the nav — and the reverse in both cases. It + # also holds the two boundaries hand review kept losing: no platform names + # another, and every class a platform ships is on its page. + - name: Check the docs match what the code declares + run: python3 scripts/check_platform_docs.py + # Guard the build hooks against regression — the source-link rewrite's # Markdown parsing, and the meta-description map's nav validation. # Image digest below is mkdocs-mkdoxy sha-34173c0 (see docs/containers.md). diff --git a/Bdd/Targets/FreeRtosLwip/README.md b/Bdd/Targets/FreeRtosLwip/README.md index c7ee560c..a40d26ca 100644 --- a/Bdd/Targets/FreeRtosLwip/README.md +++ b/Bdd/Targets/FreeRtosLwip/README.md @@ -64,7 +64,7 @@ so the resolve completes on-device without a DNS server. > timeout branches are unit-tested in > `Tests/Lwip/SolidSyslogLwipRawDnsResolverTest`, consistent with the project's > integration-over-BDD stance for paths the harness can't realistically drive. -> See [`docs/integrating-lwip.md`](../../../docs/integrating-lwip.md#dns). +> See [lwIP setup — DNS](../../../docs/platforms/lwipraw/setup.md#dns). ## Build diff --git a/Bdd/features/mtls_transport.feature b/Bdd/features/mtls_transport.feature index 67583fd0..cc49e5be 100644 --- a/Bdd/features/mtls_transport.feature +++ b/Bdd/features/mtls_transport.feature @@ -1,10 +1,10 @@ @mtls Feature: Mutual TLS message delivery The BDD target authenticates itself to the oracle with a client - certificate over RFC 5425 TLS, exercising mTLS end-to-end. Satisfies - IEC 62443 CR 2.12 (non-repudiation, SL3+) — mutual TLS cryptographically - identifies the sender, while the SIEM detects sequence gaps. Cross-platform: Linux runner uses syslog-ng, - Windows runner uses otelcol-contrib with client_ca_file. + certificate over RFC 5425 TLS, exercising mTLS end-to-end. Mutual TLS + cryptographically identifies the sender, while the SIEM detects sequence + gaps. Cross-platform: Linux runner uses syslog-ng, Windows runner uses + otelcol-contrib with client_ca_file. Scenario: Message delivered over mutual TLS Given the syslog oracle is running diff --git a/CLAUDE.md b/CLAUDE.md index 8fbfd08b..0b531654 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ what matters — it becomes the permanent commit message on `main` on squash mer **Branch protection rules (configured on GitHub):** - Direct pushes to `main` are blocked -- PRs require all status checks to pass before merging: CodeQL, analyze-codeql, analyze-cppcheck, analyze-format, analyze-iwyu, analyze-iwyu-freertos-lwip, analyze-iwyu-freertos-plustcp, analyze-markdown, analyze-tidy, analyze-tidy-freertos-lwip, analyze-tidy-freertos-plustcp, bdd-freertos-qemu-lwip, bdd-freertos-qemu-plustcp, bdd-linux-syslog-ng, bdd-windows-otel, build-freertos-host-tdd-plustcp, build-freertos-target-lwip, build-freertos-target-plustcp, build-linux-clang, build-linux-gcc, build-linux-tunable-override, build-windows-msvc, consumer-smoke-freertos-cross, consumer-smoke-linux, coverage-linux-gcc, docs-build, integration-linux-mbedtls, integration-linux-openssl, integration-windows-openssl, sanitize-linux-gcc, summary, verify-manifest +- PRs require all status checks to pass before merging: CodeQL, analyze-codeql, analyze-cppcheck, analyze-format, analyze-iwyu, analyze-iwyu-freertos-lwip, analyze-iwyu-freertos-plustcp, analyze-markdown, analyze-tidy, analyze-tidy-freertos-lwip, analyze-tidy-freertos-plustcp, bdd-freertos-qemu-lwip, bdd-freertos-qemu-plustcp, bdd-linux-syslog-ng, bdd-windows-otel, build-freertos-host-tdd-plustcp, build-freertos-target-lwip, build-freertos-target-plustcp, build-linux-c89-headers, build-linux-c99, build-linux-clang, build-linux-gcc, build-linux-tunable-override, build-windows-msvc, consumer-smoke-freertos-cross, consumer-smoke-linux, coverage-linux-gcc, docs-build, integration-linux-mbedtls, integration-linux-openssl, integration-windows-openssl, sanitize-linux-gcc, summary, verify-manifest - The `analyze-iwyu*` lanes run `continue-on-error: true` — they are required contexts but advisory in substance, so they report success whatever IWYU finds - Code scanning contributes two contexts and both are required. `analyze-codeql` is the Actions job in `codeql.yml`, and proves the analysis ran; `CodeQL` is the code-scanning results check, and is the one that fails when a PR introduces a new alert. Requiring only the job would let a PR add findings and still merge green - Feeding the `summary` aggregator does **not** make a lane blocking. `summary` is declared `if: always()` and asserts nothing about `needs.*.result`, so a new lane gates merges only once its own context is added to the required list above @@ -427,7 +427,7 @@ Core headers live under `Core/Interface/`; each platform pack puts its own under **Each header's `@file` brief is the authoritative description of what it provides and why.** Read the header rather than a copy of it. For the wider map: -- `docs/roles/index.md` — the twelve roles, each with its vtable contract and the backends +- `docs/roles/index.md` — the roles, each with its vtable contract and the backends that realise it. - `docs/platforms/*.md` — what each platform pack supplies. - `docs/api-reference/` plus the generated Doxygen indexes (`docs/api/files.md`, @@ -500,10 +500,10 @@ format-on-save: - **`InsertBraces: true`** combined with `AllowShortIfStatementsOnASingleLine: Never`, `AllowShortLoopsOnASingleLine: false`, `AllowShortFunctionsOnASingleLine: None`, and - `AllowShortBlocksOnASingleLine: Never` — formatter-side enforcement of **MISRA 15.6** - (the body of an iteration- or selection-statement shall be a compound-statement). clang-format - rewrites your code to add the braces if they are missing, and the `AllowShort*` settings - stop them being collapsed back onto a single line. + `AllowShortBlocksOnASingleLine: Never` — formatter-side enforcement of **MISRA 15.6**, + which is why every `if`, `else`, `for` and `while` body in this project is braced. + clang-format rewrites your code to add the braces if they are missing, and the + `AllowShort*` settings stop them being collapsed back onto a single line. - **`RemoveParentheses: Leave`** — keeps the project **MISRA 12.1 safe**. The advisory rule prefers explicit precedence parentheses; flipping this to `MultipleParentheses` would let clang-format strip them. @@ -513,6 +513,58 @@ See `docs/misra-deviations.md` for the project's stance on MISRA conformance. --- +## Documentation + +Three rules, and they matter more than anything about wording. A wrong claim +costs one edit to fix; a wrong claim that has been copied costs an audit of +every page to find, and the copies rot silently because nothing checks them. + +### Verify before asserting + +Every statement about what the code does is read out of the code, not inferred +from a name, a neighbouring document, or something written earlier in the same +session. This is absolute for **failure modes**: before writing that something +fails, degrades, is silent, or is not reported, open the function and confirm +it. Claims that an error is *not* reported are the ones most often wrong, and +the most damaging, because they push an integrator into defending against a +problem that does not exist. + +A document that is already in the repository is not evidence. It may be the +thing that is wrong. + +### One claim, one place + +Every fact has exactly one home. Everywhere else links to it, or omits it. + +| The fact | Its home | +|---|---| +| What a config field or parameter means, including its edge values | the doc comment on that field | +| What a role's contract requires | that role's `SolidSyslogDefinition.h` | +| What a platform ships, needs, guarantees, and leaves to the integrator | that platform's page under `docs/platforms//` | +| How to wire a platform, and what will catch you out | that platform's `setup.md` | +| What Core does | the Core headers; `docs/core/index.md` curates and links them | +| How to get it building | `docs/build-integration.md` | + +Writing the same sentence on a second page is the signal that it belongs on +neither — find its home, put it there once, and link. Do not restate a fact to +make a page self-contained: self-contained pages are how a set of documents +drifts out of agreement with itself. + +This applies with particular force to the compliance guides, which attract +detail they should not hold. `docs/iec62443.md` is a quick reference that +reassures a developer or a security officer that the library can meet their +needs. It is **not** an audit artefact, and it is not a place to gather role +behaviour, platform behaviour, or catalogues of failure modes. + +### A platform page never describes another platform + +Naming a second platform to contrast behaviour, or to say where a capability +comes from, couples the two: the eleventh platform then has to be added to ten +pages. State this platform's own behaviour completely, and point at the +capability matrix in `docs/platforms/index.md` for who fills what. + +--- + ## Design Patterns These patterns are re-affirmed each time we do a code-hygiene pass. New diff --git a/CMakeLists.txt b/CMakeLists.txt index 7cba08aa..48061b2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,8 +47,8 @@ if(SOLIDSYSLOG_IS_TOP_LEVEL) endif() # Default to C11 (for the optional C11 atomics counter), but let an externally -# supplied standard win so the pre-release C99 portability check can build the -# library at -std=c99 without editing this file. See docs/local-checks.md. +# supplied standard win so the `c99` preset can build the library at -std=c99 +# without editing this file. See docs/builds.md. if(NOT DEFINED CMAKE_C_STANDARD) set(CMAKE_C_STANDARD 11) endif() diff --git a/Core/Interface/SolidSyslogStreamDefinition.h b/Core/Interface/SolidSyslogStreamDefinition.h index f480df16..4a837246 100644 --- a/Core/Interface/SolidSyslogStreamDefinition.h +++ b/Core/Interface/SolidSyslogStreamDefinition.h @@ -17,15 +17,29 @@ SOLIDSYSLOG_EXTERN_C_BEGIN /** The contract a byte-stream transport (TCP, TLS over TCP) fills in; the * library drives it from the servicing pass, so it need not be reentrant. - * Each slot's semantics are the corresponding SolidSyslogStream_* function - * in SolidSyslogStream.h; an implementer must honour them, notably the - * non-blocking bounded behaviour and the close-on-failure lifecycle that - * lets the caller reconnect with a bare Open. */ + * What a caller may expect of each call is documented on the corresponding + * SolidSyslogStream_* function in SolidSyslogStream.h; what follows is the + * same contract as obligations on the implementer. */ struct SolidSyslogStream { + /** Bound the connect, and any handshake above it. One servicing pass + * drives every sender, so a connect that blocks stalls the whole drain, + * not just this stream. Leave nothing open on a failed path: the caller + * retries with a bare Open and never calls Close first. */ bool (*Open)(struct SolidSyslogStream* base, const struct SolidSyslogAddress* addr); + /** All-or-nothing. Never report a partial write as success — the record + * is gone from the caller's hands once you return true. If the whole + * buffer cannot go, close internally and return false; the caller + * reopens and store-and-forward replays. */ bool (*Send)(struct SolidSyslogStream* base, const void* buffer, size_t size); + /** Return 0 when nothing is available, never a negative — the two are + * acted on differently, and a would-block reported as an error costs a + * reconnect on an idle link. Reserve the negative return for a real + * teardown, and close internally before making it. */ SolidSyslogSsize (*Read)(struct SolidSyslogStream* base, void* buffer, size_t size); + /** Idempotent, and leaves the instance reusable — a later Open + * reconnects it. Called on a stream that is already closed, on one that + * never opened, and again from Destroy. */ void (*Close)(struct SolidSyslogStream* base); }; diff --git a/Core/Source/SolidSyslogMacros.h b/Core/Source/SolidSyslogMacros.h index 2eb0be95..6aefbfd0 100644 --- a/Core/Source/SolidSyslogMacros.h +++ b/Core/Source/SolidSyslogMacros.h @@ -1,10 +1,10 @@ #ifndef SOLIDSYSLOGMACROS_H #define SOLIDSYSLOGMACROS_H -/* Compile-time assertion. C++ and C11 have native primitives that carry the - message into the diagnostic; a strict C99 toolchain — the optional - portability target, exercised by the pre-release `c99` preset (see - docs/local-checks.md) — has neither, so it falls back to declaring an array +/* Compile-time assertion. C++11 and C11 have native primitives that carry the + message into the diagnostic; a strict C99 toolchain — the conformance + baseline, built on every pull request by the `build-linux-c99` lane (see + docs/builds.md) — has neither, so it falls back to declaring an array whose length goes negative (a constraint violation every C99 compiler rejects) when cond is false. The fallback uses a fixed name: repeated identical extern declarations in one translation unit are compatible, so no diff --git a/Platform/FreeRtos/Source/SolidSyslogFreeRtosMutex.c b/Platform/FreeRtos/Source/SolidSyslogFreeRtosMutex.c index 1f2867cb..2d4d0e09 100644 --- a/Platform/FreeRtos/Source/SolidSyslogFreeRtosMutex.c +++ b/Platform/FreeRtos/Source/SolidSyslogFreeRtosMutex.c @@ -26,12 +26,12 @@ static inline SemaphoreHandle_t FreeRtosMutex_AsHandle(struct SolidSyslogFreeRto void FreeRtosMutex_Initialise(struct SolidSyslogMutex* base) { struct SolidSyslogFreeRtosMutex* self = FreeRtosMutex_SelfFromBase(base); - /* xSemaphoreCreateMutexStatic returns NULL only when - * configSUPPORT_STATIC_ALLOCATION is not 1 — a compile-time config - * gate, not a runtime failure mode. Guarded anyway so a misconfigured - * integrator falls back to the NullMutex vtable instead of corrupting - * Lock/Unlock with a dangling handle, mirroring PosixMutex's defence - * against pthread_mutex_init failure. */ + /* The storage is ours, so this cannot fail for want of memory; the kernel + * returns NULL only when handed a NULL buffer, which this call never does. + * configSUPPORT_STATIC_ALLOCATION is a compile-time requirement rather than + * a runtime one — without it the function does not exist to call. The + * branch is therefore defensive: an unexpected NULL leaves the NullMutex + * vtable in place rather than a dangling handle in Lock/Unlock. */ if (xSemaphoreCreateMutexStatic(&self->Buffer) != NULL) { self->Base.Lock = FreeRtosMutex_Lock; diff --git a/Platform/FreeRtos/Source/SolidSyslogFreeRtosMutexPrivate.h b/Platform/FreeRtos/Source/SolidSyslogFreeRtosMutexPrivate.h index b0e3eff3..59bb6d2d 100644 --- a/Platform/FreeRtos/Source/SolidSyslogFreeRtosMutexPrivate.h +++ b/Platform/FreeRtos/Source/SolidSyslogFreeRtosMutexPrivate.h @@ -13,8 +13,8 @@ /* xSemaphoreCreateMutexStatic returns a handle that is the same pointer * as the StaticSemaphore_t passed in, so the per-instance struct doesn't - * carry a separate SemaphoreHandle_t — the kernel-primitive layout matches - * the Posix (pthread_mutex_t) and Windows (CRITICAL_SECTION) adapters. */ + * carry a separate SemaphoreHandle_t — the primitive is embedded directly, + * as in every Mutex adapter. */ struct SolidSyslogFreeRtosMutex { struct SolidSyslogMutex Base; diff --git a/Platform/LwipRaw/Interface/SolidSyslogLwipRawDnsResolver.h b/Platform/LwipRaw/Interface/SolidSyslogLwipRawDnsResolver.h index 90107be9..4036bfd3 100644 --- a/Platform/LwipRaw/Interface/SolidSyslogLwipRawDnsResolver.h +++ b/Platform/LwipRaw/Interface/SolidSyslogLwipRawDnsResolver.h @@ -18,7 +18,7 @@ * - Any other immediate rejection, or the deadline elapsing, fails the Resolve * so the caller's unresolved-host error path runs. * - * The transport is ignored. Requires LWIP_DNS=1. See docs/integrating-lwip.md. */ + * The transport is ignored. Requires LWIP_DNS=1. See docs/platforms/lwipraw/setup.md. */ #ifndef SOLIDSYSLOGLWIPRAWDNSRESOLVER_H #define SOLIDSYSLOGLWIPRAWDNSRESOLVER_H diff --git a/Platform/LwipRaw/Interface/SolidSyslogLwipRawMarshal.h b/Platform/LwipRaw/Interface/SolidSyslogLwipRawMarshal.h index 7ea714cc..99f71457 100644 --- a/Platform/LwipRaw/Interface/SolidSyslogLwipRawMarshal.h +++ b/Platform/LwipRaw/Interface/SolidSyslogLwipRawMarshal.h @@ -10,13 +10,15 @@ * - NO_SYS=1 (bare metal, no RTOS): the default direct-call marshal is correct * — one execution context, no core to protect. * - NO_SYS=0 (RTOS with a tcpip thread): the integrator installs a marshal that - * hops onto that thread — e.g. tcpip_callback_with_block, or a - * LOCK_TCPIP_CORE / UNLOCK_TCPIP_CORE pair. + * hops onto that thread — a LOCK_TCPIP_CORE / UNLOCK_TCPIP_CORE pair, or a + * mailbox post that waits for the callback to run. * * The marshal MUST invoke its callback synchronously, before it 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. See docs/integrating-lwip.md. */ + * asynchronous marshal is caller error. Core locking satisfies this directly. + * A mailbox post does not on its own — tcpip_callback_with_block(.., block=1) + * blocks until the message is accepted, not until it is executed — so such a + * marshal must wait for completion itself. See docs/platforms/lwipraw/setup.md. */ #ifndef SOLIDSYSLOGLWIPRAWMARSHAL_H #define SOLIDSYSLOGLWIPRAWMARSHAL_H diff --git a/Platform/LwipRaw/Interface/SolidSyslogLwipRawTcpStream.h b/Platform/LwipRaw/Interface/SolidSyslogLwipRawTcpStream.h index 7e6192d2..0215ac15 100644 --- a/Platform/LwipRaw/Interface/SolidSyslogLwipRawTcpStream.h +++ b/Platform/LwipRaw/Interface/SolidSyslogLwipRawTcpStream.h @@ -1,6 +1,6 @@ /** @file * A TCP stream over the lwIP Raw API, for a StreamSender or as the byte - * transport under a TlsStream. + * transport under a TLS stream. * * Every lwIP call runs under the SolidSyslogLwipRaw_Marshal hop; the callbacks * lwIP fires back (connected / recv / err) only flip flags, so they stay @@ -28,7 +28,7 @@ * callback nulls the pcb pointer, and Close only calls tcp_close when the * pointer is still live, so a released pcb is never closed twice. Accepted * pbufs are always freed on close regardless of pcb state. See - * docs/integrating-lwip.md for the full integrator guide. */ + * docs/platforms/lwipraw/setup.md for the full integrator guide. */ #ifndef SOLIDSYSLOGLWIPRAWTCPSTREAM_H #define SOLIDSYSLOGLWIPRAWTCPSTREAM_H diff --git a/Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c b/Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c index d0d4e282..29e9640c 100644 --- a/Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c +++ b/Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c @@ -339,7 +339,7 @@ static bool LwipRawTcpStream_OutputResultIsAcceptable(err_t outputErr) static SolidSyslogSsize LwipRawTcpStream_Read(struct SolidSyslogStream* base, void* buffer, size_t size) { /* SolidSyslogStream_Read returns < 0 to signal EOF/error (socket closed - * internally); -1 is the in-tree convention shared with Posix/Winsock/PlusTcp. */ + * internally); -1 is the in-tree convention across the Stream adapters. */ static const SolidSyslogSsize READ_FAILED = -1; struct SolidSyslogLwipRawTcpStream* self = LwipRawTcpStream_SelfFromBase(base); diff --git a/Platform/MbedTls/Interface/SolidSyslogMbedTlsAesGcmPolicy.h b/Platform/MbedTls/Interface/SolidSyslogMbedTlsAesGcmPolicy.h index 0f3a8f10..e0110d83 100644 --- a/Platform/MbedTls/Interface/SolidSyslogMbedTlsAesGcmPolicy.h +++ b/Platform/MbedTls/Interface/SolidSyslogMbedTlsAesGcmPolicy.h @@ -33,7 +33,7 @@ SOLIDSYSLOG_EXTERN_C_BEGIN void* KeyContext; /**< Passed back to GetKey unchanged; NULL is fine. */ struct mbedtls_ctr_drbg_context* Rng; /**< Seeded CTR-DRBG each record's 12-byte nonce is drawn from; required and caller-owned. Injected because mbedTLS has no - context-free RNG, unlike the OpenSSL sibling's RAND_bytes. */ + context-free RNG. */ }; /** Draw an AES-GCM policy from the pool. Bad config (NULL GetKey or NULL Rng) diff --git a/Platform/MbedTls/Interface/SolidSyslogMbedTlsStream.h b/Platform/MbedTls/Interface/SolidSyslogMbedTlsStream.h index ca8ebff1..bd15816e 100644 --- a/Platform/MbedTls/Interface/SolidSyslogMbedTlsStream.h +++ b/Platform/MbedTls/Interface/SolidSyslogMbedTlsStream.h @@ -1,8 +1,8 @@ /** @file * TLS over an injected byte-transport Stream via Mbed TLS, itself a Stream — so * a StreamSender speaks TLS to a remote collector without knowing the transport - * underneath (PosixTcpStream, PlusTcpTcpStream, or any caller-supplied byte - * Stream). + * underneath, whether a TCP stream from a platform pack or one the caller + * supplies. * * What the stream does through its vtable is the substance: * @@ -27,7 +27,7 @@ * ssl_config / ssl_context state and never calls process-global mbedTLS APIs * (platform setup/teardown, psa_crypto_init, threading-alt, debug hooks), so it * drops into an integrator process that already uses Mbed TLS elsewhere. See - * docs/integrating-mbedtls.md. */ + * docs/platforms/mbedtls/setup.md. */ #ifndef SOLIDSYSLOGMBEDTLSSTREAM_H #define SOLIDSYSLOGMBEDTLSSTREAM_H diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c index 5a380091..d80e80ec 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c @@ -149,7 +149,7 @@ static bool MbedTlsAesGcmPolicy_FetchKey(struct SolidSyslogMbedTlsAesGcmPolicy* * adjacent as same-typed scalar parameters; the trailer/header layout lives in * one place. The nonce is expected already in Trailer[0..GCM_NONCE_SIZE). One- * shot AEAD — mbedTLS computes the whole tag in a single call (output == input - * is permitted for GCM encryption), unlike OpenSSL's incremental EVP chain. */ + * is permitted for GCM encryption). */ static bool MbedTlsAesGcmPolicy_GcmEncrypt(const struct SolidSyslogSecurityRecord* record, const uint8_t* key) { uint8_t* body = &record->Content[record->HeaderLength]; diff --git a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c index 62f5a866..5c901340 100644 --- a/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c +++ b/Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c @@ -100,8 +100,8 @@ static inline struct SolidSyslogMbedTlsStream* MbedTlsStream_SelfFromBase(struct void MbedTlsStream_Cleanup(struct SolidSyslogStream* base) { - /* Mirror the OpenSslStream pattern: an integrator who destroys a - * still-Open stream must not leak the underlying TLS state. */ + /* An integrator who destroys a still-Open stream must not leak the + * underlying TLS state. */ MbedTlsStream_Close(base); /* Overwrite the abstract base with the shared NullStream vtable so * use-after-destroy is a safe no-op rather than a NULL-fn-pointer crash. */ @@ -168,7 +168,8 @@ static inline void MbedTlsStream_ApplyTlsPolicy(struct SolidSyslogMbedTlsStream* mbedtls_ssl_conf_authmode(&self->SslConfig, MBEDTLS_SSL_VERIFY_REQUIRED); /* Pin the floor at TLS 1.2 rather than inheriting MBEDTLS_SSL_PRESET_DEFAULT, * which can negotiate down to TLS 1.0/1.1 on permissive integrator builds. - * Matches the OpenSSL adapter's explicit floor (downgrade-resistance parity). */ + * The floor is stated here so downgrade resistance does not depend on the + * preset the integrator happens to have compiled in. */ mbedtls_ssl_conf_min_tls_version(&self->SslConfig, MBEDTLS_SSL_VERSION_TLS1_2); mbedtls_ssl_conf_ca_chain(&self->SslConfig, self->Config.CaChain, NULL); mbedtls_ssl_conf_rng(&self->SslConfig, mbedtls_ctr_drbg_random, self->Config.Rng); @@ -240,7 +241,7 @@ static inline void MbedTlsStream_InstallTransportCallbacks(struct SolidSyslogMbe * spin) until either the handshake completes, hits a hard error, or the * bounded budget expires. Each non-success exit emits a distinct * protocol-level error code so the integrator can tell rejection from - * timeout. Same shape as OpenSslStream_PerformHandshake. */ + * timeout. */ static inline bool MbedTlsStream_PerformHandshake(struct SolidSyslogMbedTlsStream* self) { uint32_t budgetMs = MbedTlsStream_ResolveHandshakeTimeoutMs(self); @@ -327,8 +328,8 @@ static int MbedTlsStream_BioRecv(void* ctx, unsigned char* buf, size_t len) } /* TLS-level write failure means the session state is unrecoverable — close - * so the StreamSender reconnect path runs on the next tick. Mirrors the - * OpenSslStream_Send fail-fast contract. */ + * so the StreamSender reconnect path runs on the next tick. Fail-fast is the + * contract every TLS stream adapter honours. */ static inline bool MbedTlsStream_Send(struct SolidSyslogStream* base, const void* buffer, size_t size) { struct SolidSyslogMbedTlsStream* self = MbedTlsStream_SelfFromBase(base); @@ -346,8 +347,7 @@ static inline bool MbedTlsStream_Send(struct SolidSyslogStream* base, const void * right now → WANT_READ → return 0, mirroring the transport contract. * 2. Any other negative return (alerts, renegotiation surfacing as * WANT_WRITE, hard transport error) is fatal under fail-fast semantics - * — close internally; the caller reopens and store-and-forward replays. - * Same shape as the OpenSslStream_Read. */ + * — close internally; the caller reopens and store-and-forward replays. */ static inline SolidSyslogSsize MbedTlsStream_Read(struct SolidSyslogStream* base, void* buffer, size_t size) { struct SolidSyslogMbedTlsStream* self = MbedTlsStream_SelfFromBase(base); diff --git a/Platform/PlusTcp/Interface/SolidSyslogPlusTcpTcpStream.h b/Platform/PlusTcp/Interface/SolidSyslogPlusTcpTcpStream.h index 169d4726..2d86300c 100644 --- a/Platform/PlusTcp/Interface/SolidSyslogPlusTcpTcpStream.h +++ b/Platform/PlusTcp/Interface/SolidSyslogPlusTcpTcpStream.h @@ -1,6 +1,6 @@ /** @file * A TCP stream over a FreeRTOS-Plus-TCP socket, for a StreamSender or as the - * byte transport under a TlsStream. + * byte transport under a TLS stream. * * What the stream does through its vtable is the substance: * diff --git a/Platform/PlusTcp/Source/SolidSyslogPlusTcpDatagram.c b/Platform/PlusTcp/Source/SolidSyslogPlusTcpDatagram.c index f505d603..45b87acc 100644 --- a/Platform/PlusTcp/Source/SolidSyslogPlusTcpDatagram.c +++ b/Platform/PlusTcp/Source/SolidSyslogPlusTcpDatagram.c @@ -109,8 +109,8 @@ static enum SolidSyslogDatagramSendResult PlusTcpDatagram_SendTo( } /* FreeRTOS-Plus-TCP does not queue datagrams while ARP resolves: a sendto to - * an unresolved peer drops at the IP layer. Linux/Windows kernels mask this - * with internal ARP queuing; FreeRTOS does not. So on cache miss we issue a + * an unresolved peer drops at the IP layer. Host-class kernels mask this + * with internal ARP queuing; this stack does not. So on cache miss we issue a * probe and yield once for the reply to land. If the reply hasn't arrived in * time the sendto is allowed to fail or be dropped — UDP is best-effort and * retry belongs in the store-and-forward layer above, not here. */ diff --git a/Platform/PlusTcp/Source/SolidSyslogPlusTcpTcpStream.c b/Platform/PlusTcp/Source/SolidSyslogPlusTcpTcpStream.c index 0c86ecd6..81877111 100644 --- a/Platform/PlusTcp/Source/SolidSyslogPlusTcpTcpStream.c +++ b/Platform/PlusTcp/Source/SolidSyslogPlusTcpTcpStream.c @@ -29,7 +29,7 @@ const struct SolidSyslogErrorSource PlusTcpTcpStreamErrorSource = {"PlusTcpTcpSt struct SolidSyslogAddress; /* SolidSyslogStream_Read returns < 0 to signal EOF/error (socket closed - * internally); -1 is the in-tree convention shared with Posix/Winsock. */ + * internally); -1 is the in-tree convention across the Stream adapters. */ static const SolidSyslogSsize READ_FAILED = -1; enum @@ -215,7 +215,7 @@ static uint32_t PlusTcpTcpStream_ResolveConnectTimeoutMs(struct SolidSyslogPlusT * before FreeRTOS_connect runs. Without this, the cold-start SYN is dropped * at the IP layer (FreeRTOS-Plus-TCP does not queue while ARP resolves) and * the bounded 200 ms connect timeout expires before the SYN-and-resend - * cycle completes. Symmetric with SolidSyslogPlusTcpDatagram::SendTo. */ + * cycle completes. Symmetric with PlusTcpDatagram_SendTo. */ static inline void PlusTcpTcpStream_PrimeArpIfMissing(uint32_t ip) { /* Yield window for the IP task to receive an ARP reply and populate diff --git a/Platform/Posix/Interface/SolidSyslogPosixTcpStream.h b/Platform/Posix/Interface/SolidSyslogPosixTcpStream.h index d443d2cf..d5d5f7c9 100644 --- a/Platform/Posix/Interface/SolidSyslogPosixTcpStream.h +++ b/Platform/Posix/Interface/SolidSyslogPosixTcpStream.h @@ -1,6 +1,6 @@ /** @file * A non-blocking TCP stream over a POSIX socket, for a StreamSender or as the - * byte transport under a TlsStream. + * byte transport under a TLS stream. * * What the stream does through its vtable is the substance: * diff --git a/Platform/Windows/Interface/SolidSyslogWinsockTcpStream.h b/Platform/Windows/Interface/SolidSyslogWinsockTcpStream.h index cbb8c08b..3126d493 100644 --- a/Platform/Windows/Interface/SolidSyslogWinsockTcpStream.h +++ b/Platform/Windows/Interface/SolidSyslogWinsockTcpStream.h @@ -1,6 +1,6 @@ /** @file * A non-blocking TCP stream over a Winsock socket, for a StreamSender or as the - * byte transport under a TlsStream. + * byte transport under a TLS stream. * * What the stream does through its vtable is the substance: * diff --git a/Platform/Windows/Source/SolidSyslogWindowsClockInternal.h b/Platform/Windows/Source/SolidSyslogWindowsClockInternal.h index 97cfdff8..7007f581 100644 --- a/Platform/Windows/Source/SolidSyslogWindowsClockInternal.h +++ b/Platform/Windows/Source/SolidSyslogWindowsClockInternal.h @@ -3,7 +3,7 @@ /* Library-internal test seam. Tests replace this function pointer via CppUTest's UT_PTR_SET to inject a fake FILETIME source (MSVC does not - support GCC's weak/strong symbol override trick used by the POSIX fakes). */ + support GCC's weak/strong symbol override trick the fakes rely on). */ #include "SolidSyslogExternC.h" diff --git a/Platform/Windows/Source/SolidSyslogWindowsFile.c b/Platform/Windows/Source/SolidSyslogWindowsFile.c index 3f78fbf9..a67dd83a 100644 --- a/Platform/Windows/Source/SolidSyslogWindowsFile.c +++ b/Platform/Windows/Source/SolidSyslogWindowsFile.c @@ -79,10 +79,6 @@ static inline struct SolidSyslogWindowsFile* WindowsFile_SelfFromBase(struct Sol static bool WindowsFile_Open(struct SolidSyslogFile* base, const char* path) { - /* _sopen_s is the non-deprecated MSVC equivalent of POSIX open(): the - * plain _open triggers C4996 (Microsoft's safe-CRT preference) and - * _CRT_SECURE_NO_WARNINGS is forbidden by the project's banned-API - * policy. _SH_DENYNO matches POSIX open()'s default of no share mode. */ struct SolidSyslogWindowsFile* self = WindowsFile_SelfFromBase(base); errno_t err = _sopen_s(&self->Fd, path, DEFAULT_OPEN_FLAGS, _SH_DENYNO, DEFAULT_FILE_PERMISSIONS); if (err != 0) diff --git a/Platform/Windows/Source/SolidSyslogWindowsHostnameInternal.h b/Platform/Windows/Source/SolidSyslogWindowsHostnameInternal.h index f529c1ed..ab6a2887 100644 --- a/Platform/Windows/Source/SolidSyslogWindowsHostnameInternal.h +++ b/Platform/Windows/Source/SolidSyslogWindowsHostnameInternal.h @@ -3,7 +3,7 @@ /* Library-internal test seam. Tests replace this function pointer via CppUTest's UT_PTR_SET to inject a fake hostname source (MSVC does not - support GCC's weak/strong symbol override trick used by the POSIX fakes). */ + support GCC's weak/strong symbol override trick the fakes rely on). */ #include "SolidSyslogExternC.h" diff --git a/Platform/Windows/Source/SolidSyslogWindowsProcessIdInternal.h b/Platform/Windows/Source/SolidSyslogWindowsProcessIdInternal.h index d7b69fe7..0af0df41 100644 --- a/Platform/Windows/Source/SolidSyslogWindowsProcessIdInternal.h +++ b/Platform/Windows/Source/SolidSyslogWindowsProcessIdInternal.h @@ -3,7 +3,7 @@ /* Library-internal test seam. Tests replace this function pointer via CppUTest's UT_PTR_SET to inject a deterministic PID (MSVC does not - support GCC's weak/strong symbol override trick used by the POSIX fakes). */ + support GCC's weak/strong symbol override trick the fakes rely on). */ #include "SolidSyslogExternC.h" diff --git a/Platform/Windows/Source/SolidSyslogWindowsSysUpTimeInternal.h b/Platform/Windows/Source/SolidSyslogWindowsSysUpTimeInternal.h index 85401bd4..2026d15b 100644 --- a/Platform/Windows/Source/SolidSyslogWindowsSysUpTimeInternal.h +++ b/Platform/Windows/Source/SolidSyslogWindowsSysUpTimeInternal.h @@ -3,7 +3,7 @@ /* Library-internal test seam. Tests replace this function pointer via CppUTest's UT_PTR_SET to inject a fake tick source (MSVC does not - support GCC's weak/strong symbol override trick used by the POSIX fakes). */ + support GCC's weak/strong symbol override trick the fakes rely on). */ #include "SolidSyslogExternC.h" diff --git a/Platform/Windows/Source/SolidSyslogWinsockDatagramInternal.h b/Platform/Windows/Source/SolidSyslogWinsockDatagramInternal.h index 3264799c..2b479990 100644 --- a/Platform/Windows/Source/SolidSyslogWinsockDatagramInternal.h +++ b/Platform/Windows/Source/SolidSyslogWinsockDatagramInternal.h @@ -3,7 +3,7 @@ /* Library-internal test seam. Tests replace these function pointers via CppUTest's UT_PTR_SET to inject fakes (MSVC does not support GCC's - weak/strong symbol override trick used by the POSIX SocketFake). */ + weak/strong symbol override trick the SocketFake relies on). */ #include "SolidSyslogExternC.h" diff --git a/Platform/Windows/Source/SolidSyslogWinsockResolverInternal.h b/Platform/Windows/Source/SolidSyslogWinsockResolverInternal.h index e219b7ca..6fa04283 100644 --- a/Platform/Windows/Source/SolidSyslogWinsockResolverInternal.h +++ b/Platform/Windows/Source/SolidSyslogWinsockResolverInternal.h @@ -3,7 +3,7 @@ /* Library-internal test seam. Tests replace these function pointers via CppUTest's UT_PTR_SET to inject fakes (MSVC does not support GCC's - weak/strong symbol override trick used by the POSIX SocketFake). */ + weak/strong symbol override trick the SocketFake relies on). */ #include "SolidSyslogExternC.h" diff --git a/Platform/Windows/Source/SolidSyslogWinsockTcpStream.c b/Platform/Windows/Source/SolidSyslogWinsockTcpStream.c index 5cd53fe4..5784a015 100644 --- a/Platform/Windows/Source/SolidSyslogWinsockTcpStream.c +++ b/Platform/Windows/Source/SolidSyslogWinsockTcpStream.c @@ -110,10 +110,9 @@ enum but POSIX-portable callers must pass the highest fd + 1. Pass any positive value to keep the call well-formed against either ABI. */ WINSOCK_NFDS_IGNORED = 1, - /* Keepalive parameters mirror the POSIX TCP stream so the dead-peer - detection window is the same on both platforms: idle 45 + 4 * 10 = 85 s - worst case. Windows has no TCP_USER_TIMEOUT analogue, so the pending- - write case relies on the OS-default retransmit timeout. */ + /* Dead-peer detection window: idle 45 + 4 * 10 = 85 s worst case. + Windows has no TCP_USER_TIMEOUT analogue, so the pending-write case + relies on the OS-default retransmit timeout. */ KEEPALIVE_IDLE_SECONDS = 45, KEEPALIVE_INTERVAL_SECONDS = 10, KEEPALIVE_PROBE_COUNT = 4 @@ -244,9 +243,9 @@ static void WinsockTcpStream_EnableTcpNoDelay(SOCKET fd) WinsockTcpStream_setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (const char*) &enable, (int) sizeof(enable)); } -/* Mirrors the POSIX WinsockTcpStream_EnableKeepalive — Windows 10 1709+ exposes TCP_KEEPIDLE / - * TCP_KEEPINTVL / TCP_KEEPCNT via setsockopt (declared in ), so the - * shape matches the POSIX path one-for-one. No TCP_USER_TIMEOUT analogue. */ +/* Windows 10 1709+ exposes TCP_KEEPIDLE / TCP_KEEPINTVL / TCP_KEEPCNT via + * setsockopt (declared in ), so idle, interval and count are each + * set directly. No TCP_USER_TIMEOUT analogue. */ static void WinsockTcpStream_EnableKeepalive(SOCKET fd) { int enable = 1; diff --git a/Platform/Windows/Source/SolidSyslogWinsockTcpStreamInternal.h b/Platform/Windows/Source/SolidSyslogWinsockTcpStreamInternal.h index 2b712359..1cb9ff11 100644 --- a/Platform/Windows/Source/SolidSyslogWinsockTcpStreamInternal.h +++ b/Platform/Windows/Source/SolidSyslogWinsockTcpStreamInternal.h @@ -3,7 +3,7 @@ /* Library-internal test seam. Tests replace these function pointers via CppUTest's UT_PTR_SET to inject fakes (MSVC does not support GCC's - weak/strong symbol override trick used by the POSIX SocketFake). + weak/strong symbol override trick the SocketFake relies on). Names are namespaced WinsockTcpStream_* to avoid linker collisions with the un-namespaced Winsock_* symbols already exported by SolidSyslogWinsockDatagram and SolidSyslogWinsockResolver. */ diff --git a/README.md b/README.md index 84f922e0..09b0330b 100644 --- a/README.md +++ b/README.md @@ -96,8 +96,8 @@ header per component it wires. The [API reference](https://docs.cososo.co.uk/solid-syslog/api-reference/) explains that split and links the generated reference for every header, type and symbol. The -[porting guide](https://docs.cososo.co.uk/solid-syslog/porting/) covers the twelve -roles, the anatomy of an adapter, and the Null object that stands in for any role you +[porting guide](https://docs.cososo.co.uk/solid-syslog/porting/) covers the roles, +the anatomy of an adapter, and the Null object that stands in for any role you leave unfilled. [`Bdd/Targets/`](Bdd/Targets/) holds one BDD-driven binary per platform — Linux, diff --git a/docs/README.md b/docs/README.md index acee343e..e00ef553 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,21 +12,21 @@ and [solid-syslog-example-make](https://github.com/cososo-ltd/solid-syslog-examp Pick a lane: -- [Overview](#overview): what SolidSyslog is and how it helps with CRA and IEC 62443 compliance. -- [Adopt it](#adopt): get a syslog stack compiling and sending in your product. -- [Port it to a new platform](#port-a-new-platform): fill a role for an OS, network stack, filesystem, or crypto library we don't ship yet. +- [Core](#core): the library itself — the protocol, the pipeline, and the portable pieces that run the same everywhere. +- [Integrate it](#integrate): get a syslog stack compiling and sending in your product. +- [Platforms](#platforms): what reaches your hardware — the shipped adapters, and how to write one for a target we don't cover. - [Compliance](#compliance): the CRA, IEC 62443, the RFCs, and the security posture. - [API reference](#api-reference): the public contracts, by audience. - [Maintaining the library](#maintaining): building, testing, and releasing SolidSyslog itself. --- -## Overview +## Core -- Why SolidSyslog, and capability at a glance: the [project README](../README.md). -- [Compliance in one page](overview.md): the evaluator's one-screen orientation. What CRA and IEC 62443 ask of an audit-logging function, and how SolidSyslog helps. +- [Core](core/index.md): what is always compiled and depends on nothing external — the syslog protocol, the assembly, buffering, storage and sending pipeline, and the portable role implementations. Where a platform exists to reach your hardware, Core exists to be the same everywhere. +- Why SolidSyslog, and capability at a glance: the project `README.md`. -## Adopt +## Integrate Everything you need to consume SolidSyslog in your product. @@ -34,19 +34,18 @@ Everything you need to consume SolidSyslog in your product. - [Adding it to your build](build-integration.md): the build detail. The capability matrix, the three ways to consume the library (CMake, Make, and a source manifest for an IDE project), and the compile-time tunables. - [Authoring custom structured data](structured-data.md): attaching RFC 5424 SD-ELEMENTs. - [Error-event severity policy](error-severity.md): installing an error handler and reading the event axes. -- Platform integration guides: - - [lwIP (Raw API)](integrating-lwip.md) - - [Mbed TLS](integrating-mbedtls.md) - - [FreeRTOS-Plus-FAT](integrating-plusfat.md) -- Tunables: the compile-time limits, all `#ifndef`-guarded. See [Adding it to your build → Tunables](build-integration.md#tunables) and [`Core/Interface/SolidSyslogTunablesDefaults.h`](../Core/Interface/SolidSyslogTunablesDefaults.h). +- Tunables: the compile-time limits, all `#ifndef`-guarded. See [Adding it to your build → Tunables](build-integration.md#tunables) and [`Core/Interface/SolidSyslogTunablesDefaults.h`](api/SolidSyslogTunablesDefaults_8h.md). -## Port a new platform +## Platforms -Porting SolidSyslog is filling roles, not editing Core: omit an adapter and -Core's Null object stands in. +A platform is a set of adapters wrapping one upstream thing — a network stack, a +TLS library, a filesystem, an OS — behind the library's vtables. Each page says +what that platform ships and what wiring it needs; the pages above speak of them +in the general case. -- [Porting guide](porting.md): the role model, the anatomy of an adapter (instance shape, the no-`malloc` static pool, the error convention), the invariants every adapter must honour, and the twelve vtable role contracts, each with its Null fallback and shipped reference implementation (POSIX where available, otherwise a Core composition). -- The contracts themselves: the `SolidSyslog*Definition.h` vtables under [`Core/Interface/`](../Core/Interface/). [`Platform/Posix/`](../Platform/Posix/) is the reference implementation to read alongside them. +- [Platform × capability matrix](platforms/index.md): start here. Read across a row for what a platform gives you, down a column for who provides a capability. +- [Porting guide](porting.md): nothing shipped fits your target. The role model, the anatomy of an adapter (instance shape, the no-`malloc` static pool, the error convention), the invariants every adapter must honour, and the vtable role contracts, each with its shipped reference implementation. +- The contracts themselves: the `SolidSyslog*Definition.h` vtables under `Core/Interface/`. `Platform/Posix/` is the reference implementation to read alongside them. ## Compliance @@ -60,7 +59,7 @@ Core's Null object stands in. - [Software Bill of Materials (SBOM)](security/sbom.md) - [Vulnerability triage runbook](security/triage-runbook.md) - [Release verification guide](security/release-verification.md) - - Reporting a vulnerability: [`SECURITY.md`](../SECURITY.md) + - Reporting a vulnerability: [Security policy](security/policy.md) ## API reference @@ -75,7 +74,7 @@ reference — [Headers](api/files.md), [Data structures](api/annotated.md), ## Maintaining For contributors and maintainers of SolidSyslog itself (not for consuming it, -which is [Adopt](#adopt) above). +which is [Integrate](#integrate) above). - [Building and testing](builds.md): the CMake preset catalogue. - [Pre-PR local checks](local-checks.md): the tiered pre-PR check budget. diff --git a/docs/assets/postit/README.md b/docs/assets/postit/README.md index 7bd7a6df..353c662e 100644 --- a/docs/assets/postit/README.md +++ b/docs/assets/postit/README.md @@ -36,7 +36,7 @@ relationship. One exception: [`architecture-overview.svg`](architecture-overview.svg) is **generated**, not hand-drawn — its layout (boundary crossings, fan-out lines) -is computed. Edit [`scripts/gen_architecture_overview.py`](../../../scripts/gen_architecture_overview.py) +is computed. Edit `scripts/gen_architecture_overview.py` and re-run it; do not hand-edit the SVG. It keeps the same palette but drops the sticky rotation/shadow for a precise architectural read, and colours by provenance (Core / adapter / third party) with the boundary marking Core. diff --git a/docs/assets/postit/api-audiences.svg b/docs/assets/postit/api-audiences.svg index ba5ad226..a435b3aa 100644 --- a/docs/assets/postit/api-audiences.svg +++ b/docs/assets/postit/api-audiences.svg @@ -155,7 +155,7 @@ Setup includes one header per component it wires. - The Linux reference target pulls around two dozen; a lean SL1 stack, a + The Linux reference target pulls around two dozen; a minimal build, a handful. Every file that logs or drains includes only SolidSyslog.h. diff --git a/docs/assets/stylesheets/brand.css b/docs/assets/stylesheets/brand.css index b6f79dfc..b6fd3a26 100644 --- a/docs/assets/stylesheets/brand.css +++ b/docs/assets/stylesheets/brand.css @@ -233,3 +233,53 @@ .postit-note a:hover { color: #3e2199; } + +/* ── Platform doorways ────────────────────────────────────────────────────── + Two links every platform page carries, and the one every generated API page + for a platform header carries back. Both use the UML component symbol, and + the adapter blue of the post-it kit (docs/assets/postit/README.md), where + blue already means "a backend that realises a role" — which is what a + platform adapter is. The glyphs are data URIs so the Markdown stays a plain + link and still reads on GitHub, where none of this CSS applies. */ + +.md-typeset .ss-chip { + display: inline-flex; + align-items: center; + gap: 0.38rem; + padding: 0.2rem 0.6rem 0.2rem 0.45rem; + border: 1px solid #d5d9f0; + border-radius: 999px; + background: #eef0fa; + color: #2f2a55; + font-size: 0.76rem; + text-decoration: none; +} + +.md-typeset .ss-chip:hover { + background: #e2e6f7; + color: #2f2a55; +} + +.md-typeset .ss-chip + .ss-chip { + margin-left: 0.4rem; +} + +.md-typeset .ss-chip::before { + content: ""; + width: 16px; + height: 16px; + background-repeat: no-repeat; + background-position: center; +} + +/* The doorway on a generated API page back to its platform. Same pill as + the platform page's own chips — one chip style across the site, not two. + It sits in its own paragraph, so the spacing below it is the paragraph's. */ +.md-typeset .ss-chip--api::before, +.md-typeset .ss-chip--platform::before { + background-image: url('data:image/svg+xml;utf8,'); +} + +.md-typeset .ss-chip--setup::before { + background-image: url('data:image/svg+xml;utf8,'); +} diff --git a/docs/bdd.md b/docs/bdd.md index b2da0672..a5c074dd 100644 --- a/docs/bdd.md +++ b/docs/bdd.md @@ -40,7 +40,7 @@ fail here. BDD targets pair with their own oracle so jobs and developers switching containers never interfere. Each pair lives on its own bridge network so the `syslog-ng` DNS alias is scoped per-pair; -see [`Bdd/README.md`](../Bdd/README.md) for the per-target compose +see `Bdd/README.md` for the per-target compose layout, the `BDD_TARGET` env-var contract, and FreeRTOS local fault-finding tips. @@ -229,7 +229,7 @@ docker compose -f .devcontainer/docker-compose.yml run --rm behave-linux \ ``` For the FreeRTOS pair (cross-build the ELF, then run Behave via QEMU -inside `freertos-target`), see [`Bdd/README.md`](../Bdd/README.md). +inside `freertos-target`), see `Bdd/README.md`. ## Verifying syslog-ng manually diff --git a/docs/build-integration.md b/docs/build-integration.md index c473d500..580e0a42 100644 --- a/docs/build-integration.md +++ b/docs/build-integration.md @@ -44,66 +44,27 @@ Two facts decide everything below: --- -## Pick your stack — capability matrix - -Choose one provider per role you need. Roles you don't need: leave the adapter -out and the Core Null object stands in. - -*Files* lists the file group (`Solid…` prefix elided). Most adapters are -`Adapter.c` + `AdapterStatic.c` (the `Static` file is the instance pool) plus an -`AdapterPrivate.h`; compile the `.c` files, keep the directory on the include -path. *Pool tunable* is the `SOLIDSYSLOG_` slot count (see -[Tunables](#tunables)). - -### Networking - -| Role | Provider | Files (`Solid…`) | Upstream / config | Pool tunable | -|---|---|---|---|---| -| Resolver | Posix | `PosixResolver*` | POSIX `getaddrinfo` | `RESOLVER_POOL_SIZE` | -| | Winsock | `WinsockResolver*` | Win32 | ″ | -| | PlusTcp (DNS) | `PlusTcpResolver*` | FreeRTOS-Plus-TCP + `ipconfigUSE_DNS=1` | ″ | -| | LwipRaw numeric | `LwipRawResolver*` | lwIP `ipaddr_aton` | ″ | -| | LwipRaw DNS | `LwipRawDnsResolver*` | lwIP `LWIP_DNS=1` + Sleep cb | ″ | -| Datagram (UDP) | Posix / Winsock / PlusTcp / LwipRaw | `{Posix,Winsock,PlusTcp,LwipRaw}Datagram*` | resp. stacks | `DATAGRAM_POOL_SIZE` | -| Stream (TCP) | Posix / Winsock / PlusTcp / LwipRaw | `{…}TcpStream*` | resp. stacks (lwIP also needs a Sleep cb) | `TCP_STREAM_POOL_SIZE` | -| Address | Posix / Winsock / PlusTcp / LwipRaw | `{…}Address*` | resp. `sockaddr` | `ADDRESS_POOL_SIZE` | -| Marshal (lwIP) | LwipRawMarshal | `LwipRawMarshal` | lwIP, for `NO_SYS=0` | — | - -### Transport security (TLS) and at-rest integrity - -| Role | Provider | Files (`Solid…`) | Upstream / config | Pool tunable | -|---|---|---|---|---| -| TLS Stream | OpenSSL | `OpenSslStream*` | OpenSSL ≥ 3.0 | `TLS_STREAM_POOL_SIZE` | -| | Mbed TLS | `MbedTlsStream*` | `mbedtls_config.h` | ″ | -| SecurityPolicy | CRC-16 | `Crc16Policy*` + `Crc16` | — | — | -| | HMAC-SHA256 | `{OpenSsl,MbedTls}HmacSha256Policy*` | OpenSSL or Mbed TLS | `HMAC_SHA256_POLICY_POOL_SIZE` | -| | AES-GCM | `{OpenSsl,MbedTls}AesGcmPolicy*` | OpenSSL or Mbed TLS | `AES_GCM_POLICY_POOL_SIZE` | - -### OS primitives, storage, structured data - -| Role | Provider | Files (`Solid…`) | Upstream / config | Pool tunable | -|---|---|---|---|---| -| Mutex | Posix / Windows / FreeRtos | `{…}Mutex*` | FreeRtos needs `configSUPPORT_STATIC_ALLOCATION=1` | `MUTEX_POOL_SIZE` | -| AtomicCounter | StdAtomic / Windows | `StdAtomicCounter*` / `WindowsAtomicCounter*` | C11 `` / Win32 | `ATOMIC_COUNTER_POOL_SIZE` | -| Buffer | Passthrough / Circular / Posix mq | `PassthroughBuffer*` / `CircularBuffer*` / `PosixMessageQueueBuffer*` | — / — / POSIX | resp. pool sizes | -| Store | BlockStore | `BlockStore*`, `RecordStore*`, `BlockSequence*` | — | `BLOCK_STORE_POOL_SIZE` | -| BlockDevice | FileBlockDevice | `FileBlockDevice*` | — | `FILE_BLOCK_DEVICE_POOL_SIZE` | -| File | Posix / Windows / FatFs / PlusFat | `{Posix,Windows}File*` / `FatFsFile*` / `PlusFatFile*` | FatFs: `ffconf.h` + `diskio.c`; PlusFat: `FreeRTOSFATConfig.h` + `FF_Disk_t` | `FILE_POOL_SIZE` | -| Structured Data | Meta / TimeQuality / Origin | `{Meta,TimeQuality,Origin}Sd*` | — | resp. pool sizes | - -### Bring-your-own callbacks - -A small tier is just function pointers. Host platforms ship a provider; embedded -targets supply a one-line callback. (Provider authoring for these is tracked in a -later epic, but the seams exist today.) - -| Role | Host provider | Embedded | -|---|---|---| -| Clock | `{Posix,Windows}Clock` | BYO `SolidSyslogClockFunction` | -| SysUpTime | `{Posix,Windows,FreeRtos}SysUpTime` | FreeRtos provided; else BYO | -| Sleep | `{Posix,Windows}Sleep` | BYO (needed by TLS + lwIP) — e.g. a `vTaskDelay` wrapper | -| Hostname / ProcessId | `{Posix,Windows}…` | BYO header-field callbacks | -| AtomicCounter | C11 / Win32 (above) | BYO, else sequence-id degrades to Null (always 1) | +## Pick your stack + +Choose one provider per role you need; leave the rest out and Core's Null +object stands in. Which platform fills which role is the +[platform × capability matrix](platforms/index.md), and each platform's own +page states what its adapters need from your build. + +Two things are build-specific rather than platform-specific, and belong here. + +**The file list is generated, not written.** Every platform publishes a +manifest of the exact `.c` files to compile and the include directories they +need — see [Worked manifest](#worked-manifest--the-beta-stack) below. CI +regenerates these and fails on any difference, so they cannot drift from the +tree the way a hand-maintained table would. + +**Some roles are callbacks rather than components.** The clock, the host name, +the process id and a bounded sleep are function pointers on +`SolidSyslogConfig`. Hosted platforms ship one of each ready to use; on a bare +target you write them, and they are usually a line apiece. A sleep is required +by the TLS adapters and by the lwIP TCP stream, which is the one that catches +people, because it has no default. --- @@ -243,7 +204,7 @@ build. You can drop the `set()` entirely and let auto-detection find the same stack; declare it when you would rather the build state its platforms than infer them. -[`ci/consumer-smoke/`](../ci/consumer-smoke/) is a working consumer of this +`ci/consumer-smoke/` is a working consumer of this shape, kept honest by CI: it cross-builds with the environment scrubbed, so `SOLIDSYSLOG_PLATFORMS` is the only thing that can select a platform, and it links them rather than merely checking the targets exist. @@ -253,13 +214,13 @@ SolidSyslog also has `SOLIDSYSLOG_LWIP_PATH` and siblings. Those build headers — a consumer never sets them. See the worked target wiring in -[`Bdd/Targets/FreeRtos/`](../Bdd/Targets/FreeRtos/) and -[`Bdd/Targets/FreeRtosLwip/`](../Bdd/Targets/FreeRtosLwip/) (both consume the +`Bdd/Targets/FreeRtos/` and +`Bdd/Targets/FreeRtosLwip/` (both consume the umbrellas), and the platform-specific guides: -- [Integrating with lwIP (Raw API)](integrating-lwip.md) -- [Integrating with Mbed TLS](integrating-mbedtls.md) -- [Integrating with FreeRTOS-Plus-FAT](integrating-plusfat.md) +- [Integrating with lwIP (Raw API)](platforms/lwipraw/setup.md) +- [Integrating with Mbed TLS](platforms/mbedtls/setup.md) +- [Integrating with FreeRTOS-Plus-FAT](platforms/plusfat/setup.md) --- @@ -452,19 +413,19 @@ you. - Sleep: required by Mbed TLS (handshake retry) and the lwIP TCP stream (bounded synchronous open). Wrap `vTaskDelay`. -- Clock, Hostname, ProcessId: small callbacks (see the matrix). -- AtomicCounter: only if you want RFC 5424 sequence-ids; otherwise it - degrades to the Null counter (always 1). +- Clock, Hostname, ProcessId: small callbacks you supply. +- AtomicCounter: only if you want RFC 5424 sequence-ids. -For the exact wiring of each adapter's `_Create` config struct, follow the -platform guides: [lwIP](integrating-lwip.md), [Mbed TLS](integrating-mbedtls.md). +For the exact wiring of each adapter's `_Create` config struct, follow +that platform's own setup guide, reached from its page in +[Platforms](platforms/index.md). --- ## Tunables All compile-time limits live in -[`Core/Interface/SolidSyslogTunablesDefaults.h`](../Core/Interface/SolidSyslogTunablesDefaults.h), +[`Core/Interface/SolidSyslogTunablesDefaults.h`](api/SolidSyslogTunablesDefaults_8h.md), multiple values, every one `#ifndef`-guarded so you override without editing the library. Two equivalent mechanisms (works the same for CMake and non-CMake): @@ -512,9 +473,7 @@ SOLIDSYSLOG_USER_TUNABLES := -DSOLIDSYSLOG_USER_TUNABLES_FILE=\"$(CURDIR)/$(APP_ ## Where to go next - [Building up the protection you need](hardening-path.md): what to wire and why, stage by stage -- [Integrating with lwIP (Raw API)](integrating-lwip.md) -- [Integrating with Mbed TLS](integrating-mbedtls.md) -- [Integrating with FreeRTOS-Plus-FAT](integrating-plusfat.md) +- [Platforms](platforms/index.md): which platform fills which role, what each needs from your build, and how to wire it - [Porting to a new platform](porting.md): writing an adapter for an OS, network stack, filesystem, or crypto library we don't ship - [Structured data](structured-data.md) - [Error handling and severity](error-severity.md) diff --git a/docs/builds.md b/docs/builds.md index 71d242c4..9e55a4e0 100644 --- a/docs/builds.md +++ b/docs/builds.md @@ -87,7 +87,7 @@ The CI gate is 90% line and branch. The target is 100%. ## Static analysis — `tidy` Runs clang-tidy on all source files. All warnings are errors. -Checks are configured in [.clang-tidy](../.clang-tidy). +Checks are configured in `.clang-tidy`. ```bash cmake --preset tidy @@ -162,8 +162,8 @@ cmake --build --preset freertos-cross --target SolidSyslogBddTarget The ELF lands at `build/freertos-cross/Bdd/Targets/FreeRtos/SolidSyslogBddTarget.elf`. -See [`Bdd/Targets/FreeRtos/README.md`](../Bdd/Targets/FreeRtos/README.md) for run / -GDB-attach instructions and [`Bdd/README.md`](../Bdd/README.md) for driving +See `Bdd/Targets/FreeRtos/README.md` for run / +GDB-attach instructions and `Bdd/README.md` for driving it under Behave + the syslog-ng oracle. ## FreeRTOS + lwIP cross — `freertos-cross-lwip` @@ -205,7 +205,7 @@ behave Bdd/features/ In the behave-linux container, Ctrl+Shift+B runs `behave Bdd/features/` automatically. For the FreeRTOS pair (cross-build the BDD target ELF, then drive QEMU through -Behave inside `freertos-target`), see [`Bdd/README.md`](../Bdd/README.md). +Behave inside `freertos-target`), see `Bdd/README.md`. See [BDD testing](bdd.md) for architecture details and the `BDD_TARGET` / `@freertoswip` contract. diff --git a/docs/ci.md b/docs/ci.md index 974e2cf7..ace38c16 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -19,21 +19,58 @@ without renaming what's already there. | `analyze-cppcheck` | `cppcheck` | cppcheck static analysis | | `analyze-codeql` | — | CodeQL over the library as a consumer builds it; findings in Security → Code scanning. Its own workflow (`codeql.yml`) — see *Code scanning* | | `analyze-format` | — | clang-format dry-run; fails if any file needs reformatting | -| `analyze-iwyu` | `iwyu` | include-what-you-use; fails on missing or unused `#include` directives | +| `analyze-iwyu` | `iwyu` | include-what-you-use; fails on missing or unused `#include` directives. Advisory — runs `continue-on-error` | +| `analyze-tidy-freertos-plustcp` | `tidy` | clang-tidy over the FreeRTOS / FreeRTOS-Plus-TCP / Mbed TLS / Plus-FAT trees, which the base `analyze-tidy` lane cannot reach | +| `analyze-tidy-freertos-lwip` | `tidy` | The same, over the lwIP and ChaN FatFs trees | +| `analyze-iwyu-freertos-plustcp` | `iwyu` | IWYU over the same FreeRTOS-Plus-TCP set. Advisory | +| `analyze-iwyu-freertos-lwip` | `iwyu` | IWYU over the same lwIP set. Advisory | +| `analyze-markdown` | — | `markdownlint-cli2` over every tracked `.md` file | | `integration-linux-openssl` | `debug` | Runs the in-process TLS integration tests against libssl (no network oracle) | +| `integration-linux-mbedtls` | `debug` | The same integration tests against Mbed TLS, exercising `SolidSyslogMbedTlsStream` and the Mbed TLS security policies | | `integration-windows-openssl` | `msvc-debug` | Same TLS integration tests on `windows-latest` against libssl from vcpkg | +| `build-linux-c89-headers` | — | Compiles every public header standalone as ISO C89 with `-pedantic-errors`, via `scripts/check_headers_c89.py`. Also proves each header is self-contained, since a header needing a companion first fails here | +| `build-linux-c99` | `c99`, `c99-platforms` | Builds Core alone at strict `-std=c99` (`CMAKE_C_EXTENSIONS=OFF`, no tests), then the POSIX and OpenSSL packs at C99 as a drift check. Proves the C99 conformance claim per PR | +| `build-linux-tunable-override` | `tunable-override-debug` | Builds against a user tunables header to prove `SOLIDSYSLOG_USER_TUNABLES_FILE` overrides the defaults | | `bdd-linux-syslog-ng` | — | End-to-end BDD test via Docker Compose (`syslog-ng-linux` + `behave-linux`), Linux runner | | `bdd-windows-otel` | — | Windows-eligible BDD scenarios driven against an OTel Collector oracle | -| `build-freertos-host-tdd` | `debug` | Host-TDD of FreeRTOS adapters against fakes; runs inside `cpputest-freertos` (FreeRTOS upstream sources at fixed paths) | -| `build-freertos-target` | `freertos-cross` | ARM cross-build (Cortex-M3, mps2-an385) of the BDD target ELF; uploads it as an artifact for `bdd-freertos-qemu` | -| `bdd-freertos-qemu` | — | Pulls the BDD target ELF artifact, brings up the freertos compose pair (`syslog-ng-freertos` + `behave-freertos`); Behave drives the target through `qemu-system-arm`'s UART | +| `build-freertos-host-tdd-plustcp` | `debug` | Host-TDD of the FreeRTOS, FreeRTOS-Plus-TCP, Plus-FAT, FatFs and Mbed TLS adapters against fakes; runs inside `cpputest-freertos` (upstream sources at fixed paths) | +| `build-freertos-target-plustcp` | `freertos-cross` | ARM cross-build (Cortex-M3, mps2-an385) of the BDD target ELF over FreeRTOS-Plus-TCP; uploads it as an artifact | +| `build-freertos-target-lwip` | `freertos-cross-lwip` | The same cross-build over lwIP with ChaN FatFs (`FreeRtos;LwipRaw;MbedTls;FatFs;StdAtomic`) | +| `bdd-freertos-qemu-plustcp` | — | Pulls the Plus-TCP target ELF, brings up the freertos compose pair (`syslog-ng-freertos` + `behave-freertos`); Behave drives the target through `qemu-system-arm`'s UART | +| `bdd-freertos-qemu-lwip` | — | The same scenarios against the lwIP target ELF | +| `consumer-smoke-linux` | — | Builds `ci/consumer-smoke/` as a FetchContent consumer, proving the documented integration path still works | +| `consumer-smoke-freertos-cross` | — | The same consumer project cross-compiled for ARM with `LwipRaw;FreeRtos` | +| `verify-manifest` | — | Regenerates the Core and per-platform source manifests and fails if they differ from the committed ones | | `docs-build` | — | Builds the MkDocs + mkdoxy site with `mkdocs build --strict`; on `main`, `deploy-docs-pages` publishes it to GitHub Pages | +| `summary` | — | Aggregates the JUnit artifacts into a run summary. Declared `if: always()` and asserts nothing about the other jobs' results | ## Branch protection -Every job in `ci.yml` is a required status check, as are the two contexts code -scanning contributes. A PR cannot be merged unless all checks pass. Direct pushes -to `main` are blocked. Squash merge only. +Every job in `ci.yml` is a required status check except `deploy-docs-pages`, which +only runs on `main`, and so are the two contexts code scanning contributes — +`analyze-codeql` and `CodeQL`. That is 34 required contexts. A PR cannot be merged +unless all of them pass. Direct pushes to `main` are blocked. Squash merge only. + +Two qualifications on what "required" buys. The `analyze-iwyu*` lanes run +`continue-on-error`, so they are required contexts that report success whatever IWYU +finds — required in form, advisory in substance. And feeding the `summary` aggregator +does not make a lane blocking: `summary` is declared `if: always()` and asserts nothing +about `needs.*.result`, so a new lane gates merges only once its own context is added +to the required list. + +## What each lane exercises + +The lane names say the platform and toolchain but not the adapter, so: + +| Adapter | Where it is exercised | +|---|---| +| OpenSSL (`SolidSyslogOpenSslStream`, security policies) | `integration-linux-openssl`, `integration-windows-openssl` against real libssl | +| Mbed TLS (`SolidSyslogMbedTlsStream`, security policies) | `integration-linux-mbedtls` against real Mbed TLS; both FreeRTOS QEMU BDD lanes over a real handshake | +| FreeRTOS-Plus-TCP | `build-freertos-host-tdd-plustcp` against fakes; `bdd-freertos-qemu-plustcp` end to end under QEMU | +| lwIP | `bdd-freertos-qemu-lwip` end to end under QEMU; static analysis via the `*-freertos-lwip` lanes | +| ChaN FatFs | Built and analysed in the lwIP lanes; store-and-forward scenarios run in `bdd-freertos-qemu-lwip` | +| FreeRTOS-Plus-FAT | Host-TDD against fakes in `build-freertos-host-tdd-plustcp`, and built in the Plus-TCP cross lanes | +| POSIX, Windows | The `build-linux-*` and `build-windows-msvc` lanes, plus both host BDD lanes | ## Code scanning @@ -42,7 +79,7 @@ and weekly so that queries GitHub ships later are applied to unchanged code. Fin appear under **Security → Code scanning**, not in the job log. CodeQL analyses what the compiler compiled and nothing else, so the build it observes -is the analysis scope. The lane builds [`ci/consumer-smoke/`](../ci/consumer-smoke/) — +is the analysis scope. The lane builds `ci/consumer-smoke/` — the same FetchContent consumer documented in [Adding it to your build](build-integration.md) — so the code analysed is the code an integrator compiles. Consuming the library as a subproject also scopes the database diff --git a/docs/containers.md b/docs/containers.md index fe4aba7b..f47bfca3 100644 --- a/docs/containers.md +++ b/docs/containers.md @@ -194,4 +194,4 @@ The same VS Code keys work across every service: QEMU run for sanity-checking the build, output to the integrated terminal. Use only in the `freertos-target` service. -For the FreeRTOS BDD target, see [Bdd/Targets/FreeRtos/README.md](../Bdd/Targets/FreeRtos/README.md) for build / run / GDB-attach instructions. +For the FreeRTOS BDD target, see `Bdd/Targets/FreeRtos/README.md` for build / run / GDB-attach instructions. diff --git a/docs/cra.md b/docs/cra.md index cd8b4bac..62064be3 100644 --- a/docs/cra.md +++ b/docs/cra.md @@ -85,8 +85,8 @@ consuming. |---|---|---| | **(1)** | identify and document components, including a software bill of materials in a machine-readable format | A CycloneDX [SBOM](security/sbom.md) per release, to fold into your product's own | | **(2)**, **(4)** | remediate without delay; disclose fixed vulnerabilities | The [vulnerability triage runbook](security/triage-runbook.md) sets out how reports are assessed and published | -| **(5)**, **(6)** | a coordinated vulnerability disclosure policy, and a contact address | [`SECURITY.md`](../SECURITY.md) | -| **(7)** | securely distribute updates | [Release verification](security/release-verification.md): signed, reproducible artefacts you can check before adopting | +| **(5)**, **(6)** | a coordinated vulnerability disclosure policy, and a contact address | [Security policy](security/policy.md) | +| **(7)** | securely distribute updates | [Release verification](security/release-verification.md): a signed source-tree hash and signed CycloneDX SBOM, both verifiable against the workflow and tag that produced them. They are published by the release workflow, so verify their presence for the release you are adopting rather than assuming it | Secure-by-design evidence for your technical file is in the [threat model](security/threat-model.md) and [at-rest diff --git a/docs/hardening-path.md b/docs/hardening-path.md index e31a9b3d..1349d222 100644 --- a/docs/hardening-path.md +++ b/docs/hardening-path.md @@ -39,6 +39,12 @@ your compiler and its options, and what your application already links. The exam repositories carry the exact figures, the diff that produced each, and the reasoning behind it. +Because each stage is rounded up on its own, the stages deliberately do not add up to +the totals quoted elsewhere: twenty roundings-up accumulate, so the sum overshoots. +Measured cumulatively against the same baseline, the whole path costs about 13.5 KB of +flash rather than the ~13.9 KB the column sums to. Take a stage's figure as the cost of +that step and the example's own measurements as the cost of the path. + ## How to read this The order of the stages is a logical way to integrate syslog in small steps, each one diff --git a/docs/iec62443.md b/docs/iec62443.md index 51d373b4..6334ca33 100644 --- a/docs/iec62443.md +++ b/docs/iec62443.md @@ -39,16 +39,16 @@ what the library does not do. | Control | What SolidSyslog provides | Gaps | |---|---|---| -| **CR 1.5** — Authenticator management | `SolidSyslogOpenSslStream` mTLS — caller supplies `ClientCertChainPath` / `ClientKeyPath`; `SSL_CTX_check_private_key` validates the key/cert pair before any bytes hit the wire. Rotation by file replacement plus reconnect (natural reconnect or explicit `SolidSyslogSender_Disconnect`) | Library ships no default authenticators; at-rest key protection (filesystem permissions, HSM) is the integrator's responsibility | -| **CR 1.8** — Public key infrastructure certificates | `SolidSyslogOpenSslStream` — `CaBundlePath` loads trust anchors via `SSL_CTX_load_verify_locations`; `SSL_VERIFY_PEER` pinned on the CTX; hostname verification via `SSL_set1_host`. CTX rebuilt on every `Open` so file replacement + reconnect refreshes trust anchors | Revocation (CRL / OCSP) is not performed by the library; whether it is enforced depends on the TLS backend and platform you configure, and verifying that is the integrator's responsibility. Enrolment is the caller's PKI process | +| **CR 1.5** — Authenticator management | The Stream role carries transport security, and a TLS backend filling it can present a client credential so the collector authenticates the device. The credential is supplied by the integrator: the library holds no keys of its own and reads whatever material it is given. Refreshing it is a deployment operation — how, and whether a reconnection is needed, is a property of the backend | The library ships no default authenticators, and protection of the key at rest — file permissions, a secure element, a hardware security module — is outside it. Whether a partially supplied credential is refused or quietly ignored differs between backends; the [platform pages](platforms/index.md) state which | +| **CR 1.8** — Public key infrastructure certificates | A TLS backend filling the Stream role verifies the collector's certificate against trust anchors you supply, and checks the collector's identity against a name you declare. Both are integrator inputs; neither has a default | Revocation is not performed by any shipped backend, by certificate revocation list or by online status protocol. Where a deployment requires it, it must come from your own configuration of the underlying library, and confirming it is in force is yours. Enrolment is your public-key infrastructure's process | | **CR 2.8** — Auditable events | `SolidSyslog_Log` formats events per RFC 5424. Structured data attached via `SolidSyslogMetaSd` / `SolidSyslogTimeQualitySd` / `SolidSyslogOriginSd`, or caller-supplied SD | Which internal activity is security-relevant is your decision: the library carries whatever your application raises and has no view of what it omitted. The categories the control expects to see audited follow from your own risk assessment | -| **CR 2.9** — Audit storage capacity | `SolidSyslogBlockStore` — rotating blocks, configurable `max-blocks` and `max-block-size`, configurable discard policy (`oldest` / `newest` / `halt`). Backed by `SolidSyslogFileBlockDevice` over `SolidSyslogPosixFile` (POSIX), `SolidSyslogWindowsFile` (Windows), `SolidSyslogFatFsFile` (ChaN FatFs) or `SolidSyslogPlusFatFile` (FreeRTOS-Plus-FAT), or an integrator-supplied `SolidSyslogBlockDevice` over flash. The control's own requirement enhancement calls for a warning when the storage threshold is reached: `SolidSyslogStoreThresholdFunction` + `SolidSyslogStoreThresholdCallback` provide it — edge-triggered, fires once when used-bytes crosses the threshold, re-arms when usage falls back below | Capacity has to be sized to the deployment's outage budget, which is yours to know; the library enforces the number you give it. Durability of the medium — flash wear, filesystem behaviour on power loss — belongs to the block device and the platform beneath it | +| **CR 2.9** — Audit storage capacity | `SolidSyslogBlockStore` — rotating blocks, configurable `max-blocks` and `max-block-size`, configurable discard policy (`oldest` / `newest` / `halt`). It sits over `SolidSyslogFileBlockDevice`, which sits over the File role a platform fills, or over a BlockDevice you write against raw flash. The control's own requirement enhancement calls for a warning when the storage threshold is reached: `SolidSyslogStoreThresholdFunction` + `SolidSyslogStoreThresholdCallback` provide it — edge-triggered, fires once when used-bytes crosses the threshold, re-arms when usage falls back below | Capacity has to be sized to the deployment's outage budget, which is yours to know; the library enforces the number you give it. Durability of the medium — flash wear, filesystem behaviour on power loss — belongs to the platform beneath the File role, and no shipped filesystem backend is journalling | | **CR 2.10** — Response to audit processing failures | `SolidSyslogStoreFullCallback` (halt policy) and the discard-policy enum. Caller picks the policy that fits the deployment's audit-loss tolerance. The early-warning threshold callback (CR 2.9) fires before discard / halt engages, giving the application time to act (notify operator, reduce verbosity, tighten retention); at 100% with HALT both fire on the same Write with threshold first then `onStoreFull`. Failures elsewhere in the path surface through the error handler — see [error severity](error-severity.md) | The response itself is your application's. The library reports the failure and applies the policy you configured; deciding what an operator is told, and whether the device keeps running, is above it | -| **CR 2.11** — Timestamps | Caller-injected `SolidSyslogClockFunction` — `SolidSyslogPosixClock_GetTimestamp` (POSIX) or `SolidSyslogWindowsClock_GetTimestamp` (Windows). Quality metadata via `SolidSyslogTimeQualitySd` (`tzKnown` / `isSynced` / `syncAccuracy`), so a collector can tell how far to trust the time rather than assuming it. A clock that cannot answer yields NILVALUE rather than a plausible wrong time | Clock accuracy and synchronisation are the platform's: the library reports the quality you declare, it does not establish it, and it cannot detect a clock that reports itself synchronised while wrong. Time-source integrity is likewise outside the library | -| **CR 2.12** — Non-repudiation | At the wire: `SolidSyslogOpenSslStream` mutual TLS cryptographically identifies the TLS peer to the receiver. Loss detection: `SolidSyslogMetaSd` sequenceId, gap detection at the SIEM — informational, not cryptographically bound, so it evidences loss rather than origin. At rest: cryptographic integrity via `SolidSyslog{OpenSsl,MbedTls}HmacSha256Policy`, or authenticated encryption via `SolidSyslog{OpenSsl,MbedTls}AesGcmPolicy`; `SolidSyslogCrc16Policy` remains for accidental-corruption detection | Mutual TLS authenticates the TLS peer, so where a relay terminates the connection the receiver authenticates the relay rather than the originating device, and no library mechanism binds origin across that hop. Cryptographic at-rest policies are keyed — key custody, rotation, and tamper-evident storage are integrator responsibilities. See [at-rest cryptography](security/at-rest-cryptography.md) and the [threat model](security/threat-model.md) | -| **CR 3.9** — Protection of audit information | The library's contribution is tamper evidence: where the medium is exposed, a keyed at-rest policy (`SolidSyslog{OpenSsl,MbedTls}HmacSha256Policy`) makes an unauthorised edit detectable. `SolidSyslogCrc16Policy` does not serve this control: it is unkeyed, so anyone able to modify a record can recompute the checksum. Related but distinct controls: **CR 3.4** — Software and information integrity, and **CR 4.1** — Information confidentiality, in transit with `SolidSyslogOpenSslStream` and at rest with `SolidSyslog{OpenSsl,MbedTls}AesGcmPolicy`. See [at-rest cryptography](security/at-rest-cryptography.md) | The access and deletion half of the control is not the library's: it neither sets nor checks filesystem permissions, privileged access or retention on the store, and those remain yours to configure and to verify. The cryptographic mechanisms are integrator choices, not CR 3.9 mandates; key management is the integrator's responsibility. The control's own requirement enhancement calls for audit records on write-once media, which is a property of the storage medium and outside anything a library can supply | -| **CR 6.1** — Audit log accessibility | Two wirings, two cost models. **Single-task (`SolidSyslogPassthroughBuffer`):** `SolidSyslog_Log` performs the transport `Send` synchronously on the caller's thread. The TCP socket is non-blocking from the moment it is opened (see [`SolidSyslogPosixTcpStream.c`](../Platform/Posix/Source/SolidSyslogPosixTcpStream.c) and [`SolidSyslogWinsockTcpStream.c`](../Platform/Windows/Source/SolidSyslogWinsockTcpStream.c)), so `Send` returns immediately on a wedged peer or a full kernel buffer; the bounded blocking surface is the initial `connect()`, sized by `SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS` (default 200 ms) or runtime-overridable per Stream via `GetConnectTimeoutMs(ConnectTimeoutContext)`. On the TLS path the same applies to the handshake, via `SOLIDSYSLOG_TLS_HANDSHAKE_TIMEOUT_MS` (default 5000 ms) or `GetHandshakeTimeoutMs(HandshakeTimeoutContext)` for both OpenSSL and Mbed TLS adapters. Long-term silent peer death is detected out-of-band via kernel `TCP_KEEPALIVE` + `TCP_USER_TIMEOUT`. Suitable when the application has no real-time deadline or the transport latency is known to be sub-millisecond. **Buffered (`SolidSyslogCircularBuffer` with an injected `SolidSyslogMutex` — Posix / Windows / FreeRTOS / Null / caller-supplied RTOS primitive — or `SolidSyslogPosixMessageQueueBuffer`):** `SolidSyslog_Log` is non-blocking: it formats and enqueues, returning before any I/O. `SolidSyslog_Service`, called on the integrator's chosen thread (typically a dedicated service thread), performs the transport I/O against the same non-blocking socket; the bounded surface is the same connect / handshake budget as the single-task wiring. Caller picks the wiring that fits the deployment's audit-loss / latency trade-off | Neither wiring is chosen for you, and the choice has consequences the library cannot make for you. Access to the records once delivered — query, retention, export — belongs to the collector, not to this library. The control's own requirement enhancement calls for programmatic access to the audit log, which on this design is the collector's interface rather than the device's | -| **CR 6.2** — Continuous monitoring | TCP / TLS delivery confirmation via `SolidSyslogStreamSender`. Replay across outages via `SolidSyslogBlockStore` store-and-forward. `SolidSyslogMetaSd` sequenceId is assigned at the point of raise, so a gap reflects loss anywhere in the pipeline rather than transport loss alone | The monitoring is the collector's: the library emits a sequence that makes loss detectable, it does not detect it, alert on it, or know whether anyone is watching. Continuous monitoring in the control's sense is a property of the deployment | +| **CR 2.11** — Timestamps | The clock is an injected `SolidSyslogClockFunction` rather than a component, so the time source is yours to choose; platforms that have one ship an implementation. Quality metadata via `SolidSyslogTimeQualitySd` (`tzKnown` / `isSynced` / `syncAccuracy`), so a collector can tell how far to trust the time rather than assuming it. A clock that cannot answer yields NILVALUE rather than a plausible wrong time | Clock accuracy and synchronisation are the platform's: the library reports the quality you declare, it does not establish it, and it cannot detect a clock that reports itself synchronised while wrong. Time-source integrity is likewise outside the library | +| **CR 2.12** — Non-repudiation | At the wire: mutual TLS through the Stream role cryptographically identifies the TLS peer to the collector. Loss detection: `SolidSyslogMetaSd` sequenceId, gap detection at the SIEM — informational, not cryptographically bound, so it evidences loss rather than origin. At rest: the SecurityPolicy role, filled by a keyed policy, gives cryptographic integrity or authenticated encryption; Core's unkeyed CRC-16 policy detects accidental corruption only | Mutual TLS authenticates the TLS peer, so where a relay terminates the connection the collector authenticates the relay rather than the originating device, and no library mechanism binds origin across that hop. Keyed policies are only as good as their key — custody, rotation and tamper-evident storage are integrator responsibilities. See [at-rest cryptography](security/at-rest-cryptography.md) and the [threat model](security/threat-model.md) | +| **CR 3.9** — Protection of audit information | The library's contribution is tamper evidence: where the medium is exposed, a keyed SecurityPolicy makes an unauthorised edit detectable. Core's CRC-16 policy does not serve this control — it is unkeyed, so anyone able to modify a record can recompute the checksum. Related but distinct controls: **CR 3.4** — Software and information integrity, and **CR 4.1** — Information confidentiality, in transit through a TLS backend and at rest through an authenticated-encryption policy. See [at-rest cryptography](security/at-rest-cryptography.md) | The access and deletion half of the control is not the library's: it neither sets nor checks filesystem permissions, privileged access or retention on the store, and those remain yours to configure and to verify. The cryptographic mechanisms are integrator choices, not CR 3.9 mandates; key management is the integrator's responsibility. The control's own requirement enhancement calls for audit records on write-once media, which is a property of the storage medium and outside anything a library can supply | +| **CR 6.1** — Audit log accessibility | Two wirings, two cost models, chosen by which Buffer you fill the role with. **Inline (`SolidSyslogPassthroughBuffer`):** `SolidSyslog_Log` performs the transport send on the caller's thread, so the call costs whatever delivery costs. Suitable where the application has no real-time deadline. **Queued (a buffering Buffer):** `SolidSyslog_Log` formats and enqueues, and every buffering Buffer the library ships returns before any transport I/O; `SolidSyslog_Service` then performs that work on a thread you choose. The role's contract does not itself forbid a blocking `Write`, so a Buffer you write defines this for you. On a stream transport the phases that can wait are the connection and, on a secured path, the handshake; each has a tunable budget, overridable per instance at run time. Neither bounds the transfer of a record once the connection is up, and a datagram transport has neither phase. What a given transport actually waits on is for its platform page to state — see the [platform pages](platforms/index.md). Caller picks the wiring that fits the deployment's audit-loss / latency trade-off | Neither wiring is chosen for you, and the choice has consequences the library cannot make for you. Access to the records once delivered — query, retention, export — belongs to the collector, not to this library. The control's own requirement enhancement calls for programmatic access to the audit log, which on this design is the collector's interface rather than the device's | +| **CR 6.2** — Continuous monitoring | TCP / TLS delivery confirmation via `SolidSyslogStreamSender`. Replay across outages via `SolidSyslogBlockStore` store-and-forward. `SolidSyslogMetaSd` sequenceId is assigned at the point of raise, so a gap reflects loss anywhere in the pipeline rather than transport loss alone | The monitoring is the collector's: the library emits a sequence that makes loss detectable, it does not detect it, alert on it, or know whether anyone is watching. Continuous monitoring in the control's sense is a property of the deployment. | The identity controls (CR 1.5, CR 1.8) are met by mutual TLS, which authenticates the device to the collector. Both controls have requirement enhancements calling for @@ -60,95 +60,6 @@ The library's error guards bear on **CR 3.7** — Error handling: every failure reports through the injected error handler rather than failing silently, and the severity convention is documented in [error severity](error-severity.md). -### The OpenSSL TLS substrate - -`SolidSyslogOpenSslStream` (the OpenSSL-backed client-side TLS substrate) ships -with the client-side half of the transport-security controls: - -- Hostname verification. `SolidSyslogOpenSslStreamConfig.ServerName` is - forwarded to both SNI (`SSL_set_tlsext_host_name`) and the cert check - (`SSL_set1_host`). Both return values are checked; handshake is aborted on - any setup failure, preventing a silent "handshake succeeded without checking - the name" bypass. -- Trust chain. `CaBundlePath` loads trust anchors via - `SSL_CTX_load_verify_locations`; `SSL_VERIFY_PEER` is pinned on the CTX. -- TLS 1.2 floor. `SSL_CTX_set_min_proto_version(TLS1_2_VERSION)` is - return-checked; Open fails if libssl refuses the floor. -- Cipher pinning. `SolidSyslogOpenSslStreamConfig.CipherList` is forwarded to - `SSL_CTX_set_cipher_list`. The library ships no baked-in list: the - cipher policy lives with the caller, which understands its own security - profile. A reasonable starting point is - `"ECDHE+AESGCM:ECDHE+CHACHA20"` (TLS 1.2 AEAD with forward secrecy); tune to - match the libssl build on the target platform. -- Mutual TLS (non-repudiation, CR 2.12). Optional - `ClientCertChainPath` / `ClientKeyPath` load a client cert and private key - via `SSL_CTX_use_certificate_chain_file` + `SSL_CTX_use_PrivateKey_file`, - with `SSL_CTX_check_private_key` confirming the pairing locally before any - bytes hit the wire. Both fields are opt-in and all-or-nothing: supply both - to enable mTLS, leave both NULL for server-auth TLS. Supplying only one is - rejected at Open time, so this adapter cannot silently downgrade. The Mbed TLS - adapter behaves differently — see below. -- Certificate rotation (CR 1.5 authenticator refresh, CR 1.8 PKI update). - `SolidSyslogOpenSslStream` rebuilds the `SSL_CTX` on every `Open`, re-reading the - cert material from `CaBundlePath` / `ClientCertChainPath` / `ClientKeyPath` - from disk each time. Rotation is therefore a deployment operation: replace - the files on disk, and the new material takes effect on the next reconnect, - via natural churn (retry, outage recovery) or by calling - `SolidSyslogSender_Disconnect` to force one. No library-level callback or - version-fingerprint API is needed to satisfy the capability requirement. -- Resource lifecycle. `Close`/`Destroy` release `SSL_CTX`, `SSL`, and - `BIO_METHOD` idempotently: no leaks on partial Open failure, no double - frees if both are called. - -### The Mbed TLS substrate for embedded targets - -`SolidSyslogMbedTlsStream` is the Mbed TLS-backed reference adapter for -embedded / FreeRTOS / bare-metal deployments where OpenSSL is too -heavy. It satisfies the same CR 1.5 / CR 1.8 / CR 2.12 / CR 3.9 controls -as `SolidSyslogOpenSslStream` and surfaces the same `SolidSyslogStream` -vtable, so the rest of the wiring (`SolidSyslogStreamSender`, buffer, -store) is unchanged. - -- Hostname verification. `SolidSyslogMbedTlsStreamConfig.ServerName` - is forwarded to both SNI (`mbedtls_ssl_set_hostname`) and the cert SAN - check (mbedTLS verifies the peer cert against the same string by - default with `MBEDTLS_SSL_VERIFY_REQUIRED`). -- Trust chain. `CaChain` is a caller-built `mbedtls_x509_crt*` - passed through dependency injection; there is no filesystem coupling - inside the adapter (`MBEDTLS_FS_IO` is typically off on embedded - targets). `mbedtls_ssl_conf_authmode` is pinned to - `MBEDTLS_SSL_VERIFY_REQUIRED`. -- TLS 1.2+ floor. Inherited from `mbedtls_ssl_config_defaults(... - PRESET_DEFAULT)`. TLS 1.3 negotiates automatically when both peers - support it. -- Mutual TLS (non-repudiation, CR 2.12). Optional `ClientCertChain` - / `ClientKey` handles. Both NULL = server-auth-only. Both non-NULL = - mTLS. Supplying only one configures no client certificate and `Open` - proceeds with server-authenticated TLS rather than failing — unlike the - OpenSSL adapter above, which rejects partial credentials at `Open`. If a - half-supplied credential must be an error on this adapter, the caller checks - before `Open`. -- Certificate rotation (CR 1.5 / CR 1.8). Because the adapter - consumes pre-built handles rather than file paths, rotation is "parse - a new `mbedtls_x509_crt`, destroy and recreate the adapter (or the - parent `SolidSyslogStreamSender` so the next Connect picks it up)." A - deployment running a vendor key-rotation service drives this via its - existing reload hook. -- Coexistence with an existing Mbed TLS integration. Auditable - contract: `Platform/MbedTls/Source/` never calls - `mbedtls_platform_setup` / `_teardown`, never installs threading-alt - hooks, never calls `psa_crypto_init` itself, never resets the global - RNG, never replaces the integrator's debug callback. Devices that - already wire Mbed TLS for another subsystem (cloud, OTA, vendor - framework) keep that wiring intact. -- Resource lifecycle. `Close` / `Destroy` invoke - `mbedtls_ssl_close_notify` / `_ssl_free` / `_ssl_config_free` - idempotently and never free integrator-owned handles. - -Integrator guide: [`docs/integrating-mbedtls.md`](integrating-mbedtls.md). -Reference wiring: the FreeRTOS QEMU mps2-an385 BDD target at -[`Bdd/Targets/Common/BddTargetTlsSender_MbedTls_PlusTcpTcp.c`](../Bdd/Targets/Common/BddTargetTlsSender_MbedTls_PlusTcpTcp.c). - ## Architecture for Security These are properties of the library itself. They describe how it is built, not what @@ -184,38 +95,11 @@ elimination removes unused components. The development-process evidence IEC 62443-4-1 asks for is published separately: the [threat model](security/threat-model.md), the [SBOM](security/sbom.md), the -[vulnerability triage runbook](security/triage-runbook.md), [`SECURITY.md`](../SECURITY.md) +[vulnerability triage runbook](security/triage-runbook.md), [Security policy](security/policy.md) for coordinated disclosure, and the test and analysis gates described in [CI](ci.md). -## Deployment Considerations - -### Minimal footprint (bare-metal / RTOS) - -For resource-constrained targets, a working audit trail needs no more than: - -- No heap allocation -- No POSIX dependency (provide your own clock, hostname, and transport) -- Single-task operation with `SolidSyslogPassthroughBuffer`, or multi-task on - bare-metal / RTOS with `SolidSyslogCircularBuffer` driven by a - `SolidSyslogMutex` you supply (the library ships `SolidSyslogPosixMutex`, - `SolidSyslogWindowsMutex`, `SolidSyslogFreeRtosMutex`, and `SolidSyslogNullMutex`) -- Flash-based store-and-forward via an integrator-supplied - `SolidSyslogBlockDevice` driving `SolidSyslogBlockStore`. - The library ships `SolidSyslogFileBlockDevice` as a file-backed reference; - flash hardware drivers are out-of-scope and supplied by the platform. - -### RTOS integration - -`SolidSyslogCircularBuffer` is the portable choice for the producer / consumer -seam between the application thread (calling `Log()`) and the service thread -(calling `Service()`): caller-allocated ring memory, mutex injected via the -`SolidSyslogMutex` vtable, no POSIX dependency. Wrap your RTOS mutex primitive -in the vtable and you're done. `SolidSyslogPosixMessageQueueBuffer` remains -available where a POSIX `mqueue` is preferred for kernel-level back-pressure -semantics. - -### SIEM integration +## SIEM integration All output is RFC 5424 compliant structured syslog. Every release is verified in CI against syslog-ng and against an OpenTelemetry Collector, end to end through the BDD diff --git a/docs/integrating-lwip.md b/docs/integrating-lwip.md deleted file mode 100644 index cb8193eb..00000000 --- a/docs/integrating-lwip.md +++ /dev/null @@ -1,503 +0,0 @@ -# Integrating SolidSyslog with lwIP (Raw API) - -`Platform/LwipRaw/` wraps lwIP's Raw API to provide the same -`SolidSyslogDatagram` / `SolidSyslogStream` / `SolidSyslogAddress` / -`SolidSyslogResolver` vtables the rest of the library composes against. -It is the right choice when: - -- Your target runs lwIP (bare-metal, FreeRTOS, Zephyr, ThreadX, NuttX), - including `NO_SYS=1` deployments where sockets/NETCONN aren't - available. -- You want to share TCP/IP between SolidSyslog and other lwIP-using - subsystems (HTTP server, MQTT client, OTA updater) without - duplicating the stack. -- You're on FreeRTOS but prefer lwIP to FreeRTOS-Plus-TCP for licence - or sizing reasons. Both backends ship in the same library; pick at - CMake time by naming `LwipRaw` rather than `PlusTcp` in `SOLIDSYSLOG_PLATFORMS`. - -This document covers what you, the integrator, plug in. It does not -re-teach lwIP; for that, see the -[upstream lwIP documentation](https://www.nongnu.org/lwip/2_1_x/index.html). - ---- - -## What ships in `Platform/LwipRaw/` - -| 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 — use `SolidSyslogLwipRawDnsResolver` for those. Needs no `LWIP_DNS`. | -| `SolidSyslogLwipRawDnsResolver` | `dns_gethostbyname` | DNS name resolution (superset — numeric literals, the DNS cache, and the local hostlist all resolve too). Bridges lwIP's async DNS to the synchronous `Resolve()` contract via a bounded spin (see [DNS](#dns) below). Requires `LWIP_DNS=1`. | -| `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. | - -`Platform/LwipRaw/Source/` is OS-agnostic: it wraps lwIP only and -contains zero direct calls to FreeRTOS, POSIX, Win32, Zephyr, or any -other host primitive. The one host primitive the TcpStream needs (a -bounded sleep for the synchronous-Open spin loop) is abstracted behind -the `SolidSyslogSleepFunction` typedef and supplied by you at -configure time. - -mbedTLS layering is unchanged: `SolidSyslogMbedTlsStream` consumes -`SolidSyslogLwipRawTcpStream` as its byte transport without -modification. See [`docs/integrating-mbedtls.md`](integrating-mbedtls.md) -for the TLS side. - ---- - -## `NO_SYS=1` vs `NO_SYS=0` - -lwIP supports both threading models. SolidSyslog supports both; the -adapter code is the same. The difference is entirely in how you -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). -Every lwIP Raw API call must happen on that same thread. - -SolidSyslog's `Service` loop fits this naturally: call it from your -main loop alongside `sys_check_timeouts()`. The TcpStream's bounded -synchronous-Open spin loop calls your injected `Sleep` callback -between polls; under `NO_SYS=1` your Sleep implementation should -tick the lwIP machinery while it waits: - -```c -void MyLwipSleep(int milliseconds) -{ - uint32_t deadline = MyTimebase_NowMs() + (uint32_t) milliseconds; - while (MyTimebase_NowMs() < deadline) - { - sys_check_timeouts(); - MyNetif_DrivePolledRx(); /* your BSP's RX pump */ - } -} -``` - -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) — option A: `tcpip_callback` - -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: - -```c -#include "lwip/tcpip.h" -#include "SolidSyslogLwipRawMarshal.h" - -struct MarshalHop -{ - SolidSyslogLwipRawCallback callback; - void* context; -}; - -static void RunHop(void* ctx) -{ - struct MarshalHop* hop = (struct MarshalHop*) ctx; - hop->callback(hop->context); -} - -void MyTcpipMarshal(SolidSyslogLwipRawCallback callback, void* context) -{ - /* 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); -} - -/* ... 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) -{ - LOCK_TCPIP_CORE(); - callback(context); - UNLOCK_TCPIP_CORE(); -} - -/* ... at boot ... */ -SolidSyslogLwipRaw_SetMarshal(MyCoreLockMarshal); -``` - -`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) -{ - vTaskDelay(pdMS_TO_TICKS((uint32_t) milliseconds)); -} -``` - -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. - ---- - -## `lwipopts.h` expectations - -The adapter wraps a specific subset of lwIP; your `lwipopts.h` needs -those features compiled in. Defaults that already cover us are noted; -features you must enable are flagged. - -| Setting | Required | Notes | -|---|---|---| -| `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` | DNS-dependent | Required for `SolidSyslogLwipRawDnsResolver`. The numeric `SolidSyslogLwipRawResolver` needs nothing. See [DNS](#dns). | -| `DNS_LOCAL_HOSTLIST` | Optional | A static name→address map consulted before any DNS server, returned synchronously. Pins names without a server (handy for fixed deployments and for test topologies a real resolver can't reach). See [DNS](#dns). | -| `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. | -| `PBUF_POOL_SIZE` | Per-traffic | Size the pool generously. The Datagram path borrows one `MEMP_PBUF` header per send; the TcpStream RX path borrows one pbuf per segment until `Stream_Read` drains it. | -| `MEMP_NUM_TCP_PCB` | Per-deployment | At least the number of concurrent `SolidSyslogLwipRawTcpStream` instances (default pool = 2 for the TLS-over-plain-TCP pair). | -| `MEMP_NUM_UDP_PCB` | Per-deployment | At least the number of concurrent `SolidSyslogLwipRawDatagram` instances (default pool = 1). | - ---- - -## Wiring example — bare-metal `NO_SYS=1` - -```c -#include "SolidSyslog.h" -#include "SolidSyslogConfig.h" -#include "SolidSyslogLwipRawAddress.h" -#include "SolidSyslogLwipRawDatagram.h" -#include "SolidSyslogLwipRawResolver.h" -#include "SolidSyslogLwipRawTcpStream.h" -#include "SolidSyslogPassthroughBuffer.h" -#include "SolidSyslogStreamSender.h" -#include "SolidSyslogUdpSender.h" - -extern void MyLwipSleep(int milliseconds); /* sys_check_timeouts + RX pump */ - -static struct SolidSyslog* g_syslog; -static struct SolidSyslogBuffer* g_buffer; - -void LogPipelineInit(void) -{ - struct SolidSyslogResolver* resolver = SolidSyslogLwipRawResolver_Create(); - struct SolidSyslogAddress* udpAddr = SolidSyslogLwipRawAddress_Create(); - struct SolidSyslogDatagram* datagram = SolidSyslogLwipRawDatagram_Create(); - - struct SolidSyslogUdpSenderConfig udpCfg = { - .Resolver = resolver, - .Datagram = datagram, - .Address = udpAddr, - .Endpoint = MyEndpoint, /* your SolidSyslogEndpointFunction */ - .EndpointVersion = MyEndpointVersion, - }; - struct SolidSyslogSender* sender = SolidSyslogUdpSender_Create(&udpCfg); - - g_buffer = SolidSyslogPassthroughBuffer_Create(sender); - - struct SolidSyslogConfig syslogCfg = { - .Hostname = MyHostname, - .AppName = MyAppName, - .ProcessId = MyProcessId, - .Clock = MyClock, - .Buffer = g_buffer, - }; - g_syslog = SolidSyslog_Create(&syslogCfg); -} - -void MainLoop(void) -{ - for (;;) - { - sys_check_timeouts(); - MyNetif_DrivePolledRx(); - SolidSyslog_Service(g_syslog); - /* … rest of your application … */ - } -} -``` - -For TCP, swap the UDP sender for a `SolidSyslogStreamSender` whose -`Stream` is a `SolidSyslogLwipRawTcpStream` built with your `Sleep`: - -```c -struct SolidSyslogLwipRawTcpStreamConfig streamCfg = { - .GetConnectTimeoutMs = NULL, /* falls back to SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS */ - .ConnectTimeoutContext = NULL, - .Sleep = MyLwipSleep, /* required */ -}; -struct SolidSyslogStream* tcpStream = SolidSyslogLwipRawTcpStream_Create(&streamCfg); -``` - ---- - -## Adapter-specific notes - -### Datagram — pbuf strategy - -`SolidSyslogLwipRawDatagram` uses `PBUF_REF`: a single pbuf header is -allocated per `SendTo`, its `payload` is pointed at the caller's -buffer, `udp_sendto` is called, and the header is `pbuf_free`d before -return. Zero copy on the hot path. - -This is safe across ARP queueing because lwIP's `etharp_query` does -`pbuf_clone(…, PBUF_RAM, q)`: it copies the referenced payload into -a private RAM pbuf before queueing, so the caller's buffer only needs -to live for the `udp_sendto` call itself (which is the synchronous -guarantee `SolidSyslogDatagram_SendTo` already provides). - -### TcpStream — `tcp_write` strategy - -`SolidSyslogLwipRawTcpStream` uses `TCP_WRITE_FLAG_COPY`: lwIP copies -your bytes into its own pbufs before `tcp_write` returns. This costs -one `memcpy` per send but honours the synchronous `Stream_Send(buf, -len)` lifetime contract: caller buffers are free at return, -regardless of when the peer ACKs. - -`tcp_output` is called after every successful `tcp_write` to nudge -transmission. If `tcp_output` returns `ERR_MEM`, the data is already -in `pcb->snd_buf`; lwIP will retry on the next `tcp_tmr` tick and -the wrapper reports Send-success (lwIP owns the bytes, exactly -matching POSIX's "kernel accepted the data into the send buffer" -semantics). - -### TcpStream — synchronous Open via spin-with-sleep - -`tcp_connect` is asynchronous: it returns immediately and lwIP fires -the registered `connected_cb` when the SYN/SYN-ACK exchange -completes. `SolidSyslogStream_Open` is synchronous. The wrapper -bridges by spinning on a `Connected` flag set by its `connected_cb`, -sleeping `SOLIDSYSLOG_LWIP_RAW_TCP_CONNECT_POLL_MS` (default 10 ms) -between checks via your injected `Sleep`, bounded by the -`GetConnectTimeoutMs` getter (default `SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS` -= 200 ms; install a runtime getter if you need to vary it). - -Timeout → `tcp_abort` on the pcb, Open returns `false`. Errored -callback → `tcp_abort`, Open returns `false`. Immediate non-`ERR_OK` -from `tcp_connect` → `tcp_abort`, Open returns `false`. - -### TcpStream — RX queue - -lwIP's `tcp_recv` callback fires when bytes arrive. The wrapper owns -a bounded ring of pbuf pointers sized by `SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE` -(default 8). Each `Stream_Read` drains bytes from the head pbuf, -calls `tcp_recved(pcb, n)` to ACK back to lwIP's receive window, and -`pbuf_free`s the head when fully drained. Queue full → the -callback returns non-`ERR_OK` so lwIP retains the pbuf and replays -the callback later (lwIP's flow-control hook). - -> Chained pbufs. lwIP hands a single received segment as a pbuf -> chain (`p->tot_len > p->len`, `p->next != NULL`) whenever the -> payload spans more than one pool pbuf: normal once a segment exceeds -> one `PBUF_POOL_BUFSIZE`, and influenced by the peer / on-path -> segmentation. The wrapper drains the whole chain keyed off -> `tot_len` (via `pbuf_copy_partial`, walking `p->next`) across one or -> more `Stream_Read` calls, and `pbuf_free`s the head (which frees -> every link) only once the chain is fully consumed. Integrators -> supplying their own `SolidSyslogStream` byte transport must honour the -> same contract: never read only `head->len` and then free the chain, or -> the tail bytes are lost (this corrupts a stacked TLS record stream). - -The default-8 queue size is sized for the typical mTLS handshake -flight (ServerHello + Certificate + ServerKeyExchange + -ServerHelloDone is 2–4 segments). Bump it for streaming server -responses; lower it if your `MEMP_NUM_PBUF` is constrained and you -need lwIP to backpressure sooner. - -### TcpStream — lifecycle ownership - -The wrapper owns its `tcp_pcb` end-to-end. Three things to know: - -1. `tcp_err` releases the pcb upstream. When lwIP fires - `tcp_err` for a fatal event (RST, OOM, ABRT), the pcb is gone - from lwIP's side before the callback runs. The wrapper's - `tcp_err` handler nulls its internal `Pcb` field and sets an - `Errored` flag. The next `Stream_Send` returns `false`; the next - `Stream_Read` returns `-1`. Crucially, calling `tcp_close` on a - pcb that was already released by `tcp_err` is a use-after-free - in lwIP, so the wrapper guards against this with a `Pcb != NULL` - check before `tcp_close`. You never see this rule unless you - bypass the abstraction and poke at lwIP pcbs directly through - your own code, so don't. - -2. Peer FIN (`tcp_recv` with `p == NULL`) drains before EOF. - The half-close sets `Errored`; the next `Stream_Read` that finds - the queue empty returns `-1` and internally `tcp_close`s the - pcb. Already-queued bytes drain first. - -3. `Close` is idempotent. Second `Close` is a no-op. `Destroy` - internally calls `Close` (which drains the RX queue's pbufs and - then `tcp_close`s if the pcb is still around), then overwrites - the abstract base with `SolidSyslogNullStream` so use-after- - destroy is a safe no-op rather than a NULL-fn-pointer crash. - ---- - -## Tunables - -All tunables live in `Core/Interface/SolidSyslogTunablesDefaults.h`. -Override by `#define`ing them in a user-tunables header passed via -the `SOLIDSYSLOG_USER_TUNABLES_FILE` CMake variable. - -| Tunable | Default | Adjust when | -|---|---|---| -| `SOLIDSYSLOG_RESOLVER_POOL_SIZE` | `1U` | You wire more than one resolver. Role pool shared by the numeric and DNS resolvers — counts instances, not implementations, so wiring **both** the numeric and DNS resolver in one build means setting this to `2U`. | -| `SOLIDSYSLOG_DNS_RESOLVE_TIMEOUT_MS` | `5000U` | Default suits a healthy recursive resolver. Raise for a slow / distant DNS server. | -| `SOLIDSYSLOG_LWIP_RAW_DNS_RESOLVE_POLL_MS` | `10U` | Default gives 500 polls inside the 5 s resolve deadline. Lower to notice completion sooner; raise to reduce spin overhead on a constrained MCU. | -| `SOLIDSYSLOG_DATAGRAM_POOL_SIZE` | `1U` | You wire more than one UDP sender. | -| `SOLIDSYSLOG_TCP_STREAM_POOL_SIZE` | `2U` | You wire more than the canonical plain-TCP + TLS-underlying-TCP pair. | -| `SOLIDSYSLOG_ADDRESS_POOL_SIZE` | `3U` | Shared with PlusTcp / Posix / Winsock — bump if you need >3 concurrent destinations. | -| `SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS` | `200U` | Default suits loopback / LAN. Raise for WAN deployments behind a high-RTT link; or install a runtime `GetConnectTimeoutMs` getter for per-instance tuning. | -| `SOLIDSYSLOG_LWIP_RAW_TCP_CONNECT_POLL_MS` | `10U` | Default gives 20 polls inside the 200 ms connect deadline. Lower it to notice a fast connect sooner; raise it to reduce spin overhead on a constrained MCU. | -| `SOLIDSYSLOG_LWIP_RAW_TCP_RX_QUEUE_SIZE` | `8U` | Sized for the typical mTLS handshake flight. Bump for streaming server responses; lower if `MEMP_NUM_PBUF` is tight. | - ---- - -## DNS - -`SolidSyslogLwipRawDnsResolver` resolves the destination by name via -lwIP's `dns_gethostbyname`. It is a superset of the numeric -`SolidSyslogLwipRawResolver` (numeric literals, DNS-cache hits, and -local-hostlist entries all resolve through it too), so you only need one -resolver, picked by whether you compile DNS in. - -### The async bridge - -`dns_gethostbyname` is asynchronous: a cold lookup returns `ERR_INPROGRESS` -and lwIP invokes a `dns_found_callback` later, on the tcpip thread, when the -answer arrives. The library's `SolidSyslogResolver_Resolve` contract is -synchronous, so the adapter bridges the two exactly as `SolidSyslogLwipRawTcpStream` -bridges `tcp_connect`: - -1. The `dns_gethostbyname` call is made under the marshal hop - (`SolidSyslogLwipRaw_SetMarshal`): it touches lwIP core state, unlike the - numeric resolver's pure `ipaddr_aton` parse, which is why only this resolver - marshals. -2. `ERR_OK` (synchronous hit: numeric, cache, or local hostlist) → the address - is ready immediately; no spin. -3. `ERR_INPROGRESS` → the adapter spins on the caller's thread, sleeping via - your injected `Sleep` between polls of a flag the callback sets, until the - answer arrives or the deadline elapses. The spin never sleeps the tcpip - thread (that would starve the DNS retransmit timer and RX). -4. A delivered address → success; a delivered `NULL` (no such host) → failure; - deadline exceeded → failure plus a `SolidSyslog_Error` report - (`SOLIDSYSLOG_LWIPRAW_DNS_RESOLVER_ERROR_RESOLVE_TIMEOUT`). - -`Sleep` is required (a `NULL` config falls back to `NullResolver`). The -deadline and poll period come from `SOLIDSYSLOG_DNS_RESOLVE_TIMEOUT_MS` (5 s) -and `SOLIDSYSLOG_LWIP_RAW_DNS_RESOLVE_POLL_MS` (10 ms), build-time only; DNS -timeout rarely needs runtime tuning, so there is no per-instance getter. - -### Local hostlist (no DNS server) - -For fixed deployments (or test topologies where a real resolver can't return a -reachable address), `DNS_LOCAL_HOSTLIST` maps names statically. A hostlist hit -returns `ERR_OK` before any server is consulted, so the resolve completes -synchronously and entirely on-device: - -```c -/* lwipopts.h */ -#define LWIP_DNS 1 -#define DNS_LOCAL_HOSTLIST 1 -#define DNS_LOCAL_HOSTLIST_INIT \ - { DNS_LOCAL_HOSTLIST_ELEM("syslog-ng", IPADDR4_INIT_BYTES(10, 0, 2, 2)) } -``` - -### Wiring - -```c -#include "SolidSyslogLwipRawDnsResolver.h" - -extern void MyLwipSleep(int milliseconds); /* same Sleep the TcpStream uses */ - -struct SolidSyslogLwipRawDnsResolverConfig dnsCfg = { .Sleep = MyLwipSleep }; -struct SolidSyslogResolver* resolver = SolidSyslogLwipRawDnsResolver_Create(&dnsCfg); -/* hand `resolver` to the UdpSender / StreamSender config exactly as the - numeric resolver — the destination host is now a name, e.g. "logs.example.com". */ -``` - -> Known limitation: over-the-wire DNS is not BDD-covered. The -> FreeRTOS-on-lwIP BDD target resolves the oracle by name, but only through the -> synchronous local-hostlist branch: the QEMU slirp + docker topology cannot -> hand the guest a reachable address for the `syslog-ng` alias over real DNS -> (slirp's forwarder resolves it to a docker-bridge IP the guest has no route -> to; only `10.0.2.2` reaches the oracle). The async / over-the-wire / timeout -> branches are unit-tested instead -> (`Tests/Lwip/SolidSyslogLwipRawDnsResolverTest`), consistent with the -> project's integration-over-BDD stance for paths the harness can't realistically -> drive. - ---- - -## What this guide does not cover - -- 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. diff --git a/docs/integrating-mbedtls.md b/docs/integrating-mbedtls.md deleted file mode 100644 index 06916d81..00000000 --- a/docs/integrating-mbedtls.md +++ /dev/null @@ -1,228 +0,0 @@ -# Integrating SolidSyslog with mbedTLS - -`SolidSyslogMbedTlsStream` lets you deliver RFC 5425 (syslog over TLS) -records from SolidSyslog through Mbed TLS instead of OpenSSL. It is the -recommended adapter on embedded / FreeRTOS / bare-metal targets where -OpenSSL is too large or impractical. Hosted Linux / Windows deployments -should use `SolidSyslogOpenSslStream` (OpenSSL); both adapters expose the -same `SolidSyslogStream` vtable, so the rest of the wiring -(`SolidSyslogStreamSender`, your buffer, your store) is identical. - -This document covers what you, the integrator, plug in. It does not -re-teach mbedTLS. For that, see the -[upstream Mbed TLS documentation](https://mbed-tls.readthedocs.io/). - ---- - -## The shape - -```text -SolidSyslog_Log ─▶ Buffer ─▶ Sender ─▶ SolidSyslogStreamSender - │ - ▼ - SolidSyslogMbedTlsStream ◀── you build CA/cert/key/DRBG handles - │ - ▼ - SolidSyslogStream ◀── you pick / write the TCP backend - │ - ▼ - (your TCP/IP stack) -``` - -You supply two things directly: the byte-transport `SolidSyslogStream` and -the per-context mbedTLS handles passed through -`SolidSyslogMbedTlsStreamConfig`. - ---- - -## What you need to provide - -| Item | Owner | Notes | -|---|---|---| -| `Transport` | You | A `SolidSyslogStream*` carrying TCP. The library ships `SolidSyslogPosixTcpStream` (POSIX), `SolidSyslogWinsockTcpStream` (Windows), and `SolidSyslogPlusTcpTcpStream` (FreeRTOS-Plus-TCP). If your TCP/IP stack is different (LwIP, NicheStack, vendor BSP), write your own `SolidSyslogStream`; see [`Platform/Posix/Source/SolidSyslogPosixTcpStream.c`](../Platform/Posix/Source/SolidSyslogPosixTcpStream.c) as a reference. | -| `Sleep` | You | A `SolidSyslogSleepFunction`. Drives the bounded handshake retry between `WANT_READ` / `WANT_WRITE` polls. On FreeRTOS use a `vTaskDelay`-backed wrapper; on POSIX `SolidSyslogPosixSleep` is the natural fit. Required. | -| `GetHandshakeTimeoutMs` / `HandshakeTimeoutContext` | You (optional) | Per-instance accessor pair for the bounded handshake budget. `NULL` falls back to the `SOLIDSYSLOG_TLS_HANDSHAKE_TIMEOUT_MS` compile-time tunable (default 5000 ms). Install when you need to runtime-tune the handshake deadline: slow peers on a constrained link, or per-tenant policy from your existing configuration store. The accessor is called on every `Open`. | -| `Rng` | You | `mbedtls_ctr_drbg_context*` you seeded yourself. The adapter calls `mbedtls_ctr_drbg_random` against it. Required. | -| `CaChain` | You | `mbedtls_x509_crt*` you parsed yourself (from filesystem, baked-in PEM, HSM, whatever fits your build). Required. | -| `ServerName` | You | SNI + peer-identity check string. A non-empty name is verified against the server certificate (SAN/CN), rejecting any other CA-issued cert. `NULL` connects but the peer is unverified (any cert chaining to a trusted CA is accepted, MITM-class), so the library emits a `WARNING` (`BAD_CONFIG` / `SOLIDSYSLOG_MBEDTLS_STREAM_ERROR_SERVER_NAME_NOT_SET`). Note that having no DNS does not force this: an IP-pinned target can still set `ServerName` to the name (or IP SAN) on its certificate and get full identity verification. Use `""` only as a deliberate opt-out for closed networks / private CAs where there is genuinely no name to verify; it connects chain-only with no diagnostic. | -| `ClientCertChain` / `ClientKey` | You | `mbedtls_x509_crt*` + `mbedtls_pk_context*` for mTLS. Both `NULL` = server-auth-only TLS. Both non-`NULL` = mTLS. Supplying only one is treated as "no client cert"; the adapter never half-configures. | - -The full struct shape lives in -[`Platform/MbedTls/Interface/SolidSyslogMbedTlsStream.h`](../Platform/MbedTls/Interface/SolidSyslogMbedTlsStream.h). - -The adapter pins the minimum protocol version to TLS 1.2 on its own -`ssl_config` rather than inheriting `MBEDTLS_SSL_PRESET_DEFAULT`, which, on a -permissive mbedTLS build (2.x, or 3.x with `MBEDTLS_SSL_PROTO_TLS1_0/1_1` -enabled), can otherwise negotiate down to TLS 1.0/1.1. This matches the OpenSSL -reference adapter's explicit floor, so the two are equivalent in downgrade -resistance; it is not something you configure. - ---- - -## Scenario A: you already have Mbed TLS in your image - -If your firmware already wires Mbed TLS for another subsystem (a cloud -client, an OTA updater, a vendor security framework), you keep that wiring -intact. The adapter consumes the handles you've already built; it never -calls `mbedtls_platform_setup` / `_teardown`, never installs -threading-alt hooks, never resets the global RNG, never replaces your -debug callback. See the [coexistence contract](#coexistence-contract) -below for the auditable list. - -Concretely, on top of your existing setup: - -1. Pick a `SolidSyslogStream` for the byte transport. Use one of the - shipped adapters that matches your TCP/IP stack - (`SolidSyslogPlusTcpTcpStream`, `SolidSyslogPosixTcpStream`, - `SolidSyslogWinsockTcpStream`) or write your own backing the same - `SolidSyslogStream` vtable. If you wrote your own, the existing - shipped adapters are the worked examples. -2. Fill in `SolidSyslogMbedTlsStreamConfig` with the handles you - already have: - - ```c - struct SolidSyslogMbedTlsStreamConfig cfg = { - .Transport = myTcpStream, /* from step 1 */ - .Sleep = MyVTaskDelayWrapper, /* or PosixSleep / similar */ - .GetHandshakeTimeoutMs = NULL, /* defaults to SOLIDSYSLOG_TLS_HANDSHAKE_TIMEOUT_MS (5000 ms) */ - .HandshakeTimeoutContext = NULL, - .Rng = &myAlreadySeededDrbg, - .CaChain = &myAlreadyParsedCaChain, - .ServerName = "syslog.example.com", - .ClientCertChain = &myClientCert, /* NULL for server-auth-only */ - .ClientKey = &myClientKey, /* paired with ClientCertChain */ - }; - struct SolidSyslogStream* tlsStream = SolidSyslogMbedTlsStream_Create(&cfg); - ``` - -3. Wire `tlsStream` into a `SolidSyslogStreamSender` as the `Stream` - field, the same way you'd wire a plain TCP stream. RFC 6587 - octet-counting framing is applied by `StreamSender` on top of the - adapter. - -That's the whole integration on the SolidSyslog side. There are no -process-wide hooks to install and nothing to teardown beyond the matching -`SolidSyslogMbedTlsStream_Destroy` when you tear the sender down. - ---- - -## Scenario B: you do not have Mbed TLS yet - -If you're bringing Mbed TLS in fresh for SolidSyslog, do that work first -following the upstream -[Mbed TLS porting guide](https://mbed-tls.readthedocs.io/en/latest/kb/how-to/how-do-i-port-mbed-tls-to-a-new-environment-OS/). -Once Mbed TLS itself is building on your target, you need the following -specifically for this adapter: - -- A seeded `mbedtls_ctr_drbg_context`. `mbedtls_entropy_init` + - `mbedtls_entropy_add_source` for at least one source registered as - `MBEDTLS_ENTROPY_SOURCE_STRONG` (without a STRONG-tagged source, - `mbedtls_entropy_func` never satisfies its internal threshold and - every `mbedtls_ctr_drbg_seed` call returns - `MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED`, silent on the wire, - loud in your tests), then `mbedtls_ctr_drbg_init` + - `mbedtls_ctr_drbg_seed`. Production-quality entropy is a hardware - question: TRNG, vendor HSM, or a board-specific source. -- `psa_crypto_init()` called *after* the DRBG is seeded. Mbed TLS - 3.6's TLS 1.3 code path routes through PSA. If PSA isn't initialised, - the first handshake state transition returns - `MBEDTLS_ERR_ERROR_GENERIC_ERROR` (-0x0001) before any TLS bytes leave - the socket. If your target has no platform entropy source (a common - embedded case), `#define MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG` in your - mbedTLS config and provide - `mbedtls_psa_external_get_random` that wraps the DRBG you just seeded; - this keeps PSA and the classic mbedTLS API on the same entropy chain. -- A parsed CA chain. `mbedtls_x509_crt_init` + - `mbedtls_x509_crt_parse` against whatever delivery mechanism fits your - build (filesystem on POSIX, baked-in array via `xxd -i` on bare-metal, - HSM-pulled blob, etc.). PEM input must be NUL-terminated. -- (mTLS only) a parsed client cert chain and private key. Same - pattern as the CA chain plus `mbedtls_pk_init` / - `mbedtls_pk_parse_key`. -- A byte-transport `SolidSyslogStream` matching your TCP/IP stack, - exactly as in [Scenario A](#scenario-a-you-already-have-mbed-tls-in-your-image). - -A worked end-to-end example for all of the above lives at -[`Bdd/Targets/Common/BddTargetTlsSender_MbedTls_PlusTcpTcp.c`](../Bdd/Targets/Common/BddTargetTlsSender_MbedTls_PlusTcpTcp.c) -(FreeRTOS-Plus-TCP on QEMU mps2-an385). The matching Mbed TLS config -overrides live at -[`Bdd/Targets/FreeRtos/mbedtls_user_config.h`](../Bdd/Targets/FreeRtos/mbedtls_user_config.h). - ---- - -## Coexistence contract - -`Platform/MbedTls/Source/` is auditably free of process-global Mbed TLS -calls. The adapter never: - -- calls `mbedtls_platform_setup` / `_teardown` -- calls `mbedtls_threading_set_alt` -- calls `psa_crypto_init` (you do) -- calls `mbedtls_platform_set_calloc_free` (you do, if you need it) -- calls `mbedtls_debug_set_threshold` / `mbedtls_ssl_conf_dbg` -- frees any handle you passed in via the config struct - -Everything in that list is global state your existing integration may -already own. Auditors verify the contract by grepping -`Platform/MbedTls/Source/`; any future change that introduces a global -call must be flagged in review. - ---- - -## FreeRTOS-specific gotchas - -These bit us during the BDD-target bring-up. If you're on FreeRTOS with -newlib, treat them as integrator-side checklist items: - -- Route mbedTLS allocations to the RTOS heap. Mbed TLS calls libc - `calloc`, which on newlib targets typically hits a tiny `_sbrk`-backed - syscall heap (4 KiB in the SolidSyslog BDD reference at - [`Bdd/Targets/FreeRtos/Common/Syscalls.c`](../Bdd/Targets/FreeRtos/Common/Syscalls.c)). - A single `mbedtls_ssl_setup` wants ~10–16 KiB and will fail with - `MBEDTLS_ERR_SSL_ALLOC_FAILED` (-0x7F00). Set - `MBEDTLS_PLATFORM_MEMORY` in your config and call - `mbedtls_platform_set_calloc_free(yourCalloc, yourFree)` (pvPortMalloc - / vPortFree) before any `mbedtls_*_init`. -- Shrink the TLS record buffers from the 16 KiB default. Set - `MBEDTLS_SSL_IN_CONTENT_LEN` to the largest TLS record your peer will - send (server cert + chain is typically 2–4 KiB), and - `MBEDTLS_SSL_OUT_CONTENT_LEN` to your largest application message. - The defaults cost ~32 KiB of FreeRTOS heap per TLS context. -- `mbedtls_ssl_setup` allocates roughly `IN + OUT + ~3 KiB` of - handshake state. Size your FreeRTOS heap - (`configTOTAL_HEAP_SIZE`) accordingly across all concurrent TLS - contexts. -- `MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG` + the external RNG hook are - effectively mandatory if you've defined - `MBEDTLS_NO_PLATFORM_ENTROPY` (which you typically have on - embedded). Without it, `psa_crypto_init` returns - `PSA_ERROR_INSUFFICIENT_ENTROPY` (-148). - -The BDD target's -[mbedtls_user_config.h](../Bdd/Targets/FreeRtos/mbedtls_user_config.h) -shows the minimal config that satisfies the above for QEMU mps2-an385. - ---- - -## Reference integrations - -| Target | Adapter source | Mbed TLS config | Notes | -|---|---|---|---| -| FreeRTOS QEMU mps2-an385 + FreeRTOS-Plus-TCP | [BddTargetTlsSender_MbedTls_PlusTcpTcp.c](../Bdd/Targets/Common/BddTargetTlsSender_MbedTls_PlusTcpTcp.c) | [mbedtls_user_config.h](../Bdd/Targets/FreeRtos/mbedtls_user_config.h) | Demo-quality entropy and baked-in PEMs; loudly tagged not-for-production. | -| Linux host (host-TDD parity with the embedded path) | [Tests/MbedTlsIntegration/](../Tests/MbedTlsIntegration/) | — | In-process TLS server drives a real handshake against the wrapper. | -| POSIX (OpenSSL reference, for comparison) | [BddTargetTlsSender_OpenSsl_PosixTcp.c](../Bdd/Targets/Common/BddTargetTlsSender_OpenSsl_PosixTcp.c) | — | Same composition shape using `SolidSyslogOpenSslStream` for the TLS layer. | - ---- - -## What this adapter does not own - -- PEM-to-handle conversion: you parse, in whatever way fits your - build. -- Certificate rotation: re-parse and rebuild the adapter, or destroy - / re-create the `SolidSyslogStreamSender` so the next Connect picks up - the new chain. -- HSM / TRNG integration: your entropy source feeds CTR_DRBG; the - adapter consumes the seeded DRBG. -- Per-connection TLS configuration: one adapter instance, one - `ssl_config`. If you need per-peer cipher / version pinning, build - multiple adapters. diff --git a/docs/integrating-plusfat.md b/docs/integrating-plusfat.md deleted file mode 100644 index 5b0c1fdc..00000000 --- a/docs/integrating-plusfat.md +++ /dev/null @@ -1,128 +0,0 @@ -# Integrating SolidSyslog with FreeRTOS-Plus-FAT - -`SolidSyslogPlusFatFile` is the `SolidSyslogFile` adapter backed by -[FreeRTOS-Plus-FAT](https://www.freertos.org/Documentation/03-Libraries/05-FreeRTOS-labs/04-FreeRTOS-plus-FAT/01-FreeRTOS-plus-FAT) -(the `ff_stdio` API). It is the FreeRTOS-Plus ecosystem counterpart to the -OS-agnostic [ChaN FatFs adapter](../Platform/FatFs/); pair it with -FreeRTOS-Plus-TCP for a coherent all-FreeRTOS-Plus storage + transport stack. It -gives the store-and-forward layer (`SolidSyslogBlockStore` over -`SolidSyslogFileBlockDevice`) a real on-flash file backend. - -This guide covers what you must supply around the adapter. For the file seam -itself see [`SolidSyslogFile.h`](../Core/Interface/SolidSyslogFile.h); for the -store see [`SolidSyslogBlockStore.h`](../Core/Interface/SolidSyslogBlockStore.h). - -## The shape - -```text -SolidSyslogBlockStore - │ (SolidSyslogStore vtable) -SolidSyslogFileBlockDevice <-- sequence-numbered .log files - │ (SolidSyslogFile vtable) -SolidSyslogPlusFatFile <-- this adapter: ff_fopen / ff_fread / ff_fwrite / … - │ (ff_stdio API) -FreeRTOS-Plus-FAT core (ff_*.c) <-- vendor library, you compile it - │ (FF_Disk_t block callbacks) -your FF_Disk_t media driver <-- YOU write this: SD / eMMC / QSPI-flash / RAM -``` - -The adapter owns only the middle box. The vendor core and the media driver are -yours to provide; the library never reaches the block device directly. - -## What you must provide - -1. The FreeRTOS-Plus-FAT sources, compiled into your image (`ff_crc.c`, - `ff_dir.c`, `ff_error.c`, `ff_fat.c`, `ff_file.c`, `ff_format.c`, `ff_ioman.c`, - `ff_locking.c`, `ff_memory.c`, `ff_stdio.c`, `ff_string.c`, `ff_sys.c`, - `ff_time.c`). These are not -Wsign-conversion / -Wconversion clean; compile - them under a relaxed warning set, as you would the FreeRTOS kernel. - -2. An `FF_Disk_t` media driver for your storage hardware: `FF_CreateIOManager` - with read/write block callbacks, `FF_Mount` (format-on-first-use via - `FF_Partition` + `FF_Format` when no FAT is present), and `FF_FS_Add("/", disk)` - to register the volume in `ff_stdio`'s virtual file system. Plus-FAT ships - reference drivers under `portable/` (`ff_ramdisk.c` is the clearest template). - The library's BDD target ships a semihosting example, - [`Bdd/Targets/Common/FFSemihostingDisk.c`](../Bdd/Targets/Common/FFSemihostingDisk.c). - -3. A `FreeRTOSFATConfig.h` on your include path. `ff_headers.h` pulls it via - `#include "FreeRTOSFATConfig.h"`; unlike ChaN FatFs's `ffconf.h`, it resolves - off the `-I` path (no source-tree colocation needed). At minimum set - `ffconfigBYTE_ORDER` and `ffconfigCWD_THREAD_LOCAL_INDEX`; - `FreeRTOSFATConfigDefaults.h` fills the rest. See - [`Bdd/Targets/FreeRtos/FreeRTOSFATConfig.h`](../Bdd/Targets/FreeRtos/FreeRTOSFATConfig.h). - -4. Kernel configuration in `FreeRTOSConfig.h`: - - `configUSE_RECURSIVE_MUTEXES = 1` (Plus-FAT's `ff_locking.c` enforces this). - - Event groups compiled in (`event_groups.c`); the IO manager uses them. - - `configNUM_THREAD_LOCAL_STORAGE_POINTERS >= ffconfigCWD_THREAD_LOCAL_INDEX + 3`. - `ff_stdio` stores its `errno`, CWD, and `FF_Error` in per-task thread-local - storage at offsets `ffconfigCWD_THREAD_LOCAL_INDEX + {0,1,2}`. With the index - at 0 that means at least 3 slots; `ff_stdio.h` enforces this at compile - time. - - `configSUPPORT_STATIC_ALLOCATION = 1` if your media driver creates its IO - manager mutex statically (the example does); dynamic allocation otherwise. - -## Path convention — absolute paths - -With `ffconfigHAS_CWD = 0` (the default), Plus-FAT's `ff_stdio` accepts only -absolute paths; its relative-path resolver (`prvABSPath`) is a pass-through. -Configure `SolidSyslogFileBlockDevice` with an absolute path prefix, e.g. -`/STORE`, so the store files land at the volume root as `/STORE00.log`, -`/STORE01.log`, … (A leading `/` is equally valid for the ChaN FatFs adapter, so -the same prefix works for either backend.) The default 8.3 short-filename mode -(`ffconfigLFN_SUPPORT = 0`) is sufficient for that naming. - -## Durability contract - -`SolidSyslogPlusFatFile_Write` flushes after every complete write so a power -loss never loses a record the `BlockStore` was told had been stored. Two notes -specific to Plus-FAT: - -- There is no per-file flush. `ff_stdio.h` *declares* `ff_fflush`, but the - library (as of SHA `8d38036`) never *defines* it. The adapter instead calls - `FF_FlushCache(file->pxIOManager)`, the IO-manager cache flush, which is the - real durability primitive. -- The directory entry (file size) is committed on `Close`, not on each - flush. `FF_FlushCache` persists the file's data sectors; the dirent's size - field is written by `ff_fclose`. A graceful shutdown (the library's - `SolidSyslog` teardown closes the store file) therefore leaves both data and - metadata consistent. If your platform must survive a hard power cut - mid-record, size the records and the discard policy with that in mind. - -## Heap usage - -The adapter struct itself is pool-allocated (`SOLIDSYSLOG_FILE_POOL_SIZE`) and -never calls `malloc`. FreeRTOS-Plus-FAT, however, does allocate: the IO -manager sector cache and internal buffers come from the FreeRTOS heap (`heap_4` -or your `pvPortMalloc`). This is integrator-scoped and expected, exactly the -mbedTLS precedent: the vendor library allocates; SolidSyslog's own structures do -not. Size your heap for the IO-manager cache you request in `FF_CreateIOManager`. - -## Reference integration - -[`Bdd/Targets/FreeRtos/`](../Bdd/Targets/FreeRtos/) is the worked example, the -FreeRTOS-Plus-TCP + FreeRTOS-Plus-FAT QEMU BDD target. It wires: - -- [`FFSemihostingDisk.c`](../Bdd/Targets/Common/FFSemihostingDisk.c): an - `FF_Disk_t` over an ARM-semihosting host-backed flat disk (8 MiB, FAT16), - modelled on Plus-FAT's `ff_ramdisk.c` but persistent (mount-or-format-on-first- - use, so a power cycle keeps its data). -- [`BddTargetPlusFatMount.c`](../Bdd/Targets/Common/BddTargetPlusFatMount.c): - the mount/unmount + `SolidSyslogPlusFatFile` create/destroy wired into the - shared FreeRTOS pipeline's FS-mount seam. -- [`FreeRTOSFATConfig.h`](../Bdd/Targets/FreeRtos/FreeRTOSFATConfig.h) and the - `configNUM_THREAD_LOCAL_STORAGE_POINTERS` knob in - [`FreeRTOSConfig.h`](../Bdd/Targets/FreeRtos/FreeRTOSConfig.h). - -The full store / capacity / power-cycle-replay BDD suite runs against this target -on QEMU (`bdd-freertos-qemu-plustcp`). - -## What this adapter does not own - -- The media driver. SD/eMMC/flash/RAM block I/O is yours (or a Plus-FAT - `portable/` driver). The adapter speaks only `ff_stdio`. -- Mounting and formatting. The adapter opens/reads/writes files; bringing the - volume up (`FF_Mount` / `FF_Format` / `FF_FS_Add`) is the media driver's job. -- The FreeRTOS-Plus-FAT sources and their licence. You vendor and compile - them. diff --git a/docs/misra-deviations.md b/docs/misra-deviations.md index af4df433..e00b951d 100644 --- a/docs/misra-deviations.md +++ b/docs/misra-deviations.md @@ -3,7 +3,29 @@ SolidSyslog is MISRA-informed, not certified-compliant. The project adopts a curated subset of MISRA C:2012 rules per tier (see `docs/NAMING.md` for the tier model). This document records every -deliberate deviation from a rule the project otherwise enforces. +deliberate deviation from a rule the project otherwise enforces, and every +suppression that is not a deviation at all. + +Those are two different things and each entry says which it is. A **deviation** +is a departure: the code does something the guideline forbids, and the entry +justifies it. A **tool limitation** is not: the code complies, cppcheck-misra +reports a finding anyway, and the entry explains why the report is wrong. Both +are recorded here because a suppression exists either way, and the authorisation +for a suppression belongs in one place. Counting the entries is therefore not a +count of departures: seven of the entries record a departure. Where the +distinction does not matter, the words *deviation* and *entry* are used +interchangeably below. + +This register is published as evidence of process, not as a compliance +submission. It exists so that an integrator building SolidSyslog into a +MISRA-constrained product can see where the code knowingly departs from a +guideline, and judge each departure against their own risk posture. It makes no +conformance claim on your behalf, and it is an input to your compliance +documentation rather than a substitute for it. + +MISRA and MISRA C are registered trade marks of The MISRA Consortium Limited, +used here for identification only. This project is neither endorsed by nor +affiliated with MISRA. Each deviation is paired with a matching entry in `misra_suppressions.txt` (the cppcheck-misra input). The two files are complementary: @@ -13,33 +35,89 @@ Each deviation is paired with a matching entry in `misra_suppressions.txt` | `misra_suppressions.txt` | cppcheck-misra | Machine-readable suppressions per rule / file / line | | `docs/misra-deviations.md` | Reviewers, auditors, integrators | Why each deviation exists, with rationale, scope, approval | -Each entry in `misra_suppressions.txt` shall reference the section of -this document that authorises it. The suppressions file was populated +`misra_suppressions.txt` is the authoritative instance-level trace: its entries +are line-specific, so every individual site a deviation authorises appears there +by rule, file and line, and each block back-references the deviation in this +document that authorises it. A finding on a line not listed is not covered by +any deviation and surfaces in CI. The suppressions file was populated under [S10.06](https://github.com/cososo-ltd/solid-syslog/issues/367) after the rule subset was curated; before then it carried only a header comment. -The format below is patterned on MISRA's own deviation record template -(MISRA Compliance:2020 §4.2). +Each entry follows a fixed shape: the guideline and its category, the construct +at issue, the scope the entry covers, the engineering rationale, the +residual risk and how it is mitigated, and a named approval with its dates. The +register follows the deviation record structure of MISRA Compliance:2020 +(Section 4, example record at Appendix B), adopted voluntarily; it is mandatory +only from MISRA C:2023 onward. + +Every deviation here is raised and approved by the same person. SolidSyslog is +developed by a one-person consultancy, so the project owner holds the designated +technical authority for these decisions; there is no second reviewer and the +record does not pretend otherwise. An integrator running their own compliance +process should re-review each deviation against their own risk posture rather +than inheriting this approval. + +## Guideline text is not reproduced here + +Each entry identifies its guideline by number and category and then describes +**the construct in SolidSyslog that the finding lands on** — not what the +guideline says. +MISRA C:2012 is copyrighted and not redistributable, so its rule text, +amplification and examples are omitted deliberately rather than by oversight. +Read them in your own licensed copy, available from +[misra.org.uk](https://misra.org.uk/); Appendix A lists every guideline with its +category. + +Nothing is lost by this. A deviation record exists to show that the project +understood the guideline and reasoned about the risk of departing from it, and a +precise description of our own code demonstrates that better than a restated +headline an assessor already has in front of them. + +## Language edition for clause references + +Unless otherwise stated, all clause references are to ISO/IEC 9899:1999 (C99). +C99 is the conformance baseline — the edition the source is written against and +claims to be valid under. It is not the same thing as the build configuration: +the default build selects C11 (`CMAKE_C_STANDARD` is 11 unless overridden), and +the `build-linux-c99` lane verifies the baseline on every pull request. The claim +is about `Core/`: the lane builds Core alone at strict `-std=c99`, and the +platform packs are a best-effort drift check rather than a guarantee — see +[builds.md](builds.md#c99-portability--c99). Where a deviation concerns a code path +that only a later edition compiles, that entry names the applicable edition and +cites both. + +Paragraph numbering differs between editions — §6.7.2.1 renumbered when C11 +added anonymous structure and union members — so a C11 paragraph number read +against a C99 copy lands on the wrong sentence. That is why the edition is fixed +here rather than left to the reader. --- ## D.001 — Rule 5.1 external identifier uniqueness relaxed to 63 characters -### Rule +### Guideline -> **Rule 5.1 (Required)** — External identifiers shall be distinct. +**MISRA C:2012 Rule 5.1** — Required. +**Rule text:** not reproduced (see [above](#guideline-text-is-not-reproduced-here)). +**Classification:** Deviation — the code departs from the guideline. -The "distinct" requirement is parameterised by the implementation's -significant-character count for external identifiers. C99 §5.2.4.1 -specifies a minimum of 31 significant characters in external -identifiers — i.e. a conforming compiler may treat two external -identifiers that agree in the first 31 characters as the same identifier. +### Construct -### Deviation +SolidSyslog requires external identifiers to be distinct within their first +63 characters rather than their first 31. -SolidSyslog requires external identifiers to be distinct in the first -63 characters rather than the first 31. +Thirty-one is the floor the C language sets: C99 §5.2.4.1 guarantees +only 31 significant characters in an external identifier, so a conforming +compiler is permitted to treat two identifiers that agree that far as the same +identifier. The project asserts a longer guarantee than the language requires, +and states the number rather than leaving it implicit. + +Every toolchain this project builds and tests on resolves external identifiers +well past 63 characters, and the table under *Rationale* below records what +each of them documents. Yours may not be among them: if you build SolidSyslog +with a different compiler or linker, confirm its significant-character limit +before relying on this deviation. ### Scope @@ -53,16 +131,14 @@ tiers (rule 5.1 is not enforced there at all). ### Rationale The C99 31-character limit is a legacy linker artifact from the late -1980s. Every toolchain that SolidSyslog targets — hosted or embedded — -supports external identifiers well in excess of 63 significant -characters: +1980s. No toolchain SolidSyslog targets imposes anything close to it: | Toolchain | External identifier behaviour | |-----------|-------------------------------| | GCC (incl. `arm-none-eabi-gcc`) | No compiler-imposed limit; identifier length is delegated to the target's linker, and all characters are significant on every linker SolidSyslog targets (ld, gold, lld, link.exe). See GCC manual, "Implementation-defined behavior". | | Clang / LLVM (incl. Arm Compiler 6 / armclang) | Same rule as GCC for external identifiers — no compiler-imposed limit. | -| MSVC 2015+ | Documented maximum identifier length **2,047 characters** ([Microsoft Learn — C Identifiers](https://learn.microsoft.com/en-us/cpp/c-language/c-identifiers)). | -| IAR Embedded Workbench | C/C++ compiler reference manual documents an identifier limit well above 63 characters in every currently shipping version (verify on the target SKU's compiler reference for ports of SolidSyslog to non-standard SKUs). | +| MSVC | Documented maximum identifier length **2,047 characters** ([Microsoft Learn — C Identifiers](https://learn.microsoft.com/en-us/cpp/c-language/c-identifiers)). CI builds with the `windows-latest` toolchain; older MSVC releases are not tested. | +| IAR Embedded Workbench, Keil ARMCC 6 | Not built in CI. Identifier limits are documented per compiler SKU; confirm against your SKU's reference at port time. | The Tier 1 naming scheme in `docs/NAMING.md` (form `SolidSyslogClass_Function`) routinely produces identifiers in the @@ -74,8 +150,9 @@ distinctness would either collapse identifier pairs that read identically up to a trailing word (`SolidSyslogPlusTcpResolver_Create` vs `SolidSyslogPlusTcpResolver_Destroy`) into a single name, or force unidiomatic abbreviation throughout the public -API. Neither outcome serves clarity or MISRA's underlying intent -("the reader can tell two identifiers apart"); 63 characters does. +API. Neither outcome serves clarity, and neither preserves the +distinguishability the guideline exists to protect. Sixty-three characters +preserves both. 63 was chosen rather than "unlimited" so the project still names a concrete number that every targeted toolchain comfortably exceeds. It @@ -85,9 +162,9 @@ identifiers (§5.2.4.1) — a single number applies project-wide. ### Risk and mitigation - **Portability** — Constrained to toolchains that support ≥ 63 - significant characters in external identifiers. The table above - covers every supported target; adding a new target requires verifying - this constraint. + significant characters in external identifiers. The table above states + which of those are proven by CI and which rest on documentation; + adding a target requires verifying the constraint on it. - **Tooling** — cppcheck-misra applies its default 31-character window for rule 5.1. The deviation only matters when a real collision would resolve at 63 characters but not at 31 — at @@ -105,25 +182,24 @@ identifiers (§5.2.4.1) — a single number applies project-wide. ### Approval -Project owner — David Cozens. Recorded as the founding entry in this -document under [S10.01](https://github.com/cososo-ltd/solid-syslog/issues/357). +Raised and approved 2026-05-14 by the project owner, David Cozens. Recorded as +the founding entry in this document under +[S10.01](https://github.com/cososo-ltd/solid-syslog/issues/357). --- ## D.002 — Rules 11.2 / 11.3 / 11.5: vtable downcasts + Formatter -### Rule +### Guidelines + +- **MISRA C:2012 Rule 11.2** — Required. +- **MISRA C:2012 Rule 11.3** — Required. +- **MISRA C:2012 Rule 11.5** — Advisory. -> **Rule 11.3 (Required)** — A cast shall not be performed between a -> pointer to object type and a pointer to a different object type. -> -> **Rule 11.2 (Required)** — Conversions shall not be performed between -> a pointer to an incomplete type and any other type. -> -> **Rule 11.5 (Advisory)** — A conversion should not be performed from -> pointer to `void` into pointer to object. +**Rule text:** not reproduced (see [above](#guideline-text-is-not-reproduced-here)). +**Classification:** Deviation — the code departs from the guideline. -### Deviation +### Construct SolidSyslog accepts two structural pointer conversions that are identified in code as `SelfFromBase` (vtable) or `(struct X*) storage` @@ -159,7 +235,10 @@ public type, fully defined per platform as `SolidSyslogAddress_AsConstFreertosSockaddr`) plus a `HandleFromIndex(size_t)` helper in `*AddressStatic.c` that converts a pool slot index back to the public handle type. Rule 11.3 fires on -every such cast. +every such cast, and rule 11.2 fires alongside it here but not on the +other vtable classes: `struct SolidSyslogAddress` is deliberately an +incomplete type in the public header, so these conversions involve a +pointer to an incomplete type as well as to a different object type. This is the standard OO-in-C "interface pointer back to derived implementation" cast. @@ -201,6 +280,24 @@ byte-buffer reinterpretation that crosses the same advisory rule. The third-party API contract (the public `Send` / `SendTo` interface) is `void*` for opacity; the byte-level work needs a concrete unit type. +### Scope + +- **Strict tier** — `Core/Source/`: the `SelfFromBase` helpers on every vtable + class, and the Formatter storage cast of sub-case (b). 14 sites. +- **Pragmatic tier** — `Platform/*/Source/`: the same `SelfFromBase` shape in + each adapter, the per-platform Address downcasts, and the callback `void*` + casts of sub-case (c). 66 sites, across the StdAtomic, FatFs, FreeRtos, + LwipRaw, MbedTls, OpenSsl, PlusFat, PlusTcp, Posix and Windows packs. + +80 line-specific suppressions in total — 12 against rule 11.2, 57 against 11.3 +and 11 against 11.5. The deviation does not extend to `Tests/` or `Bdd/`, where +these rules are not enforced. + +A new class added to any vtable role inherits this shape, and its suppressions +belong in this block; adding them is a review step, not an automatic +consequence, because the reviewer has to confirm the new site really is the +`SelfFromBase` pattern and not a different conversion wearing the same name. + ### Rationale Every class with a Create/Destroy lifecycle uses the static pool @@ -231,10 +328,20 @@ opaque-type design). `mbedtls_ssl_set_bio(..., self, ...)`) is the same pointer that comes back — the library is a pass-through; the cast can only succeed against the type the wrapper passed in. -- **Alignment** — Storage types are declared as `intptr_t storage[N]` - (or a struct of the same shape), giving alignment at least as strict - as any pointer or scalar the impl contains. The cast is therefore - well-defined per §6.3.2.3. +- **Validity of the conversion, sub-cases (a) and (c)** — These do not + rest on an alignment argument at all. The public base struct is the + first member of the concrete struct, and §6.7.2.1 ¶13 guarantees that + a pointer to a structure object, suitably converted, points to its + initial member and back again. The address is the same address by + definition, so no alignment question arises. Sub-case (c) is the same + guarantee reached through the library's own `void*` round trip: the + pointer that comes back is the one that went out. +- **Alignment, sub-case (b) only** — The Formatter is the case where + alignment is the load-bearing argument, because its storage is a + caller-declared array rather than a struct whose first member is the + base. Storage is declared as `intptr_t storage[N]` (or a struct of the + same shape), giving alignment at least as strict as any pointer or + scalar the impl contains. - **Static analysis** — These rules are advisory (11.5) or required (11.2, 11.3). All current findings are suppressed via `misra_suppressions.txt` referencing this section. The pattern is @@ -242,7 +349,7 @@ opaque-type design). ### Approval -Project owner — David Cozens. Recorded under +Raised 2026-05-14, approved 2026-05-15 by the project owner, David Cozens. Recorded under [S10.06](https://github.com/cososo-ltd/solid-syslog/issues/367); scope narrowed under [S11.11](https://github.com/cososo-ltd/solid-syslog/issues/414) once @@ -257,15 +364,17 @@ caller-supplied-storage exception. ## D.003 — Rule 5.7: repeating struct tags (no-typedef-struct convention) -### Rule +### Guideline -> **Rule 5.7 (Required)** — A tag name shall be a unique identifier. +**MISRA C:2012 Rule 5.7** — Required. +**Rule text:** not reproduced (see [above](#guideline-text-is-not-reproduced-here)). +**Classification:** Tool limitation — the code complies; cppcheck-misra reports a finding regardless. -cppcheck-misra interprets this strictly — every repeated `struct X` +cppcheck-misra interprets Rule 5.7 strictly — every repeated `struct X` declaration counts as a non-unique tag, including forward declarations in headers and the matching definition in source. -### Deviation +### Construct SolidSyslog uses `struct SolidSyslogX` directly throughout the public API and source rather than typedef'ing it (see `docs/NAMING.md`, Tier 1 @@ -312,19 +421,40 @@ and in the definition. The repetition is the convention, not a defect. ### Approval -Project owner — David Cozens. Recorded under +Raised 2026-05-14, approved 2026-05-15 by the project owner, David Cozens. Recorded under [S10.06](https://github.com/cososo-ltd/solid-syslog/issues/367). --- +## D.004 — Rule 18.4: pointer arithmetic on record buffers (retired) + +**Retired in S10.19.** This deviation authorised `uint8_t*` pointer arithmetic in +`Core/Source/RecordStore.c`, where four field-offset helpers walked the +`[magic][length][message]` record layout by adding an offset to a base address. +S10.19 rewrote them to take the address of an indexed element (`&base[OFFSET]`) +instead. That is the same address by definition, but rule 18.4 fires on the `+`, +`-`, `+=` and `-=` operators specifically rather than on subscripting, so the +finding no longer arises and the deviation had nothing left to authorise. +cppcheck-misra reports no 18.4 finding in `RecordStore.c`; the suppression was +removed from `misra_suppressions.txt` at the same time. + +The entry is kept, rather than the number reused, so the register has no gaps and +a reader of an older revision can still resolve D.004. Raised 2026-05-14 and +approved 2026-05-15 by the project owner, David Cozens, alongside the other +founding entries; retired 2026-05-23 under +[#436](https://github.com/cososo-ltd/solid-syslog/pull/436). + +--- + ## D.005 — Rule 18.7: flexible array members -### Rule +### Guideline -> **Rule 18.7 (Required)** — Flexible array members shall not be -> declared. +**MISRA C:2012 Rule 18.7** — Required. +**Rule text:** not reproduced (see [above](#guideline-text-is-not-reproduced-here)). +**Classification:** Deviation — the code departs from the guideline. -### Deviation +### Construct `struct SolidSyslogFormatter` ends with a flexible array member that holds the caller-supplied backing storage: @@ -355,7 +485,7 @@ declares a storage buffer of arbitrary size (with a minimum enforced by bookkeeping fields at the start, payload bytes filling the rest. The flexible array member is C99's standard mechanism for exactly this -shape (§6.7.2.1 ¶18). The alternatives all regress: +shape (§6.7.2.1 ¶16). The alternatives all regress: | Alternative | Why rejected | |-------------|--------------| @@ -365,9 +495,14 @@ shape (§6.7.2.1 ¶18). The alternatives all regress: ### Risk and mitigation -- **Compiler support.** All target toolchains support C99 flexible - array members (gcc, clang, MSVC 2013+, IAR, Keil ARMCC 6). The - project's CI builds prove this on every push. +- **Compiler support.** GCC, Clang and the ARM cross-compilers accept + the construct as the C99 feature it is, and CI compiles it on every + push. MSVC compiles it too, but reports C4200 — it treats a trailing + unsized array as a nonstandard extension — so the build carries + `/wd4200` for this construct specifically (see `CMakeLists.txt`); + without it, `/WX` would fail the Windows lane. IAR and Keil ARMCC 6 + are not built in CI, so support there rests on their documentation + and is confirmed at port time rather than per push. - **Allocation surprise.** The `_Static_assert` accompanying each flexible-array struct pins the storage-type-to-impl-type relationship at build time; an undersized storage allocation is a @@ -375,20 +510,21 @@ shape (§6.7.2.1 ¶18). The alternatives all regress: ### Approval -Project owner — David Cozens. Recorded under +Raised 2026-05-14, approved 2026-05-15 by the project owner, David Cozens. Recorded under [S10.06](https://github.com/cososo-ltd/solid-syslog/issues/367). --- ## D.006 — Rule 11.8: `const` qualification under field access of `const struct*` -### Rule +### Guideline -> **Rule 11.8 (Required)** — A cast shall not be performed that -> removes any `const` or `volatile` qualification from the type -> pointed to by a pointer. +**MISRA C:2012 Rule 11.8** — Required. +**Rule text:** not reproduced (see [above](#guideline-text-is-not-reproduced-here)). +**Classification:** both kinds, which is why they share an entry — category 1 +below is a tool limitation, category 2 is a genuine deviation. -### Deviation +### Construct Two distinct site categories trigger this rule: @@ -405,14 +541,23 @@ Two distinct site categories trigger this rule: } ``` - Per C11 §6.5.2.3 ¶3, the result of `->` is the type of the named - member; `config->buffer` evaluates to `struct SolidSyslogBuffer *` - with no `const` qualifier on the pointed-to object. cppcheck-misra - nonetheless flags the field access as a const-strip — it tracks the + §6.5.2.3 ¶4 governs this, and it has to be read whole. The result of + `->` is the named member's type; and where the left operand is a + pointer to a qualified type, the result carries the so-qualified + version of that type. That second clause looks at first like it works + against us. It does not. The member's type here is + `struct SolidSyslogBuffer *`, so the so-qualified version is + `struct SolidSyslogBuffer * const` — the qualification attaches to the + pointer, not to the object the pointer designates. Passing it by value + to `InstallBuffer` copies the pointer, and a top-level qualifier on a + copied value is discarded. + + So no qualification is removed from the pointed-to type, and no cast + is performed. cppcheck-misra flags the access anyway, tracking the outer `const` on `*config` rather than the type of the member - expression. The accepted interpretation in the MISRA community is - that 11.8 applies to *pointer casts*, not to member-access yielding - a non-`const`-qualified pointer rvalue. + expression. The project assesses this as a tool limitation rather + than a departure from the guideline, and records it here so the + assessment is visible rather than silent. The same pattern recurs in `SolidSyslogMessageFormatter_Format(const struct @@ -447,10 +592,10 @@ Two distinct site categories trigger this rule: `SolidSyslogDatagram_SendTo` takes the caller's buffer as `const void*` — the contract is read-only inside the library. - lwIP's `struct pbuf::payload` is declared `void*` (no `const` + the `payload` field of lwIP's `struct pbuf` is declared `void*` (no `const` variant in the lwIP headers); `udp_sendto` only reads the - payload (the PBUF_REF zero-copy contract — see handoff design - decision #1) but the field type does not encode that. Assigning + payload — that is the `PBUF_REF` zero-copy contract — but the field + type does not encode that. Assigning our `const void*` parameter to lwIP's `void*` field strips the qualifier at the platform-API boundary, same shape as the Winsock `select()` site above. Alternatives considered and @@ -485,12 +630,14 @@ standard; reorganising the code to avoid the cppcheck-misra false-positive would either drop the outer `const` qualifier on `*config` / `*blockSequence` / `*config` (the wrong direction) or introduce a no-op `const_cast`-style explicit cast that the tool would -still flag. A site-local deviation is the honest record. - -The two platform-API sites are the canonical "forced by an external -interface" pattern MISRA itself calls out in the deviation guidance for -rule 11.8. Both upstream declarations (Microsoft's `select()` timeout, -lwIP's `pbuf::payload`) are fixed by their vendors; the SolidSyslog seam +still flag. Recording the finding here, with the reasoning, is the honest +alternative to bending the code around a tool. + +The two platform-API sites are the standard case of a const-correct interior +forced to strip qualification at a fixed third-party API boundary. Both +upstream declarations (Microsoft's `select()` timeout, +lwIP's `struct pbuf` `payload` field) are fixed by their vendors; the +SolidSyslog seam keeps the const-correctness contract on the caller's side of the boundary. @@ -507,20 +654,23 @@ boundary. ### Approval -Project owner — David Cozens. Recorded under +Raised 2026-05-14, approved 2026-05-15 by the project owner, David Cozens. Recorded under [S10.06](https://github.com/cososo-ltd/solid-syslog/issues/367). --- ## D.007 — Rule 21.10: transitive `` via `` -### Rule +### Guideline + +**MISRA C:2012 Rule 21.10** — Required. +**Rule text:** not reproduced (see [above](#guideline-text-is-not-reproduced-here)). +**Classification:** Tool limitation — the code complies; cppcheck-misra reports a finding regardless. -> **Rule 21.10 (Required)** — The Standard Library time and date -> functions shall not be used. (cppcheck-misra also flags -> `` inclusion under this rule.) +cppcheck-misra also raises this rule for `` inclusion, which is what +brings the construct below into scope. -### Deviation +### Construct Three POSIX platform sources include `` for `struct timespec` and `clock_gettime`/`nanosleep`: @@ -560,19 +710,20 @@ unavoidable on this platform. ### Approval -Project owner — David Cozens. Recorded under +Raised 2026-05-14, approved 2026-05-15 by the project owner, David Cozens. Recorded under [S10.06](https://github.com/cososo-ltd/solid-syslog/issues/367). --- ## D.008 — Rule 21.6: `` for `SEEK_SET` / `SEEK_END` only -### Rule +### Guideline -> **Rule 21.6 (Required)** — The Standard Library input/output -> functions shall not be used. +**MISRA C:2012 Rule 21.6** — Required. +**Rule text:** not reproduced (see [above](#guideline-text-is-not-reproduced-here)). +**Classification:** Deviation — the code departs from the guideline. -### Deviation +### Construct `Platform/Windows/Source/SolidSyslogWindowsFile.c` includes `` solely to obtain the `SEEK_SET` and `SEEK_END` constants used by @@ -615,33 +766,35 @@ use. The deviation is narrow and visible. ### Approval -Project owner — David Cozens. Recorded under +Raised 2026-05-14, approved 2026-05-15 by the project owner, David Cozens. Recorded under [S10.06](https://github.com/cososo-ltd/solid-syslog/issues/367). --- ## D.009 — Rules 2.4 / 5.7: anonymous `enum` used as named-constant container -### Rules +### Guidelines -> **Rule 2.4 (Advisory)** — A project should not contain unused tag -> declarations. -> **Rule 5.7 (Required)** — A tag name shall be a unique identifier. +- **MISRA C:2012 Rule 2.4** — Advisory. +- **MISRA C:2012 Rule 5.7** — Required. + +**Rule text:** not reproduced (see [above](#guideline-text-is-not-reproduced-here)). +**Classification:** Tool limitation — the code complies; cppcheck-misra reports a finding regardless. cppcheck-misra interprets an anonymous `enum { ... };` declaration (no enum tag, no `typedef`) two ways: -- as a "tag declared but unused" (2.4) — the enumerators are used - as named constants but the enum type itself is never referenced; -- as a non-unique tag (5.7) — every anonymous `enum` shares the - same empty tag identifier, so the second and subsequent ones - collide. +- under 2.4 it reports the enum tag as unused — the enumerators are + used as named constants but the enum type itself is never referenced; +- under 5.7 it reports the tag as non-unique — every anonymous `enum` + shares the same empty tag identifier, so the second and subsequent + ones collide. Both findings originate from the same syntactic shape — the anonymous-`enum` named-constant idiom — and are covered by a single deviation here. -### Deviation +### Construct SolidSyslog uses the anonymous-`enum` idiom across the codebase as a portable mechanism for declaring named integer constants in @@ -655,21 +808,19 @@ enum }; ``` -There are approximately 31 such declarations across `Core/` and -`Platform/*/Source/`. Adding inline-suppress comments at every +There are 45 such declarations across 44 files in `Core/` and +`Platform/`. Adding inline-suppress comments at every site would add visual noise next to a project-wide intentional idiom — listing them in `misra_suppressions.txt` under this deviation keeps the source clean. -**Suppression-file layout.** 5.7 anonymous-enum suppressions -were historically grouped in the D.003 block (struct-tag -repetition) because D.003 was the original 5.7 deviation; per-group -conformance stories migrate them into the D.009 block as they -review their cluster (S10.16 moved the Senders-cluster ones). -Until every group has run, both blocks contain rule 5.7 lines -and the deviation that authorises each is determined by the kind -of identifier the cppcheck-misra finding lands on (struct tag → -D.003; anonymous enum → D.009). +**Suppression-file layout.** Two deviations authorise rule 5.7 findings, and +which one applies is decided by the identifier the finding lands on: a repeated +struct tag is D.003, an anonymous enum is D.009. That is a standing convention, +not a transitional state — both kinds of finding exist permanently in this +codebase, so both blocks permanently carry rule 5.7 lines. Each block in +`misra_suppressions.txt` is headed by the deviation that authorises its entries, +so the mapping is explicit per line rather than inferred. ### Scope @@ -713,19 +864,20 @@ would not be substitutable for any other type. ### Approval -Project owner — David Cozens. Recorded under +Raised 2026-05-14, approved 2026-05-15 by the project owner, David Cozens. Recorded under [S10.06](https://github.com/cososo-ltd/solid-syslog/issues/367). --- ## D.010 — Rule 20.10: `#` stringification in the `SOLIDSYSLOG_STATIC_ASSERT` polyfill -### Rule +### Guideline -> **Rule 20.10 (Advisory)** — The `#` and `##` preprocessor operators -> should not be used. +**MISRA C:2012 Rule 20.10** — Advisory. +**Rule text:** not reproduced (see [above](#guideline-text-is-not-reproduced-here)). +**Classification:** Deviation — the code departs from the guideline. -### Deviation +### Construct `Core/Source/SolidSyslogMacros.h` defines `SOLIDSYSLOG_STATIC_ASSERT`. The native C++/C11 expansions need a string-literal message, so the macro @@ -755,12 +907,19 @@ declarations in one translation unit are compatible, so no `__LINE__` pasting ### Rationale +`SOLIDSYSLOG_STATIC_ASSERT` selects one of three expansions on +`__cplusplus` / `__STDC_VERSION__`, and all three are compiled: C++ +`static_assert` in the CppUTest harnesses, C11 `_Static_assert` in the default +build (`CMAKE_C_STANDARD` is 11 unless overridden), and the C99 +negative-array-size fallback in the `build-linux-c99` lane, which compiles +`Core/` at strict `-std=c99` on every pull request. The deviation is confined to the two +string-literal forms; only they need the message stringified, and the C99 +fallback uses no preprocessor operator at all. + C++ `static_assert` and C11 `_Static_assert` are the standard compile-time -assertion primitives, and the project compiles at `--std=c11` (so the -`_Static_assert` branch is what every normal and CI build — including the -cppcheck-misra lane — compiles). Their message argument is a string literal, -and there is no way to convert an arbitrary identifier-shaped message into one -without `#`. The alternatives all regress: +assertion primitives for their editions. Their message argument is a string +literal, and there is no way to convert an arbitrary identifier-shaped message +into one without `#`. The alternatives all regress: | Alternative | Why rejected | |-------------|--------------| @@ -783,19 +942,20 @@ been since C89; it is neither opaque nor novel. ### Approval -Project owner — David Cozens. Recorded under +Raised 2026-05-15, approved 2026-05-16 by the project owner, David Cozens. Recorded under [S10.10](https://github.com/cososo-ltd/solid-syslog/issues/375). --- ## D.011 — Rule 2.5: public API macros consumed outside the cppcheck-misra scope -### Rule +### Guideline -> **Rule 2.5 (Advisory)** — A project should not contain unused macro -> definitions. +**MISRA C:2012 Rule 2.5** — Advisory. +**Rule text:** not reproduced (see [above](#guideline-text-is-not-reproduced-here)). +**Classification:** Tool limitation — the code complies; cppcheck-misra reports a finding regardless. -### Deviation +### Construct `Core/Interface/SolidSyslogCircularBuffer.h` declares one function-like macro — `SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES` — that integrator code @@ -809,15 +969,16 @@ checker. `Core/Interface/SolidSyslogCircularBuffer.h` — one macro definition. -A future per-component sweep may surface similar findings on other -public API macros (per the tier model, MISRA enforcement does not cross -into `Tests/` or `Bdd/`). When that happens, the deviation extends to -those files; the rule still catches genuinely-unused macros inside the -scanned scope. +This entry authorises that one macro and no other. Per the tier model, MISRA +enforcement does not cross into `Tests/` or `Bdd/`, so a future sweep may +surface the same shape on another public API macro. That does not extend this +deviation automatically: each new instance is reviewed on its merits and either +amends this entry with the file named, or is raised as its own. Until then the +rule still catches genuinely unused macros inside the scanned scope. ### Rationale -The macros *are* used by integrators in `Tests/` and `Bdd/Targets/`. +The macro *is* used by integrators in `Tests/` and `Bdd/Targets/`. Verified by `grep` over the tree: ```text @@ -837,9 +998,9 @@ The alternatives all regress: | Alternative | Why rejected | |-------------|--------------| -| Inline `cppcheck-suppress misra-c2012-2.5` at each macro | Inline suppressions are weaker by MISRA Compliance:2020 §4.2 (rationale scattered, not centrally auditable). Project preference is structural deviations in this document. | +| Inline `cppcheck-suppress misra-c2012-2.5` at the macro | **Project preference.** Deviations are recorded structurally in this document so the rationale is centrally auditable rather than scattered across call sites. | | Widen the cppcheck-misra scan to include `Tests/` | Tests are the Consistency-only tier per E10's tier model; running MISRA there is out of scope by design. | -| Move the macros into `Core/Source/` | Public API by definition lives under `Core/Interface/`. Moving them would break the audience-segregated header layout. | +| Move the macro into `Core/Source/` | Public API by definition lives under `Core/Interface/`. Moving it would break the audience-segregated header layout. | ### Risk and mitigation @@ -853,19 +1014,20 @@ The alternatives all regress: ### Approval -Project owner — David Cozens. Recorded under +Raised 2026-05-15, approved 2026-05-16 by the project owner, David Cozens. Recorded under [S10.10](https://github.com/cososo-ltd/solid-syslog/issues/375). --- ## D.012 — Rule 8.9: file-scope `static const` referenced from a file-scope enum + one function -### Rule +### Guideline -> **Rule 8.9 (Advisory)** — An object should be defined at block scope -> if its identifier only appears in a single function. +**MISRA C:2012 Rule 8.9** — Advisory. +**Rule text:** not reproduced (see [above](#guideline-text-is-not-reproduced-here)). +**Classification:** Tool limitation — the code complies; cppcheck-misra reports a finding regardless. -### Deviation +### Construct `Core/Source/SolidSyslogFileBlockDevice.c:20` declares `static const char FILE_EXTENSION[] = ".log"`. The constant is the @@ -890,11 +1052,12 @@ function would break the enum's compile-time `sizeof()` evaluation. `Core/Source/SolidSyslogFileBlockDevice.c:20` — one declaration. -A future per-component sweep may surface this shape on other -file-scope `static const` objects whose identifier is used by a -file-scope enum's `sizeof()`/value initialiser and exactly one -function. When that happens, the deviation extends to those files; -the rule still catches genuinely single-function-scoped objects. +This entry authorises that one declaration and no other. A future sweep may +surface the same shape elsewhere — a file-scope `static const` whose identifier +is read by a file-scope enum initialiser and exactly one function. Each such +instance is reviewed on its merits and either amends this entry with the file +named, or is raised as its own; it is not covered by this record until that +happens. The rule still catches genuinely single-function-scoped objects. ### Rationale @@ -919,7 +1082,7 @@ Summary: | Alternative | Why rejected | |-------------|--------------| -| Inline `cppcheck-suppress misra-c2012-8.9` at the declaration | Inline suppressions are weaker by MISRA Compliance:2020 §4.2 (rationale scattered, not centrally auditable). Project preference is structural deviations in this document. | +| Inline `cppcheck-suppress misra-c2012-8.9` at the declaration | **Project preference.** Deviations are recorded structurally in this document so the rationale is centrally auditable rather than scattered across call sites. | | Inline the `".log"` literal at both use sites | DRY violation for a single-source-of-truth on-disk constant. | | Promote the dependent enum entries to `static const size_t` | Verified to not satisfy 8.9; instead surfaces a second false positive on the new constant. | | Promote to `#define FILE_EXTENSION ".log"` | Introduces a string macro inconsistent with the file-scope-const pattern used elsewhere in storage code. | @@ -936,42 +1099,50 @@ Summary: ### Approval -Project owner — David Cozens. Recorded under +Raised and approved 2026-05-22 by the project owner, David Cozens. Recorded under [S10.18](https://github.com/cososo-ltd/solid-syslog/issues/430). --- -## D.013 — Rule 11.5: `void*` ↔ `unsigned char*` at third-party byte-buffer API boundaries +## D.013 — Rule 11.5: `void*` ↔ a byte pointer at third-party byte-buffer API boundaries -### Rule +### Guideline -> **Rule 11.5 (Advisory)** — A conversion should not be performed from -> pointer to `void` into pointer to object. +**MISRA C:2012 Rule 11.5** — Advisory. +**Rule text:** not reproduced (see [above](#guideline-text-is-not-reproduced-here)). +**Classification:** Deviation — the code departs from the guideline. -### Deviation +### Construct -`SolidSyslogStream::Send` takes `const void*` and `SolidSyslogStream::Read` +`SolidSyslogStream_Send` takes `const void*` and `SolidSyslogStream_Read` takes `void*` — the project-wide byte-buffer contract used by every -Stream implementation. Some third-party C libraries (notably mbedTLS) -type their byte buffers as `const unsigned char*` / `unsigned char*` -rather than `void*`. The implementation cast bridging the two is -unavoidable at the API boundary: +Stream implementation, and `SolidSyslogDatagram_SendTo` takes `const void*` +likewise. Some third-party C libraries type their byte buffers as a character +pointer rather than `void*`: mbedTLS uses `const unsigned char*` / +`unsigned char*`, and the Winsock socket calls use `const char*` / `char*` +where their POSIX counterparts use `void*`. The implementation cast bridging +the two is unavoidable at the API boundary: ```c -int rc = mbedtls_ssl_write(&self->SslContext, (const unsigned char*) buffer, size); +int rc = mbedtls_ssl_write(&self->SslContext, (const unsigned char*) buffer, size); +int sent = WinsockTcpStream_send(self->Fd, (const char*) buffer, (int) size, 0); ``` Rule 11.5 fires on each such adapter cast. ### Scope -`Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c` — two sites -(`MbedTlsStream_Send`, `MbedTlsStream_Read`). - -The deviation extends to any future Stream / Datagram / hash / MAC -implementation that wraps a byte-typed third-party C API -(`unsigned char*` rather than `void*`). The OpenSSL adapter -(`Platform/OpenSsl/Source/SolidSyslogOpenSslStream.c`) does not fall +- `Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c` — two sites + (`MbedTlsStream_Send`, `MbedTlsStream_Read`), `unsigned char*`. +- `Platform/Windows/Source/SolidSyslogWinsockTcpStream.c` — two sites + (`WinsockTcpStream_Send`, `WinsockTcpStream_Read`), `char*`. +- `Platform/Windows/Source/SolidSyslogWinsockDatagram.c` — one site + (`WinsockDatagram_SendTo`), `char*`. + +A future Stream, Datagram, hash or MAC implementation wrapping a byte-typed +third-party C API will meet the same boundary, but is not covered by this +record until reviewed and added to it — or given its own entry. The OpenSSL +adapter (`Platform/OpenSsl/Source/SolidSyslogOpenSslStream.c`) does not fall under this deviation — `SSL_write` / `SSL_read` take `void*` and so no cast is needed. @@ -981,32 +1152,40 @@ The alternatives all regress: | Alternative | Why rejected | |-------------|--------------| -| Refactor `SolidSyslogStream::Send`/`Read` to use `unsigned char*` | Public-API ABI change that propagates to every Stream implementation (Posix TCP, Winsock TCP, FreeRTOS TCP, OpenSSL TLS, mbedTLS TLS, NullStream) and every Stream caller (`SolidSyslogStreamSender`). The `void*` byte-buffer contract is the conventional C idiom for transport interfaces and matches POSIX `send`/`recv`, OpenSSL `SSL_write`/`SSL_read`, etc. Changing it for the sake of one third-party API's typing choice is the wrong direction. | +| Refactor `SolidSyslogStream_Send` to `const unsigned char*` and `SolidSyslogStream_Read` to `unsigned char*` | Public-API ABI change that propagates to every Stream implementation (Posix TCP, Winsock TCP, FreeRTOS TCP, OpenSSL TLS, mbedTLS TLS, NullStream) and every Stream caller (`SolidSyslogStreamSender`). The `void*` byte-buffer contract is the conventional C idiom for transport interfaces and matches POSIX `send`/`recv`, OpenSSL `SSL_write`/`SSL_read`, etc. Changing it for the sake of one third-party API's typing choice is the wrong direction. | | Copy through an `unsigned char` scratch buffer per call | Runtime cost on the hot send/receive path; adds a fixed-size scratch or a stack-allocated VLA in a critical-path function. Defeats the zero-copy intent of the Stream contract. | -| Inline `cppcheck-suppress misra-c2012-11.5` at each site | Inline suppressions are weaker by MISRA Compliance:2020 §4.2 (rationale scattered, not centrally auditable). Project preference is structural deviations in this document. | +| Inline `cppcheck-suppress misra-c2012-11.5` at each site | **Project preference.** Deviations are recorded structurally in this document so the rationale is centrally auditable rather than scattered across call sites. | -The cast is well-defined: `unsigned char` may alias any object type -(C99 §6.5 ¶7), so reinterpreting a `void*` byte buffer as +The cast is well-defined: a character type may alias any object type +(§6.5 ¶7), so reinterpreting a `void*` byte buffer as `char*` or `unsigned char*` and back is a no-op at the abstract-machine level. ### Risk and mitigation - **Alignment** — Both representations are byte-addressed; no - alignment promotion occurs. The cast targets `unsigned char*`, which + alignment promotion occurs. The cast targets a character pointer, which has the weakest alignment requirement of any object pointer. -- **Type safety** — The caller-supplied buffer originates as a - contiguous byte sequence (typically the formatted syslog record); - treating it as `unsigned char*` at the third-party API boundary is - the same byte sequence under a different pointer type. -- **Elimination path** — A future revision of the Stream API that - adopts `unsigned char*` directly would retire this deviation. +- **Type safety** — Both directions carry a contiguous byte sequence and + neither is interpreted as anything else: on the send path it is the + formatted record the caller hands over, and on the read path it is the + caller's destination, written into and never read as a wider type. + Treating either as `char*` or `unsigned char*` at the third-party API + boundary is the same bytes under a different pointer type. +- **Elimination path** — The deviation retires per API, not as a whole, and + each direction keeps its qualification. `SolidSyslogStream_Send` typed to + `const unsigned char*` and `SolidSyslogStream_Read` to `unsigned char*` + would retire the Stream sites; `SolidSyslogDatagram_SendTo` typed to + `const unsigned char*` would retire `WinsockDatagram_SendTo`. Either would + only retire the sites whose third-party spelling it matched. Tracked as a possible E10-successor refactor, not scheduled. ### Approval -Project owner — David Cozens. Recorded under +Raised and approved 2026-05-23 by the project owner, David Cozens. Recorded under [S10.20](https://github.com/cososo-ltd/solid-syslog/issues/437). +--- + ## D.014 — Rule 8.7: public-API `SolidSyslogErrorSource` objects (retired) **Retired in S12.26.** This deviation covered the crypto-policy @@ -1014,11 +1193,19 @@ Project owner — David Cozens. Recorded under `_Report` wrapper confined every emission to the source's own `*Messages.c`, a single translation unit. S12.26 decoupled error text from the library (deleting the `*Messages.c` message tables) and unwound the `_Report` wrapper, -so each source is now defined in its class's vtable TU and referenced from both -that TU's emit sites and its `*Static.c` lifecycle code, genuinely cross-TU, -exactly the resolution this deviation's "Risk and mitigation" anticipated. +so each source is now defined in its class's vtable TU and reached from the +`_Report` inline in its `*Private.h`, which both that TU and its +`*Static.c` lifecycle code include — so the object is used from more than one +translation unit, which is the resolution the entry's risk analysis anticipated +before it was collapsed to this note; see the revision prior to retirement for +that text. cppcheck-misra reports no 8.7 finding for any error source; the suppression lines were removed. -Recorded under [S17.02](https://github.com/cososo-ltd/solid-syslog/issues/493), -retired under [S12.26](https://github.com/cososo-ltd/solid-syslog/issues/507). +Raised and approved 2026-05-31 by the project owner, David Cozens, under +[S17.02](https://github.com/cososo-ltd/solid-syslog/issues/493); retired +2026-06-03 under +[S12.26](https://github.com/cososo-ltd/solid-syslog/issues/507). The story +numbers run backwards because they are numbered by epic rather than +chronologically — E12 was elaborated after E17 — so read the dates, not the +labels, for the order of events. diff --git a/docs/platforms/fatfs.md b/docs/platforms/fatfs.md deleted file mode 100644 index abf77c7a..00000000 --- a/docs/platforms/fatfs.md +++ /dev/null @@ -1,19 +0,0 @@ -# FatFs - -`Platform/FatFs/` wraps ChaN FatFs as the File layer -([FatFs documentation](http://elm-chan.org/fsw/ff/)). RTOS-agnostic — bare-metal, -FreeRTOS, Zephyr, NuttX. - -Fills the [File](../api/structSolidSyslogFile.md) role — the primitive beneath a -BlockDevice. - -## What it ships - -| Class | Role | -|---|---| -| [`SolidSyslogFatFsFile`](../api/SolidSyslogFatFsFile_8h.md) | file — `f_sync` after every write | - -## Requirements - -Your `ffconf.h`, a `diskio.c` media driver, and — if `FF_FS_REENTRANT=1` — an -`ffsystem.c`. diff --git a/docs/platforms/fatfs/index.md b/docs/platforms/fatfs/index.md new file mode 100644 index 00000000..3b8cf9f3 --- /dev/null +++ b/docs/platforms/fatfs/index.md @@ -0,0 +1,40 @@ +# FatFs + +`Platform/FatFs/` wraps ChaN FatFs as the File layer +([FatFs documentation](http://elm-chan.org/fsw/ff/)). RTOS-agnostic — bare-metal +or under any RTOS. + +Fills the [File](../../api/structSolidSyslogFile.md) role — the primitive beneath a +BlockDevice. + +## What it ships + +## Requirements + +Your `ffconf.h`, a `diskio.c` media driver, and — if `FF_FS_REENTRANT=1` — an +`ffsystem.c`. + +## Security behaviour and obligations + +### The file layer offers no confidentiality or tamper evidence + +Records are written as given. Detecting modification of a stored record, or +keeping it unreadable, is the SecurityPolicy role's job, not this one — see +[at-rest cryptography](../../security/at-rest-cryptography.md). + +### Durability is bounded by the write, not guaranteed by it + +`f_sync` runs after every write: it writes back the cached data, updates the +directory entry so the recorded file size includes the record, and issues +`CTRL_SYNC` to your driver. The loss window is therefore one incomplete write rather +than everything since the last close. Whether the sync reaches the medium, and +what the FAT metadata looks like afterwards, is a property of your `diskio.c` +driver and the hardware under it. FAT is not a journalling filesystem, and a +partially written directory entry is possible on a device that loses power +mid-update. + +### The media driver is yours + +`diskio.c`, and `ffsystem.c` where `FF_FS_REENTRANT=1`, are supplied by you. So +is any wear levelling: the store rewrites the same blocks in rotation, which on +raw flash without wear levelling concentrates erase cycles. diff --git a/docs/platforms/fatfs/setup.md b/docs/platforms/fatfs/setup.md new file mode 100644 index 00000000..b534c81e --- /dev/null +++ b/docs/platforms/fatfs/setup.md @@ -0,0 +1,75 @@ +# FatFs setup + +Giving store-and-forward a file backend. [FatFs](index.md) covers what the +adapter fills and what it leaves to you. + +## The shape + +```text +SolidSyslogBlockStore + │ +SolidSyslogFileBlockDevice ◀── sequence-numbered .log files + │ +SolidSyslogFatFsFile ◀── this adapter + │ +FatFs core ◀── vendor sources, you compile them + │ +your disk I/O driver ◀── you write this +``` + +The adapter owns the middle box only. It never reaches the storage medium. + +## What to link + +FatFs is configured by a header you own, so the adapter compiles inside your +target against your configuration: + +```cmake +set(SOLIDSYSLOG_PLATFORMS "FatFs;;") +target_link_libraries(my_app PRIVATE SolidSyslog SolidSyslog::FatFs) +``` + +This platform fills the File role only; the placeholders are whichever platforms +the [capability matrix](../index.md) says fill the rest of what your build +needs. See +[naming your platforms](../../build-integration.md#cmake) for how the list is +read. + +## What you must provide + +**The FatFs sources**, compiled into your image, and a `ffconf.h` where the +library expects to find it — beside the sources rather than on the include +path, which differs from most configuration headers. + +**A disk I/O driver** for your storage hardware, implementing the read, write +and control entry points FatFs calls. This is the part that knows about your +medium, and nothing in this library reaches past it. + +**A system layer** if you have enabled re-entrancy, providing the +synchronisation objects FatFs expects. + +**Mounting** is yours. The adapter opens, reads and writes files; bringing the +volume up belongs to your start-up code. + +## Durability + +The adapter calls `f_sync` after every complete write. That writes back the +cached data, updates the directory entry so the file's recorded size includes +the record, and asks your driver to sync. The loss window is therefore one +incomplete write rather than everything written since the file was last closed. + +Whether the sync reaches the medium is your driver's `CTRL_SYNC` and the +hardware beneath it: a driver that reports success without flushing a device +cache narrows nothing. + +FAT is not a journalling file system. A device that loses power partway through +a directory update can leave that entry inconsistent, which is a property of +the format rather than of this adapter. Where records must survive a hard power +cut, size them and choose the store's discard policy with that in mind. + +## When it does not work + +Failures report through the error handler rather than silently. Install one +before you start, and read [error severity](../../error-severity.md) for what +each level is telling you — a `CRITICAL` at create time means the file fell +back to the Null object, and nothing will be stored. diff --git a/docs/platforms/freertos.md b/docs/platforms/freertos.md deleted file mode 100644 index d6a64c79..00000000 --- a/docs/platforms/freertos.md +++ /dev/null @@ -1,19 +0,0 @@ -# FreeRTOS - -`Platform/FreeRtos/` wraps FreeRTOS kernel primitives -([FreeRTOS documentation](https://www.freertos.org/Documentation/00-Overview)). -Networking is a separate backend — [FreeRTOS-Plus-TCP](plustcp.md) or -[lwIP](lwip.md). - -Fills the Mutex [role](../roles/index.md), plus a sysUpTime callback. - -## What it ships - -| Class | Role | -|---|---| -| [`SolidSyslogFreeRtosMutex`](../api/SolidSyslogFreeRtosMutex_8h.md) | mutex (`xSemaphoreCreateMutexStatic`) | -| [`SolidSyslogFreeRtosSysUpTime`](../api/SolidSyslogFreeRtosSysUpTime_8h.md) | uptime (`xTaskGetTickCount`) | - -## Requirements - -`configSUPPORT_STATIC_ALLOCATION=1` — the mutex uses static allocation. diff --git a/docs/platforms/freertos/index.md b/docs/platforms/freertos/index.md new file mode 100644 index 00000000..a178124f --- /dev/null +++ b/docs/platforms/freertos/index.md @@ -0,0 +1,34 @@ +# FreeRTOS + +`Platform/FreeRtos/` wraps FreeRTOS kernel primitives +([FreeRTOS documentation](https://www.freertos.org/Documentation/00-Overview)). +Networking comes from a separate platform; the +[platform × capability matrix](../index.md) shows which fill it. + +Fills the Mutex [role](../../roles/index.md), plus a sysUpTime callback. + +## What it ships + +## Requirements + +`configSUPPORT_STATIC_ALLOCATION=1` — the mutex uses static allocation. + +## Security behaviour and obligations + +### The mutex guards a buffer shared between tasks + +The circular buffer uses it when the task calling `Log` is not the task calling +`Service`. Where both run on one task, the Null mutex is the correct choice and +costs nothing. + +### Static allocation is required, and is the point + +`configSUPPORT_STATIC_ALLOCATION=1` is not a convenience: the kernel object is +created from storage inside the library's own pool, so the adapter allocates +nothing at run time and cannot fail for want of heap. + +### Uptime is a tick count, not a clock + +The sysUpTime callback reports kernel ticks since boot. It is not wall-clock +time and carries no timezone or synchronisation quality — the clock callback is +a separate injection point. diff --git a/docs/platforms/freertos/setup.md b/docs/platforms/freertos/setup.md new file mode 100644 index 00000000..4e01d69d --- /dev/null +++ b/docs/platforms/freertos/setup.md @@ -0,0 +1,64 @@ +# FreeRTOS setup + +Wiring the kernel primitives. [FreeRTOS](index.md) covers what they fill and +what they leave to you. + +## What to link + +FreeRTOS is configured by a header you own, so the adapter cannot be +precompiled: its sources compile inside your target, against your +`FreeRTOSConfig.h`. Select it and link the target it exports: + +```cmake +set(SOLIDSYSLOG_PLATFORMS "FreeRtos;;") +target_link_libraries(my_app PRIVATE SolidSyslog SolidSyslog::FreeRtos) +``` + +This platform fills the Mutex role and the sysUpTime callback; the placeholders +are whichever platforms the [capability matrix](../index.md) says fill the rest +of what your build needs. See +[naming your platforms](../../build-integration.md#cmake) for how the list is +read. + +Your target supplies the kernel's include path, because it is your kernel and +your configuration. + +`configSUPPORT_STATIC_ALLOCATION` must be 1. The mutex is created from storage +inside the library's own pool, so nothing is allocated at run time and creation +cannot fail for want of heap. + +## Wiring the mutex + +The mutex exists to make a buffer safe when the task calling `SolidSyslog_Log` +is not the task calling `SolidSyslog_Service`: + +```c +struct SolidSyslogMutex* mutex = SolidSyslogFreeRtosMutex_Create(); + +struct SolidSyslogBuffer* buffer = + SolidSyslogCircularBuffer_Create(&(struct SolidSyslogCircularBufferConfig) { + .Sender = sender, + .Mutex = mutex, + /* ring storage sized with SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES */ + }); +``` + +The ring memory and the mutex must both outlive the buffer. + +If both calls happen on one task, leave the role unfilled — the Null mutex is +the right answer and costs nothing. + +## Uptime + +`SolidSyslogFreeRtosSysUpTime` reports kernel ticks since boot. It is not +wall-clock time — the clock callback in `SolidSyslogConfig` is a separate +injection point, and on a target with no real-time clock a timestamp the +library cannot establish is emitted as absent rather than as a plausible wrong +value. + +## When it does not work + +Failures report through the error handler rather than silently. Install one +before you start, and read [error severity](../../error-severity.md) for what +each level is telling you — a `CRITICAL` at create time means the component +fell back to its Null object, and nothing will be delivered. diff --git a/docs/platforms/index.md b/docs/platforms/index.md index b64393c0..afd856f7 100644 --- a/docs/platforms/index.md +++ b/docs/platforms/index.md @@ -13,16 +13,16 @@ vtable. | Platform | Wraps | Network | TLS | At-rest crypto | Files | OS primitives | Time & host | |---|---|:-:|:-:|:-:|:-:|:-:|:-:| -| [Posix](posix.md) | POSIX / BSD sockets | ● | | | ● | ● | ● | -| [Windows](windows.md) | Win32 / Winsock | ● | | | ● | ● | ● | -| [FreeRTOS](freertos.md) | FreeRTOS kernel | | | | | ● | ● | -| [FreeRTOS-Plus-TCP](plustcp.md) | FreeRTOS-Plus-TCP | ● | | | | | | -| [lwIP (Raw API)](lwip.md) | lwIP Raw API | ● | | | | | | -| [OpenSSL](openssl.md) | OpenSSL ≥ 3.0 | | ● | ● | | | | -| [Mbed TLS](mbedtls.md) | Mbed TLS | | ● | ● | | | | -| [FatFs](fatfs.md) | ChaN FatFs | | | | ● | | | -| [FreeRTOS-Plus-FAT](plusfat.md) | FreeRTOS-Plus-FAT | | | | ● | | | -| [C11 atomics](stdatomic.md) | `` | | | | | ● | | +| [Posix](posix/index.md) | POSIX / BSD sockets | ● | | | ● | ● | ● | +| [Windows](windows/index.md) | Win32 / Winsock | ● | | | ● | ● | ● | +| [FreeRTOS](freertos/index.md) | FreeRTOS kernel | | | | | ● | ● | +| [FreeRTOS-Plus-TCP](plustcp/index.md) | FreeRTOS-Plus-TCP | ● | | | | | | +| [lwIP (Raw API)](lwipraw/index.md) | lwIP Raw API | ● | | | | | | +| [OpenSSL](openssl/index.md) | OpenSSL ≥ 3.0 | | ● | ● | | | | +| [Mbed TLS](mbedtls/index.md) | Mbed TLS | | ● | ● | | | | +| [FatFs](fatfs/index.md) | ChaN FatFs | | | | ● | | | +| [FreeRTOS-Plus-FAT](plusfat/index.md) | FreeRTOS-Plus-FAT | | | | ● | | | +| [C11 atomics](stdatomic/index.md) | `` | | | | | ● | | The at-rest-crypto column is the keyed policies (HMAC-SHA256, AES-256-GCM); the unkeyed CRC-16 policy is Core. Buffer, Store, and Structured Data are roles Core diff --git a/docs/platforms/lwip.md b/docs/platforms/lwip.md deleted file mode 100644 index df81ef83..00000000 --- a/docs/platforms/lwip.md +++ /dev/null @@ -1,58 +0,0 @@ -# lwIP (Raw API) - -`Platform/LwipRaw/` wraps the Raw API of lwIP -([lwIP documentation](https://www.nongnu.org/lwip/2_1_x/index.html)). Its `.c` -files compile against your `lwipopts.h`, so the adapter inherits your stack's -configuration. - -Fills the Resolver, Datagram and Stream [roles](../roles/index.md), plus the -address handle they share. Layer [Mbed TLS](mbedtls.md) over the TCP stream for -TLS. - -## What it ships - -| Class | Role | -|---|---| -| [`SolidSyslogLwipRawAddress`](../api/SolidSyslogLwipRawAddress_8h.md) | destination handle | -| [`SolidSyslogLwipRawResolver`](../api/SolidSyslogLwipRawResolver_8h.md) | numeric IPv4 resolver | -| [`SolidSyslogLwipRawDnsResolver`](../api/SolidSyslogLwipRawDnsResolver_8h.md) | DNS resolver (`LWIP_DNS=1`) | -| [`SolidSyslogLwipRawDatagram`](../api/SolidSyslogLwipRawDatagram_8h.md) | UDP sender | -| [`SolidSyslogLwipRawTcpStream`](../api/SolidSyslogLwipRawTcpStream_8h.md) | TCP byte transport | - -The source calls lwIP only — no direct OS calls. The TCP stream's synchronous -Open needs a bounded sleep, injected as a `SolidSyslogSleepFunction`. - -## The marshal - -Not a role: [`SolidSyslogLwipRaw_SetMarshal`](../api/SolidSyslogLwipRawMarshal_8h.md) -is a process-global seam, not a component you wire into the config. Every lwIP call -the Datagram and TcpStream make is routed through one marshal hop. - -- `NO_SYS=1` — bare metal, one execution context. Do nothing; the default - direct-call marshal is correct. -- `NO_SYS=0` — an RTOS tcpip thread - ([lwIP multithreading documentation](https://www.nongnu.org/lwip/2_1_x/multithreading.html)). - Call `SolidSyslogLwipRaw_SetMarshal(fn)` once at boot, before creating any - adapter, passing a function that runs its callback on the core-owning thread. - -The marshal must invoke its callback synchronously — the adapter reads results the -moment the hop returns. `tcpip_callback_with_block(…, 1)` or a `LOCK_TCPIP_CORE` / -`UNLOCK_TCPIP_CORE` pair satisfy that; a bare `tcpip_callback` does not. Worked -example: [`Bdd/Targets/FreeRtosLwip/main.c`](../../Bdd/Targets/FreeRtosLwip/main.c). - -## Requirements - -Your `lwipopts.h` must enable the features the adapter wraps: - -| Setting | For | -|---|---| -| `LWIP_RAW=1` | the Raw API | -| `LWIP_UDP=1` | the UDP datagram | -| `LWIP_TCP=1` | the TCP stream | -| `LWIP_DNS=1` | the DNS resolver only | - -Also set `ARP_QUEUEING=1` (else the first datagram to an unresolved peer is -dropped) and `LWIP_TCP_KEEPALIVE=1`, and size `PBUF_POOL_SIZE` / -`MEMP_NUM_TCP_PCB` / `MEMP_NUM_UDP_PCB` to your instance counts. - -Full setup — config, marshal, DNS — is [Integrating lwIP](../integrating-lwip.md). diff --git a/docs/platforms/lwipraw/index.md b/docs/platforms/lwipraw/index.md new file mode 100644 index 00000000..7c24f84d --- /dev/null +++ b/docs/platforms/lwipraw/index.md @@ -0,0 +1,78 @@ +# lwIP (Raw API) + +`Platform/LwipRaw/` wraps the Raw API of lwIP +([lwIP documentation](https://www.nongnu.org/lwip/2_1_x/index.html)). Its `.c` +files compile against your `lwipopts.h`, so the adapter inherits your stack's +configuration. + +Fills the Resolver, Datagram and Stream [roles](../../roles/index.md), plus the +address handle they share. A TLS platform layers over the TCP stream; the +[platform × capability matrix](../index.md) shows which provide it. + +## What it ships + +## The marshal + +Not a role: [`SolidSyslogLwipRaw_SetMarshal`](../../api/SolidSyslogLwipRawMarshal_8h.md) +is a process-global seam, not a component you wire into the config. Every lwIP call +the Datagram and TcpStream make is routed through one marshal hop. + +- `NO_SYS=1` — bare metal, one execution context. Do nothing; the default + direct-call marshal is correct. +- `NO_SYS=0` — an RTOS tcpip thread + ([lwIP multithreading documentation](https://www.nongnu.org/lwip/2_1_x/multithreading.html)). + Call `SolidSyslogLwipRaw_SetMarshal(fn)` once at boot, before creating any + adapter, passing a function that runs its callback on the core-owning thread. + +The marshal must invoke its callback synchronously — the adapter reads results the +moment the hop returns. A `LOCK_TCPIP_CORE` / `UNLOCK_TCPIP_CORE` pair satisfies +that directly. Posting to lwIP's mailbox does not, in any of its forms: +`tcpip_callback_with_block` blocks until the message is accepted, not until the +callback runs, so a mailbox marshal has to wait for completion itself. + +## Requirements + +The source calls lwIP only — no direct OS calls. The TCP stream's synchronous +Open needs a bounded sleep, injected as a `SolidSyslogSleepFunction`. + +Your `lwipopts.h` must enable the features the adapter wraps: + +| Setting | For | +|---|---| +| `LWIP_RAW=1` | the Raw API | +| `LWIP_UDP=1` | the UDP datagram | +| `LWIP_TCP=1` | the TCP stream | +| `LWIP_DNS=1` | the DNS resolver only | + +Also set `ARP_QUEUEING=1` (else the first datagram to an unresolved peer is +dropped) and `LWIP_TCP_KEEPALIVE=1`, and size `PBUF_POOL_SIZE` / +`MEMP_NUM_TCP_PCB` / `MEMP_NUM_UDP_PCB` to your instance counts. + +## Security behaviour and obligations + +### The transport carries syslog in clear + +Neither the datagram nor the TCP stream provides confidentiality, integrity or +peer authentication. TLS is a separate role filled by a different platform — the +[platform × capability matrix](../index.md) shows which — layered over this +stream rather than replacing it. + +### The marshal is a correctness requirement, not a tuning knob + +On a build with an lwIP thread (`NO_SYS=0`), every call this adapter makes must +reach the core-owning context, and it must do so synchronously because results +are read the moment the hop returns. An asynchronous marshal, or none at all, +corrupts lwIP's internal state rather than failing cleanly. Install it once at +boot, before any adapter is created. + +### Resolution is trusted as the stack returns it + +The DNS resolver forwards what lwIP answers. A deployment that cannot trust its +DNS should give the collector a numeric address, so that no resolution step +exists to be poisoned. + +### Pool sizing is yours, and exhaustion is silent at the stack + +`PBUF_POOL_SIZE`, `MEMP_NUM_TCP_PCB` and `MEMP_NUM_UDP_PCB` must cover the +instances you create alongside everything else using the stack. Under-sizing +shows as dropped records rather than as an error from lwIP. diff --git a/docs/platforms/lwipraw/setup.md b/docs/platforms/lwipraw/setup.md new file mode 100644 index 00000000..224b1411 --- /dev/null +++ b/docs/platforms/lwipraw/setup.md @@ -0,0 +1,169 @@ +# lwIP (Raw API) setup + +Wiring the lwIP adapters so a sender delivers over UDP or TCP. +[lwIP (Raw API)](index.md) covers what the adapters fill and what your +`lwipopts.h` must enable; the config fields are documented on the structs +themselves. This page is the wiring, and the one thing that is easy to get +wrong. + +## The marshal + +Every Raw API call the adapters make is routed through a single hop, installed +once at boot: + +```c +#include "SolidSyslogLwipRawMarshal.h" + +SolidSyslogLwipRaw_SetMarshal(MyMarshal); +``` + +**The marshal must run its callback synchronously, before it returns.** The +adapter reads results the moment the hop returns. One global slot serves the +process, because there is one lwIP instance to protect. + +Which marshal you install depends only on how lwIP is built. + +### `NO_SYS=1` — bare metal + +Install nothing. There is one execution context and no core to protect, so the +default direct call is correct. + +The trap is the `Sleep` callback. The TCP stream's `Open` is synchronous over an +asynchronous `tcp_connect`, so it spins and sleeps while waiting for the +connection callback — and on bare metal nothing else is driving lwIP during that +sleep. Your `Sleep` must keep the stack running: + +```c +void MyLwipSleep(int milliseconds) +{ + /* Elapsed rather than a deadline, so the loop is correct across a + * timebase wrap; unsigned subtraction wraps with it. */ + uint32_t start = MyTimebase_NowMs(); + uint32_t duration = (milliseconds > 0) ? (uint32_t) milliseconds : 0U; + while ((MyTimebase_NowMs() - start) < duration) + { + sys_check_timeouts(); + MyNetif_DrivePolledRx(); /* your board's receive pump */ + } +} +``` + +A `Sleep` that merely busy-waits leaves lwIP unable to advance its state +machine, so the connection callback never fires and `Open` times out with +nothing visibly wrong. + +Call `SolidSyslog_Service` from the same loop that calls `sys_check_timeouts`. + +### `NO_SYS=0` — an lwIP thread + +A dedicated thread owns lwIP's state, so every call has to reach it. With core +locking compiled in, take the lock around the hop: + +```c +void MyCoreLockMarshal(SolidSyslogLwipRawCallback callback, void* context) +{ + LOCK_TCPIP_CORE(); + callback(context); + UNLOCK_TCPIP_CORE(); +} +``` + +Core locking is the route to prefer, and it is what the reference target uses. +It runs the callback in your own task under the lock, so it is synchronous by +construction, independent of task priority, and costs no mailbox message. + +Without core locking you must post to lwIP's mailbox — and then wait for the +callback to *run*, which the post alone does not do. `tcpip_callback_with_block` +blocks until the mailbox accepts the message, not until the tcpip thread +executes it, so returning at that point would break the contract and leave the +hop's stack frame dangling under the callback. Carry a semaphore: + +```c +struct MarshalHop +{ + SolidSyslogLwipRawCallback callback; + void* context; + sys_sem_t done; +}; + +static void RunHop(void* ctx) +{ + struct MarshalHop* hop = ctx; + hop->callback(hop->context); + sys_sem_signal(&hop->done); +} + +void MyTcpipMarshal(SolidSyslogLwipRawCallback callback, void* context) +{ + if (MyPort_CurrentTaskIsTcpipThread()) /* else this deadlocks */ + { + callback(context); + return; + } + struct MarshalHop hop = {callback, context, {0}}; + if (sys_sem_new(&hop.done, 0) == ERR_OK) + { + if (tcpip_callback_with_block(RunHop, &hop, 1) == ERR_OK) + { + sys_arch_sem_wait(&hop.done, 0); + } + sys_sem_free(&hop.done); + } +} +``` + +The `err_t` matters: a failed post means `RunHop` never runs, so waiting on the +semaphore would hang and returning without waiting would let the adapter read +result state nothing wrote. + +Two more things to check in your port: the mailbox must be sized for a blocking +post, and lwIP exposes no portable "am I on the lwIP thread?" predicate, so +most ports compare the current task handle against the one given to +`tcpip_init`. Without that guard, an adapter call made from inside a callback +lwIP itself invoked will deadlock. + +Here `Sleep` is only a yield — the lwIP thread is running concurrently — so +whatever your RTOS offers is right. + +Marshal at the individual lwIP call, which is what this seam gives you. Wrapping +`SolidSyslog_Service` instead puts file I/O, crypto, buffer locking and record +formatting on the lwIP thread, none of which touch lwIP, and starves its timer +and receive path under load. + +## Resolving by name + +The DNS resolver bridges lwIP's asynchronous `dns_gethostbyname` to the +synchronous resolve contract the same way the TCP stream bridges connect: a +cache or hostlist hit returns immediately, and anything else spins on your +thread — never lwIP's — sleeping via your `Sleep` until the answer arrives or +the deadline passes. It needs `LWIP_DNS=1` and a `Sleep`; without one it falls +back to the Null resolver. + +Where there is no DNS server, `DNS_LOCAL_HOSTLIST` maps names statically and +resolves entirely on-device: + +```c +#define LWIP_DNS 1 +#define DNS_LOCAL_HOSTLIST 1 +#define DNS_LOCAL_HOSTLIST_INIT \ + { DNS_LOCAL_HOSTLIST_ELEM("collector", IPADDR4_INIT_BYTES(10, 0, 2, 2)) } +``` + +Both resolvers draw from one pool, so wiring the numeric and the DNS resolver +together needs `SOLIDSYSLOG_RESOLVER_POOL_SIZE` raised to 2. The pool sizes and +timeouts are in +[Adding it to your build](../../build-integration.md#tunables). + +## Limits + +The address and resolver are IPv4 only. Neither the datagram nor the TCP stream +selects an output interface — lwIP's routing table decides — and the datagram +reports a fixed conservative maximum payload rather than discovering the path +maximum transmission unit. + +## When it does not work + +Failures report through the error handler rather than silently. Install one +before you start, and read [error severity](../../error-severity.md) for what +each level is telling you — a `CRITICAL` at create time means the component +fell back to its Null object, and nothing will be delivered. diff --git a/docs/platforms/mbedtls.md b/docs/platforms/mbedtls.md deleted file mode 100644 index db62ddc9..00000000 --- a/docs/platforms/mbedtls.md +++ /dev/null @@ -1,25 +0,0 @@ -# Mbed TLS - -`Platform/MbedTls/` wraps Mbed TLS for TLS transport and keyed at-rest crypto on -embedded targets ([Mbed TLS documentation](https://mbed-tls.readthedocs.io/)). - -Fills the [Stream](../api/structSolidSyslogStream.md) role with TLS, and the -[SecurityPolicy](../api/structSolidSyslogSecurityPolicy.md) role for at-rest -integrity. - -## What it ships - -| Class | Role | -|---|---| -| [`SolidSyslogMbedTlsStream`](../api/SolidSyslogMbedTlsStream_8h.md) | TLS stream over an injected byte transport | -| [`SolidSyslogMbedTlsHmacSha256Policy`](../api/SolidSyslogMbedTlsHmacSha256Policy_8h.md) | at-rest HMAC-SHA256 | -| [`SolidSyslogMbedTlsAesGcmPolicy`](../api/SolidSyslogMbedTlsAesGcmPolicy_8h.md) | at-rest AES-256-GCM | - -## Requirements - -Your `mbedtls_config.h`. You pass caller-built handles (RNG, cert chain, key), not -file paths. `Platform/MbedTls/Source/` never calls process-global Mbed TLS APIs -(`mbedtls_platform_setup`, `psa_crypto_init`, …) — you own those, so SolidSyslog -coexists with your other Mbed TLS use. - -Full setup is [Integrating Mbed TLS](../integrating-mbedtls.md). diff --git a/docs/platforms/mbedtls/index.md b/docs/platforms/mbedtls/index.md new file mode 100644 index 00000000..58d1ad77 --- /dev/null +++ b/docs/platforms/mbedtls/index.md @@ -0,0 +1,92 @@ +# Mbed TLS + +`Platform/MbedTls/` wraps [Mbed TLS](https://mbed-tls.readthedocs.io/) for TLS +transport and keyed at-rest cryptography on embedded targets. It fills the +[Stream](../../api/structSolidSyslogStream.md) role with TLS and the +[SecurityPolicy](../../api/structSolidSyslogSecurityPolicy.md) role for at-rest +integrity and confidentiality. + +## What it ships + +## Requirements + +The adapter sources compile in your target against your own +`mbedtls_config.h`, so the features you enable are the features it gets. + +Credentials are passed as caller-built, caller-owned handles rather than file +paths: a seeded `mbedtls_ctr_drbg_context` for the handshake, an +`mbedtls_x509_crt` trust chain, and for mutual TLS an `mbedtls_x509_crt` and +`mbedtls_pk_context` pair. No part of the adapter opens a file, which is what +allows it to run on targets built without `MBEDTLS_FS_IO`. Each handle must +remain valid for the lifetime of the stream. + +A `SolidSyslogSleepFunction` is required and has no default. + +## Security behaviour and obligations + +The per-field detail is in +[`SolidSyslogMbedTlsStream.h`](../../api/SolidSyslogMbedTlsStream_8h.md), +alongside the fields themselves. What follows is the behaviour of the adapter as +a whole, and the work it leaves to you. + +### Transport security is fixed by the adapter + +Peer certificate verification is pinned to `MBEDTLS_SSL_VERIFY_REQUIRED` and the +protocol floor to TLS 1.2, both set on the adapter's own `ssl_config`. The floor +is set explicitly rather than inherited from `MBEDTLS_SSL_PRESET_DEFAULT`, which +on a permissive build can negotiate down to TLS 1.0 or 1.1. TLS 1.3 is +negotiated when both peers support it. + +### Peer identity is yours to declare + +The `ServerName` field supplies both the Server Name Indication sent in the +handshake and the identity checked against the peer certificate. It has a +distinct meaning when set, when empty, and when NULL — including one value that +disables endpoint verification without reporting anything — and the three are +documented on the field. Choosing between them is a deployment decision the +adapter cannot make. + +### Mutual TLS is optional and is not validated locally + +A client certificate is presented only when both `ClientCertChain` and +`ClientKey` are supplied. If either is absent, no client certificate is +configured and `Open` proceeds with server-authenticated TLS rather than +failing. Where a half-supplied credential must be treated as an error, check for +it before calling `Open`. + +The adapter performs no local check that the key matches the certificate, and +does not report a failure to install the pair. A mismatch is therefore seen as a +handshake rejection from the collector rather than as a setup error on the +device. + +### Rotation requires a restart of the stream + +Because the adapter consumes pre-built handles, refreshing credentials means +parsing new ones and recreating the stream, or the parent +`SolidSyslogStreamSender` so that the next connection uses them. There is no +reload callback. + +### Key custody is outside the library + +The library holds no keys of its own and uses whatever material is passed to it. +Where a private key is stored, how it is protected at rest, and whether it is +held in a secure element are properties of your platform. The same applies to +the at-rest policies: HMAC-SHA256 and AES-256-GCM are keyed, and storing and +rotating that key is yours. + +### Revocation is not checked + +The adapter performs no revocation checking, by Certificate Revocation List or +by the Online Certificate Status Protocol. Where a deployment requires it, it +must come from your own configuration of Mbed TLS, and confirming that it is in +force is part of your assessment rather than something the adapter reports. + +### Coexistence is an auditable contract + +`Platform/MbedTls/Source/` calls no process-global Mbed TLS API. It does not +call `mbedtls_platform_setup` or `mbedtls_platform_teardown`, install +threading-alt hooks, call `psa_crypto_init`, reset the global random number +generator, or replace a debug callback. TLS policy is applied per `ssl_config`, +so it cannot affect the ones you build elsewhere. A device that already uses +Mbed TLS for firmware update or a vendor cloud SDK keeps that configuration +intact, and the claim can be checked against the directory. diff --git a/docs/platforms/mbedtls/setup.md b/docs/platforms/mbedtls/setup.md new file mode 100644 index 00000000..d1d6425f --- /dev/null +++ b/docs/platforms/mbedtls/setup.md @@ -0,0 +1,108 @@ +# Mbed TLS setup + +Wiring `SolidSyslogMbedTlsStream` so a `SolidSyslogStreamSender` delivers +RFC 5425 syslog over TLS. [Mbed TLS](index.md) covers what the adapter +guarantees and what it leaves to you; the config fields are documented on the +struct itself. This page is the wiring, and the things that bite. + +## The layering + +TLS is a Stream wrapped around another Stream. The TLS adapter carries the +records; the transport underneath carries the bytes. + +```text +SolidSyslog_Log ─▶ Buffer ─▶ SolidSyslogStreamSender + │ + ▼ + SolidSyslogMbedTlsStream ◀── your CA / cert / key / DRBG handles + │ + ▼ + a byte-transport Stream ◀── your TCP/IP stack +``` + +You supply two things: the byte transport, and the Mbed TLS handles. Everything +above the TLS stream is unchanged from a plaintext wiring — `StreamSender` +applies RFC 6587 octet-counting framing on top either way. + +## Wiring it + +```c +struct SolidSyslogMbedTlsStreamConfig cfg = { + .Transport = myTcpStream, + .Sleep = MySleep, /* required — no fallback */ + .Rng = &mySeededDrbg, + .CaChain = &myParsedCaChain, + .ServerName = "syslog.example.com", + .ClientCertChain = &myClientCert, /* both, or neither */ + .ClientKey = &myClientKey, +}; +struct SolidSyslogStream* tls = SolidSyslogMbedTlsStream_Create(&cfg); +``` + +Wire `tls` into a `SolidSyslogStreamSender` as its `Stream`, exactly as you +would a plain TCP stream, and call `SolidSyslogMbedTlsStream_Destroy` when the +sender is torn down. There is nothing process-wide to install. + +If your firmware already uses Mbed TLS for something else — a cloud client, an +OTA updater, a vendor framework — that is the whole integration: the adapter +consumes handles you have already built and touches no global state. + +## Bringing Mbed TLS up, if it is new to the target + +Port Mbed TLS itself first, following the +[upstream porting guide](https://mbed-tls.readthedocs.io/en/latest/kb/how-to/how-do-i-port-mbed-tls-to-a-new-environment-OS/). +Four things then matter specifically for this adapter, and three of them fail in +ways that are hard to read. + +**Seed the DRBG from a source registered as `MBEDTLS_ENTROPY_SOURCE_STRONG`.** +Without a strong-tagged source, `mbedtls_entropy_func` never reaches its +internal threshold and every `mbedtls_ctr_drbg_seed` returns +`MBEDTLS_ERR_CTR_DRBG_ENTROPY_SOURCE_FAILED`. Production entropy is a hardware +question — a true random number generator, a vendor security element, or a +board-specific source. + +**Call `psa_crypto_init()` after the DRBG is seeded, not before.** Mbed TLS +3.6 routes TLS 1.3 through PSA, and if PSA is uninitialised the first handshake +state transition returns `MBEDTLS_ERR_ERROR_GENERIC_ERROR` before any byte +reaches the socket. + +**On a target with no platform entropy, give PSA a strong source.** With +`MBEDTLS_NO_PLATFORM_ENTROPY` defined — usual on embedded — `mbedtls_entropy_init` +registers no source of its own, and `psa_crypto_init` then fails with +`PSA_ERROR_INSUFFICIENT_ENTROPY`. Two routes out, and either is enough: + +- Define `MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG` and provide + `mbedtls_psa_external_get_random` wrapping the DRBG you just seeded. PSA + bypasses the entropy subsystem entirely, so PSA and the classic API run off + one chain. +- Or define `MBEDTLS_ENTROPY_HARDWARE_ALT` and provide `mbedtls_hardware_poll`. + `mbedtls_entropy_init` then registers it as a strong source, and PSA seeds its + own DRBG through the standard path. + +The first keeps one chain and is the simpler thing to reason about; the second +suits a target whose randomness already arrives through a hardware poll. + +**Parse the CA chain, and the client credential if you are using mutual TLS**, +by whatever route suits the build: a filesystem, a baked-in array, a blob pulled +from a security element. PEM input must be NUL-terminated. + +## Memory + +The adapter allocates nothing itself. Everything a TLS session costs is Mbed +TLS's own allocation, governed by your `mbedtls_config.h` — the record buffer +sizes dominate it, and their defaults are sized for a general-purpose host +rather than a constrained target. Budget for every TLS session you intend to +run concurrently, not one, and take the sizing guidance from the +[upstream documentation](https://mbed-tls.readthedocs.io/) rather than from +here. + +Where Mbed TLS takes its memory from is also your configuration. On a target +whose libc heap is not the one you intend it to use, `mbedtls_ssl_setup` is +where that shows up. + +## When it does not work + +Failures report through the error handler rather than silently. Install one +before you start, and read [error severity](../../error-severity.md) for what +each level is telling you — a `CRITICAL` at create time means the stream fell +back to the Null object, and nothing will be delivered. diff --git a/docs/platforms/openssl.md b/docs/platforms/openssl.md deleted file mode 100644 index 2582e148..00000000 --- a/docs/platforms/openssl.md +++ /dev/null @@ -1,20 +0,0 @@ -# OpenSSL - -`Platform/OpenSsl/` wraps OpenSSL for TLS transport and keyed at-rest crypto -([OpenSSL documentation](https://docs.openssl.org/)). - -Fills the [Stream](../api/structSolidSyslogStream.md) role with TLS, and the -[SecurityPolicy](../api/structSolidSyslogSecurityPolicy.md) role for at-rest -integrity. - -## What it ships - -| Class | Role | -|---|---| -| [`SolidSyslogOpenSslStream`](../api/SolidSyslogOpenSslStream_8h.md) | TLS stream — server-cert + hostname verification, cipher pinning, optional mutual TLS | -| [`SolidSyslogOpenSslHmacSha256Policy`](../api/SolidSyslogOpenSslHmacSha256Policy_8h.md) | at-rest HMAC-SHA256 | -| [`SolidSyslogOpenSslAesGcmPolicy`](../api/SolidSyslogOpenSslAesGcmPolicy_8h.md) | at-rest AES-256-GCM | - -## Requirements - -OpenSSL 3.0 or later. diff --git a/docs/platforms/openssl/index.md b/docs/platforms/openssl/index.md new file mode 100644 index 00000000..a7eb48b4 --- /dev/null +++ b/docs/platforms/openssl/index.md @@ -0,0 +1,84 @@ +# OpenSSL + +`Platform/OpenSsl/` wraps [OpenSSL](https://docs.openssl.org/) for TLS transport +and keyed at-rest cryptography on hosted targets. It fills the +[Stream](../../api/structSolidSyslogStream.md) role with TLS and the +[SecurityPolicy](../../api/structSolidSyslogSecurityPolicy.md) role for at-rest +integrity and confidentiality. + +## What it ships + +## Requirements + +OpenSSL 3.0 or later. + +Credentials are file paths, read when the stream is opened: a PEM trust bundle, +and for mutual TLS a PEM client certificate chain and private key. A +`SolidSyslogSleepFunction` is required and has no default. + +## Security behaviour and obligations + +The per-field detail is in +[`SolidSyslogOpenSslStream.h`](../../api/SolidSyslogOpenSslStream_8h.md), alongside the +fields themselves. What follows is the behaviour of the adapter as a whole, and +the work it leaves to you. + +### Transport security is fixed by the adapter + +`SSL_VERIFY_PEER` is pinned on the context and the protocol floor is set to +TLS 1.2. Both are return-checked, so the stream fails to open rather than +proceeding if the underlying libssl refuses the floor. Every setup call on the +handshake path is checked in the same way, which is what prevents a handshake +completing without the identity check having been applied. + +### The trust bundle is mandatory + +The trust bundle must load. If it does not, the stream fails to open — there is +no fallback to a system trust store. + +### Peer identity is yours to declare + +The `ServerName` field supplies both the Server Name Indication sent in the +handshake and the identity checked against the peer certificate. It has a +distinct meaning when set, when empty, and when NULL — including one value that +disables endpoint verification without reporting anything — and the three are +documented on the field. Choosing between them is a deployment decision the +adapter cannot make. + +### Mutual TLS is optional and all-or-nothing + +A client certificate chain and its private key are supplied together or not at +all. Supplying one without the other is rejected when the stream is opened, so a +partially configured credential cannot result in a connection that silently +omits the client certificate. The key is also checked against the certificate +locally, before any bytes reach the network. + +### The cipher policy is yours + +A cipher list is passed through to OpenSSL unchanged, and omitting it takes the +OpenSSL default. The library pins no list of its own: the appropriate one +depends on the libssl build present on the target and on the profile the +deployment is held to. + +### Rotation is a file replacement and a reconnection + +The `SSL_CTX` is rebuilt each time the stream is opened, re-reading every +credential file named in the configuration — the trust anchors always, the client +certificate and key only where mutual TLS is configured. So replacing them takes effect on the next connection — either through +ordinary reconnection after an outage, or by calling +`SolidSyslogSender_Disconnect` to force one. No reload callback is needed. + +### Key custody is outside the library + +The library holds no keys of its own and reads whatever material it is pointed +at. Filesystem permissions on the private key, whether it is held in a hardware +security module, and how it is rotated are properties of your deployment. The +same applies to the at-rest policies: HMAC-SHA256 and AES-256-GCM are keyed, and +storing and rotating that key is yours. + +### Revocation is not checked + +The adapter performs no revocation checking, by Certificate Revocation List or +by the Online Certificate Status Protocol. Where a deployment requires it, it +must come from your own configuration of OpenSSL, and confirming that it is in +force is part of your assessment rather than something the adapter reports. diff --git a/docs/platforms/openssl/setup.md b/docs/platforms/openssl/setup.md new file mode 100644 index 00000000..825248ca --- /dev/null +++ b/docs/platforms/openssl/setup.md @@ -0,0 +1,88 @@ +# OpenSSL setup + +Wiring `SolidSyslogOpenSslStream` so a `SolidSyslogStreamSender` delivers RFC 5425 +syslog over TLS. [OpenSSL](index.md) covers what the adapter guarantees and what +it leaves to you; the config fields are documented on the struct itself. This +page is the wiring. + +## What you need + +OpenSSL 3.0 or later on the include and link path, and a platform supplying the +TCP stream underneath — the [capability matrix](../index.md) shows which fill +that role. + +```cmake +set(SOLIDSYSLOG_PLATFORMS "OpenSsl;") +``` + +`` is whichever platform the [capability matrix](../index.md) says +fills that role on your target — see +[naming your platforms](../../build-integration.md#cmake) for how the list is +read. + +OpenSSL is a stable system API rather than a header-configured upstream, so the +adapter compiles straight into `libSolidSyslog.a` and there is no separate +target to link. [Adding it to your build](../../build-integration.md) covers the +Make and IDE routes. + +## The layering + +TLS is a Stream wrapped around another Stream. The TLS adapter carries the +records; the transport underneath carries the bytes, and it can be any Stream. + +```text +StreamSender → SolidSyslogOpenSslStream → your TCP stream → socket +``` + +The TLS stream **borrows** its transport. It may close it, but it never destroys +it: the transport is yours to create and to destroy, and it must stay valid +until `SolidSyslogOpenSslStream_Destroy`. + +## Wiring it + +```c +/* Your TCP stream and sleep, from the platform that supplies them. */ +struct SolidSyslogStream* transport = CreateTcpStream(); + +static struct SolidSyslogOpenSslStreamConfig tlsConfig; +tlsConfig = (struct SolidSyslogOpenSslStreamConfig) {0}; +tlsConfig.Transport = transport; +tlsConfig.Sleep = MySleep; /* required — no fallback */ +tlsConfig.CaBundlePath = "/etc/ssl/collector-ca.pem"; +tlsConfig.ServerName = "collector.example.net"; + +struct SolidSyslogStream* tls = SolidSyslogOpenSslStream_Create(&tlsConfig); +``` + +Zero-initialise the config before filling it. + +For mutual TLS, add the client credential — both fields or neither, since +supplying one without the other is rejected at `Open`: + +```c +tlsConfig.ClientCertChainPath = "/etc/ssl/device-chain.pem"; +tlsConfig.ClientKeyPath = "/etc/ssl/device-key.pem"; +``` + +Then the sender, unchanged from the plain-TCP case — it sees a Stream and does +not know or care that it is a TLS one: + +```c +static struct SolidSyslogStreamSenderConfig senderConfig; +senderConfig = (struct SolidSyslogStreamSenderConfig) {0}; +senderConfig.Resolver = resolver; +senderConfig.Stream = tls; +senderConfig.Address = CreateAddress(); /* your platform's Address */ +senderConfig.Endpoint = GetEndpoint; +struct SolidSyslogSender* sender = SolidSyslogStreamSender_Create(&senderConfig); +``` + +Tear down in reverse order: sender, address, TLS stream, then the transport you +created. + +## When it does not work + +Failures report through the error handler rather than silently. Install one +before you start, and read [error severity](../../error-severity.md) for what +each level is telling you — a `CRITICAL` at create time means the stream fell +back to the Null object, and nothing will be delivered. diff --git a/docs/platforms/plusfat.md b/docs/platforms/plusfat.md deleted file mode 100644 index 3c00c1aa..00000000 --- a/docs/platforms/plusfat.md +++ /dev/null @@ -1,20 +0,0 @@ -# FreeRTOS-Plus-FAT - -`Platform/PlusFat/` wraps FreeRTOS-Plus-FAT as the File layer -([FreeRTOS-Plus-FAT documentation](https://www.freertos.org/Documentation/03-Libraries/05-FreeRTOS-labs/04-FreeRTOS-plus-FAT/01-FreeRTOS-plus-FAT)). - -Fills the [File](../api/structSolidSyslogFile.md) role — the primitive beneath a -BlockDevice. - -## What it ships - -| Class | Role | -|---|---| -| [`SolidSyslogPlusFatFile`](../api/SolidSyslogPlusFatFile_8h.md) | file — `ff_fflush` after every write | - -## Requirements - -FreeRTOS — Plus-FAT is FreeRTOS-coupled. Supply an `FF_Disk_t` media driver and -`FreeRTOSFATConfig.h`. - -Full setup is [Integrating FreeRTOS-Plus-FAT](../integrating-plusfat.md). diff --git a/docs/platforms/plusfat/index.md b/docs/platforms/plusfat/index.md new file mode 100644 index 00000000..5320b031 --- /dev/null +++ b/docs/platforms/plusfat/index.md @@ -0,0 +1,43 @@ +# FreeRTOS-Plus-FAT + +`Platform/PlusFat/` wraps FreeRTOS-Plus-FAT as the File layer +([FreeRTOS-Plus-FAT documentation](https://www.freertos.org/Documentation/03-Libraries/05-FreeRTOS-labs/04-FreeRTOS-plus-FAT/01-FreeRTOS-plus-FAT)). + +Fills the [File](../../api/structSolidSyslogFile.md) role — the primitive beneath a +BlockDevice. + +## What it ships + +## Requirements + +FreeRTOS — Plus-FAT is FreeRTOS-coupled. Supply an `FF_Disk_t` media driver and +`FreeRTOSFATConfig.h`. + +## Security behaviour and obligations + +### The file layer offers no confidentiality or tamper evidence + +Records are written as given. Detecting modification of a stored record, or +keeping it unreadable, is the SecurityPolicy role's job, not this one — see +[at-rest cryptography](../../security/at-rest-cryptography.md). + +### Durability is bounded by the write, not guaranteed by it + +The adapter flushes after every write, and that flush persists the record's data +sectors. It does not commit the file's recorded size: the directory entry is +written when the file is closed, and the store holds a block file open across +appends. A power cut can therefore leave records on the medium that the recorded +size excludes, and the store will not read those back. + +It flushes the IO manager's cache rather than the file: `ff_stdio.h` declares +`ff_fflush` but the library never defines it, so `FF_FlushCache` is the real +durability primitive. Whether that reaches the medium is a property of your +`FF_Disk_t` driver and the hardware under it. FAT is not a journalling +filesystem, and a partially written directory entry is possible on a device that +loses power mid-update. + +### The media driver is yours + +The `FF_Disk_t` implementation is supplied by you, and so is any wear levelling: +the store rewrites the same blocks in rotation, which on raw flash without wear +levelling concentrates erase cycles. diff --git a/docs/platforms/plusfat/setup.md b/docs/platforms/plusfat/setup.md new file mode 100644 index 00000000..f05789a4 --- /dev/null +++ b/docs/platforms/plusfat/setup.md @@ -0,0 +1,83 @@ +# FreeRTOS-Plus-FAT setup + +Giving store-and-forward a real file backend. +[FreeRTOS-Plus-FAT](index.md) covers what the adapter fills and what it leaves +to you. This page is what you must supply around it. + +## The shape + +```text +SolidSyslogBlockStore + │ +SolidSyslogFileBlockDevice ◀── sequence-numbered .log files + │ +SolidSyslogPlusFatFile ◀── this adapter, over the ff_stdio API + │ +FreeRTOS-Plus-FAT core ◀── vendor sources, you compile them + │ +your FF_Disk_t media driver ◀── you write this +``` + +The adapter owns the middle box only. It speaks `ff_stdio` and never reaches +the block device. + +## What you must provide + +**The FreeRTOS-Plus-FAT sources**, compiled into your image. They are not clean +under the conversion warnings this project builds with, so compile them under a +relaxed warning set as you would the kernel itself. + +**An `FF_Disk_t` media driver** for your storage hardware — the IO manager with +its read and write block callbacks, the mount, format-on-first-use where no +file system is present, and registration of the volume in the virtual file +system. Plus-FAT ships reference drivers; its RAM disk is the clearest +template. + +**A `FreeRTOSFATConfig.h`** on your include path. Unlike some file systems this +resolves off `-I` rather than needing to sit beside the sources. Set at least +the byte order and the thread-local storage index; the defaults header fills +the rest. + +**Kernel configuration**, because Plus-FAT requires it: + +- recursive mutexes, which its locking enforces +- event groups, used by the IO manager +- enough thread-local storage slots — `ff_stdio` keeps its error state, working + directory and error code in three consecutive slots from the index you + configure, and its header enforces this at compile time +- static allocation, if your media driver creates its IO manager mutex + statically + +## Paths must be absolute + +Without the working-directory option compiled in, `ff_stdio` accepts absolute +paths only — its relative-path resolver is a pass-through. Give +`SolidSyslogFileBlockDevice` an absolute prefix such as `/STORE`, so files land +at the volume root as `/STORE00.log`, `/STORE01.log`. The default short +filename mode is sufficient for that naming. + +## Durability + +What the flush does and does not commit is on the platform page, under +[durability](index.md#durability-is-bounded-by-the-write-not-guaranteed-by-it). +Read it before choosing your sizes: it decides two settings here. + +A graceful shutdown closes the file and leaves data and metadata consistent, so +give the application a path that tears the logger down rather than relying on +power being cut cleanly. And if the device must survive a hard cut mid-record, +size your records and choose the store's discard policy with the loss window in +mind. + +## Memory + +The adapter allocates nothing; its instance comes from a static pool. Plus-FAT +does allocate — the IO manager's sector cache and its internal buffers come +from the RTOS heap — so size that heap for the cache you ask the IO manager +for. + +## When it does not work + +Failures report through the error handler rather than silently. Install one +before you start, and read [error severity](../../error-severity.md) for what +each level is telling you — a `CRITICAL` at create time means the file fell +back to the Null object, and nothing will be stored. diff --git a/docs/platforms/plustcp.md b/docs/platforms/plustcp.md deleted file mode 100644 index e5319d02..00000000 --- a/docs/platforms/plustcp.md +++ /dev/null @@ -1,22 +0,0 @@ -# FreeRTOS-Plus-TCP - -`Platform/PlusTcp/` wraps FreeRTOS-Plus-TCP for networking on FreeRTOS targets -([FreeRTOS-Plus-TCP documentation](https://www.freertos.org/Documentation/03-Libraries/02-FreeRTOS-plus/02-FreeRTOS-plus-TCP/01-FreeRTOS-Plus-TCP)). - -Fills the Resolver, Datagram and Stream [roles](../roles/index.md), plus the -address handle they share. - -## What it ships - -| Class | Role | -|---|---| -| [`SolidSyslogPlusTcpResolver`](../api/SolidSyslogPlusTcpResolver_8h.md) | DNS resolver (`FreeRTOS_getaddrinfo`) | -| [`SolidSyslogPlusTcpAddress`](../api/SolidSyslogPlusTcpAddress_8h.md) | address (`freertos_sockaddr`) | -| [`SolidSyslogPlusTcpDatagram`](../api/SolidSyslogPlusTcpDatagram_8h.md) | UDP sender | -| [`SolidSyslogPlusTcpTcpStream`](../api/SolidSyslogPlusTcpTcpStream_8h.md) | TCP stream (bounded connect) | - -## Requirements - -FreeRTOS-Plus-TCP, selected at CMake time with -naming `PlusTcp` in `SOLIDSYSLOG_PLATFORMS`. The resolver wraps `FreeRTOS_getaddrinfo`, so -your `FreeRTOSIPConfig.h` needs `ipconfigUSE_DNS=1`. diff --git a/docs/platforms/plustcp/index.md b/docs/platforms/plustcp/index.md new file mode 100644 index 00000000..f7923d81 --- /dev/null +++ b/docs/platforms/plustcp/index.md @@ -0,0 +1,37 @@ +# FreeRTOS-Plus-TCP + +`Platform/PlusTcp/` wraps FreeRTOS-Plus-TCP for networking on FreeRTOS targets +([FreeRTOS-Plus-TCP documentation](https://www.freertos.org/Documentation/03-Libraries/02-FreeRTOS-plus/02-FreeRTOS-plus-TCP/01-FreeRTOS-Plus-TCP)). + +Fills the Resolver, Datagram and Stream [roles](../../roles/index.md), plus the +address handle they share. + +## What it ships + +## Requirements + +FreeRTOS-Plus-TCP, selected at CMake time with +naming `PlusTcp` in `SOLIDSYSLOG_PLATFORMS`. The resolver wraps `FreeRTOS_getaddrinfo`, so +your `FreeRTOSIPConfig.h` needs `ipconfigUSE_DNS=1`. + +## Security behaviour and obligations + +### The transport carries syslog in clear + +Neither the datagram nor the TCP stream provides confidentiality, integrity or +peer authentication. TLS is a separate role filled by a different platform — the +[platform × capability matrix](../index.md) shows which — layered over this +stream rather than replacing it. + +### Resolution is trusted as the stack returns it + +The resolver forwards what FreeRTOS-Plus-TCP answers. A deployment that cannot +trust its DNS should give the collector a numeric address rather than a name, so +that no resolution step exists to be poisoned. + +### The stack's configuration is yours + +Buffer counts, socket limits and timer behaviour are set in your +`FreeRTOSIPConfig.h`. Sizing them for the number of adapter instances you create +is part of the integration, and exhaustion surfaces as a failure to send rather +than as a crash. diff --git a/docs/platforms/plustcp/setup.md b/docs/platforms/plustcp/setup.md new file mode 100644 index 00000000..93a3120b --- /dev/null +++ b/docs/platforms/plustcp/setup.md @@ -0,0 +1,63 @@ +# FreeRTOS-Plus-TCP setup + +Wiring the networking adapters. [FreeRTOS-Plus-TCP](index.md) covers what they +fill and what they leave to you; the config fields are documented on the +structs themselves. + +## What to link + +The stack is configured by a header you own, so the adapters compile inside +your target against your `FreeRTOSIPConfig.h`. This platform fills the network +role only, so select it alongside whichever platform supplies your mutex and +clock — the [capability matrix](../index.md) shows which platforms fill those: + +```cmake +set(SOLIDSYSLOG_PLATFORMS "PlusTcp;") +target_link_libraries(my_app PRIVATE SolidSyslog SolidSyslog::PlusTcp) +``` + +`` is whichever platform the [capability matrix](../index.md) +says fills the Mutex and clock roles on your target — see +[naming your platforms](../../build-integration.md#cmake) for how the list is +read. + +Bring the stack up and let it acquire an address before creating any sender. + +Resolving the collector by name needs DNS compiled into the stack. If you give +the collector a numeric address instead, you need neither DNS nor the resolver +it backs — which is also the more predictable choice where the network cannot +be trusted. + +## Wiring a sender + +```c +struct SolidSyslogResolver* resolver = SolidSyslogPlusTcpResolver_Create(); +struct SolidSyslogAddress* address = SolidSyslogPlusTcpAddress_Create(); +struct SolidSyslogStream* stream = SolidSyslogPlusTcpTcpStream_Create(NULL); + +static struct SolidSyslogStreamSenderConfig senderConfig; +senderConfig = (struct SolidSyslogStreamSenderConfig) {0}; +senderConfig.Resolver = resolver; +senderConfig.Stream = stream; +senderConfig.Address = address; +senderConfig.Endpoint = MyEndpoint; +struct SolidSyslogSender* sender = SolidSyslogStreamSender_Create(&senderConfig); +``` + +For UDP, build a `SolidSyslogPlusTcpDatagram` and a `SolidSyslogUdpSender` +instead. Passing `NULL` to the TCP stream takes the default connect budget. + +## Sizing + +The stack's socket and buffer limits are yours to set, and they have to cover +what this library creates alongside everything else using the network. +Under-sizing shows up as a failure to send rather than as a crash, so it is +worth counting the instances you create rather than discovering the limit in +the field. + +## When it does not work + +Failures report through the error handler rather than silently. Install one +before you start, and read [error severity](../../error-severity.md) for what +each level is telling you — a `CRITICAL` at create time means the component +fell back to its Null object, and nothing will be delivered. diff --git a/docs/platforms/posix.md b/docs/platforms/posix.md deleted file mode 100644 index 4d6b8559..00000000 --- a/docs/platforms/posix.md +++ /dev/null @@ -1,32 +0,0 @@ -# Posix - -`Platform/Posix/` wraps the standard POSIX APIs — BSD sockets, pthreads, POSIX -message queues, `clock_gettime`, stdio -([POSIX.1-2017 specification](https://pubs.opengroup.org/onlinepubs/9699919799/)). -Linux is the reference target. - -Fills the Resolver, Datagram, Stream, Buffer, File and Mutex -[roles](../roles/index.md), plus the clock / hostname / process-id / sleep -callbacks. - -## What it ships - -| Class | Role | -|---|---| -| [`SolidSyslogPosixResolver`](../api/SolidSyslogPosixResolver_8h.md) | resolver (`getaddrinfo`) | -| [`SolidSyslogPosixAddress`](../api/SolidSyslogPosixAddress_8h.md) | address | -| [`SolidSyslogPosixDatagram`](../api/SolidSyslogPosixDatagram_8h.md) | UDP sender | -| [`SolidSyslogPosixTcpStream`](../api/SolidSyslogPosixTcpStream_8h.md) | TCP stream (non-blocking, bounded connect) | -| [`SolidSyslogPosixFile`](../api/SolidSyslogPosixFile_8h.md) | file | -| [`SolidSyslogPosixMessageQueueBuffer`](../api/SolidSyslogPosixMessageQueueBuffer_8h.md) | message-queue buffer | -| [`SolidSyslogPosixMutex`](../api/SolidSyslogPosixMutex_8h.md) | mutex | -| [`SolidSyslogPosixClock`](../api/SolidSyslogPosixClock_8h.md) | clock | -| [`SolidSyslogPosixHostname`](../api/SolidSyslogPosixHostname_8h.md) | hostname | -| [`SolidSyslogPosixProcessId`](../api/SolidSyslogPosixProcessId_8h.md) | process-id | -| [`SolidSyslogPosixSleep`](../api/SolidSyslogPosixSleep_8h.md) | sleep | -| [`SolidSyslogPosixSysUpTime`](../api/SolidSyslogPosixSysUpTime_8h.md) | uptime (`CLOCK_BOOTTIME`) | - -## Requirements - -A POSIX-conformant OS; Linux is the tested target. The message-queue buffer needs -POSIX message queues (link `-lrt` on glibc). diff --git a/docs/platforms/posix/index.md b/docs/platforms/posix/index.md new file mode 100644 index 00000000..4e334bdf --- /dev/null +++ b/docs/platforms/posix/index.md @@ -0,0 +1,48 @@ +# Posix + +`Platform/Posix/` wraps the standard POSIX APIs — BSD sockets, pthreads, POSIX +message queues, `clock_gettime`, stdio +([POSIX.1-2017 specification](https://pubs.opengroup.org/onlinepubs/9699919799/)). +Linux is the reference target. + +Fills the Resolver, Datagram, Stream, Buffer, File and Mutex +[roles](../../roles/index.md), plus the clock / hostname / process-id / sleep +callbacks. + +## What it ships + +## Requirements + +A POSIX-conformant OS; Linux is the tested target. The message-queue buffer needs +POSIX message queues (link `-lrt` on glibc). + +## Security behaviour and obligations + +### The transport carries syslog in clear + +Neither the datagram nor the TCP stream provides confidentiality, integrity or +peer authentication. Anything on the path can read and alter the records. TLS is +a separate role filled by a different platform — the +[platform × capability matrix](../index.md) shows which — layered over this +stream rather than replacing it. + +### The store file is owner-only; its directory is yours + +Files are created readable and writable by the owning user alone. That protects +the file, not the path to it: the ownership and permissions of the directory you +place the store in, and whether another process on the device runs as the same +user, are yours to set and to verify. + +### Host identity is only as good as the operating system's + +The hostname and process id are read from the OS and forwarded unmodified. They +identify the record's origin exactly as far as the OS can be trusted to report +it, and the library performs no independent check. + +### The blocking surface is bounded but not zero + +The TCP stream's socket is non-blocking once open, so a send returns immediately +against a wedged peer. The initial connection is the bounded exception, and its +budget is a tunable. The UDP socket is left blocking: a datagram send is not +expected to wait, but nothing here bounds it if the kernel's send buffer fills. A deployment with a hard real-time deadline should drive delivery +from a service thread rather than the calling one. diff --git a/docs/platforms/posix/setup.md b/docs/platforms/posix/setup.md new file mode 100644 index 00000000..fea4bab2 --- /dev/null +++ b/docs/platforms/posix/setup.md @@ -0,0 +1,64 @@ +# Posix setup + +Wiring the POSIX adapters. [Posix](index.md) covers what they fill and what +they leave to you; the config fields are documented on the structs themselves. + +## What to link + +POSIX is a stable system interface rather than a header-configured upstream, so +the adapters compile straight into `libSolidSyslog.a`. There is nothing extra to +link: + +```cmake +set(SOLIDSYSLOG_PLATFORMS "Posix") +``` + +Selecting this platform links `rt` publicly, which older glibc needs for POSIX +message queues; from glibc 2.34 they are in `libc` and the library is empty but +harmless. You do not have to add it yourself, and it is linked whether or not +you use the message-queue buffer. + +## Wiring a sender + +```c +struct SolidSyslogResolver* resolver = SolidSyslogPosixResolver_Create(); +struct SolidSyslogAddress* address = SolidSyslogPosixAddress_Create(); +struct SolidSyslogStream* stream = SolidSyslogPosixTcpStream_Create(NULL); + +static struct SolidSyslogStreamSenderConfig senderConfig; +senderConfig = (struct SolidSyslogStreamSenderConfig) {0}; +senderConfig.Resolver = resolver; +senderConfig.Stream = stream; +senderConfig.Address = address; +senderConfig.Endpoint = MyEndpoint; +struct SolidSyslogSender* sender = SolidSyslogStreamSender_Create(&senderConfig); +``` + +For UDP, build a `SolidSyslogPosixDatagram` and a `SolidSyslogUdpSender` +instead; the resolver and address are the same. Passing `NULL` to the TCP +stream takes the default connect budget — supply a config to override it per +instance. + +Tear down in reverse order, and destroy everything you created. + +## The callbacks + +`SolidSyslogConfig` takes the clock, hostname and process id as callbacks +rather than components, and this platform supplies one of each ready to use: +`SolidSyslogPosixClock_GetTimestamp`, `SolidSyslogPosixHostname` and +`SolidSyslogPosixProcessId`. Use them directly, or wrap your own if the values +should come from somewhere other than the operating system. + +## Threading + +If your application logs from one thread and drains from another, put a +circular buffer between them with `SolidSyslogPosixMutex` filling the mutex +role. Leaving the mutex unfilled is safe only while `Log` and `Service` run on +the same thread. + +## When it does not work + +Failures report through the error handler rather than silently. Install one +before you start, and read [error severity](../../error-severity.md) for what +each level is telling you — a `CRITICAL` at create time means the component +fell back to its Null object, and nothing will be delivered. diff --git a/docs/platforms/stdatomic.md b/docs/platforms/stdatomic.md deleted file mode 100644 index 5ea31486..00000000 --- a/docs/platforms/stdatomic.md +++ /dev/null @@ -1,17 +0,0 @@ -# C11 atomics - -`Platform/StdAtomic/` is a portable AtomicCounter built on C11 `` — the -sequenceId source on any target with a C11 compiler, no OS dependency. - -Fills the [AtomicCounter](../api/structSolidSyslogAtomicCounter.md) role. - -## What it ships - -| Class | Role | -|---|---| -| [`SolidSyslogStdAtomicCounter`](../api/SolidSyslogStdAtomicCounter_8h.md) | atomic counter (`_Atomic uint32_t` CAS) | - -## Requirements - -A C11 compiler with ``. Windows toolchains without it use -[`SolidSyslogWindowsAtomicCounter`](windows.md) instead. diff --git a/docs/platforms/stdatomic/index.md b/docs/platforms/stdatomic/index.md new file mode 100644 index 00000000..9f7e6ce6 --- /dev/null +++ b/docs/platforms/stdatomic/index.md @@ -0,0 +1,28 @@ +# C11 atomics + +`Platform/StdAtomic/` is a portable AtomicCounter built on C11 `` — the +sequenceId source on any target with a C11 compiler, no OS dependency. + +Fills the [AtomicCounter](../../api/structSolidSyslogAtomicCounter.md) role. + +## What it ships + +## Requirements + +A C11 compiler with ``. Where a toolchain lacks it, the +[platform × capability matrix](../index.md) shows which other platforms fill +the AtomicCounter role. + +## Security behaviour and obligations + +### The sequence wraps, and a collector must expect it + +Values run in `[1, 2^31 - 1]` and skip zero on wrap. A long-lived device will +reuse numbers, so collector-side gap detection has to treat wrap as ordinary +rather than as a discontinuity. + +### It evidences loss, not origin + +`sequenceId` is a plain counter, not a cryptographic construction. It shows that +a record is missing; it does not bind a record to the device that raised it, and +it can be reproduced by anything that can write records. diff --git a/docs/platforms/stdatomic/setup.md b/docs/platforms/stdatomic/setup.md new file mode 100644 index 00000000..3bc8d313 --- /dev/null +++ b/docs/platforms/stdatomic/setup.md @@ -0,0 +1,63 @@ +# C11 atomics setup + +Wiring the sequence-number source. [C11 atomics](index.md) covers what it fills +and what it leaves to you. + +## What to link + +`` is part of the language rather than a configured upstream, so +the adapter compiles straight into the static library: + +```cmake +set(SOLIDSYSLOG_PLATFORMS "StdAtomic;;") +``` + +This platform fills the AtomicCounter role only; the placeholders are whichever +platforms the [capability matrix](../index.md) says fill the rest of what your +build needs. See +[naming your platforms](../../build-integration.md#cmake) for how the list is +read. + +The compiler must support C11 atomics. Where a toolchain does not, the +[platform × capability matrix](../index.md) shows which other platforms fill +the same role. + +## Wiring it + +```c +struct SolidSyslogAtomicCounter* counter = SolidSyslogStdAtomicCounter_Create(); +``` + +Hand the counter to the structured-data element that carries the sequence +number. Create takes no configuration; destroy it when the logger is torn down. + +## Why it is worth wiring + +The sequence number is what lets a collector notice that records are missing. +It is assigned when a record is raised rather than when it is sent, so a gap +reflects loss anywhere in the pipeline — the buffer, the store, or the +transport — not only on the wire. + +An exhausted counter pool and an unconfigured counter are different failures, +and they are worth telling apart. + +Exhaust the pool and `Create` hands back the Null counter, which returns 1 for +every record. The meta element is still emitted, so you still get `sysUpTime` +and `language`; only the sequence stops distinguishing records. Exhaustion is +reported through the error handler at `Create`, so it is visible rather than +something to discover later. + +Leave the counter out of the meta element's config altogether and the element +itself does not build: `SolidSyslogMetaSd_Create` reports a `WARNING` and falls +back to the Null structured data, so no meta element is attached at all — no +`sequenceId`, and no `sysUpTime` either. + +Both are safe in the sense that logging continues, but only the first still +carries the metadata. + +## When it does not work + +Failures report through the error handler rather than silently. Install one +before you start, and read [error severity](../../error-severity.md) for what +each level is telling you — a `CRITICAL` at create time means the counter fell +back to the Null object. diff --git a/docs/platforms/windows.md b/docs/platforms/windows.md deleted file mode 100644 index b67d2adf..00000000 --- a/docs/platforms/windows.md +++ /dev/null @@ -1,30 +0,0 @@ -# Windows - -`Platform/Windows/` wraps the Win32 and Winsock APIs for MSVC targets -([Winsock documentation](https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-start-page-2)). - -Fills the Resolver, Datagram, Stream, File, Mutex and AtomicCounter -[roles](../roles/index.md), plus the clock / hostname / process-id / sleep -callbacks. - -## What it ships - -| Class | Role | -|---|---| -| [`SolidSyslogWinsockResolver`](../api/SolidSyslogWinsockResolver_8h.md) | resolver | -| [`SolidSyslogWinsockAddress`](../api/SolidSyslogWinsockAddress_8h.md) | address | -| [`SolidSyslogWinsockDatagram`](../api/SolidSyslogWinsockDatagram_8h.md) | UDP sender | -| [`SolidSyslogWinsockTcpStream`](../api/SolidSyslogWinsockTcpStream_8h.md) | TCP stream (non-blocking + `select`, bounded connect) | -| [`SolidSyslogWindowsFile`](../api/SolidSyslogWindowsFile_8h.md) | file | -| [`SolidSyslogWindowsMutex`](../api/SolidSyslogWindowsMutex_8h.md) | mutex (`CRITICAL_SECTION`) | -| [`SolidSyslogWindowsAtomicCounter`](../api/SolidSyslogWindowsAtomicCounter_8h.md) | atomic counter (`Interlocked`) | -| [`SolidSyslogWindowsClock`](../api/SolidSyslogWindowsClock_8h.md) | clock | -| [`SolidSyslogWindowsHostname`](../api/SolidSyslogWindowsHostname_8h.md) | hostname | -| [`SolidSyslogWindowsProcessId`](../api/SolidSyslogWindowsProcessId_8h.md) | process-id | -| [`SolidSyslogWindowsSleep`](../api/SolidSyslogWindowsSleep_8h.md) | sleep | -| [`SolidSyslogWindowsSysUpTime`](../api/SolidSyslogWindowsSysUpTime_8h.md) | uptime (`GetTickCount64`) | - -## Requirements - -The MSVC toolchain and Winsock — call `WSAStartup` once at process init before -creating a sender. diff --git a/docs/platforms/windows/index.md b/docs/platforms/windows/index.md new file mode 100644 index 00000000..cf15ca4d --- /dev/null +++ b/docs/platforms/windows/index.md @@ -0,0 +1,41 @@ +# Windows + +`Platform/Windows/` wraps the Win32 and Winsock APIs for MSVC targets +([Winsock documentation](https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-start-page-2)). + +Fills the Resolver, Datagram, Stream, File, Mutex and AtomicCounter +[roles](../../roles/index.md), plus the clock / hostname / process-id / sleep +callbacks. + +## What it ships + +## Requirements + +The MSVC toolchain and Winsock — call `WSAStartup` once at process init before +creating a sender. + +## Security behaviour and obligations + +### The transport carries syslog in clear + +Neither the datagram nor the TCP stream provides confidentiality, integrity or +peer authentication. TLS is a separate role filled by a different platform — the +[platform × capability matrix](../index.md) shows which — layered over this +stream rather than replacing it. + +### Winsock initialisation is yours + +`WSAStartup` must be called once at process start, before any sender is created, +and the matching `WSACleanup` is yours to place. The adapter does not initialise +Winsock, because a process that already uses sockets has done it. + +### Protection of the store is a property of its directory + +Where the store is written, and which accounts can reach it, are decided by the +directory you choose and the access control on it. The library sets no policy of +its own and checks none. + +### Host identity is only as good as the operating system's + +The hostname and process id are read from Windows and forwarded unmodified. The +library performs no independent check of either. diff --git a/docs/platforms/windows/setup.md b/docs/platforms/windows/setup.md new file mode 100644 index 00000000..61ae89b6 --- /dev/null +++ b/docs/platforms/windows/setup.md @@ -0,0 +1,69 @@ +# Windows setup + +Wiring the Win32 and Winsock adapters. [Windows](index.md) covers what they +fill and what they leave to you; the config fields are documented on the +structs themselves. + +## What to link + +Win32 and Winsock are stable system interfaces, so the adapters compile +straight into the static library and there is nothing extra to link: + +```cmake +set(SOLIDSYSLOG_PLATFORMS "Windows") +``` + +## Initialise Winsock first + +Winsock must be started before any sender is created, and stopped once every +socket is closed. The adapter does not do this for you: Winsock belongs to the +process, not to this library, and a process that already uses sockets has +started it for its own reasons. Startup and cleanup are reference-counted, so +match each successful `WSAStartup` with one `WSACleanup` and let the last one +out do the teardown: + +```c +WSADATA wsaData; +(void) WSAStartup(MAKEWORD(2, 2), &wsaData); +/* ... create, use and destroy your senders ... */ +(void) WSACleanup(); +``` + +## Wiring a sender + +```c +struct SolidSyslogResolver* resolver = SolidSyslogWinsockResolver_Create(); +struct SolidSyslogAddress* address = SolidSyslogWinsockAddress_Create(); +struct SolidSyslogStream* stream = SolidSyslogWinsockTcpStream_Create(NULL); + +static struct SolidSyslogStreamSenderConfig senderConfig; +senderConfig = (struct SolidSyslogStreamSenderConfig) {0}; +senderConfig.Resolver = resolver; +senderConfig.Stream = stream; +senderConfig.Address = address; +senderConfig.Endpoint = MyEndpoint; +struct SolidSyslogSender* sender = SolidSyslogStreamSender_Create(&senderConfig); +``` + +For UDP, build a `SolidSyslogWinsockDatagram` and a `SolidSyslogUdpSender` +instead. Tear down in reverse order, before `WSACleanup`. + +## The callbacks + +`SolidSyslogConfig` takes the clock, hostname and process id as callbacks, and +this platform supplies one of each: `SolidSyslogWindowsClock_GetTimestamp`, +`SolidSyslogWindowsHostname` and `SolidSyslogWindowsProcessId`. + +## Threading + +If your application logs from one thread and drains from another, put a +circular buffer between them with `SolidSyslogWindowsMutex` filling the mutex +role. This platform also fills the atomic counter role, so the sequence number +behind gap detection needs no separate component. + +## When it does not work + +Failures report through the error handler rather than silently. Install one +before you start, and read [error severity](../../error-severity.md) for what +each level is telling you — a `CRITICAL` at create time means the component +fell back to its Null object, and nothing will be delivered. diff --git a/docs/porting.md b/docs/porting.md index 0550c552..2725408a 100644 --- a/docs/porting.md +++ b/docs/porting.md @@ -2,7 +2,7 @@ Porting SolidSyslog to a new OS, network stack, filesystem, or crypto library is filling a role, not editing Core. Core never changes. You write a small -adapter that satisfies one of the twelve vtable contracts, drop it into your +adapter that satisfies one of the vtable contracts, drop it into your build, and wire it into your config. This page is the contract those adapters honour, written from the code that already ships. @@ -21,10 +21,9 @@ contract. [Talk to us about it](https://www.cososo.co.uk/?service=solidsyslog#co ## The role model Core is a fixed set of algorithms (the formatter/message pipeline, the Service -drain loop, the buffer/store machinery) plus twelve roles. A role is a +drain loop, the buffer/store machinery) plus a set of roles. A role is a `struct` of function pointers (a vtable) declared in a -`SolidSyslogDefinition.h` header under -[`Core/Interface/`](../Core/Interface/). An *adapter* is a concrete +`SolidSyslogDefinition.h` header under `Core/Interface/`. An *adapter* is a concrete implementation of one role for one platform (`SolidSyslogPosixMutex`, `SolidSyslogLwipRawDatagram`, …). @@ -35,15 +34,14 @@ running against a well-behaved do-nothing. So porting is additive: you provide the roles your deployment needs and leave the rest to their Nulls. You never edit Core, and you never touch a role you don't use. -The [capability matrix](build-integration.md#pick-your-stack--capability-matrix) -lists every role and the adapters that ship for it; this page is what you write -when none of the shipped adapters fits your platform. +The [platform × capability matrix](platforms/index.md) lists every role and the +adapters that ship for it; this page is what you write when none of the shipped +adapters fits your platform. ## Anatomy of an adapter -Take [`SolidSyslogPosixMutex`](../Platform/Posix/Source/SolidSyslogPosixMutex.c) -as the worked example: the simplest role, but the shape is identical for all -twelve. An adapter is four files: +Take `SolidSyslogPosixMutex` as the worked example: the simplest role, but the +shape is identical for every one. An adapter is four files: | File | Holds | |---|---| @@ -76,9 +74,8 @@ There is no heap. Each adapter owns a file-scope `static` array of instances and a parallel `InUse[]` flag array, sized by a role tunable. `_Create` acquires the first free slot, initialises it, and returns `&pool[i].Base`; on exhaustion it returns the shared Null sibling and reports an error. `_Destroy` finds the slot -by handle identity, cleans it up, and releases it. The -[`SolidSyslogPoolAllocator`](../Core/Source/SolidSyslogPoolAllocator.h) owns the -slot-walk so no adapter re-implements it: +by handle identity, cleans it up, and releases it. `SolidSyslogPoolAllocator` +owns the slot-walk so no adapter re-implements it: ```c static bool PosixMutex_InUse[SOLIDSYSLOG_MUTEX_POOL_SIZE]; @@ -105,22 +102,26 @@ struct SolidSyslogMutex* SolidSyslogPosixMutex_Create(void) } ``` -See [`SolidSyslogPosixMutexStatic.c`](../Platform/Posix/Source/SolidSyslogPosixMutexStatic.c) -for the matching `SolidSyslogPosixMutex_Destroy`. The pool size is a role-named tunable, +See `SolidSyslogPosixMutexStatic.c` for the matching +`SolidSyslogPosixMutex_Destroy`. The pool size is a role-named tunable, `SOLIDSYSLOG_MUTEX_POOL_SIZE`, not a per-platform name, because a build links one implementation per role. Every tunable lives in -[`SolidSyslogTunablesDefaults.h`](../Core/Interface/SolidSyslogTunablesDefaults.h), +[`SolidSyslogTunablesDefaults.h`](api/SolidSyslogTunablesDefaults_8h.md), `#ifndef`-guarded so integrators override without editing the library. ### Error reporting — the `*Errors.h` convention Each adapter ships a `SolidSyslogErrors.h` declaring an -`enum SolidSyslogErrors` (`_ERROR_*` codes plus an -`_ERROR_MAX` bookend) and an `extern const struct SolidSyslogErrorSource`. When +`enum SolidSyslogErrors` (`SOLIDSYSLOG__ERROR_*` codes plus a +`SOLIDSYSLOG__ERROR_MAX` bookend) and an +`extern const struct SolidSyslogErrorSource`. How the class name is spelled +inside those constants — one word per PascalCase word, except that your pack's +registry token stays whole — is in +[Naming conventions](NAMING.md#spelling-a-class-name-inside-a-screaming_snake-identifier). When something fails, the adapter calls `SolidSyslog_Error(severity, source, category, detail)`: `source` is its own `ErrorSource` (matched by pointer identity in a handler), `category` is a portable reaction axis from -[`SolidSyslogErrorCategory.h`](../Core/Interface/SolidSyslogErrorCategory.h), and +[`SolidSyslogErrorCategory.h`](api/SolidSyslogErrorCategory_8h.md), and `detail` is the adapter's own enum value. A handler that doesn't care about your adapter simply never matches its source. The default handler is a silent no-op: adapters report and carry on, they never crash the caller. @@ -144,13 +145,14 @@ This is the only synchronisation primitive the pools use for their own walks. double-free if `Close` and `Destroy` are both called. Release each resource exactly once and null the handle. - Never free injected handles. An adapter frees only what it created. Handles - the integrator passed in (an `mbedtls_x509_crt*`, an RNG, a caller's socket) - are borrowed; the owner frees them. The [Mbed TLS coexistence - contract](integrating-mbedtls.md#coexistence-contract) is the template: - `Platform/MbedTls/Source/` never touches process-global Mbed TLS state. -- A Null must be safe to call. Whatever your role's Null returns (see each - contract below), it must let Core's algorithm proceed sanely: drop-on-the-floor - where a drop is harmless, `false` where the caller has an error path to run. + the integrator passed in (a certificate, an RNG, a caller's socket) are + borrowed; the owner frees them. The same applies to an upstream library's + process-global state: touch only what you were given, so the library drops + into a process already using that upstream elsewhere. +- A Null must be safe to call. Whatever your role's Null returns — each is + documented on its own `SolidSyslogNull.h` — it must let Core's algorithm + proceed sanely: drop-on-the-floor where a drop is harmless, `false` where the + caller has an error path to run. - Bounded blocking. Anything that can wedge (a `connect`, a handshake) is bounded by an explicit timeout or deadline: a timeout tunable (e.g. `SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS`) or a caller-supplied deadline. A @@ -170,8 +172,9 @@ configuration macro — lwIP's `LWIP_DNS`, FreeRTOS's **Prefer a seam.** Where the adapter is thin, take the dependency as an injected function pointer with a safe default and never name the upstream symbol. `SolidSyslogLwipRaw_SetMarshal` covers `NO_SYS=0` against `NO_SYS=1` this way: -the library calls a callback, and the integrator installs `LOCK_TCPIP_CORE` or a -`tcpip_callback_with_block` shim. Nothing to select at build time. +the library calls a callback, and the integrator installs a `LOCK_TCPIP_CORE` +pair, or a mailbox shim that waits for the callback to run. Nothing to select at +build time. **Otherwise gate the translation unit.** Where the adapter carries logic that belongs in the library — the DNS resolver's async callback handling, poll @@ -236,12 +239,12 @@ product and one further state, absent, which has no behaviour to test. [manifest](build-integration.md#ide-and-manifest-builds) generator lists the exact files for a chosen set of platforms. -## The twelve role contracts +## The role contracts -Each row is a vtable to implement. The reference column is the shipped -implementation to read alongside the contract: a `Platform/Posix/` adapter where -one exists, otherwise the Core composition over a lower role. The Null column -is the fallback Core substitutes when the role is unfilled. +Each entry is a vtable to implement, linked to its contract: every method, what +it must return, and what Core does with the answer. Each contract page also +diagrams the adapters that already fill that role, so it is where to find an +implementation to read alongside. ### Networking @@ -249,17 +252,10 @@ Most network ports implement Stream (TCP / TLS byte transport) and Datagram (UDP); `Sender` is a Core composition over them, so you rarely write one directly. -| Role | Contract | Vtable | Null fallback | Reference | -|---|---|---|---|---| -| **Resolver** | [`ResolverDefinition.h`](../Core/Interface/SolidSyslogResolverDefinition.h) | `Resolve(transport, host, port, *out)` | `Resolve` → `false`, so the caller's unresolved-host error path runs | [`PosixResolver.c`](../Platform/Posix/Source/SolidSyslogPosixResolver.c) | -| **Datagram** | [`DatagramDefinition.h`](../Core/Interface/SolidSyslogDatagramDefinition.h) | `Open` · `SendTo(...)→SendResult` · `MaxPayload` · `Close` | Open/Close no-op, `SendTo` → `SENT` (drop), `MaxPayload` → IPv6-safe default | [`PosixDatagram.c`](../Platform/Posix/Source/SolidSyslogPosixDatagram.c) | -| **Stream** | [`StreamDefinition.h`](../Core/Interface/SolidSyslogStreamDefinition.h) | `Open(addr)` · `Send` · `Read` · `Close` | Open/Close no-op, `Send` → `true` (drop), `Read` → `0` (would-block, no teardown) | [`PosixTcpStream.c`](../Platform/Posix/Source/SolidSyslogPosixTcpStream.c) | -| **Sender** | [`SenderDefinition.h`](../Core/Interface/SolidSyslogSenderDefinition.h) | `Send` · `Disconnect` | `Send` → `true` (drop), `Disconnect` no-op | [`StreamSender.c`](../Core/Source/SolidSyslogStreamSender.c) · [`UdpSender.c`](../Core/Source/SolidSyslogUdpSender.c) | - -`SendTo` returns a three-way `enum SolidSyslogDatagramSendResult` (not a bool) so -the sender can distinguish a would-block from a hard failure. A `Stream` owns its -connect/keepalive lifecycle; `Read` returns `0` for would-block and a negative -`SolidSyslogSsize` only for a real teardown. +- [Resolver](api/structSolidSyslogResolver.md) +- [Datagram](api/structSolidSyslogDatagram.md) +- [Stream](api/structSolidSyslogStream.md) +- [Sender](api/structSolidSyslogSender.md) ### Storage @@ -267,46 +263,32 @@ The store-and-forward stack is layered: Store (Core `BlockStore`) sits over BlockDevice, which sits over File. On a new platform you usually implement only File (and BlockDevice for raw flash); the rest is Core. -| Role | Contract | Vtable | Null fallback | Reference | -|---|---|---|---|---| -| **Store** | [`StoreDefinition.h`](../Core/Interface/SolidSyslogStoreDefinition.h) | `Write` · `ReadNextUnsent` · `MarkSent` · `HasUnsent` · `IsHalted` · `GetTotalBytes` · `GetUsedBytes` · `IsTransient` | No store-and-forward; `IsTransient` → `true` so a rejected `Write` falls through to the sender | [`BlockStore.c`](../Core/Source/SolidSyslogBlockStore.c) | -| **BlockDevice** | [`BlockDeviceDefinition.h`](../Core/Interface/SolidSyslogBlockDeviceDefinition.h) | `Acquire` · `Dispose` · `Exists` · `Read` · `Append` · `WriteAt` · `Size(block)` · `GetBlockSize` | Every method `false` / `0` — no disk | [`FileBlockDevice.c`](../Core/Source/SolidSyslogFileBlockDevice.c) | -| **File** | [`FileDefinition.h`](../Core/Interface/SolidSyslogFileDefinition.h) | `Open` · `Close` · `IsOpen` · `Read` · `Write` · `SeekTo` · `Size` · `Truncate` · `Exists` · `Delete` | Reads / `Exists` → `false`, `Write` / `Delete` → `true`, `Size` → `0`, seek/truncate/close no-op | [`PosixFile.c`](../Platform/Posix/Source/SolidSyslogPosixFile.c) | -| **Buffer** | [`BufferDefinition.h`](../Core/Interface/SolidSyslogBufferDefinition.h) | `Write` · `Read` | `Read` → `false` (empty), `Write` swallows | [`CircularBuffer.c`](../Core/Source/SolidSyslogCircularBuffer.c) · [`PassthroughBuffer.c`](../Core/Source/SolidSyslogPassthroughBuffer.c) | +- [Store](api/structSolidSyslogStore.md) +- [BlockDevice](api/structSolidSyslogBlockDevice.md) +- [File](api/structSolidSyslogFile.md) +- [Buffer](api/structSolidSyslogBuffer.md) -`Store.IsTransient` is the crucial hint: a *transient* store (like Null) never -retained the record, so Service may try the sender directly; a real store's `Write` -rejection is the discard policy speaking, and Service must not let a newer -record jump the queue past older ones. The portable in-memory `CircularBuffer` -takes an injected `Mutex`, so a `Buffer` port is often just a `Mutex` port. +The portable in-memory `CircularBuffer` takes an injected `Mutex`, so a `Buffer` +port is often just a `Mutex` port. ### OS primitives -| Role | Contract | Vtable | Null fallback | Reference | -|---|---|---|---|---| -| **Mutex** | [`MutexDefinition.h`](../Core/Interface/SolidSyslogMutexDefinition.h) | `Lock` · `Unlock` | No-op (single-task) | [`PosixMutex.c`](../Platform/Posix/Source/SolidSyslogPosixMutex.c) | -| **AtomicCounter** | [`AtomicCounterDefinition.h`](../Core/Interface/SolidSyslogAtomicCounterDefinition.h) | `Increment` — wrap-aware in `[1, 2³¹−1]`, never returns `0` (RFC 5424 §7.3.1) | `Increment` → `1` unconditionally | [`StdAtomicCounter.c`](../Platform/StdAtomic/Source/SolidSyslogStdAtomicCounter.c) | +- [Mutex](api/structSolidSyslogMutex.md) +- [AtomicCounter](api/structSolidSyslogAtomicCounter.md) ### Evidence and integrity -| Role | Contract | Vtable | Null fallback | Reference | -|---|---|---|---|---| -| **StructuredData** | [`StructuredDataDefinition.h`](../Core/Interface/SolidSyslogStructuredDataDefinition.h) | `Format(element)` — write one `[SD-ID …]` via the `SolidSyslogSdElement` sink | No-op (element omitted) | [`MetaSd.c`](../Core/Source/SolidSyslogMetaSd.c) | -| **SecurityPolicy** | [`SecurityPolicyDefinition.h`](../Core/Interface/SolidSyslogSecurityPolicyDefinition.h) | `TrailerSize` + `SealRecord` · `OpenRecord` over a `SolidSyslogSecurityRecord` | No integrity check; `TrailerSize` `0`, seal/open pass through | [`Crc16Policy.c`](../Core/Source/SolidSyslogCrc16Policy.c) | - -A `StructuredData.Format` writes through the opaque `SolidSyslogSdElement` sink; -it owns the brackets, the `@`-enterprise SD-ID suffix, and the escaping, so a -producer cannot break the RFC 5424 framing. A `SecurityPolicy` is handed a -`SolidSyslogSecurityRecord` split into a cleartext header (associated data) and a -body. A keyed MAC policy authenticates the whole span (tamper-evident); a -checksum policy such as the vendor-free `Crc16Policy` covers the same span but -only detects accidental corruption, not an attacker; an AEAD policy encrypts the -body in place and writes its `TrailerSize`-byte trailer. `Crc16Policy` is the -reference to read first. +- [StructuredData](api/structSolidSyslogStructuredData.md) +- [SecurityPolicy](api/structSolidSyslogSecurityPolicy.md) + +A `SecurityPolicy` is the one role where the choice is a security decision, not +a portability one: a keyed MAC is tamper-evident, a checksum such as the +vendor-free [Crc16Policy](api/SolidSyslogCrc16Policy_8h.md) detects accidental +corruption but not an attacker, and an AEAD encrypts as well as authenticates. ## Where to go next - [Adding it to your build](build-integration.md): the capability matrix, tunables, and build wiring. -- [Integrating with lwIP (Raw API)](integrating-lwip.md), [Mbed TLS](integrating-mbedtls.md), [FreeRTOS-Plus-FAT](integrating-plusfat.md): worked ports of the networking, TLS, and file roles. +- [Integrating with lwIP (Raw API)](platforms/lwipraw/setup.md), [Mbed TLS](platforms/mbedtls/setup.md), [FreeRTOS-Plus-FAT](platforms/plusfat/setup.md): worked ports of the networking, TLS, and file roles. - [Naming conventions](NAMING.md) and [MISRA deviations](misra-deviations.md): the rules Tier 1/2 adapter code follows. - [Error-event severity policy](error-severity.md): choosing the severity for your adapter's reports. diff --git a/docs/rfc-compliance.md b/docs/rfc-compliance.md index 002cd69b..3e7d1958 100644 --- a/docs/rfc-compliance.md +++ b/docs/rfc-compliance.md @@ -9,29 +9,31 @@ Status key: - Supported: implemented and tested - Partial: implemented with known limitations - Planned: tracked in an issue or epic -- N/A: not applicable to a sender implementation +- N/A: not applicable to a sender implementation, or applicable and deliberately + excluded — the note says which, and why ## RFC 5424 — The Syslog Protocol | Section | Requirement | Status | Notes | |---|---|---|---| -| 6.1 | PRI — facility * 8 + severity | Supported | Invalid values fall back to `syslog.err` (facility 5, severity 3) | -| 6.2.1 | VERSION = 1 | Supported | | -| 6.2.2 | TIMESTAMP — ISO 8601 with microseconds | Supported | 6-digit fractional seconds, UTC offset or `Z` | -| 6.2.2 | TIMESTAMP — NILVALUE when clock unavailable | Supported | NilClock produces `-` | -| 6.2.3 | HOSTNAME — max 255 chars, PRINTUSASCII | Supported | Truncated to 255. Non-PRINTUSASCII bytes substituted with `?`. Written through the public `SolidSyslogHeaderField` sink (`SolidSyslogHeaderField_PrintUsAscii`); the underlying formatter is library-private | -| 6.2.4 | APP-NAME — max 48 chars, PRINTUSASCII | Supported | Truncated to 48. Non-PRINTUSASCII bytes substituted with `?` | -| 6.2.5 | PROCID — max 128 chars, PRINTUSASCII | Supported | Truncated to 128. Non-PRINTUSASCII bytes substituted with `?` | -| 6.2.6 | MSGID — max 32 chars, PRINTUSASCII | Supported | Truncated to 32. Non-PRINTUSASCII bytes substituted with `?` | +| 6.2.1 | PRI — facility * 8 + severity | Supported | Invalid values fall back to `syslog.err` (facility 5, severity 3) | +| 6.2.2 | VERSION = 1 | Supported | | +| 6.2.3 | TIMESTAMP — ISO 8601 with microseconds | Supported | 6-digit fractional seconds, UTC offset or `Z` | +| 6.2.3 | TIMESTAMP — NILVALUE when clock unavailable | Supported | NilClock produces `-` | +| 6.2.4 | HOSTNAME — max 255 chars, PRINTUSASCII | Supported | Truncated to 255. Non-PRINTUSASCII bytes substituted with `?`. Written through the public `SolidSyslogHeaderField` sink (`SolidSyslogHeaderField_PrintUsAscii`); the underlying formatter is library-private | +| 6.2.5 | APP-NAME — max 48 chars, PRINTUSASCII | Supported | Truncated to 48. Non-PRINTUSASCII bytes substituted with `?` | +| 6.2.6 | PROCID — max 128 chars, PRINTUSASCII | Supported | Truncated to 128. Non-PRINTUSASCII bytes substituted with `?` | +| 6.2.7 | MSGID — max 32 chars, PRINTUSASCII | Supported | Truncated to 32. Non-PRINTUSASCII bytes substituted with `?` | | 6.3 | STRUCTURED-DATA — SD-ELEMENTs or NILVALUE | Supported | Extensible via `SolidSyslogStructuredData` vtable | -| 6.3.3 | SD-PARAM value escaping (`]`, `\`, `"`) | Supported | `SolidSyslogSdValue` — every SD-PARAM value is written through this sink, which applies the escaping: RFC 3629 UTF-8 validated, ill-formed input substituted per-byte with U+FFFD (Unicode §3.9). `OriginSd` streams software, swVersion, enterpriseId, and each ip into it; `MetaSd` streams language via the integrator's `SolidSyslogSdValueFunction` callback. Both get the same escaping. SD-NAME / SD-ID syntax validation only matters once callers can supply names, via Custom Structured Data; the standard SDs (meta / timeQuality / origin) use compile-time-constant names | -| 6.3.3 | timeQuality SD — tzKnown, isSynced, syncAccuracy | Supported | `SolidSyslogTimeQualitySd` | -| 6.3.4, 7.2 | origin SD — software, swVersion, enterpriseId, ip | Supported | `SolidSyslogOriginSd` covers all four §7.2 parameters. `software`, `swVersion`, and `enterpriseId` are static strings supplied via `SolidSyslogOriginSdConfig`; the config strings are borrowed for the SD's lifetime and each is escaped per §6.3.3 by the `SolidSyslogSdValue` writer it is streamed into at Format time (no pre-formatted scratch storage). `ip` is repeatable per RFC 5424 §7.2 and sourced via two callbacks (`SolidSyslogOriginIpCountFunction`, `SolidSyslogOriginIpAtFunction`) so multi-homed hosts can reflect runtime address changes; the library asks for a count then loops 0..N-1, opening an `ip` param per token (with a leading space) while the integrator's at-callback writes one IP value per call into the `SolidSyslogSdValue` it is handed, which applies the escaping. All four parameters are independently optional — a NULL field or NULL callback omits the corresponding parameter from the SD-ELEMENT. The library frames and escapes; the IP value length is the integrator's to bound (ultimately by `SOLIDSYSLOG_MAX_MESSAGE_SIZE`), as is the IP count. Bare `[origin]` with no parameters is RFC-legal (§7.2 marks all params OPTIONAL, no SHOULD enforcement) and is what the library emits when the integrator wires nothing | -| 6.3.5, 7.3 | meta SD — sequenceId, sysUpTime, language | Supported | `SolidSyslogMetaSd` covers all three IANA-registered parameters. `sequenceId` (§7.3.1) sourced via an injected `SolidSyslogAtomicCounter`. `sysUpTime` (§7.3.2 / RFC 3418 `TimeTicks`) sourced via a `SolidSyslogSysUpTimeFunction` callback returning `uint32_t` hundredths; reference platform integrations are `SolidSyslogPosixSysUpTime` (`clock_gettime(CLOCK_BOOTTIME)`) and `SolidSyslogWindowsSysUpTime` (`GetTickCount64`), with the cast to `uint32_t` providing RFC 3418's natural wrap. `language` (§7.3.3 / BCP 47) sourced via a `SolidSyslogSdValueFunction` callback streaming into a `SolidSyslogSdValue`, which applies SD-PARAM-VALUE escaping per §6.3.3. All three independently optional — a NULL field in `SolidSyslogMetaSdConfig` omits that parameter from the SD-ELEMENT | -| 6.3.5, 7.3.1 | meta SD — sequenceId wraps at 2147483647 to 1 | Supported | `SolidSyslogAtomicCounter` wraps via CAS-loop in [1, 2³¹ - 1]; never returns 0; never above max. AtomicCounter is a vtable abstraction — concrete impls are `SolidSyslogStdAtomicCounter` (C11 `` + `atomic_compare_exchange_strong_explicit`) on POSIX/clang/gcc/modern MSVC, and `SolidSyslogWindowsAtomicCounter` (`volatile LONG` + `InterlockedCompareExchange`) on legacy MSVC. The integrator picks one at setup time by calling the relevant platform's `_Create`; CMake's `HAVE_STDATOMIC_H` / `HAVE_WINDOWS_INTERLOCKED` checks gate which platform sources are compiled. sequenceId is assigned at the point of message raise (application-layer originator), preserving end-to-end loss-detection across the internal buffer / store-and-forward / transport pipeline. Trade-off: under concurrent raise from multiple threads, a small reorder window may occur in transmitted IDs (adjacent IDs may invert, since buffer/transport scheduling between raise and wire is not under library control). All IDs remain unique and non-zero — SIEMs performing gap detection identify message loss correctly; SIEMs requiring strict monotonic ordering should sort by timestamp | +| 6.3.2 | SD-ID / SD-NAME syntax validation | Planned | Not performed. It only bites once callers can supply their own names: the three standard SDs (meta / timeQuality / origin) use compile-time-constant names that are valid by construction. Tracked under Custom Structured Data (`#64`), which is what introduces caller-supplied SD-IDs and PARAM names | +| 6.3.3 | SD-PARAM value escaping (`]`, `\`, `"`) | Supported | `SolidSyslogSdValue` — every SD-PARAM value is written through this sink, which applies the escaping: RFC 3629 UTF-8 validated, ill-formed input substituted per-byte with U+FFFD (Unicode §3.9). `OriginSd` streams software, swVersion, enterpriseId, and each ip into it; `MetaSd` streams language via the integrator's `SolidSyslogSdValueFunction` callback. Both get the same escaping. | +| 7.1 | timeQuality SD — tzKnown, isSynced, syncAccuracy | Supported | `SolidSyslogTimeQualitySd` | +| 7.2 | origin SD — software, swVersion, enterpriseId, ip | Supported | `SolidSyslogOriginSd` covers all four §7.2 parameters. `software`, `swVersion`, and `enterpriseId` are static strings supplied via `SolidSyslogOriginSdConfig`; the config strings are borrowed for the SD's lifetime and each is escaped per §6.3.3 by the `SolidSyslogSdValue` writer it is streamed into at Format time (no pre-formatted scratch storage). `ip` is repeatable per RFC 5424 §7.2 and sourced via two callbacks (`SolidSyslogOriginIpCountFunction`, `SolidSyslogOriginIpAtFunction`) so multi-homed hosts can reflect runtime address changes; the library asks for a count then loops 0 to N-1, opening an `ip` param per token (with a leading space) while the integrator's at-callback writes one IP value per call into the `SolidSyslogSdValue` it is handed, which applies the escaping. All four parameters are independently optional — a NULL field or NULL callback omits the corresponding parameter from the SD-ELEMENT. The library frames and escapes; the IP value length is the integrator's to bound (ultimately by `SOLIDSYSLOG_MAX_MESSAGE_SIZE`), as is the IP count. Bare `[origin]` with no parameters is RFC-legal (§7.2 marks all params OPTIONAL, no SHOULD enforcement) and is what the library emits when the integrator wires nothing | +| 7.3 | meta SD — sequenceId, sysUpTime, language | Supported | `SolidSyslogMetaSd` covers all three IANA-registered parameters. `sequenceId` (§7.3.1) sourced via an injected `SolidSyslogAtomicCounter`. `sysUpTime` (§7.3.2 / RFC 3418 `TimeTicks`) sourced via a `SolidSyslogSysUpTimeFunction` callback returning `uint32_t` hundredths, the type giving RFC 3418's natural wrap; the [capability matrix](platforms/index.md) shows which platforms supply one. `language` (§7.3.3 / BCP 47) sourced via a `SolidSyslogSdValueFunction` callback streaming into a `SolidSyslogSdValue`, which applies SD-PARAM-VALUE escaping per §6.3.3. `sysUpTime` and `language` are independently optional — a NULL field in `SolidSyslogMetaSdConfig` omits that parameter. The counter is not: `SolidSyslogMetaSd_Create` rejects a NULL `Counter` with a `WARNING` and returns the Null structured data, so the element is not emitted at all | +| 7.3.1 | meta SD — sequenceId wraps at 2147483647 to 1 | Partial | `SolidSyslogAtomicCounter` wraps via CAS-loop in [1, 2³¹ - 1]; never returns 0; never above max. [AtomicCounter](api/structSolidSyslogAtomicCounter.md) is a vtable abstraction, so the wrap is the contract's and not any one implementation's; the integrator wires a concrete counter at setup time and the [capability matrix](platforms/index.md) shows which platforms supply one. sequenceId is assigned at the point of message raise (application-layer originator), preserving end-to-end loss-detection across the internal buffer / store-and-forward / transport pipeline. Trade-off: under concurrent raise from multiple threads, a small reorder window may occur in transmitted IDs (adjacent IDs may invert, since buffer/transport scheduling between raise and wire is not under library control). IDs from a wired counter remain unique and non-zero — SIEMs performing gap detection identify message loss correctly; SIEMs requiring strict monotonic ordering should sort by timestamp. Uniqueness is the counter's, not the contract's: exhaust a counter's pool and `Create` falls back to the Null counter, which returns 1 for every record, so gap detection stops being meaningful while logging continues | | 6.4 | MSG — UTF-8 preferred | Supported | RFC 3629 UTF-8 validated at the formatter primitives (`SolidSyslogFormatter_BoundedString`), with ill-formed input substituted per-byte with U+FFFD (Unicode §3.9). MSG is prefixed with the §6.4 UTF-8 BOM (`%xEF.BB.BF`) unconditionally; if the caller's body already begins with a BOM it is stripped so the wire frame contains exactly one. Truncation preserves codepoint boundaries at both layers: the formatter clips at `SOLIDSYSLOG_MAX_MESSAGE_SIZE` without splitting a codepoint, and on UDP the sender walks back over any partial codepoint when the kernel reports `EMSGSIZE` for the path MTU. TCP/TLS streams fragment transparently at the transport layer and so do not need a path-MTU trim | -| 8.1 | Message size — max 2048 recommended | Supported | Default `SOLIDSYSLOG_MAX_MESSAGE_SIZE` = 2048, matching the §8.1 SHOULD value; override it for memory-constrained MCUs via the standard tunable mechanism | -| 9 | PRINTUSASCII in header fields (codes 33-126) | Supported | Non-compliant bytes substituted with `?` at format time (HOSTNAME, APP-NAME, PROCID, MSGID) | +| 6.1 | Message size — max 2048 recommended | Supported | Default `SOLIDSYSLOG_MAX_MESSAGE_SIZE` = 2048, matching the largest message §6.1 says a transport receiver SHOULD accept; override it for memory-constrained MCUs via the standard tunable mechanism | +| 6 | PRINTUSASCII in header fields (codes 33-126) | Supported | Non-compliant bytes substituted with `?` at format time (HOSTNAME, APP-NAME, PROCID, MSGID) | ## RFC 5426 — Transmission of Syslog Messages over UDP @@ -40,7 +42,7 @@ Status key: | 3.1 | One message per UDP datagram | Supported | `SolidSyslogUdpSender` sends one datagram per `Send` call | | 3.2 | Default port 514 | Supported | `SOLIDSYSLOG_UDP_DEFAULT_PORT` = 514 | | 3.2 | Message fits in single datagram | Supported | Bounded by `SOLIDSYSLOG_MAX_MESSAGE_SIZE` | -| 3.2 | Avoid IP fragmentation (respect MTU) | Supported | `SolidSyslogUdpSender` lazily connects on first send, enables Linux `IP_MTU_DISCOVER` / Windows equivalent with `IP_PMTUDISC_DO` so the kernel returns `EMSGSIZE` (Winsock `WSAEMSGSIZE`) for path-MTU oversize, queries the path MTU via `getsockopt(IP_MTU)`, and resends a UTF-8-safe trimmed datagram via `SolidSyslogUdpPayload_TrimToCodepointBoundary`. Falls back to `SOLIDSYSLOG_UDP_IPV6_SAFE_PAYLOAD = 1232` (RFC 8200 §5) when the MTU lookup fails | +| 3.2 | Avoid IP fragmentation (respect MTU) | Supported | The [Datagram](api/structSolidSyslogDatagram.md) contract carries this: an oversize payload is rejected rather than fragmented, and reported distinctly from a hard failure. `SolidSyslogUdpSender` responds by asking the Datagram for the path's largest payload and resending a UTF-8-safe trimmed datagram via `SolidSyslogUdpPayload_TrimToCodepointBoundary`, falling back to `SOLIDSYSLOG_UDP_IPV6_SAFE_PAYLOAD = 1232` (RFC 8200 §5) where no path MTU is available. How a platform discovers the MTU is on its own page | | 3.3 | Unreliable delivery — no confirmation | N/A | Inherent in UDP. Caller should be aware | | 4 | No authentication/integrity/confidentiality | N/A | Use TLS transport for security | @@ -51,31 +53,35 @@ Status key: | 3.2 | Sender initiates TCP connection | Supported | `SolidSyslogStreamSender` connects lazily on first send | | 3.2 | Default port 601 | Supported | `SOLIDSYSLOG_TCP_DEFAULT_PORT = 601` per IANA assignment (defined in `Core/Interface/SolidSyslogTransport.h`) | | 3.4.1 | Octet counting framing | Supported | `MSG-LEN SP MSG` prefix on every send | -| 3.4.2 | Non-transparent framing (LF trailer) | N/A | RFC 6587 octet counting (§3.4.1) is the recommended method and is what the library ships; non-transparent framing is the legacy alternative | +| 3.4.2 | Non-transparent framing (LF trailer) | N/A | Deliberately not implemented. RFC 6587 is Historic and describes both framings without recommending either, but §3.4 records that non-transparent framing has known problems and that octet counting does not; §3.4.1 is also the framing RFC 5425 mandates for TLS, so the library ships that alone rather than a mode selector | | 3.5 | Session closure handling | Supported | On send failure the stream is closed; the next Send transparently reconnects | | 3.5 | Handle receiver-initiated close | Supported | Detected via send failure path — same reconnect-on-next-Send mechanism | | 3.5 | Address rotation without app restart | Supported | App bumps `endpointVersion`; sender Disconnects and reconnects on next Send | -| — | Partial write handling (send returns short) | Supported | A short return from `send()` is treated as failure: `Send` returns false, the caller closes and reconnects on the next attempt, store-and-forward replays the message on the fresh socket. The TCP socket is non-blocking from the moment it is created, so `Send` never blocks the service thread on a wedged peer or a full kernel send buffer — `EAGAIN`/`EWOULDBLOCK` (POSIX) and `WSAEWOULDBLOCK` (Winsock) propagate immediately as failure. On POSIX, `EINTR` is the only retried errno (portability shim for kernels without `SA_RESTART`); Winsock has no signal-interruption semantics on `send()`, so the EINTR retry path is omitted. Long-term wedge is detected out-of-band via kernel TCP keepalive (`TCP_KEEPIDLE` / `TCP_KEEPINTVL` / `TCP_KEEPCNT`) and `TCP_USER_TIMEOUT` for pending-write death; both are configured at socket open. Bounded connect is via non-blocking `connect()` + `select()`, with the timeout sourced from `SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS` (default 200 ms) or runtime-overridable via the per-Stream `GetConnectTimeoutMs(ConnectTimeoutContext)` accessor — see `Platform/Posix/Source/SolidSyslogPosixTcpStream.c` and `Platform/Windows/Source/SolidSyslogWinsockTcpStream.c`. | +| — | Partial write handling (send returns short) | Supported | The [Stream](api/structSolidSyslogStream.md) contract makes `Send` all-or-nothing: a short write is a failure, never a partial success, so the stream closes itself, the sender reconnects on its next pass, and store-and-forward replays the message on the fresh connection. The same contract keeps steady-state `Send` and `Read` non-blocking and bounds `Open`, so a wedged peer or a full send buffer cannot stall the servicing pass. The connect bound is `SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS` (default 200 ms), overridable at runtime through the per-Stream `GetConnectTimeoutMs(ConnectTimeoutContext)` accessor. How a transport detects a long-term wedge, and what it does about one, is on its own page | ## RFC 5425 — TLS Transport Mapping for Syslog -The library ships two reference TLS adapters that satisfy this RFC: `SolidSyslogOpenSslStream` (OpenSSL, the POSIX / Windows reference) and `SolidSyslogMbedTlsStream` (Mbed TLS, the embedded / FreeRTOS reference). Both implement the same `SolidSyslogStream` vtable, so the section-by-section requirements below apply to whichever the integrator selects. mbedTLS-specific integration guidance lives in [`docs/integrating-mbedtls.md`](integrating-mbedtls.md). +TLS is a [Stream](api/structSolidSyslogStream.md) wrapped around another Stream, +so these requirements are met by whichever TLS stream the integrator wires; the +[capability matrix](platforms/index.md) shows which platforms supply one. What +an adapter validates, what it leaves to you, and how credentials reach it are +stated on that platform's own page. | Section | Requirement | Status | Notes | |---|---|---|---| -| 4.1 | TLS over TCP | Supported | `SolidSyslogOpenSslStream` (OpenSSL) or `SolidSyslogMbedTlsStream` (Mbed TLS) wraps a TCP `Stream` (`SolidSyslogPosixTcpStream` / `SolidSyslogWinsockTcpStream` / `SolidSyslogPlusTcpTcpStream` / `SolidSyslogLwipRawTcpStream` / caller-supplied) | +| 4.1 | TLS over TCP | Supported | A TLS `Stream` wraps a byte-transport `Stream` — a TCP one from any platform, or a caller-supplied one | | 4.2 | Default port 6514 | Supported | `SOLIDSYSLOG_TLS_DEFAULT_PORT` constant in `SolidSyslogTransport.h`, alongside the UDP and TCP defaults. Caller-supplied via the endpoint callback so multi-port deployments can override | -| 5.1 | Server certificate validation | Supported | OpenSSL adapter: `SSL_VERIFY_PEER` + `SSL_CTX_load_verify_locations` + `SSL_set1_host`. Mbed TLS adapter: `MBEDTLS_SSL_VERIFY_REQUIRED` + `mbedtls_ssl_conf_ca_chain` against the caller's `mbedtls_x509_crt*` + `mbedtls_ssl_set_hostname` | -| 5.2 | Mutual TLS (client certificate) | Supported | OpenSSL: optional `ClientCertChainPath` / `ClientKeyPath` on `SolidSyslogOpenSslStreamConfig`; `SSL_CTX_check_private_key` confirms pairing. Mbed TLS: optional `ClientCertChain` / `ClientKey` handles on `SolidSyslogMbedTlsStreamConfig`; `mbedtls_ssl_conf_own_cert` | -| 5.3 | TLS 1.2+ cipher suites | Supported | OpenSSL: `SSL_CTX_set_min_proto_version(TLS1_2_VERSION)` pinned; caller-supplied `CipherList` via `SSL_CTX_set_cipher_list`. Mbed TLS: inherited from `mbedtls_ssl_config_defaults(... PRESET_DEFAULT)`; cipher policy owned by the integrator's `mbedtls_config.h` | -| 5.4 | Octet counting framing (mandatory for TLS) | Supported | Reuses `SolidSyslogStreamSender` (RFC 6587 framing is identical) for both adapters | -| 5.5 | TLS close_notify handling | Supported | OpenSSL: `SSL_shutdown` in `OpenSslStream_Close`. Mbed TLS: `mbedtls_ssl_close_notify` in `MbedTlsStream_Close` | +| 5.1 | Server certificate validation | Supported | Peer verification is required, not optional: the certificate must chain to the trust anchors the caller supplies, and the server identity is checked against it. What an adapter does when no identity is given — and whether it says so — is on its page | +| 5.2 | Mutual TLS (client certificate) | Supported | A client certificate and its key are optional config on the TLS stream, and are presented only when both are given. Whether an adapter validates the pair locally, and what a half-supplied credential does, is on its page | +| 5.3 | TLS 1.2+ cipher suites | Supported | The floor is pinned to TLS 1.2 by the adapter rather than inherited from the TLS library's defaults, so a permissive build cannot negotiate below it. Cipher selection within that floor is the integrator's | +| 5.4 | Octet counting framing (mandatory for TLS) | Supported | Reuses `SolidSyslogStreamSender` — RFC 6587 framing is identical | +| 5.5 | TLS close_notify handling | Supported | Close sends `close_notify` before tearing the connection down | ## Summary | RFC | Total requirements | Supported | Partial | Planned | N/A | |---|---|---|---|---|---| -| RFC 5424 | 17 | 17 | 0 | 0 | 0 | +| RFC 5424 | 18 | 16 | 1 | 1 | 0 | | RFC 5426 | 6 | 4 | 0 | 0 | 2 | | RFC 6587 | 8 | 7 | 0 | 0 | 1 | | RFC 5425 | 7 | 7 | 0 | 0 | 0 | diff --git a/docs/security/at-rest-cryptography.md b/docs/security/at-rest-cryptography.md index 86593a8b..5598de89 100644 --- a/docs/security/at-rest-cryptography.md +++ b/docs/security/at-rest-cryptography.md @@ -43,7 +43,7 @@ trailer is `nonce (12) ‖ tag (16)`. systematic reuse a reset counter would — provided each new DRBG instance is seeded from fresh entropy. On the Mbed TLS path that guarantee is yours: a CTR-DRBG re-seeded from a repeating source reproduces its output, and so reproduces nonces - under the same key. [Integrating Mbed TLS](../integrating-mbedtls.md) states the + under the same key. [Integrating Mbed TLS](../platforms/mbedtls/setup.md) states the entropy the adapter assumes and the silent failure mode when it is missing. Uniqueness stays probabilistic, bounded by the 2³² per-key envelope below. - Failure: `OpenRecord` returns a single `bool`. A tag mismatch (the diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index e042a544..8e1aef1d 100644 --- a/docs/security/threat-model.md +++ b/docs/security/threat-model.md @@ -184,7 +184,7 @@ security-conscious component supplier. See [`cra.md`](../cra.md) for the CRA Annex I audit-trail map, [`iec62443.md`](../iec62443.md) for the control-by-control map, and [`rfc-compliance.md`](../rfc-compliance.md) for the standards coverage matrix. The public disclosure process for issues found -against this model is in [`SECURITY.md`](../../SECURITY.md). +against this model is in [Security policy](policy.md). ## Review policy diff --git a/docs/security/triage-runbook.md b/docs/security/triage-runbook.md index 2233269d..d0865c5d 100644 --- a/docs/security/triage-runbook.md +++ b/docs/security/triage-runbook.md @@ -1,6 +1,6 @@ # Vulnerability Triage Runbook -This is the maintainer's operational counterpart to [`SECURITY.md`](../../SECURITY.md): +This is the maintainer's operational counterpart to [Security policy](policy.md): how a vulnerability report is handled end-to-end, from receipt to retrospective. `SECURITY.md` states the public promises (72-hour acknowledgement, 90+14 disclosure); this runbook is how they are met. diff --git a/docs/structured-data.md b/docs/structured-data.md index 2be9416f..a80ac1c6 100644 --- a/docs/structured-data.md +++ b/docs/structured-data.md @@ -16,8 +16,9 @@ spelling, the `PARAM-NAME="..."` punctuation, and the RFC 5424 §6.3.3 value esc custom element therefore cannot produce malformed structured data, and a value can never break out of its quotes; whatever bytes you hand it stay inside the `"..."`. -The complete worked example below lives at -[`Bdd/Targets/Common/BddTargetCustomSd.c`](../Bdd/Targets/Common/BddTargetCustomSd.c). +The complete worked example below is drawn from the BDD target source at +`Bdd/Targets/Common/BddTargetCustomSd.c`, which is test infrastructure rather +than shipped example code. ## The two writer types diff --git a/hooks/page_descriptions.py b/hooks/page_descriptions.py index 6edb5f04..f737a67f 100644 --- a/hooks/page_descriptions.py +++ b/hooks/page_descriptions.py @@ -9,9 +9,14 @@ above the content. Keeping them out of the pages costs proximity, so the map is checked against -the navigation on every build: a page in the nav with no description, or a -description whose key names no nav page, aborts the build. Generated ``api/`` +the documentation tree on every build: a page with no description, or a +description whose key names no page, aborts the build. Generated ``api/`` pages are exempt — their briefs come from the header doc comments. + +The check is against every hand-written page, not only the ones in the nav. +Each platform's ``setup.md`` is deliberately off-nav — it is reached from the +platform's own page, because the two have different audiences — and a page a +search engine can reach still needs its own snippet. """ import os @@ -49,19 +54,92 @@ "Which severity a SolidSyslog error event carries — the urgency ladder " "each emit site picks from, and what each level asks of your handler." ), - "integrating-lwip.md": ( + # Platforms — each platform's overview page followed by its setup guide, + # in nav order, with porting last as the other half of the same question. + "platforms/index.md": ( + "The adapter packs that reach your hardware — POSIX, Windows, FreeRTOS, " + "lwIP, OpenSSL, Mbed TLS, FatFs and more — and the capabilities each fills." + ), + "platforms/posix/index.md": ( + "The POSIX adapter pack — sockets, pthreads, message queues, " + "clock_gettime and stdio — filling six of the library's roles on Linux." + ), + "platforms/posix/setup.md": ( + "Wire the POSIX adapters: what to link, building a sender, " + "the clock and hostname callbacks, and what threading needs from you." + ), + "platforms/windows/index.md": ( + "The Win32 and Winsock adapter pack for MSVC targets, filling the " + "Resolver, Datagram, Stream, File, Mutex and AtomicCounter roles." + ), + "platforms/windows/setup.md": ( + "Wire the Win32 and Winsock adapters: starting Winsock, building " + "a sender, the callbacks, and what threading needs from you." + ), + "platforms/freertos/index.md": ( + "The FreeRTOS adapter pack: kernel primitives filling the Mutex role and " + "the sysUpTime callback. Networking comes from a separate platform." + ), + "platforms/freertos/setup.md": ( + "Wire the FreeRTOS kernel primitives: what to link, static allocation, " + "and putting the mutex under a buffer shared across tasks." + ), + "platforms/plustcp/index.md": ( + "The FreeRTOS-Plus-TCP adapter pack, filling the Resolver, Datagram and " + "Stream roles for networking on FreeRTOS targets." + ), + "platforms/plustcp/setup.md": ( + "Wire the FreeRTOS-Plus-TCP adapters: what to link, resolving the " + "collector, building a sender, and sizing the stack for it." + ), + "platforms/lwipraw/index.md": ( + "The lwIP Raw API adapter pack, filling the Resolver, Datagram and Stream " + "roles — compiled against your lwipopts.h, NO_SYS=1 builds included." + ), + "platforms/lwipraw/setup.md": ( "Wire SolidSyslog to the lwIP Raw API: what the adapter fills, what you " "supply, and how it works under NO_SYS=1 beside other lwIP subsystems." ), - "integrating-mbedtls.md": ( - "Deliver RFC 5425 syslog over TLS through Mbed TLS on embedded targets: " - "the handles you pre-build and pass in, and how they are wired." + "platforms/openssl/index.md": ( + "The OpenSSL adapter pack: TLS transport for the Stream role, and keyed " + "at-rest crypto for the SecurityPolicy role, on hosted targets." + ), + "platforms/openssl/setup.md": ( + "Wire SolidSyslogOpenSslStream over a TCP stream for RFC 5425 syslog over " + "TLS: what to link, the layering, the config, and mutual TLS." + ), + "platforms/mbedtls/index.md": ( + "The Mbed TLS adapter pack for embedded targets: TLS transport for the " + "Stream role, and keyed at-rest crypto for the SecurityPolicy role." + ), + "platforms/mbedtls/setup.md": ( + "Wire Mbed TLS for RFC 5425 syslog over TLS on an embedded target: the " + "layering, bringing the library up, and the sizing traps." + ), + "platforms/fatfs/index.md": ( + "The ChaN FatFs adapter pack, filling the File role beneath a BlockDevice " + "— RTOS-agnostic, for bare-metal targets or under any RTOS." + ), + "platforms/fatfs/setup.md": ( + "Give store-and-forward a FatFs file backend: what to link, the disk " + "I/O driver and configuration you supply, and what durability you get." ), - "integrating-plusfat.md": ( + "platforms/plusfat/index.md": ( + "The FreeRTOS-Plus-FAT adapter pack, filling the File role beneath a " + "BlockDevice for an all-FreeRTOS-Plus storage and transport stack." + ), + "platforms/plusfat/setup.md": ( "Back store-and-forward with FreeRTOS-Plus-FAT: what the ff_stdio File " "adapter needs from your build and media, and how the store sits above it." ), - # Port a new platform + "platforms/stdatomic/index.md": ( + "The portable C11 stdatomic.h AtomicCounter — the sequenceId source on " + "any target with a C11 compiler, with no OS dependency." + ), + "platforms/stdatomic/setup.md": ( + "Wire the C11 atomics sequence-number source: what to link, creating " + "the counter, and why an unfilled role silently disables gap detection." + ), "porting.md": ( "Port SolidSyslog to a new RTOS, network stack, filesystem or crypto " "library by filling a vtable contract. Core never changes." @@ -73,8 +151,8 @@ "stays yours." ), "iec62443.md": ( - "IEC 62443-4-2 and 62443-3-3 audit-logging controls mapped " - "control-by-control to SolidSyslog components, by Security Level." + "IEC 62443-4-2 audit-logging controls mapped control-by-control to " + "what SolidSyslog provides and what stays yours." ), "rfc-compliance.md": ( "Requirement-by-requirement status of SolidSyslog against the syslog " @@ -100,6 +178,18 @@ "Verify a SolidSyslog release came from this repository: cosign signature " "checks, SBOM validation and the source-tree hash, in the order to run them." ), + "security/policy.md": ( + "How to report a SolidSyslog vulnerability, what we commit to in " + "response, and the disclosure process, scope and supported versions." + ), + "license.md": ( + "SolidSyslog is licensed under PolyForm Noncommercial 1.0.0: the full " + "terms, what noncommercial covers, and where commercial use needs a licence." + ), + "support.md": ( + "Where to get help with SolidSyslog — the documentation, questions and " + "bug reports, reporting a security issue, and commercial support." + ), # API reference doorways "api-reference/index.md": ( "The doorway to the SolidSyslog API: which headers your code includes to " @@ -109,53 +199,13 @@ "What Core provides: the facade you call, the pipeline that formats and " "drains a record, and the role implementations that need no platform." ), - "platforms/index.md": ( - "The adapter packs that reach your hardware — POSIX, Windows, FreeRTOS, " - "lwIP, OpenSSL, Mbed TLS, FatFs and more — and the capabilities each fills." - ), "roles/index.md": ( - "The twelve vtable contracts SolidSyslog composes against, what fills " + "The vtable contracts SolidSyslog composes against, what fills " "each one, and the Null fallback that keeps an unfilled role safe." ), - "platforms/posix.md": ( - "The POSIX adapter pack — sockets, pthreads, message queues, " - "clock_gettime and stdio — filling six of the library's roles on Linux." - ), - "platforms/windows.md": ( - "The Win32 and Winsock adapter pack for MSVC targets, filling the " - "Resolver, Datagram, Stream, File, Mutex and AtomicCounter roles." - ), - "platforms/freertos.md": ( - "The FreeRTOS adapter pack: kernel primitives filling the Mutex role and " - "the sysUpTime callback, with networking from Plus-TCP or lwIP." - ), - "platforms/plustcp.md": ( - "The FreeRTOS-Plus-TCP adapter pack, filling the Resolver, Datagram and " - "Stream roles for networking on FreeRTOS targets." - ), - "platforms/lwip.md": ( - "The lwIP Raw API adapter pack, filling the Resolver, Datagram and Stream " - "roles — compiled against your lwipopts.h, NO_SYS=1 builds included." - ), - "platforms/openssl.md": ( - "The OpenSSL adapter pack: TLS transport for the Stream role, and keyed " - "at-rest crypto for the SecurityPolicy role, on hosted targets." - ), - "platforms/mbedtls.md": ( - "The Mbed TLS adapter pack for embedded targets: TLS transport for the " - "Stream role, and keyed at-rest crypto for the SecurityPolicy role." - ), - "platforms/fatfs.md": ( - "The ChaN FatFs adapter pack, filling the File role beneath a BlockDevice " - "— RTOS-agnostic, for bare-metal, FreeRTOS, Zephyr and NuttX targets." - ), - "platforms/plusfat.md": ( - "The FreeRTOS-Plus-FAT adapter pack, filling the File role beneath a " - "BlockDevice for an all-FreeRTOS-Plus storage and transport stack." - ), - "platforms/stdatomic.md": ( - "The portable C11 stdatomic.h AtomicCounter — the sequenceId source on " - "any target with a C11 compiler, with no OS dependency." + "assets/postit/README.md": ( + "The post-it diagram kit behind the SolidSyslog architecture pictures: " + "what each colour and arrow means, and how the diagrams are generated." ), # Maintaining "builds.md": ( @@ -197,23 +247,23 @@ def _src_uri(file): return getattr(file, "src_uri", None) or file.src_path.replace(os.sep, "/") -def _described_pages(nav): +def _hand_written_pages(files): return { - _src_uri(page.file) - for page in nav.pages - if not _src_uri(page.file).startswith(GENERATED_PREFIX) + _src_uri(file) + for file in files + if file.is_documentation_page() and not _src_uri(file).startswith(GENERATED_PREFIX) } def on_nav(nav, config, files, **kwargs): - navigated = _described_pages(nav) + written = _hand_written_pages(files) faults = [] - missing = sorted(navigated - set(DESCRIPTIONS)) + missing = sorted(written - set(DESCRIPTIONS)) if missing: faults.append("no description for " + ", ".join(missing)) - stale = sorted(set(DESCRIPTIONS) - navigated) + stale = sorted(set(DESCRIPTIONS) - written) if stale: - faults.append("described but not in the nav: " + ", ".join(stale)) + faults.append("described but no such page: " + ", ".join(stale)) if faults: raise PluginError("hooks/page_descriptions.py: " + "; ".join(faults)) return nav diff --git a/hooks/platform_backlinks.py b/hooks/platform_backlinks.py new file mode 100644 index 00000000..9db9c0df --- /dev/null +++ b/hooks/platform_backlinks.py @@ -0,0 +1,212 @@ +"""MkDocs build hook: a platform's page, its manifest, and the way back to it. + +Three jobs, none of them hand-written: + +* every generated API page for a header under ``Platform//`` gets a chip + naming the platform it belongs to — the question a reader arriving from a + search result has, and one the API reference could not previously answer; +* a platform page gets a chip to its setup guide and its setup guide one back, + and its *What it ships* section is filled with the headers it publishes; +* and the Doxygen group pages are dropped from the build. + +The groups are dropped because a platform had two pages answering "what is this +platform", and the group page's only unique content was its Files table — which +is now the manifest, on the platform page, where the prose is hand-editable, the +links are validated, and the URL is the good one. ``@defgroup`` and ``@ingroup`` +stay in the source: they cost nothing, and an integrator running Doxygen over +the headers they ship with still gets the grouping. + +Chips are injected rather than written because they are navigation furniture, +not content: generating them keeps the Markdown clean, keeps the links inside +MkDocs' own resolution and validation (raw HTML in a page is neither rewritten +nor checked), and means adding a platform needs no markup at all. + +The platform list is read from ``SOLIDSYSLOG_PLATFORM_REGISTRY`` in the +top-level CMakeLists.txt — the same table ``scripts/check_manifest.py`` treats +as authoritative — so a new platform is picked up by being registered. Its slug +is the registry token lowercased, which is what the docs folder and the Doxygen +group are both named after, and its display name is the platform page's own +heading, so the name is written once. +""" + +import os +import re + +REGISTRY = re.compile(r"set\(SOLIDSYSLOG_PLATFORM_REGISTRY(.*?)^\)", re.DOTALL | re.MULTILINE) +ROW = re.compile(r'"([^"|]+)\|[^"|]*\|[^"|]*\|[^"|]*\|([^"|]+)\|[^"]*"') +# The @file block down to its first blank comment line, and the first sentence +# of it — Doxygen's own implicit brief, so the page and the generated reference +# say the same thing because they read the same source. +FILE_BLOCK = re.compile(r"/\*\*\s*@file\s*\n(.*?)(?:\n[ \t]*\*[ \t]*\n|\*/)", re.DOTALL) +SENTENCE_END = re.compile(r"(?<=[a-z0-9)\]])\.(?:\s|$)") +# The section the manifest replaces. Everything to the next heading is +# generated, so the page source carries the heading alone. +SHIPS = re.compile(r"^(## What it ships[ \t]*\n)(.*?)(?=^## |\Z)", re.MULTILINE | re.DOTALL) +# A definition, not a forward declaration — the brace is what distinguishes +# `struct SolidSyslogMbedTlsStreamConfig {` from `struct SolidSyslogStream;`. +STRUCT_DEF = re.compile(r"struct\s+(SolidSyslog\w+)\s*\{", re.MULTILINE) + +GENERATED_PREFIX = "api/" +PLATFORM_PREFIX = "platforms/" +# The group pages and the index that lists them. Dropped from the build: their +# content lives on the platform pages now, and leaving them would publish two +# answers to the same question. +DROPPED = re.compile(r"^api/(group__platform__\w+|modules)\.md$") +# mkdoxy's index-of-indexes lists Modules among them, so dropping the page +# without dropping the entry leaves a dead link the strict build rejects. +MODULES_ENTRY = re.compile(r"^[ \t]*-[ \t]*\[Modules\]\(modules\.md\)[ \t]*\n", re.MULTILINE) + +_CACHE = {} + + +def _label(root, slug): + """The platform's own H1 — so the display name is written once, on its page.""" + path = os.path.join(root, "docs", PLATFORM_PREFIX, slug, "index.md") + with open(path, encoding="utf-8") as page: + for line in page: + if line.startswith("# "): + return line[2:].strip() + return slug + + +def _brief(text): + """A header's implicit brief — the first sentence of its @file block.""" + block = FILE_BLOCK.search(text) + if block is None: + return "" + body = " ".join(line.strip().lstrip("*").strip() for line in block.group(1).splitlines()) + split = SENTENCE_END.search(body) + return (body[: split.end()] if split else body).strip() + + +def _index(config): + """Return (headers, slugs, labels, manifest) built from the platform registry. + + ``headers`` maps both a header stem and a struct name to its platform, so + every generated page for a platform — file or data structure — can be + labelled. Core's structs are absent by construction: only the platform + Interface directories are scanned. + + ``manifest`` is the ordered (stem, brief) list per platform, which becomes + the *What it ships* table. Generated rather than written, because a list of + filenames and their own briefs is mechanical: hand-keeping one per platform + is clerical work that drifts the moment a header is added. + """ + root = os.path.dirname(config["config_file_path"]) + if root not in _CACHE: + with open(os.path.join(root, "CMakeLists.txt"), encoding="utf-8") as cmake: + registry = REGISTRY.search(cmake.read()) + headers, slugs, labels, manifest = {}, {}, {}, {} + for token, directory in ROW.findall(registry.group(1)) if registry else []: + interface = os.path.join(root, directory, "Interface") + if not os.path.isdir(interface): + continue + slug = token.lower() + labels[slug] = _label(root, slug) + pack = (labels[slug], slug) + slugs[slug] = os.path.isfile(os.path.join(root, "docs", PLATFORM_PREFIX, slug, "setup.md")) + manifest[slug] = [] + for name in sorted(os.listdir(interface)): + if not name.endswith(".h"): + continue + stem = name[: -len(".h")] + headers[stem] = pack + with open(os.path.join(interface, name), encoding="utf-8") as header: + text = header.read() + manifest[slug].append((stem, _brief(text))) + for struct in STRUCT_DEF.findall(text): + headers[struct] = pack + _CACHE[root] = (headers, slugs, labels, manifest) + return _CACHE[root] + + +def _cell(brief): + """A header's brief, safe to drop into a Markdown table cell. + + Two characters cannot travel as themselves. A pipe would end the cell. An + angle bracket opens an HTML tag, so a brief naming a standard header — + ``An AtomicCounter over C11 `` — rendered as *over C11 ,* with + the include swallowed and the punctuation around it left stranded. It read + like a typo rather than a missing word, which is why it survived review. + Doxygen escapes these on its own pages; the brief only lost them on the + route through here. + """ + for raw, escaped in (("&", "&"), ("<", "<"), (">", ">"), ("|", r"\|")): + brief = brief.replace(raw, escaped) + return brief + + +def _ships(entries): + """The manifest table: every header the platform publishes, by filename.""" + rows = ["| Header | What it is |", "|---|---|"] + for stem, brief in entries: + rows.append(f"| [`{stem}.h`](../../api/{stem}_8h.md) | {_cell(brief)} |") + return "\n".join(rows) + + +def _stem(src_uri): + """The header or struct a generated page documents, else None. + + api/SolidSyslogMbedTlsStream_8h.md -> SolidSyslogMbedTlsStream + api/structSolidSyslogMbedTlsStreamConfig.md -> SolidSyslogMbedTlsStreamConfig + """ + leaf = src_uri[len(GENERATED_PREFIX) : -len(".md")] + if leaf.endswith("_8h"): + return leaf[: -len("_8h")] + if leaf.startswith("struct"): + return leaf[len("struct") :] + return None + + +def on_files(files, config): + """Drop the group pages and the index listing them. + + mkdoxy generates a page per @defgroup whether anything links to it or not. + Left in, they publish a second answer to "what is this platform" — and a + worse one, since the platform page owns the prose, the good URL and the + meta description. Removing them here rather than removing @defgroup from + the source keeps the grouping for anyone running Doxygen themselves. + """ + kept = [f for f in files if not DROPPED.match(f.src_uri.replace(os.sep, "/"))] + # Rebuild the collection MkDocs handed us — it is a Files object with its + # own API downstream, not a plain list. + return type(files)(kept) + + +def on_page_markdown(markdown, page, config, files, **kwargs): + src_uri = getattr(page.file, "src_uri", None) or page.file.src_path.replace(os.sep, "/") + headers, slugs, labels, manifest = _index(config) + + if src_uri.startswith(GENERATED_PREFIX): + if src_uri == f"{GENERATED_PREFIX}links.md": + return MODULES_ENTRY.sub("", markdown, count=1) + + stem = _stem(src_uri) + pack = headers.get(stem) if stem else None + if pack is None: + return markdown + label, slug = pack + chip = f"[{label} platform](../{PLATFORM_PREFIX}{slug}/index.md){{ .ss-chip .ss-chip--platform }}" + return f"{chip}\n\n{markdown}" + + if src_uri.startswith(PLATFORM_PREFIX) and src_uri.endswith("/setup.md"): + slug = src_uri[len(PLATFORM_PREFIX) : -len("/setup.md")] + if slug not in slugs: + return markdown + chip = f"[{labels[slug]} platform](index.md){{ .ss-chip .ss-chip--platform }}" + title, _, body = markdown.partition("\n") + return f"{title}\n\n{chip}\n\n{body.lstrip()}" + + if src_uri.startswith(PLATFORM_PREFIX) and src_uri.endswith("/index.md"): + slug = src_uri[len(PLATFORM_PREFIX) : -len("/index.md")] + if slug not in slugs: + return markdown + if slugs[slug]: + chip = "[Setup](setup.md){ .ss-chip .ss-chip--setup }" + # Placed under the title rather than above it, so the page still + # opens with its name. + title, _, body = markdown.partition("\n") + markdown = f"{title}\n\n{chip}\n\n{body.lstrip()}" + return SHIPS.sub(lambda m: f"{m.group(1)}\n{_ships(manifest[slug])}\n\n", markdown, count=1) + + return markdown diff --git a/hooks/root_pages.py b/hooks/root_pages.py new file mode 100644 index 00000000..6da4f9ad --- /dev/null +++ b/hooks/root_pages.py @@ -0,0 +1,83 @@ +"""MkDocs build hook: publish repository-root Markdown as pages of the site. + +A few documents have to live at the repository root — GitHub reads SECURITY.md +to drive its vulnerability-reporting UI, and a licence is looked for beside the +code — but they are documentation the site should hold rather than send a reader +away for. Publishing them here keeps one source with two renderings instead of a +root copy and a docs copy drifting apart. + +The file is read at build time and handed to MkDocs as generated content, so +nothing is copied into the working tree and nothing needs a symlink (which the +Windows side would not thank us for). + +Their links are written to resolve from the repository root, where `docs/…` is +how they reach a documentation page. Inside the site that prefix is wrong, so it +is stripped and the remainder made relative to wherever the page is published. +`mkdocs build --strict` fails on any that does not land. +""" + +import os +import re + +from mkdocs.structure.files import File + +# Root file → where it is published. Both halves matter: the source path is +# fixed by convention (GitHub looks for these names, in this place), and the +# destination is ours to choose so the page sits with its siblings. +PUBLISHED = { + "SECURITY.md": "security/policy.md", + "LICENSE.md": "license.md", + "SUPPORT.md": "support.md", +} + +# The site's landing page, which is what `docs/` means to a reader at the root. +HOME = "README.md" +# Any link in a root document. Whether it needs re-aiming depends on where it +# points, which _target decides. +LINK = re.compile(r"\[([^\]]+)\]\(([^)]+)\)") + + +def _target(href): + """Where this link lands inside the site, or None if it is not ours to move. + + Two kinds re-aim: a path into the documentation tree — `docs/` alone being + the landing page — and a sibling root document that is also published. An + external URL is already right. + """ + if href.startswith("docs/"): + return href[len("docs/") :] or HOME + return PUBLISHED.get(href) + + +def _relink(markdown, published_at, root): + """Re-aim a root file's links at this page's position in the site. + + A link to a root document we do not publish becomes plain text rather than + a jump out to the repository host — the same treatment the documentation + gives a source file it cannot render. Publishing the target instead would + cascade: CONTRIBUTING.md reaches CLAUDE.md, which is a working agreement + between maintainers and not part of the product's documentation. + """ + page_dir = os.path.dirname(published_at) + + def rewrite(match): + text, href = match.group(1), match.group(2) + href, _, fragment = href.partition("#") + target = _target(href) + if target is None: + unpublished = "/" not in href and os.path.isfile(os.path.join(root, href)) + return text if unpublished else match.group(0) + relative = os.path.relpath(target, page_dir) if page_dir else target + return f"[{text}]({relative}{'#' + fragment if fragment else ''})" + + return LINK.sub(rewrite, markdown) + + +def on_files(files, config): + root = os.path.dirname(config["config_file_path"]) + for source, destination in PUBLISHED.items(): + with open(os.path.join(root, source), encoding="utf-8") as handle: + markdown = handle.read() + content = _relink(markdown, destination, root) + files.append(File.generated(config, destination, content=content)) + return files diff --git a/hooks/test_page_descriptions.py b/hooks/test_page_descriptions.py index 35174cd2..4f97cc91 100644 --- a/hooks/test_page_descriptions.py +++ b/hooks/test_page_descriptions.py @@ -3,8 +3,8 @@ Run: python3 hooks/test_page_descriptions.py (or: python3 -m unittest discover -s hooks -p 'test_*.py') -The strict build already proves the happy path — every nav page carries a -description, or on_nav aborts. What needs its own test is the abort itself, and +The strict build already proves the happy path — every hand-written page carries +a description, or on_nav aborts. What needs its own test is the abort itself, and the description text, which no build can check for length or duplication. """ @@ -19,37 +19,47 @@ MAX_LENGTH = 200 -def nav_of(*src_uris): - pages = [types.SimpleNamespace(file=types.SimpleNamespace(src_uri=uri)) for uri in src_uris] - return types.SimpleNamespace(pages=pages) +NAV = types.SimpleNamespace(pages=[]) + + +def files_of(*src_uris, assets=()): + pages = [types.SimpleNamespace(src_uri=uri, is_documentation_page=lambda: True) for uri in src_uris] + other = [types.SimpleNamespace(src_uri=uri, is_documentation_page=lambda: False) for uri in assets] + return pages + other def page_of(src_uri, meta=None): return types.SimpleNamespace(file=types.SimpleNamespace(src_uri=src_uri), meta=meta or {}) -class NavValidation(unittest.TestCase): +class TreeValidation(unittest.TestCase): def setUp(self): self.described = sorted(h.DESCRIPTIONS) - def test_accepts_a_nav_of_described_pages(self): - nav = nav_of(*self.described) - self.assertIs(h.on_nav(nav, {}, None), nav) + def test_accepts_a_tree_of_described_pages(self): + self.assertIs(h.on_nav(NAV, {}, files_of(*self.described)), NAV) - def test_undescribed_nav_page_aborts_the_build_and_is_named(self): + def test_undescribed_page_aborts_the_build_and_is_named(self): with self.assertRaises(h.PluginError) as raised: - h.on_nav(nav_of(*self.described, "brand-new.md"), {}, None) + h.on_nav(NAV, {}, files_of(*self.described, "brand-new.md")) self.assertIn("no description for brand-new.md", str(raised.exception)) - def test_description_for_a_page_no_longer_in_the_nav_aborts_the_build(self): + def test_description_for_a_page_that_no_longer_exists_aborts_the_build(self): kept = [uri for uri in self.described if uri != "porting.md"] with self.assertRaises(h.PluginError) as raised: - h.on_nav(nav_of(*kept), {}, None) - self.assertIn("not in the nav: porting.md", str(raised.exception)) + h.on_nav(NAV, {}, files_of(*kept)) + self.assertIn("no such page: porting.md", str(raised.exception)) def test_generated_api_pages_need_no_description(self): - nav = nav_of(*self.described, "api/files.md") - self.assertIs(h.on_nav(nav, {}, None), nav) + files = files_of(*self.described, "api/files.md") + self.assertIs(h.on_nav(NAV, {}, files), NAV) + + # An off-nav page still needs a description, so the check reads the file + # tree rather than the nav — which means it must ignore everything in that + # tree that is not a page. + def test_assets_are_not_pages_and_need_no_description(self): + files = files_of(*self.described, assets=("assets/images/logo.svg",)) + self.assertIs(h.on_nav(NAV, {}, files), NAV) class PageMeta(unittest.TestCase): @@ -64,8 +74,10 @@ def test_front_matter_wins(self): self.assertEqual(page.meta["description"], "hand written") def test_unmapped_page_falls_back_to_site_description(self): - page = page_of("assets/postit/README.md") - h.on_page_markdown("# Post-its", page, {}, None) + # A page name deliberately not in the map — on_nav is what makes a real + # unmapped page fail the build, so this only covers the fallback itself. + page = page_of("no-such-page.md") + h.on_page_markdown("# Nothing", page, {}, None) self.assertNotIn("description", page.meta) def test_markdown_is_returned_unchanged(self): diff --git a/hooks/test_platform_backlinks.py b/hooks/test_platform_backlinks.py new file mode 100644 index 00000000..c090f49a --- /dev/null +++ b/hooks/test_platform_backlinks.py @@ -0,0 +1,234 @@ +"""Regression tests for the platform doorway hook (hooks/platform_backlinks.py). + +Run: python3 hooks/test_platform_backlinks.py + (or: python3 -m unittest discover -s hooks -p 'test_*.py') + +The strict build proves the links resolve. What it cannot prove is that the +right pages get chips and the wrong ones do not — a Core header quietly +labelled with a platform, or a platform page silently missing its doorway, +builds perfectly well. +""" + +import glob +import os +import sys +import types +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import platform_backlinks as h # noqa: E402 + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +CONFIG = {"config_file_path": os.path.join(ROOT, "mkdocs.yml")} + + +def page_of(src_uri): + return types.SimpleNamespace(file=types.SimpleNamespace(src_uri=src_uri)) + + +def render(src_uri, markdown="# Title\n\nBody.\n"): + return h.on_page_markdown(markdown, page_of(src_uri), CONFIG, None) + + +class GeneratedApiPages(unittest.TestCase): + def test_a_platform_header_is_labelled_with_its_platform(self): + out = render("api/SolidSyslogMbedTlsStream_8h.md") + self.assertIn("[Mbed TLS platform](../platforms/mbedtls/index.md){ .ss-chip .ss-chip--platform }", out) + + def test_the_label_is_the_platform_pages_own_heading(self): + # Not the registry token, which is "LwipRaw". + self.assertIn("[lwIP (Raw API) platform]", render("api/SolidSyslogLwipRawTcpStream_8h.md")) + + def test_a_platform_data_structure_is_labelled_too(self): + out = render("api/structSolidSyslogMbedTlsStreamConfig.md") + self.assertIn("[Mbed TLS platform](../platforms/mbedtls/index.md)", out) + + def test_a_core_data_structure_is_not_labelled(self): + # Only the platform Interface directories are scanned, so a Core struct + # cannot pick up a platform label. + markdown = "# Config\n\nBody.\n" + self.assertEqual(render("api/structSolidSyslogConfig.md", markdown), markdown) + + def test_a_forward_declaration_does_not_claim_the_struct(self): + # SolidSyslogMbedTlsStream.h forward-declares struct SolidSyslogStream; + # only definitions map, or Core's Stream would be labelled Mbed TLS. + headers, _slugs, _labels, _manifest = h._index(CONFIG) + self.assertNotIn("SolidSyslogStream", headers) + + def test_a_core_header_is_not_labelled(self): + markdown = "# Core\n\nBody.\n" + self.assertEqual(render("api/SolidSyslogConfig_8h.md", markdown), markdown) + + def test_a_generated_index_is_not_labelled(self): + markdown = "# Files\n" + self.assertEqual(render("api/files.md", markdown), markdown) + + +class PlatformPages(unittest.TestCase): + def test_chips_go_under_the_title_not_above_it(self): + out = render("platforms/mbedtls/index.md") + self.assertTrue(out.startswith("# Title\n\n["), out[:40]) + + def test_a_platform_with_a_setup_guide_gets_the_setup_chip(self): + out = render("platforms/mbedtls/index.md") + self.assertIn("[Setup](setup.md){ .ss-chip .ss-chip--setup }", out) + + def test_no_chip_points_at_a_group_page(self): + # The groups are dropped from the build; a chip to one would 404. + for slug in h._index(CONFIG)[1]: + self.assertNotIn("group__platform__", render(f"platforms/{slug}/index.md"), slug) + + def test_every_platform_has_its_setup_chip_today(self): + _, slugs, _labels, _manifest = h._index(CONFIG) + for slug in slugs: + self.assertIn(".ss-chip--setup", render(f"platforms/{slug}/index.md"), slug) + + def test_the_setup_chip_appears_only_when_the_page_does(self): + # Every platform has a setup page now, so the absent case has to be + # constructed. The chip must never point at a page that is not there. + root = os.path.dirname(CONFIG["config_file_path"]) + headers, slugs, labels, manifest = h._index(CONFIG) + h._CACHE[root] = (headers, {**slugs, "posix": False}, labels, manifest) + try: + out = render("platforms/posix/index.md") + finally: + h._CACHE.pop(root, None) + self.assertNotIn(".ss-chip--setup", out) + + def test_the_platforms_overview_is_not_a_platform(self): + markdown = "# Platforms\n\nBody.\n" + self.assertEqual(render("platforms/index.md", markdown), markdown) + + def test_a_setup_page_gets_a_chip_back_to_its_platform(self): + out = render("platforms/mbedtls/setup.md", "# Mbed TLS setup\n\nBody.\n") + self.assertIn("[Mbed TLS platform](index.md){ .ss-chip .ss-chip--platform }", out) + + def test_the_back_chip_goes_under_the_title(self): + out = render("platforms/mbedtls/setup.md", "# Mbed TLS setup\n\nBody.\n") + self.assertTrue(out.startswith("# Mbed TLS setup\n\n["), out[:40]) + + def test_every_setup_page_can_get_back_to_its_platform(self): + _, slugs, _labels, _manifest = h._index(CONFIG) + for slug in slugs: + out = render(f"platforms/{slug}/setup.md", "# Setup\n\nBody.\n") + self.assertIn("(index.md){ .ss-chip .ss-chip--platform }", out, slug) + + def test_an_ordinary_page_is_untouched(self): + markdown = "# Porting\n\nBody.\n" + self.assertEqual(render("porting.md", markdown), markdown) + + +class ShipsManifest(unittest.TestCase): + """The *What it ships* table is generated from the platform's own headers, + so a header added to a pack cannot go unlisted and a brief cannot go stale.""" + + PAGE = "# Mbed TLS\n\nIntro.\n\n## What it ships\n\n## Requirements\n\nNeeds things.\n" + + def ships(self, slug="mbedtls", markdown=None): + body = render(f"platforms/{slug}/index.md", markdown or self.PAGE) + return body.split("## What it ships")[1].split("## ")[0] + + def test_every_header_is_listed_by_filename(self): + table = self.ships() + for name in ("SolidSyslogMbedTlsStream.h", "SolidSyslogMbedTlsAesGcmPolicy.h", + "SolidSyslogMbedTlsHmacSha256Policy.h"): + self.assertIn(f"[`{name}`]", table) + + def test_error_headers_are_listed_too(self): + # The group page listed these; nothing else on the platform route did. + self.assertIn("[`SolidSyslogMbedTlsStreamErrors.h`]", self.ships()) + + def test_a_filename_links_to_its_generated_page(self): + self.assertIn("(../../api/SolidSyslogMbedTlsStream_8h.md)", self.ships()) + + def test_the_brief_is_the_headers_own_first_sentence(self): + self.assertIn("Error codes and Source identity for the MbedTlsStream adapter.", self.ships()) + + def test_a_brief_naming_a_standard_header_keeps_it(self): + # is an HTML tag to the Markdown renderer, so unescaped it + # vanished and took the sentence's sense with it. + self.assertIn("C11 <stdatomic.h>", self.ships("stdatomic")) + + def test_a_cell_escapes_what_would_break_it(self): + self.assertEqual(h._cell("a c | d & e"), r"a <b> c \| d & e") + + def test_a_multi_sentence_brief_stops_at_the_first_sentence(self): + row = [line for line in self.ships("stdatomic").splitlines() + if "SolidSyslogStdAtomicCounter.h" in line] + self.assertEqual(len(row), 1) + self.assertIn("backing the RFC 5424 sequenceId.", row[0]) + self.assertNotIn("Increment runs", row[0]) + + def test_the_heading_survives_and_the_body_is_replaced(self): + out = render("platforms/mbedtls/index.md", self.PAGE) + self.assertIn("## What it ships", out) + self.assertIn("## Requirements", out) + self.assertIn("Needs things.", out) + + def test_every_platform_lists_every_header_it_ships(self): + root = os.path.dirname(CONFIG["config_file_path"]) + _, _slugs, _labels, manifest = h._index(CONFIG) + for slug, entries in manifest.items(): + interface = glob.glob(os.path.join(root, "Platform", "*", "Interface")) + interface = [p for p in interface if os.path.basename(os.path.dirname(p)).lower() == slug] + expected = sorted(n[: -len(".h")] for n in os.listdir(interface[0]) if n.endswith(".h")) + self.assertEqual(sorted(stem for stem, _ in entries), expected, slug) + + def test_a_pipe_in_a_brief_cannot_break_the_table(self): + self.assertNotIn("| |", h._ships([("X", "a | b")]).splitlines()[-1].replace("| X", "")) + + +class DroppedGroupPages(unittest.TestCase): + """A platform answers "what is this" in one place. The group pages said it + a second time, worse, so they never reach the site.""" + + def files_after(self, *src_uris): + kept = h.on_files([types.SimpleNamespace(src_uri=u) for u in src_uris], CONFIG) + return [f.src_uri for f in kept] + + def test_every_group_page_is_dropped(self): + _, slugs, _labels, _manifest = h._index(CONFIG) + pages = [f"api/group__platform__{slug}.md" for slug in slugs] + self.assertEqual(self.files_after(*pages), []) + + def test_the_modules_index_goes_with_them(self): + # It exists only to list the groups, so it would link ten dead pages. + self.assertEqual(self.files_after("api/modules.md"), []) + + def test_a_header_page_is_kept(self): + self.assertEqual( + self.files_after("api/SolidSyslogMbedTlsStream_8h.md"), + ["api/SolidSyslogMbedTlsStream_8h.md"], + ) + + def test_a_platform_page_is_kept(self): + self.assertEqual(self.files_after("platforms/mbedtls/index.md"), ["platforms/mbedtls/index.md"]) + + def test_a_similarly_named_page_is_not_caught(self): + # group__platform__* only — a Core group, were one added, would stay. + self.assertEqual(self.files_after("api/group__core.md"), ["api/group__core.md"]) + + def test_the_index_of_indexes_stops_linking_modules(self): + raw = " - [Related Pages](pages.md)\n - [Modules](modules.md)\n - [Files](files.md)\n" + out = render("api/links.md", raw) + self.assertNotIn("modules.md", out) + self.assertIn("[Files](files.md)", out) + self.assertIn("[Related Pages](pages.md)", out) + + +class Registry(unittest.TestCase): + def test_every_registered_platform_has_a_docs_folder(self): + _, slugs, _labels, _manifest = h._index(CONFIG) + for slug in slugs: + self.assertTrue( + os.path.isfile(os.path.join(ROOT, "docs", "platforms", slug, "index.md")), + f"registry names {slug} but docs/platforms/{slug}/index.md is missing", + ) + + def test_the_registry_yields_all_ten_platforms(self): + _, slugs, _labels, _manifest = h._index(CONFIG) + self.assertEqual(len(slugs), 10, sorted(slugs)) + + +if __name__ == "__main__": + unittest.main() diff --git a/hooks/test_root_pages.py b/hooks/test_root_pages.py new file mode 100644 index 00000000..5555048c --- /dev/null +++ b/hooks/test_root_pages.py @@ -0,0 +1,84 @@ +"""Regression tests for the root-document hook (hooks/root_pages.py). + +Run: python3 hooks/test_root_pages.py + (or: python3 -m unittest discover -s hooks -p 'test_*.py') + +The strict build proves the published pages exist and their links resolve. What +it cannot prove is that the right files are published and that a link left +pointing at the repository root is caught rather than quietly rendered — a +SECURITY.md whose reporting route silently vanished still builds perfectly well. +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import root_pages as h # noqa: E402 + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def relink(markdown, published_at): + return h._relink(markdown, published_at, ROOT) + + +class WhatIsPublished(unittest.TestCase): + def test_the_security_policy_is_published(self): + # Five pages link it and it has no in-site equivalent. + self.assertEqual(h.PUBLISHED["SECURITY.md"], "security/policy.md") + + def test_every_published_source_exists_at_the_root(self): + for source in h.PUBLISHED: + self.assertTrue(os.path.isfile(os.path.join(ROOT, source)), source) + + def test_the_maintainers_working_agreement_is_not_published(self): + # CLAUDE.md is how we work, not part of the product's documentation. + self.assertNotIn("CLAUDE.md", h.PUBLISHED) + + +class Relinking(unittest.TestCase): + def test_a_docs_link_is_made_relative_to_the_published_page(self): + out = relink("See [the model](docs/security/threat-model.md).", "security/policy.md") + self.assertIn("[the model](threat-model.md)", out) + + def test_a_docs_link_from_a_page_at_the_site_root_keeps_its_path(self): + out = relink("See [the model](docs/security/threat-model.md).", "support.md") + self.assertIn("[the model](security/threat-model.md)", out) + + def test_docs_on_its_own_means_the_landing_page(self): + self.assertIn("[docs](README.md)", relink("[docs](docs/)", "support.md")) + + def test_a_link_to_another_published_root_file_finds_its_new_home(self): + out = relink("[Security issues](SECURITY.md)", "support.md") + self.assertIn("[Security issues](security/policy.md)", out) + + def test_a_link_to_an_unpublished_root_file_becomes_plain_text(self): + # Publishing the target instead would cascade — CONTRIBUTING.md reaches + # CLAUDE.md — so the link is dropped rather than followed out of the site. + out = relink("See [Contributing](CONTRIBUTING.md) first.", "support.md") + self.assertEqual(out, "See Contributing first.") + + def test_an_external_url_is_left_alone(self): + url = "[Talk to us](https://www.cososo.co.uk/?service=solidsyslog#contact)" + self.assertEqual(relink(url, "support.md"), url) + + def test_a_fragment_survives_the_rewrite(self): + out = relink("[scope](docs/security/threat-model.md#scope)", "security/policy.md") + self.assertIn("[scope](threat-model.md#scope)", out) + + def test_a_link_already_inside_the_docs_tree_is_untouched(self): + # Only a docs/-prefixed path is the root file's way of reaching a page. + self.assertEqual(relink("[x](other.md)", "support.md"), "[x](other.md)") + + +class RealFiles(unittest.TestCase): + def test_the_published_security_policy_keeps_a_route_to_the_threat_model(self): + with open(os.path.join(ROOT, "SECURITY.md"), encoding="utf-8") as handle: + out = relink(handle.read(), h.PUBLISHED["SECURITY.md"]) + self.assertIn("(threat-model.md)", out) + self.assertNotIn("(docs/", out) + + +if __name__ == "__main__": + unittest.main() diff --git a/misra_suppressions.txt b/misra_suppressions.txt index b8048ee1..2802bc59 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -79,18 +79,15 @@ misra-c2012-11.3:Platform/Windows/Source/SolidSyslogWindowsAtomicCounter.c:33 misra-c2012-11.3:Platform/Windows/Source/SolidSyslogWindowsFile.c:77 misra-c2012-11.3:Platform/Windows/Source/SolidSyslogWindowsMutex.c:38 misra-c2012-11.3:Platform/Windows/Source/SolidSyslogWinsockDatagram.c:98 -misra-c2012-11.3:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:185 +misra-c2012-11.3:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:184 misra-c2012-11.5:Core/Source/SolidSyslogUdpSender.c:227 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawDatagram.c:79 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawDnsResolver.c:154 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawDnsResolver.c:216 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:147 misra-c2012-11.5:Platform/LwipRaw/Source/SolidSyslogLwipRawTcpStream.c:155 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:298 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:310 -misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockDatagram.c:142 -misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:357 -misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:377 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:299 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:311 # D.003 — Rule 5.7: repeating struct tags (no-typedef-struct convention) # See docs/misra-deviations.md#d003 @@ -197,7 +194,10 @@ misra-c2012-2.5:Core/Interface/SolidSyslogCircularBuffer.h:41 # See docs/misra-deviations.md#d012 misra-c2012-8.9:Core/Source/SolidSyslogFileBlockDevice.c:20 -# D.013 — Rule 11.5: void* ↔ unsigned char* at third-party byte-buffer API boundaries +# D.013 — Rule 11.5: void* <-> a byte pointer at third-party byte-buffer API boundaries # See docs/misra-deviations.md#d013 -misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:335 +misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:336 misra-c2012-11.5:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:354 +misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockDatagram.c:142 +misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:356 +misra-c2012-11.5:Platform/Windows/Source/SolidSyslogWinsockTcpStream.c:376 diff --git a/mkdocs.yml b/mkdocs.yml index 9fbbad14..6332d887 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -74,6 +74,10 @@ markdown_extensions: # before MkDocs validates links. Pin the ref per build with SOLIDSYSLOG_DOCS_REF # (a release tag or commit SHA); it defaults to main. hooks: + # Publishes the repository-root documents the site should hold rather than + # link away to — SECURITY.md and friends stay at the root because GitHub + # looks for them there, and are rendered here from that same source. + - hooks/root_pages.py - hooks/source_links.py # Injects post-it "realised by" relationship diagrams at the top of selected # generated API pages (experiment — see hooks/relationship_diagrams.py). @@ -81,6 +85,11 @@ hooks: # Hides the one-entry table of contents on the generated Functions / Macros # indexes, whose letter buckets collapse under the common SolidSyslog prefix. - hooks/api_index_toc.py + # Puts a "Part of the platform" banner on every generated API page + # for a header under Platform//, linking to that pack's own page. The + # pack list comes from SOLIDSYSLOG_PLATFORM_REGISTRY, so registering a new + # platform is all it takes. + - hooks/platform_backlinks.py # Gives each hand-written page its own , the snippet # a search result shows, instead of the site_description above. The map lives # in the hook rather than in front matter because the same Markdown is read on @@ -108,11 +117,24 @@ plugins: - mkdoxy: projects: api: - src-dirs: "Core/Interface Platform/StdAtomic/Interface Platform/FatFs/Interface Platform/FreeRtos/Interface Platform/LwipRaw/Interface Platform/MbedTls/Interface Platform/OpenSsl/Interface Platform/PlusFat/Interface Platform/PlusTcp/Interface Platform/Posix/Interface Platform/Windows/Interface" + # Platform is swept whole rather than listed pack by pack, so a new + # platform's headers and its group file are picked up by existing + # there. EXCLUDE_PATTERNS below keeps the private Source/ headers out. + src-dirs: "Core/Interface Platform" full-doc: true doxy-cfg: FILE_PATTERNS: "*.h" RECURSIVE: true + # Platform//Source/ holds the *Private.h / *Internal.h + # headers, which are implementation detail and not public API. + EXCLUDE_PATTERNS: "*/Source/*" + # No verbatim source listings in the reference. The docs describe the + # contract; the code is read in the repository, and the two example + # repositories are where working code is shown. Without this, every + # file page carries a "Go to the source code of this file" link to a + # generated copy of the header. + XML_PROGRAMLISTING: false + VERBATIM_HEADERS: false EXTRACT_ALL: true OPTIMIZE_OUTPUT_FOR_C: true # First sentence of each /** */ block becomes the API-index brief. @@ -139,19 +161,46 @@ plugins: nav: - Home: README.md - - Overview: - - Compliance in one page: overview.md - - Adopt: + # Published from SUPPORT.md at the repository root by hooks/root_pages.py. + - Support: support.md + # Core is the product: the portable library everything else exists to serve. + # It sits at the top level beside Platforms, which is the convenience layer, + # rather than below the generated indexes inside API reference — the nav said + # the opposite of the product for as long as it did. One child, so + # navigation.indexes promotes it into the tab link. + - Core: + - core/index.md + - Integrate: - Building up the protection you need: hardening-path.md - Adding it to your build: build-integration.md - Structured data: structured-data.md - Error handling: error-severity.md - - Integrating lwIP: integrating-lwip.md - - Integrating Mbed TLS: integrating-mbedtls.md - - Integrating FreeRTOS-Plus-FAT: integrating-plusfat.md - - Port a new platform: - - Porting guide: porting.md + # Platforms answers "will this run on my target" — an integration question, not + # a reference one, so it is a tab rather than a child of API reference. Porting + # sits directly beneath the overview as the other half of it: nothing shipped + # fits, so write one. The packs themselves are then a flat alphabetical list, + # which is how a sidebar gets used — to look one up. Each pack's setup guide is + # deliberately absent here and reached from the pack's own page instead: the + # two have different audiences, and a nested pair per pack made the sidebar + # unreadable. + - Platforms: + - platforms/index.md + - Port a new platform: porting.md + - C11 atomics: platforms/stdatomic/index.md + - FatFs: platforms/fatfs/index.md + - FreeRTOS: platforms/freertos/index.md + - FreeRTOS-Plus-FAT: platforms/plusfat/index.md + - FreeRTOS-Plus-TCP: platforms/plustcp/index.md + - lwIP (Raw API): platforms/lwipraw/index.md + - Mbed TLS: platforms/mbedtls/index.md + - OpenSSL: platforms/openssl/index.md + - Posix: platforms/posix/index.md + - Windows: platforms/windows/index.md - Compliance: + # The one-screen orientation on both frameworks, so it leads the section + # the frameworks are in rather than sitting alone under a tab called + # Overview, where its own title contradicted the label above it. + - Compliance in one page: overview.md - CRA guide: cra.md - IEC 62443 guide: iec62443.md - RFC matrix: rfc-compliance.md @@ -160,37 +209,25 @@ nav: - SBOM: security/sbom.md - Vulnerability triage: security/triage-runbook.md - Release verification: security/release-verification.md + # Published from SECURITY.md at the repository root by hooks/root_pages.py. + - Security policy: security/policy.md + # Likewise LICENSE.md — the terms are a compliance question for a reader + # deciding whether they may use the library. + - Licence: license.md - API reference: # api-reference/index.md is the tab's own first child, so navigation.indexes # promotes it into the tab link — and the narrow-width drawer opens on this - # landing at the tab level, where Core / Platforms / Roles are its siblings. - # Nesting it inside a same-named section (as before) made the drawer open one - # level deeper, hiding those doorways behind the back arrow. + # landing at the tab level, where Headers and Symbols / Roles are its + # siblings. Nesting it inside a same-named section (as before) made the + # drawer open one level deeper, hiding those doorways behind the back arrow. - api-reference/index.md - # The raw Doxygen indexes as one peer section beside Core / Platforms / Roles; - # labelled distinctly (not "API reference") so it doesn't shadow the tab name. + # The raw Doxygen indexes as one peer section beside Roles; labelled + # distinctly (not "API reference") so it doesn't shadow the tab name. - Headers and Symbols: - Headers: api/files.md - Data structures: api/annotated.md - Functions: api/functions.md - Macros: api/macros.md - # A section (not a bare page) so Material gives it md-nav__item--section and - # it reads with the same weight as Platforms / Roles. Its index is its only - # child — navigation.indexes promotes it into the section link. - - Core: - - core/index.md - - Platforms: - - platforms/index.md - - Posix: platforms/posix.md - - Windows: platforms/windows.md - - FreeRTOS: platforms/freertos.md - - FreeRTOS-Plus-TCP: platforms/plustcp.md - - lwIP (Raw API): platforms/lwip.md - - OpenSSL: platforms/openssl.md - - Mbed TLS: platforms/mbedtls.md - - FatFs: platforms/fatfs.md - - FreeRTOS-Plus-FAT: platforms/plusfat.md - - C11 atomics: platforms/stdatomic.md # A role *is* its vtable, so each entry links straight to the generated # contract page; detail belongs in the SolidSyslogDefinition.h # doc comment, not a hand-maintained copy here. diff --git a/scripts/check_platform_docs.py b/scripts/check_platform_docs.py new file mode 100644 index 00000000..e14e9dfb --- /dev/null +++ b/scripts/check_platform_docs.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +"""Assert the documentation matches what the code declares. + +Two things are declared in the code and enumerated in the docs — the platforms +and the roles — and in both cases the enumeration is written by a person and +checked here, never generated. A missing entry is a decision not yet made, not a +mechanical gap; generating a placeholder would hide it. + +A platform is declared once, in SOLIDSYSLOG_PLATFORM_REGISTRY in the top-level +CMakeLists.txt. Everything else follows from its token: + + token LwipRaw + directory Platform/LwipRaw/ + docs docs/platforms/lwipraw/{index,setup}.md + nav an entry in mkdocs.yml + description an entry in hooks/page_descriptions.py + +This checks that each of those exists for each row, and the reverse — a docs +folder or a group with no row behind it. Registering a platform is then the one +edit that cannot be forgotten, because forgetting anything else fails the build. + +It also holds two boundaries that hand review kept losing: + +* **No platform names another.** A platform describes itself completely; where + a capability comes from is the capability matrix's job. Naming a sibling + couples the two, so the eleventh platform means editing ten pages. Applies to + the platform's whole tree and its docs folder alike. +* **Every header its platform ships is on its page.** The *What it ships* + manifest is generated from the Interface directory, so this asserts the + heading the generator writes into is still there. + +A role is declared by a Core/Interface/SolidSyslogDefinition.h header, and +listed in three hand-written places: the porting guide, the roles index, and the +mkdocs nav. Each must link the role's generated contract page. The count used to +be stated in prose in six places, so a thirteenth role meant six edits and no +failure; the count is gone and this is what replaces it. + +Run: python3 scripts/check_platform_docs.py +""" + +import os +import re +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +REGISTRY = re.compile(r"set\(SOLIDSYSLOG_PLATFORM_REGISTRY(.*?)^\)", re.DOTALL | re.MULTILINE) +ROW = re.compile(r'"([^"|]+)\|[^"|]*\|[^"|]*\|[^"|]*\|([^"|]+)\|[^"]*"') + +# What each platform is called in prose, where that differs from its registry +# token. Tokens and class names are derived, so this is the only hand-kept part +# of the vocabulary — and every registered token must appear, so adding a +# platform forces the decision rather than silently widening the gap. +ALIASES = { + "StdAtomic": [], + "FatFs": ["FatFs", "ChaN"], + "FreeRtos": ["FreeRTOS"], + "LwipRaw": ["lwIP"], + "MbedTls": ["Mbed TLS", "mbedTLS", "mbedtls"], + "OpenSsl": ["OpenSSL"], + # Plus-FAT and Plus-TCP are FreeRTOS-Plus-* products, so "FreeRTOS" is part + # of their own identity as much as it is the kernel pack's — each may name + # the kernel it sits on. The pack's token stays the pack's, so a page that + # points at the FreeRtos platform is still caught. + "PlusFat": ["FreeRTOS-Plus-FAT", "FreeRTOS"], + "PlusTcp": ["FreeRTOS-Plus-TCP", "FreeRTOS"], + "Posix": ["POSIX"], + "Windows": ["Winsock", "Win32"], +} + +# Deliberate exemptions, each with the reason it is not a boundary breach. +# Printed on every run: an exemption nobody sees is an exemption nobody +# revisits. +ALLOWED = [ + ( + "Platform/Windows/Source/SolidSyslogWindowsFile.c", + "POSIX", + "names the OS standard the MSVC call is measured against, not the Posix platform", + ), + ( + "Platform/Windows/Source/SolidSyslogWinsockTcpStream.c", + "POSIX", + "names the OS standard the MSVC call is measured against, not the Posix platform", + ), +] + +SCANNED_SUFFIXES = (".c", ".h", ".md") + +# An #include names a header the compiler must find, not a platform the prose +# is describing. The boundary is editorial; what a translation unit depends on +# is the build's business and is governed there. +INCLUDE = re.compile(r"^\s*#\s*include") + + +def read(*parts): + with open(os.path.join(ROOT, *parts), encoding="utf-8") as handle: + return handle.read() + + +def registered(): + """[(token, directory)] from the registry, which is the single declaration.""" + found = REGISTRY.search(read("CMakeLists.txt")) + if found is None: + sys.exit("SOLIDSYSLOG_PLATFORM_REGISTRY not found in CMakeLists.txt") + return ROW.findall(found.group(1)) + + +def documented(): + """Slugs that have a docs folder, whether or not anything registered them.""" + platforms = os.path.join(ROOT, "docs", "platforms") + return { + name + for name in os.listdir(platforms) + if os.path.isdir(os.path.join(platforms, name)) + } + + +def vocabulary(rows): + """Every term that names a platform, mapped to the tokens that may use it. + + Three sources, only one of them hand-kept: the registry token itself, the + prose aliases above, and the class names taken from the platform's own + Interface headers — in both their full and bare spellings, since prose says + PosixTcpStream where code says SolidSyslogPosixTcpStream. + + A term can have more than one owner: "FreeRTOS" belongs to the kernel pack + and to the two FreeRTOS-Plus-* platforms built on it alike. + """ + terms = {} + for token, directory in rows: + for alias in [token] + ALIASES.get(token, []): + terms.setdefault(alias, set()).add(token) + interface = os.path.join(ROOT, directory, "Interface") + for header in sorted(os.listdir(interface)) if os.path.isdir(interface) else []: + if header.endswith(".h"): + stem = header[: -len(".h")] + terms.setdefault(stem, set()).add(token) + terms.setdefault(stem[len("SolidSyslog") :], set()).add(token) + return terms + + +def scanned(directory, slug): + """Everything that speaks for one platform: its own tree, and its pages.""" + for base in (os.path.join(ROOT, directory), os.path.join(ROOT, "docs", "platforms", slug)): + for path, _, names in os.walk(base): + for name in sorted(names): + if name.endswith(SCANNED_SUFFIXES): + yield os.path.relpath(os.path.join(path, name), ROOT) + + +DESCRIPTIONS = os.path.join("hooks", "page_descriptions.py") + + +def described(slug): + """The meta descriptions a platform's pages publish, with line numbers. + + They are page content — the snippet a search result shows — but they live in + a hook rather than in the page, so the walk above never reached them. That + gap is not theoretical: three platform descriptions named a sibling platform + while this check reported the boundary clean. + """ + keys = (f'"platforms/{slug}/index.md":', f'"platforms/{slug}/setup.md":') + inside = False + for number, line in enumerate(read(DESCRIPTIONS).splitlines(), 1): + if line.strip().startswith(keys): + inside = True + if inside: + yield number, line + if line.rstrip().endswith("),"): + inside = False + + +def speaks_for(directory, slug): + """Every line that speaks for one platform, as (source, line number, text). + + Its own tree, its pages, and the descriptions published for those pages. + """ + for relative in scanned(directory, slug): + for number, line in enumerate(read(relative).splitlines(), 1): + yield relative, number, line + for number, line in described(slug): + yield DESCRIPTIONS, number, line + + +def naming_faults(rows, terms): + """Flag a platform naming another, longest term first so FreeRTOS-Plus-TCP + is read as itself rather than as FreeRTOS.""" + ordered = sorted(terms, key=len, reverse=True) + pattern = re.compile(r"(?Definition.h, the declaration.""" + interface = os.path.join(ROOT, "Core", "Interface") + suffix = "Definition.h" + return sorted( + name[len("SolidSyslog") : -len(suffix)] + for name in os.listdir(interface) + if name.startswith("SolidSyslog") and name.endswith(suffix) + ) + + +def role_faults(): + """Every declared role is listed wherever roles are enumerated, and nothing + is listed that is not declared.""" + faults = [] + # Where roles are enumerated, and how far each sits from the api/ tree. + listings = { + os.path.join("docs", "porting.md"): "api/", + os.path.join("docs", "roles", "index.md"): "../api/", + "mkdocs.yml": "api/", + } + roles = declared_roles() + + for listing, prefix in listings.items(): + text = read(listing) + for role in roles: + if f"{prefix}structSolidSyslog{role}.md" not in text: + faults.append(f"{role}: declared by its Definition.h but not linked from {listing}") + # A role page that outlived its contract — the rename nobody finished. + for orphan in re.findall(rf"{re.escape(prefix)}structSolidSyslog(\w+)\.md", text): + if orphan not in roles: + faults.append(f"{listing} links {orphan} as a role, but no SolidSyslog{orphan}Definition.h declares it") + return faults + + +def unlisted_headers(rows): + """The *What it ships* manifest is generated by hooks/platform_backlinks.py + from the platform's own Interface directory, so every header is listed by + construction. What can still go wrong is the heading disappearing, which + would silently take the whole manifest with it.""" + faults = [] + for token, _ in rows: + page = os.path.join("docs", "platforms", token.lower(), "index.md") + if os.path.isfile(os.path.join(ROOT, page)) and "## What it ships" not in read(page): + faults.append(f"{token}: {page} has no '## What it ships' heading for the manifest") + return faults + + +def check(): + faults = [] + nav = read("mkdocs.yml") + descriptions = read("hooks", "page_descriptions.py") + matrix = read("docs", "platforms", "index.md") + slugs = set() + rows = registered() + + for token, _ in rows: + if token not in ALIASES: + faults.append(f"{token}: no ALIASES entry — add its prose spellings, or [] if the token is the only one") + + for token, directory in rows: + slug = token.lower() + slugs.add(slug) + docs = os.path.join("docs", "platforms", slug) + + for page in ("index.md", "setup.md"): + if not os.path.isfile(os.path.join(ROOT, docs, page)): + faults.append(f"{token}: {docs}/{page} is missing") + + if f"platforms/{slug}/index.md" not in nav: + faults.append(f"{token}: no entry in the mkdocs.yml nav") + for page in ("index.md", "setup.md"): + if f'"platforms/{slug}/{page}"' not in descriptions: + faults.append(f"{token}: no meta description for platforms/{slug}/{page}") + + if f"]({slug}/index.md)" not in matrix: + faults.append(f"{token}: not a row in the docs/platforms/index.md matrix") + + for slug in sorted(documented() - slugs): + faults.append(f"docs/platforms/{slug}/ documents a platform that is not registered") + + faults.extend(unlisted_headers(rows)) + faults.extend(naming_faults(rows, vocabulary(rows))) + faults.extend(role_faults()) + return faults + + +if __name__ == "__main__": + problems = check() + for problem in problems: + print(f"error: {problem}", file=sys.stderr) + if problems: + print( + f"\n{len(problems)} problem(s). A platform is declared in " + "SOLIDSYSLOG_PLATFORM_REGISTRY; everything above follows from its token.", + file=sys.stderr, + ) + sys.exit(1) + for path, term, reason in ALLOWED: + print(f"allowed: {path} may say {term} — {reason}") + print( + f"docs match the code: {len(registered())} platforms, all documented and none naming " + f"another; {len(declared_roles())} roles, each listed everywhere roles are enumerated" + )