diff --git a/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c b/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c new file mode 100644 index 00000000..d02a59f8 --- /dev/null +++ b/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c @@ -0,0 +1,891 @@ +/* Shared FreeRTOS BDD-target pipeline — see BddTargetFreeRtosPipeline.h. + * + * Extracted from the two near-identical FreeRTOS target main.c files + * (Bdd/Targets/FreeRtos = FreeRTOS-Plus-TCP, Bdd/Targets/FreeRtosLwip = lwIP) + * in SolidSyslog S29.03. Everything here is platform-independent; the network + * backend each target wires stays in its main.c behind the + * BddTargetFreeRtosPipelineConfig seam. */ + +#include "BddTargetFreeRtosPipeline.h" + +#include "BddTargetEnterpriseId.h" +#include "BddTargetErrorText.h" +#include "BddTargetInteractive.h" +#include "BddTargetIps.h" +#include "BddTargetLanguage.h" +#include "BddTargetSwitchConfig.h" +#include "BddTargetTlsSender.h" +#include "CmsdkUart.h" + +#include "SolidSyslog.h" +#include "SolidSyslogAtomicCounter.h" +#include "SolidSyslogBlockStore.h" +#include "SolidSyslogCircularBuffer.h" +#include "SolidSyslogConfig.h" +#include "SolidSyslogCrc16Policy.h" +#include "SolidSyslogEndpoint.h" +#include "SolidSyslogError.h" +#include "SolidSyslogFatFsFile.h" +#include "SolidSyslogFileBlockDevice.h" +#include "SolidSyslogFormatter.h" +#include "SolidSyslogFreeRtosMutex.h" +#include "SolidSyslogFreeRtosSysUpTime.h" +#include "SolidSyslogMbedTlsAesGcmPolicy.h" +#include "SolidSyslogMbedTlsHmacSha256Policy.h" +#include "SolidSyslogMetaSd.h" +#include "SolidSyslogMutex.h" +#include "SolidSyslogNullSecurityPolicy.h" +#include "SolidSyslogNullStore.h" +#include "SolidSyslogOriginSd.h" +#include "SolidSyslogPrival.h" +#include "SolidSyslogStdAtomicCounter.h" +#include "SolidSyslogTimeQuality.h" +#include "SolidSyslogTimeQualitySd.h" +#include "SolidSyslogTunables.h" + +#include "ff.h" /* f_mount / f_mkfs — eager mount-or-format on the `set store file` rebuild trigger. */ + +#include +#include + +#include +#include +#include +#include +#include + +/* Unprivileged mirror of SOLIDSYSLOG_UDP_DEFAULT_PORT (514) for BDD listeners. */ +#define BDD_TARGET_UDP_PORT 5514U + +/* The injected platform seam — set once via _SetConfig before the tasks run. */ +static const struct BddTargetFreeRtosPipelineConfig* g_config = NULL; + +/* Mutable walking-skeleton state. Defaults populated at boot; the interactive + * `set ` command rewrites these in-place via OnSet. Storage sizes + * match RFC 5424 maxima where applicable (APP-NAME 48, MSGID 32) plus null + * terminator; MSG matches SOLIDSYSLOG_MAX_MESSAGE_SIZE; host fits an IPv4 + * dotted-quad or the short DNS alias. */ +static char appName[49] = "SolidSyslogBddTarget"; +static char messageId[33] = "example"; +static char msg[SOLIDSYSLOG_MAX_MESSAGE_SIZE] = "Hello from FreeRTOS"; +static char host[16] = ""; +static uint16_t port = (uint16_t) BDD_TARGET_UDP_PORT; +static uint32_t endpointVersion = 0U; + +static struct SolidSyslogMessage testMessage = { + .Facility = SOLIDSYSLOG_FACILITY_LOCAL0, + .Severity = SOLIDSYSLOG_SEVERITY_INFORMATIONAL, + .MessageId = messageId, + .Msg = msg, +}; + +/* CircularBuffer + FreeRtosMutex for cross-task emission. 8 max-sized messages + * is comfortably above the 3-message BDD scenarios. */ +enum +{ + BDD_TARGET_BUFFER_MESSAGES = 8 +}; + +static uint8_t bufferRing[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(BDD_TARGET_BUFFER_MESSAGES)]; + +/* Lifecycle mutex serialises SolidSyslog_Service against the rebuild path + * (`set store file` swaps NullStore for the file-backed BlockStore) and against + * teardown. solidSyslogTeardown is set inside the critical section so Service + * observes it atomically with the destroy and self-deletes before the mutex + * goes. */ +static struct SolidSyslogMutex* lifecycleMutex = NULL; +static struct SolidSyslog* solidSyslog = NULL; +static volatile bool solidSyslogReady = false; +static volatile bool solidSyslogTeardown = false; + +/* File-backed store storage. Lives in .bss so it persists across the `set store + * file` rebuild; only populated when that command fires. STORE_PATH_PREFIX is + * "STORE" — sequence-numbered FAT filenames land as STORE00.log, STORE01.log, + * … which fit 8.3 short-filename mode (LFN=0 in our shared ffconf.h). */ +static const char STORE_PATH_PREFIX[] = "STORE"; + +/* FATFS object lives in .bss because f_mount stores its address inside the FatFs + * volume registry — the object must outlive every f_open / f_stat / f_unlink. + * One per volume (FF_VOLUMES = 1). */ +static FATFS fatfs; +static bool fatfsMounted = false; + +static struct SolidSyslogFile* storeFile = NULL; +static struct SolidSyslogBlockDevice* storeBlockDevice = NULL; +static struct SolidSyslogStore* currentStore = NULL; +static bool currentStoreIsFile = false; + +/* Pending values populated by the `set max-blocks` / `max-block-size` / + * `discard-policy` / `halt-exit` / `capacity-threshold` / `security-policy` / + * `no-sd` commands and consumed by `set store file`. */ +enum +{ + DEFAULT_PENDING_MAX_BLOCKS = 10, + DEFAULT_PENDING_MAX_BLOCK_SIZE = 65536, +}; + +static size_t pendingMaxBlocks = DEFAULT_PENDING_MAX_BLOCKS; +static size_t pendingMaxBlockSize = DEFAULT_PENDING_MAX_BLOCK_SIZE; +static const char* pendingDiscardPolicy = "oldest"; +static volatile bool pendingHaltExit = false; +static size_t pendingCapacityThreshold = 0; +/* At-rest integrity policy for the file store: "crc16" (default), "hmac-sha256" + * (mbedTLS), "aes-256-gcm" (mbedTLS AEAD), or "null". Set before `store file`; + * consumed by RebuildWithFileStore. currentPolicy holds the created handle so + * DestroyCurrentStore can release it. */ +static const char* pendingSecurityPolicy = "crc16"; +static struct SolidSyslogSecurityPolicy* currentPolicy = NULL; +/* The policy kind captured when currentPolicy was built — DestroySecurityPolicy + * must dispatch on this, NOT pendingSecurityPolicy, which a later `set + * security-policy` can change out from under the installed policy. */ +static const char* installedSecurityPolicy = "crc16"; +/* When true, SolidSyslog gets only the meta SD — timeQuality and origin are + * dropped. Mirrors Linux's --no-sd. */ +static volatile bool pendingNoSd = false; + +/* Holds the final SolidSyslog config so the rebuild path can rewrite .Store and + * pass the same struct back into SolidSyslog_Create. */ +static struct SolidSyslogConfig solidSyslogConfig; +static struct SolidSyslogStructuredData* sdList[3]; +static struct SolidSyslogAtomicCounter* atomicCounter = NULL; +static struct SolidSyslogStructuredData* metaSd = NULL; +static struct SolidSyslogStructuredData* timeQualitySd = NULL; +static struct SolidSyslogStructuredData* originSd = NULL; + +static struct SolidSyslogBuffer* buffer = NULL; +static struct SolidSyslogMutex* bufferMutex = NULL; + +/* Service task handle is self-registered by ServiceTask so the interactive task + * can report its peak stack alongside its own on `quit` and bound the teardown + * wait on a real "service stopped" signal. */ +static TaskHandle_t serviceTaskHandle = NULL; +/* The task awaiting ServiceTask's exit during teardown; ServiceTask notifies it + * the instant before it self-deletes. */ +static TaskHandle_t serviceStopWaiter = NULL; + +/* Upper bound on the teardown wait for ServiceTask to self-delete. */ +enum +{ + SERVICE_STOP_TIMEOUT_MS = 1000, +}; + +static bool TryUpdateString(char* storage, size_t storageSize, const char* value); +static bool TryParseUInt(const char* value, unsigned long* out); +static bool OnSet(const char* name, const char* value); +static struct SolidSyslogSecurityPolicy* CreateSecurityPolicy(void); +static void DestroySecurityPolicy(void); +static bool RebuildWithFileStore(void); +static bool EnsureFatFsMounted(void); +static void DestroyCurrentStore(void); +static enum SolidSyslogDiscardPolicy MapDiscardPolicy(const char* policy); +static void OnStoreFull(void* context); +static size_t GetCapacityThreshold(void* context); +static void OnThresholdCrossed(void* context); +static void TeardownAll(void); +static void GetAppName(struct SolidSyslogFormatter* formatter); +static void GetTimeQuality(struct SolidSyslogTimeQuality* timeQuality); +static void ErrorHandlerEx(void* context, const struct SolidSyslogErrorEvent* event); + +/* ---- console glue ---------------------------------------------------------- */ + +static uint32_t MmioRead32(uintptr_t address) +{ + // NOLINTNEXTLINE(performance-no-int-to-ptr) -- mapping the CMSDK UART MMIO address into a 32-bit volatile pointer. + return *(volatile uint32_t*) address; +} + +static void MmioWrite32(uintptr_t address, uint32_t value) +{ + // NOLINTNEXTLINE(performance-no-int-to-ptr) -- mapping the CMSDK UART MMIO address into a 32-bit volatile pointer. + *(volatile uint32_t*) address = value; +} + +void BddTargetFreeRtosPipeline_Sleep(int milliseconds) +{ + /* Round any non-zero millisecond request up to at least one tick so a + * sub-tick sleep (e.g. CmsdkUart's 1 ms yield against a 100 Hz tick) still + * blocks the task instead of busy-spinning. */ + TickType_t ticks = pdMS_TO_TICKS((TickType_t) milliseconds); + if ((milliseconds > 0) && (ticks == 0U)) + { + ticks = 1U; + } + vTaskDelay(ticks); +} + +static const CmsdkUartMemoryAccess MMIO_ACCESS = {MmioRead32, MmioWrite32, BddTargetFreeRtosPipeline_Sleep}; + +void BddTargetFreeRtosPipeline_InitConsole(uint32_t uartBaseAddress) +{ + CmsdkUart_Init(&MMIO_ACCESS, uartBaseAddress); +} + +void BddTargetFreeRtosPipeline_SetConfig(const struct BddTargetFreeRtosPipelineConfig* config) +{ + g_config = config; +} + +/* ---- SolidSyslog config callbacks ------------------------------------------ */ + +static void GetAppName(struct SolidSyslogFormatter* formatter) +{ + SolidSyslogFormatter_BoundedString(formatter, appName, strlen(appName)); +} + +/* No RTC and no time-sync on these reference targets — RFC 5424 §6.2.3.1 + * mandates NILVALUE TIMESTAMP, and the timeQuality SD reports tzKnown=0, + * isSynced=0. SolidSyslogConfig.Clock=NULL drops through to the library's + * NilClock. */ +static void GetTimeQuality(struct SolidSyslogTimeQuality* timeQuality) +{ + timeQuality->TzKnown = false; + timeQuality->IsSynced = false; + timeQuality->SyncAccuracyMicroseconds = SOLIDSYSLOG_SYNC_ACCURACY_OMIT; +} + +void BddTargetFreeRtosPipeline_GetEndpoint(struct SolidSyslogEndpoint* endpoint) +{ + SolidSyslogFormatter_BoundedString(endpoint->Host, host, strlen(host)); + endpoint->Port = port; +} + +uint32_t BddTargetFreeRtosPipeline_GetEndpointVersion(void) +{ + return endpointVersion; +} + +static void ErrorHandlerEx(void* context, const struct SolidSyslogErrorEvent* event) +{ + (void) context; + const char* sourceName = ""; + const struct SolidSyslogErrorSource* source = event->Source; + if (source != NULL) + { + sourceName = source->Name; + } + const char* message = BddTargetErrorText_Category(event->Category); + (void) printf( + "[solidsyslog] severity=%d [%s cat=%u detail=%ld] %s\n", + (int) event->Severity, + sourceName, + (unsigned) event->Category, + (long) event->Detail, + message + ); +} + +/* ---- interactive `set` handler --------------------------------------------- */ + +static bool OnSet(const char* name, const char* value) +{ + if (strcmp(name, "appname") == 0) + { + return TryUpdateString(appName, sizeof(appName), value); + } + if (strcmp(name, "msgid") == 0) + { + return TryUpdateString(messageId, sizeof(messageId), value); + } + if (strcmp(name, "msg") == 0) + { + return TryUpdateString(msg, sizeof(msg), value); + } + if (strcmp(name, "host") == 0) + { + return TryUpdateString(host, sizeof(host), value); + } + if (strcmp(name, "port") == 0) + { + unsigned long parsed = 0U; + if (!TryParseUInt(value, &parsed) || parsed == 0U || parsed > UINT16_MAX) + { + return false; + } + port = (uint16_t) parsed; + endpointVersion++; + return true; + } + if (strcmp(name, "facility") == 0) + { + /* Forward the parsed value unchanged so the library is the single + * authority on what's valid (out-of-range encodes as PRIVAL 43). */ + unsigned long parsed = 0U; + if (!TryParseUInt(value, &parsed)) + { + return false; + } + testMessage.Facility = (enum SolidSyslogFacility) parsed; + return true; + } + if (strcmp(name, "severity") == 0) + { + unsigned long parsed = 0U; + if (!TryParseUInt(value, &parsed)) + { + return false; + } + testMessage.Severity = (enum SolidSyslogSeverity) parsed; + return true; + } + if (strcmp(name, "transport") == 0) + { + BddTargetSwitchConfig_SetByName(value); + return true; + } + if (strcmp(name, "max-blocks") == 0) + { + unsigned long parsed = 0U; + if (!TryParseUInt(value, &parsed)) + { + return false; + } + pendingMaxBlocks = (size_t) parsed; + return true; + } + if (strcmp(name, "max-block-size") == 0) + { + unsigned long parsed = 0U; + if (!TryParseUInt(value, &parsed)) + { + return false; + } + pendingMaxBlockSize = (size_t) parsed; + return true; + } + if (strcmp(name, "discard-policy") == 0) + { + if ((strcmp(value, "oldest") != 0) && (strcmp(value, "newest") != 0) && (strcmp(value, "halt") != 0)) + { + return false; + } + /* String literal storage — target_driver.py emits one of the three + * literals so the pointer stays valid (no copy needed). */ + pendingDiscardPolicy = + (strcmp(value, "newest") == 0) ? "newest" : ((strcmp(value, "halt") == 0) ? "halt" : "oldest"); + return true; + } + if (strcmp(name, "security-policy") == 0) + { + if ((strcmp(value, "crc16") != 0) && (strcmp(value, "hmac-sha256") != 0) && + (strcmp(value, "aes-256-gcm") != 0) && (strcmp(value, "null") != 0)) + { + return false; + } + /* String literal storage — see discard-policy above. */ + if (strcmp(value, "hmac-sha256") == 0) + { + pendingSecurityPolicy = "hmac-sha256"; + } + else if (strcmp(value, "aes-256-gcm") == 0) + { + pendingSecurityPolicy = "aes-256-gcm"; + } + else if (strcmp(value, "null") == 0) + { + pendingSecurityPolicy = "null"; + } + else + { + pendingSecurityPolicy = "crc16"; + } + return true; + } + if (strcmp(name, "capacity-threshold") == 0) + { + unsigned long parsed = 0U; + if (!TryParseUInt(value, &parsed)) + { + return false; + } + pendingCapacityThreshold = (size_t) parsed; + return true; + } + if (strcmp(name, "halt-exit") == 0) + { + /* Harness emits `set halt-exit 1` (or `0`); anything non-zero trips the + * halt path. Mirrors Linux's bare --halt-exit flag. */ + unsigned long parsed = 0U; + if (!TryParseUInt(value, &parsed)) + { + return false; + } + pendingHaltExit = (parsed != 0U); + return true; + } + if (strcmp(name, "no-sd") == 0) + { + /* `set no-sd 1` drops the SD list to only metaSd — mirrors Linux's + * --no-sd. Takes effect via SolidSyslog re-Create. */ + unsigned long parsed = 0U; + if (!TryParseUInt(value, &parsed)) + { + return false; + } + pendingNoSd = (parsed != 0U); + return true; + } + if (strcmp(name, "store") == 0) + { + /* "null" is the default state — accept it as a no-op so the harness can + * pass --store null without special-casing. "file" triggers the rebuild + * (one-way for the lifetime of this QEMU instance). */ + if (strcmp(value, "null") == 0) + { + return true; + } + if (strcmp(value, "file") == 0) + { + return RebuildWithFileStore(); + } + return false; + } + if (strcmp(name, "shutdown") == 0) + { + /* Any non-zero value triggers a full teardown then exits QEMU. The BDD + * `the client is killed` step on freertos sends `set shutdown 1`. */ + unsigned long parsed = 0U; + if (!TryParseUInt(value, &parsed)) + { + return false; + } + if (parsed != 0U) + { + TeardownAll(); + BddTargetFreeRtosPipeline_Exit(0); + } + return true; + } + return false; +} + +static bool TryUpdateString(char* storage, size_t storageSize, const char* value) +{ + size_t length = strlen(value); + if ((length == 0U) || (length >= storageSize)) + { + return false; + } + memcpy(storage, value, length); + storage[length] = '\0'; + return true; +} + +static bool TryParseUInt(const char* value, unsigned long* out) +{ + if (*value == '\0') + { + return false; + } + char* end = NULL; + unsigned long parsed = strtoul(value, &end, 10); + /* strtoul accepts a leading '-' and wraps to a huge unsigned. The port call + * site bounds-checks; facility/severity intentionally don't, mirroring the + * Linux example's atoi-and-cast so wrapped values encode as PRIVAL 43. */ + if (*end != '\0') + { + return false; + } + *out = parsed; + return true; +} + +/* ---- store + security policy lifecycle ------------------------------------- */ + +/* DEMO KEY ONLY. A real integrator supplies key material from a secure element, + * a KDF, or encrypted NVM via their own SolidSyslogKeyFunction — never a + * hard-coded constant. This exists so the BDD scenario can exercise the mbedTLS + * HMAC-SHA256 / AES-256-GCM at-rest policies end-to-end with real crypto. */ +static bool BddDemoGetKey(void* context, uint8_t* keyOut, size_t capacity, size_t* keyLengthOut) +{ + enum + { + DEMO_KEY_SIZE = 32 + }; + + (void) context; + size_t written = (capacity < DEMO_KEY_SIZE) ? capacity : (size_t) DEMO_KEY_SIZE; + (void) memset(keyOut, 0x5A, written); + *keyLengthOut = written; + return true; +} + +static struct SolidSyslogSecurityPolicy* CreateSecurityPolicy(void) +{ + /* Latch the kind now so teardown destroys against what we actually built, + * not whatever `set security-policy` may set afterwards. pendingSecurityPolicy + * is always one of the four validated string literals. */ + installedSecurityPolicy = pendingSecurityPolicy; + struct SolidSyslogSecurityPolicy* policy = NULL; + if (strcmp(pendingSecurityPolicy, "hmac-sha256") == 0) + { + static const struct SolidSyslogMbedTlsHmacSha256PolicyConfig hmacConfig = {BddDemoGetKey, NULL}; + policy = SolidSyslogMbedTlsHmacSha256Policy_Create(&hmacConfig); + } + else if (strcmp(pendingSecurityPolicy, "aes-256-gcm") == 0) + { + /* Reuse the TLS module's already-seeded CTR-DRBG as the AEAD nonce + * source — see BddTargetTlsSender_GetRng. Not static const: Rng is a + * runtime handle. */ + const struct SolidSyslogMbedTlsAesGcmPolicyConfig aesConfig = + {BddDemoGetKey, NULL, BddTargetTlsSender_GetRng()}; + policy = SolidSyslogMbedTlsAesGcmPolicy_Create(&aesConfig); + } + else if (strcmp(pendingSecurityPolicy, "null") == 0) + { + policy = SolidSyslogNullSecurityPolicy_Get(); + } + else + { + policy = SolidSyslogCrc16Policy_Create(); + } + return policy; +} + +/* `set store file` trigger: swap the default NullStore for a FatFs-backed + * BlockStore. One-way for the lifetime of this QEMU instance. The lifecycle + * mutex blocks the Service task across the Destroy → re-Create transition. */ +static bool RebuildWithFileStore(void) +{ + SolidSyslogMutex_Lock(lifecycleMutex); + + /* FatFs does NOT auto-mount on first f_open — mount (and format-on-first-use) + * before tearing down the existing store so a mount failure leaves the + * target running on the original NullStore (zero-disruption); return false + * so OnSet reports the failure to the harness. */ + if (!EnsureFatFsMounted()) + { + SolidSyslogMutex_Unlock(lifecycleMutex); + return false; + } + + solidSyslogReady = false; + SolidSyslog_Destroy(solidSyslog); + solidSyslog = NULL; + DestroyCurrentStore(); + + storeFile = SolidSyslogFatFsFile_Create(); + storeBlockDevice = SolidSyslogFileBlockDevice_Create(storeFile, STORE_PATH_PREFIX); + + struct SolidSyslogSecurityPolicy* policy = CreateSecurityPolicy(); + currentPolicy = policy; + struct SolidSyslogBlockStoreConfig storeConfig = { + .BlockDevice = storeBlockDevice, + .MaxBlockSize = pendingMaxBlockSize, + .MaxBlocks = pendingMaxBlocks, + .DiscardPolicy = MapDiscardPolicy(pendingDiscardPolicy), + .SecurityPolicy = policy, + .OnStoreFull = OnStoreFull, + .StoreFullContext = NULL, + .GetCapacityThreshold = GetCapacityThreshold, + .OnThresholdCrossed = OnThresholdCrossed, + .ThresholdContext = &pendingCapacityThreshold, + }; + currentStore = SolidSyslogBlockStore_Create(&storeConfig); + currentStoreIsFile = true; + + solidSyslogConfig.Store = currentStore; + /* Re-honour `set no-sd 1` if it arrived before this rebuild — target_driver.py + * sorts `set no-sd` before `set store file`, so the value is final here. */ + solidSyslogConfig.SdCount = pendingNoSd ? 1U : (sizeof(sdList) / sizeof(sdList[0])); + solidSyslog = SolidSyslog_Create(&solidSyslogConfig); + solidSyslogReady = true; + SolidSyslogMutex_Unlock(lifecycleMutex); + return true; +} + +/* Mount volume 0; format-on-first-use if the disk image has no FAT yet. + * Idempotent — subsequent calls short-circuit on fatfsMounted. The work buffer + * for f_mkfs is sized to FF_MAX_SS (512 B), the minimum f_mkfs accepts on a + * FAT12/16 volume. */ +static bool EnsureFatFsMounted(void) +{ + if (fatfsMounted) + { + return true; + } + FRESULT res = f_mount(&fatfs, "", 1); /* opt=1 → mount immediately, surface FR_NO_FILESYSTEM here */ + if (res == FR_NO_FILESYSTEM) + { + /* Fresh disk image — lay down a FAT and re-mount. FM_FAT keeps the + * formatter on FAT12/16; at the shared 8 MiB geometry auto cluster + * sizing clears the ~4085-cluster boundary, so this lands FAT16 (the + * geometry the later FreeRTOS-Plus-FAT formatter needs). */ + static BYTE workBuffer[FF_MAX_SS]; + const MKFS_PARM opts = {.fmt = FM_FAT | FM_SFD, .n_fat = 1, .align = 1, .n_root = 0, .au_size = 0}; + res = f_mkfs("", &opts, workBuffer, sizeof(workBuffer)); + if (res == FR_OK) + { + res = f_mount(&fatfs, "", 1); + } + } + if (res != FR_OK) + { + (void) printf("[solidsyslog] fatfs mount failed: FRESULT=%d\n", (int) res); + return false; + } + fatfsMounted = true; + return true; +} + +static void DestroySecurityPolicy(void) +{ + if (strcmp(installedSecurityPolicy, "hmac-sha256") == 0) + { + SolidSyslogMbedTlsHmacSha256Policy_Destroy(currentPolicy); + } + else if (strcmp(installedSecurityPolicy, "aes-256-gcm") == 0) + { + SolidSyslogMbedTlsAesGcmPolicy_Destroy(currentPolicy); + } + else if (strcmp(installedSecurityPolicy, "crc16") == 0) + { + SolidSyslogCrc16Policy_Destroy(); + } + /* else "null": the shared NullSecurityPolicy is immutable — nothing to free. */ + currentPolicy = NULL; +} + +/* Tears down whichever store is currently installed (file-backed or null). + * FatFsFile_Destroy → Close → f_close flushes the underlying FIL's dir entry. */ +static void DestroyCurrentStore(void) +{ + if (currentStoreIsFile) + { + SolidSyslogBlockStore_Destroy(currentStore); + SolidSyslogFileBlockDevice_Destroy(storeBlockDevice); + DestroySecurityPolicy(); + SolidSyslogFatFsFile_Destroy(storeFile); + } + /* else: NullStore is shared and immutable — nothing to destroy. */ +} + +static enum SolidSyslogDiscardPolicy MapDiscardPolicy(const char* policy) +{ + if (strcmp(policy, "newest") == 0) + { + return SOLIDSYSLOG_DISCARD_POLICY_NEWEST; + } + if (strcmp(policy, "halt") == 0) + { + return SOLIDSYSLOG_DISCARD_POLICY_HALT; + } + return SOLIDSYSLOG_DISCARD_POLICY_OLDEST; +} + +static void OnStoreFull(void* context) +{ + (void) context; + if (pendingHaltExit) + { + /* Semihosting SYS_EXIT — terminates QEMU with status 2 so the BDD + * harness sees the run end deterministically. Mirrors the Linux + * example's _exit(2). */ + BddTargetFreeRtosPipeline_Exit(2); + } +} + +static size_t GetCapacityThreshold(void* context) +{ + return *(const size_t*) context; +} + +/* Stdout marker the behave harness watches for. The Linux equivalent writes a + * host file unreachable from the QEMU guest; instead we print a line-anchored + * token to the UART, which the captured-stdout reader in syslog_steps.py scans. */ +static void OnThresholdCrossed(void* context) +{ + (void) context; + (void) printf("[THRESHOLD-CROSSED]\r\n"); +} + +/* ---- teardown -------------------------------------------------------------- */ + +/* Full teardown of every shared resource. Two entry points — `quit` (falls + * through after BddTargetInteractive_Run returns) and `set shutdown 1` — both + * route through here. f_unmount fires regardless so the next session's f_mount + * finds STORE*.log directory entries up-to-date (power_cycle_replay relies on + * this). The lifecycle mutex held across the SolidSyslog + store destroy keeps + * Service from racing the teardown. The platform's network teardown runs last, + * after the shared resources are released. */ +static void TeardownAll(void) +{ + SolidSyslogMutex_Lock(lifecycleMutex); + serviceStopWaiter = xTaskGetCurrentTaskHandle(); + solidSyslogTeardown = true; + solidSyslogReady = false; + SolidSyslog_Destroy(solidSyslog); + solidSyslog = NULL; + SolidSyslogOriginSd_Destroy(originSd); + SolidSyslogTimeQualitySd_Destroy(timeQualitySd); + SolidSyslogMetaSd_Destroy(metaSd); + SolidSyslogStdAtomicCounter_Destroy(atomicCounter); + DestroyCurrentStore(); + if (fatfsMounted) + { + (void) f_unmount(""); + fatfsMounted = false; + } + SolidSyslogMutex_Unlock(lifecycleMutex); + + /* Wait for Service to observe the teardown flag and vTaskDelete itself + * before the lifecycle mutex is destroyed under it. Bounded so a Service + * task that never started (xTaskCreate failure → NULL handle) cannot wedge + * teardown. */ + if (serviceTaskHandle != NULL) + { + (void) ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(SERVICE_STOP_TIMEOUT_MS)); + } + serviceTaskHandle = NULL; + + SolidSyslogCircularBuffer_Destroy(buffer); + SolidSyslogFreeRtosMutex_Destroy(bufferMutex); + SolidSyslogFreeRtosMutex_Destroy(lifecycleMutex); + lifecycleMutex = NULL; + + /* Platform sender + network adapters last. */ + g_config->TeardownNetwork(); +} + +void BddTargetFreeRtosPipeline_Exit(int status) +{ + /* SYS_EXIT_EXTENDED (0x20) — the only ARM Semihosting exit form on AArch32 + * that propagates a non-zero status: R1 points to a { reason, subcode } + * block. QEMU terminates the VM; the for(;;) is defensive. */ + const struct + { + uint32_t reason; + uint32_t subcode; + } args = {0x20026U, (uint32_t) status}; + + register int r0 __asm("r0") = 0x20; + register const void* r1 __asm("r1") = &args; + __asm volatile("bkpt 0xAB" : "+r"(r0) : "r"(r1) : "memory"); + for (;;) + { + } +} + +/* ---- tasks ----------------------------------------------------------------- */ + +void BddTargetFreeRtosPipeline_InteractiveTask(void* argument) +{ + (void) argument; + + /* Seed the destination host with the platform default before any `set host`. */ + size_t hostLength = strlen(g_config->DefaultHost); + if (hostLength < sizeof(host)) + { + memcpy(host, g_config->DefaultHost, hostLength + 1U); + } + + /* Platform brings up its network and hands back a ready-to-use sender with + * the default transport already selected. */ + struct SolidSyslogSender* sender = g_config->BuildSender(); + + /* CircularBuffer drained by ServiceTask, with a FreeRtosMutex gating + * concurrent producers. */ + bufferMutex = SolidSyslogFreeRtosMutex_Create(); + buffer = SolidSyslogCircularBuffer_Create(bufferMutex, bufferRing, sizeof(bufferRing)); + + /* Lifecycle mutex created up front so the Service task can take it from its + * very first iteration without a NULL check. */ + lifecycleMutex = SolidSyslogFreeRtosMutex_Create(); + + /* Default store is NullStore — flipped to FatFs/BlockStore by `set store + * file` via RebuildWithFileStore(). */ + currentStore = SolidSyslogNullStore_Get(); + currentStoreIsFile = false; + + atomicCounter = SolidSyslogStdAtomicCounter_Create(); + struct SolidSyslogMetaSdConfig metaConfig = { + .Counter = atomicCounter, + .GetSysUpTime = SolidSyslogFreeRtosSysUpTime_Get, + .GetLanguage = BddTargetLanguage_Get, + }; + metaSd = SolidSyslogMetaSd_Create(&metaConfig); + timeQualitySd = SolidSyslogTimeQualitySd_Create(GetTimeQuality); + struct SolidSyslogOriginSdConfig originConfig = { + .Software = "SolidSyslogBddTarget", + .SwVersion = "0.7.0", + .EnterpriseId = BDD_TARGET_ENTERPRISE_ID, + .GetIpCount = BddTargetIps_Count, + .GetIpAt = BddTargetIps_At, + }; + originSd = SolidSyslogOriginSd_Create(&originConfig); + sdList[0] = metaSd; + sdList[1] = timeQualitySd; + sdList[2] = originSd; + + solidSyslogConfig = (struct SolidSyslogConfig) { + .Buffer = buffer, + .Sender = sender, + .Clock = NULL, + .GetHostname = g_config->GetHostname, + .GetAppName = GetAppName, + /* PROCID — RFC 5424 §6.2.6 NILVALUE: no process model on these targets. */ + .GetProcessId = NULL, + .Store = currentStore, + .Sd = sdList, + .SdCount = pendingNoSd ? 1U : (sizeof(sdList) / sizeof(sdList[0])), + }; + SolidSyslog_SetErrorHandler(ErrorHandlerEx, NULL); + solidSyslog = SolidSyslog_Create(&solidSyslogConfig); + solidSyslogReady = true; + + BddTargetInteractive_Run(solidSyslog, &testMessage, stdin, BddTargetSwitchConfig_SetByName, OnSet); + + /* Peak stack usage report on `quit`. Words, not bytes (StackType_t units). + * serviceTaskHandle is guarded because uxTaskGetStackHighWaterMark(NULL) + * means "the calling task". */ + const UBaseType_t interactiveHwm = uxTaskGetStackHighWaterMark(NULL); + const UBaseType_t serviceHwm = (serviceTaskHandle != NULL) ? uxTaskGetStackHighWaterMark(serviceTaskHandle) : 0U; + (void) printf( + "[stack-hwm] interactive=%lu words service=%lu words\n", + (unsigned long) interactiveHwm, + (unsigned long) serviceHwm + ); + + TeardownAll(); + vTaskDelete(NULL); +} + +void BddTargetFreeRtosPipeline_ServiceTask(void* argument) +{ + (void) argument; + /* Self-register so the interactive task can report our stack HWM and bound + * its teardown wait on a real "service stopped" signal. */ + serviceTaskHandle = xTaskGetCurrentTaskHandle(); + + /* Wait until the interactive task has finished initial Setup and created the + * lifecycle mutex / SolidSyslog. After that the mutex is the source of + * truth — Setup, RebuildWithFileStore, and Teardown all hold it across their + * Destroy/Create transitions. */ + while ((lifecycleMutex == NULL) || !solidSyslogReady) + { + vTaskDelay(pdMS_TO_TICKS(1)); + } + for (;;) + { + SolidSyslogMutex_Lock(lifecycleMutex); + if (solidSyslogTeardown) + { + /* Teardown set this flag inside the lifecycle critical section and is + * now blocked waiting for us. Capture the waiter under the lock, + * release, notify it, then self-delete before Teardown destroys the + * mutex. */ + TaskHandle_t waiter = serviceStopWaiter; + SolidSyslogMutex_Unlock(lifecycleMutex); + if (waiter != NULL) + { + (void) xTaskNotifyGive(waiter); + } + vTaskDelete(NULL); + } + if (solidSyslogReady) + { + SolidSyslog_Service(solidSyslog); + } + SolidSyslogMutex_Unlock(lifecycleMutex); + vTaskDelay(pdMS_TO_TICKS(1)); + } +} diff --git a/Bdd/Targets/Common/BddTargetFreeRtosPipeline.h b/Bdd/Targets/Common/BddTargetFreeRtosPipeline.h new file mode 100644 index 00000000..f4c251a5 --- /dev/null +++ b/Bdd/Targets/Common/BddTargetFreeRtosPipeline.h @@ -0,0 +1,73 @@ +#ifndef BDD_TARGET_FREE_RTOS_PIPELINE_H +#define BDD_TARGET_FREE_RTOS_PIPELINE_H + +#include "SolidSyslogEndpoint.h" +#include "SolidSyslogSender.h" +#include "SolidSyslogStringFunction.h" + +#include + +/* Shared FreeRTOS BDD-target pipeline. + * + * Everything platform-independent lives in this component: the SolidSyslog + * lifecycle, the FatFs-backed store + security-policy machinery (crc16 / + * hmac-sha256 / aes-256-gcm / null), the SD set, the interactive `set` handler, + * the CircularBuffer + Service drain task, the console glue, and the + * mount/format-on-first-use semihosting FatFs path. Both FreeRTOS BDD targets + * (Bdd/Targets/FreeRtos = FreeRTOS-Plus-TCP, Bdd/Targets/FreeRtosLwip = lwIP) + * drive this; their main.c files keep only the network backend behind the seam + * below — the adapter wiring (PlusTcp vs LwipRaw) and the IP-stack bring-up that + * genuinely differ. See SolidSyslog S29.03. */ + +/* The platform seam each target injects via BddTargetFreeRtosPipeline_SetConfig. */ +struct BddTargetFreeRtosPipelineConfig +{ + /* Default destination host before any `set host` — numeric for the no-DNS + * PlusTcp target ("10.0.2.2"); the DNS alias for lwIP ("syslog-ng"). */ + const char* DefaultHost; + /* Bring up the platform network and build the (Switching) sender, with the + * default transport already selected. Runs on the interactive task, so an + * adapter that must touch a started IP stack (LwipRaw) is safe here. */ + struct SolidSyslogSender* (*BuildSender)(void); + /* Emit the RFC 5424 HOSTNAME by reading the platform IP stack. Wired into + * SolidSyslogConfig.GetHostname. */ + SolidSyslogStringFunction GetHostname; + /* Tear down the sender + platform adapters. Runs on the interactive task + * after the shared pipeline teardown (SolidSyslog / SD / store / buffer). */ + void (*TeardownNetwork)(void); +}; + +/* Install the platform seam. Call once from main() before the tasks run. */ +void BddTargetFreeRtosPipeline_SetConfig(const struct BddTargetFreeRtosPipelineConfig* config); + +/* Endpoint callbacks (SolidSyslogEndpointFunction / …VersionFunction shaped), + * reading the shared host/port that `set host` / `set port` rewrite. A target's + * BuildSender wires these into its UdpSender / StreamSender configs. */ +void BddTargetFreeRtosPipeline_GetEndpoint(struct SolidSyslogEndpoint* endpoint); +uint32_t BddTargetFreeRtosPipeline_GetEndpointVersion(void); + +/* Initialise the CMSDK UART console with the shared MMIO access. */ +void BddTargetFreeRtosPipeline_InitConsole(uint32_t uartBaseAddress); + +/* Shared bounded-spin sleep (SolidSyslogSleepFunction-shaped). The lwIP target + * passes this into its resolver / TCP-stream config; both targets reuse it for + * the CMSDK UART yield. */ +void BddTargetFreeRtosPipeline_Sleep(int milliseconds); + +/* ARM Semihosting SYS_EXIT — terminates QEMU with the given status. Used + * internally by the `shutdown` / halt paths; exposed so a target's main() can + * bail out on an unrecoverable bring-up failure (e.g. xTaskCreate). */ +void BddTargetFreeRtosPipeline_Exit(int status); + +/* Stack depths as configMINIMAL_STACK_SIZE multipliers (the header cannot see + * the FreeRTOS config macro). Each main.c does the xTaskCreate so it controls + * the timing — the PlusTcp target on the network-up hook, lwIP from main(). */ +#define BDD_TARGET_INTERACTIVE_STACK_MULTIPLIER 48U +#define BDD_TARGET_SERVICE_STACK_MULTIPLIER 16U + +/* xTaskCreate entry points for the two shared tasks. The Service task + * self-registers its handle, so neither needs the caller to plumb anything. */ +void BddTargetFreeRtosPipeline_InteractiveTask(void* argument); +void BddTargetFreeRtosPipeline_ServiceTask(void* argument); + +#endif /* BDD_TARGET_FREE_RTOS_PIPELINE_H */ diff --git a/Bdd/Targets/FreeRtos/CMakeLists.txt b/Bdd/Targets/FreeRtos/CMakeLists.txt index f933d520..e758cf17 100644 --- a/Bdd/Targets/FreeRtos/CMakeLists.txt +++ b/Bdd/Targets/FreeRtos/CMakeLists.txt @@ -279,6 +279,7 @@ add_executable(SolidSyslogBddTarget ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicy.c ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyStatic.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetErrorText.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetInteractive.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetIps.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetLanguage.c diff --git a/Bdd/Targets/FreeRtos/main.c b/Bdd/Targets/FreeRtos/main.c index 6aa36de6..487b7a65 100644 --- a/Bdd/Targets/FreeRtos/main.c +++ b/Bdd/Targets/FreeRtos/main.c @@ -1,72 +1,33 @@ -/* FreeRTOS-Plus-TCP single-task SolidSyslog example for QEMU mps2-an385. +/* FreeRTOS-Plus-TCP SolidSyslog BDD target for QEMU mps2-an385. * - * S08.03 wired SolidSyslog over a hardcoded TEST_* configuration and - * exposed the configurable fields as `set ` commands over - * the interactive UART channel (hostname, appname, procid, msgid, msg, - * host, port, facility, severity). S08.04 swaps the original NullBuffer - * for the portable SolidSyslogCircularBuffer + SolidSyslogFreeRtosMutex - * and adds a dedicated FreeRTOS Service task that drains the ring — - * Log() is now non-blocking, the Service task does the UDP I/O. S08.05 - * adds the file-backed store: `set store file` tears down NullStore and - * rebuilds SolidSyslog with FatFsFile + FileBlockDevice + BlockStore on - * top of QEMU semihosting (see diskio.c). The four `max-blocks`, - * `max-block-size`, `discard-policy`, `halt-exit` keys update pending - * globals that the `set store file` trigger picks up. + * The platform-independent pipeline — SolidSyslog lifecycle, FatFs-backed store + * + security policies, SD set, the interactive `set` handler, the Service drain + * task, and the console glue — lives in Bdd/Targets/Common/BddTargetFreeRtosPipeline + * (shared with the lwIP target, S29.03). This file keeps only the FreeRTOS-Plus-TCP + * network backend behind the pipeline seam: static-IP bring-up, the LAN9118 IRQ + * priority fix, the per-endpoint RNG / sequence hooks, the PlusTcp sender wiring + * (UDP datagram + octet-framed TCP + TLS/mTLS via mbedTLS over PlusTcp TCP), and + * the RFC 5424 HOSTNAME read from the Plus-TCP endpoint. * - * Static IPv4 (10.0.2.15) on the QEMU slirp network with the host - * reachable at the slirp gateway 10.0.2.2; Bdd/Targets/Common/BddTargetInteractive - * runs over qemu -serial stdio (CmsdkUart RX wired into newlib's _read - * in Bdd/Targets/FreeRtos/Common/Syscalls.c). On link-up the IP-task event - * hook spawns the interactive task and the service task once; UdpSender - * drives the SolidSyslogPlusTcpDatagram via the static resolver, so - * each `send N` line over the UART emits N RFC 5424 datagrams to - * {10.0.2.2, port=port}. */ + * Static IPv4 (10.0.2.15) on the QEMU slirp network with the host reachable at + * the slirp gateway 10.0.2.2. */ -#include "CmsdkUart.h" -#include "BddTargetEnterpriseId.h" -#include "BddTargetErrorText.h" -#include "BddTargetInteractive.h" -#include "BddTargetIps.h" -#include "BddTargetLanguage.h" +#include "BddTargetFreeRtosPipeline.h" #include "BddTargetMtlsConfig.h" #include "BddTargetSwitchConfig.h" #include "BddTargetTlsConfig.h" #include "BddTargetTlsSender.h" -#include "SolidSyslog.h" -#include "SolidSyslogAtomicCounter.h" -#include "SolidSyslogStdAtomicCounter.h" -#include "SolidSyslogBlockStore.h" -#include "SolidSyslogTunables.h" -#include "SolidSyslogCircularBuffer.h" -#include "SolidSyslogConfig.h" -#include "SolidSyslogCrc16Policy.h" -#include "SolidSyslogEndpoint.h" -#include "SolidSyslogMbedTlsAesGcmPolicy.h" -#include "SolidSyslogMbedTlsHmacSha256Policy.h" -#include "SolidSyslogNullSecurityPolicy.h" -#include "SolidSyslogError.h" -#include "SolidSyslogFatFsFile.h" -#include "SolidSyslogFileBlockDevice.h" + #include "SolidSyslogFormatter.h" #include "SolidSyslogPlusTcpAddress.h" #include "SolidSyslogPlusTcpDatagram.h" -#include "SolidSyslogFreeRtosMutex.h" #include "SolidSyslogPlusTcpResolver.h" -#include "SolidSyslogFreeRtosSysUpTime.h" #include "SolidSyslogPlusTcpTcpStream.h" -#include "SolidSyslogMetaSd.h" -#include "SolidSyslogMutex.h" -#include "SolidSyslogNullStore.h" -#include "SolidSyslogOriginSd.h" -#include "SolidSyslogPrival.h" +#include "SolidSyslogSender.h" #include "SolidSyslogStreamSender.h" #include "SolidSyslogSwitchingSender.h" -#include "SolidSyslogTimeQuality.h" -#include "SolidSyslogTimeQualitySd.h" #include "SolidSyslogUdpSender.h" -#include "ff.h" /* f_mount / f_mkfs — FatFs requires an explicit volume mount before any file operation; we eagerly mount-or-format on the `set store file` rebuild trigger. */ - #include #include @@ -76,281 +37,75 @@ #include #include -#include -#include #include #define CMSDK_UART0_BASE_ADDRESS UINT32_C(0x40004000) -/* IRQ number for the QEMU mps2-an385 LAN9118 Ethernet controller. The - * upstream Plus-TCP NetworkInterface.c enables ISER for this IRQ but does - * NOT write IPR — leaving the priority at the reset default of 0, which is - * numerically more urgent than configMAX_SYSCALL_INTERRUPT_PRIORITY and - * trips configASSERT the first time the ISR calls a FreeRTOS API. We set - * IPR explicitly here before FreeRTOS_IPInit_Multi triggers the interface - * init that flips ISER. */ +/* IRQ number for the QEMU mps2-an385 LAN9118 Ethernet controller. The upstream + * Plus-TCP NetworkInterface.c enables ISER for this IRQ but does NOT write IPR — + * leaving the priority at the reset default of 0, which is numerically more + * urgent than configMAX_SYSCALL_INTERRUPT_PRIORITY and trips configASSERT the + * first time the ISR calls a FreeRTOS API. We set IPR explicitly here before + * FreeRTOS_IPInit_Multi triggers the interface init that flips ISER. */ #define ETHERNET_IRQ_NUMBER 13U /* NVIC IPR (Interrupt Priority Register) base — one byte per IRQ. */ #define NVIC_IPR_BASE_ADDRESS UINT32_C(0xE000E400) -/* NVIC IPR is 8-bit per IRQ but only the top configPRIO_BITS are - * implemented; lower (zeroed) bits read back as 0. Derive from the - * FreeRTOSConfig macros so a kernel-config change can't silently leave - * IRQ 13 at a higher-than-syscall-safe priority. */ +/* NVIC IPR is 8-bit per IRQ but only the top configPRIO_BITS are implemented. + * Derive from the FreeRTOSConfig macros so a kernel-config change can't silently + * leave IRQ 13 at a higher-than-syscall-safe priority. */ #define ETHERNET_IRQ_PRIORITY ((uint8_t) (configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8U - configPRIO_BITS))) -/* Unprivileged mirror of SOLIDSYSLOG_UDP_DEFAULT_PORT (514) for BDD listeners. */ -#define BDD_TARGET_UDP_PORT 5514U - -/* SolidSyslog_Log allocates two char[SOLIDSYSLOG_MAX_MESSAGE_SIZE] frames - * (~4 KB) plus formatter storage on its formatter path; BddTargetInteractive - * adds a SOLIDSYSLOG_MAX_MESSAGE_SIZE fgets line buffer (so a `set msg - * ` line carrying a full path-MTU-class message body lands in one - * read) plus a same-size HandleSet name buffer and newlib printf (~1 KB). - * Empirically the task hard-faults at *16 (8 KB) once the SolidSyslog - * setup runs — newlib printf and the formatter path together exceed that - * budget. *32 (16 KB) was stable while the line buffer was 256 B; the - * MAX_LINE_LENGTH bump to 2048 B in slice 6 added ~4 KB peak (line + name - * frames) so *40 (20 KB) gave ~4 KB headroom. S08.09 adds a TCP path — - * StreamSender.Connect's address+host-formatter storage and - * TransmitFramed's prefix formatter consume an extra ~512 B of stack on - * top of the UDP-only chain — empirically tipping a *40 budget over the - * edge when the SwitchingSender flips transports under repeated sends. - * *48 (24 KB) restores ~4 KB headroom. A follow-up will introduce - * CMake-driven memory scaling once the budget is properly characterised. - * The task only exists in this single-task example, so heap_4 (96 KB) - * absorbs it. */ -#define INTERACTIVE_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE * 48U) - -/* Drains the CircularBuffer in the background while the interactive task - * (and, in S08.04 slice 3, additional worker tasks) call SolidSyslog_Log. - * Equal priority to the producers — FreeRTOS round-robins time slices - * between same-priority ready tasks so the buffer is drained without the - * Service task starving slower producers. */ -#define SERVICE_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE * 16U) - -/* Static IPv4 wiring matching the QEMU slirp default. 10.0.2.15 is the - * standard slirp DHCP-allocated guest address; we hardcode it here so no - * DHCP server is required. 10.0.2.2 is the slirp gateway routed to the - * QEMU host; 10.0.2.3 is slirp's built-in DNS forwarder, used by - * SolidSyslogPlusTcpResolver when ipconfigUSE_DNS is enabled. */ +/* Static IPv4 wiring matching the QEMU slirp default. 10.0.2.15 is the standard + * slirp DHCP-allocated guest address; we hardcode it so no DHCP server is + * required. 10.0.2.2 is the slirp gateway routed to the QEMU host; 10.0.2.3 is + * slirp's built-in DNS forwarder. */ static const uint8_t TEST_IP_ADDRESS[ipIP_ADDRESS_LENGTH_BYTES] = {10U, 0U, 2U, 15U}; static const uint8_t TEST_NETMASK[ipIP_ADDRESS_LENGTH_BYTES] = {255U, 255U, 255U, 0U}; static const uint8_t TEST_GATEWAY[ipIP_ADDRESS_LENGTH_BYTES] = {10U, 0U, 2U, 2U}; static const uint8_t TEST_DNS[ipIP_ADDRESS_LENGTH_BYTES] = {10U, 0U, 2U, 3U}; static const uint8_t TEST_MAC[ipMAC_ADDRESS_LENGTH_BYTES] = {0x02U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U}; -/* Mutable walking-skeleton state. Defaults populated at boot; the - * interactive `set ` command rewrites these in-place via - * OnSet below. Storage sizes match RFC 5424 maxima where applicable - * (APP-NAME 48, MSGID 32) plus null terminator; MSG matches - * SOLIDSYSLOG_MAX_MESSAGE_SIZE so a single `set msg ` can carry a - * full path-MTU-class body; host fits an IPv4 dotted-quad. testMessage - * holds facility/severity (mutated in place) and the messageId/msg - * pointers (which target the mutable storage so contents are seen on - * each Log). */ -static char appName[49] = "SolidSyslogBddTarget"; -static char messageId[33] = "example"; -static char msg[SOLIDSYSLOG_MAX_MESSAGE_SIZE] = "Hello from FreeRTOS"; -/* Numeric, not the docker-network hostname "syslog-ng" the Linux target - * uses. 10.0.2.2 is the slirp gateway IP, which slirp NATs to the host's - * loopback — and inside the docker-compose pair the QEMU host (the - * behave-freertos container) shares its network namespace with - * syslog-ng-freertos, so loopback is where the oracle listens. The - * resolver does still wrap FreeRTOS_getaddrinfo for parity with POSIX - * and Windows; FreeRTOS_getaddrinfo handles dotted-quads via its inline - * inet_addr path with no slirp DNS lookup needed. An earlier S08.08 - * attempt to flip this to "syslog-ng" (matching the Linux default) - * broke every BDD scenario in CI — slirp DNS forwarder doesn't return - * a slirp-NAT-reachable address for the docker DNS alias. */ -static char host[16] = "10.0.2.2"; -static uint16_t port = (uint16_t) BDD_TARGET_UDP_PORT; -static uint32_t endpointVersion = 0U; - -static struct SolidSyslogMessage testMessage = { - .Facility = SOLIDSYSLOG_FACILITY_LOCAL0, - .Severity = SOLIDSYSLOG_SEVERITY_INFORMATIONAL, - .MessageId = messageId, - .Msg = msg, -}; - -/* Plus-TCP requires the network interface descriptor and its endpoint(s) - * to outlive the IP stack. */ +/* Plus-TCP requires the network interface descriptor and its endpoint(s) to + * outlive the IP stack. */ static NetworkInterface_t networkInterface; static NetworkEndPoint_t networkEndPoint; -/* CircularBuffer + FreeRtosMutex composition for cross-task emission. - * 8 max-sized messages is comfortably above the 3-message BDD scenarios - * with headroom for a brief Service drain stall, and ~16 KB of .bss is - * trivial against the mps2-an385's 16 MB SRAM. */ -enum -{ - BDD_TARGET_BUFFER_MESSAGES = 8 -}; - -static uint8_t bufferRing[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(BDD_TARGET_BUFFER_MESSAGES)]; - -/* Lifecycle mutex serialises SolidSyslog_Service against the rebuild path - * (`set store file` swaps NullStore for the file-backed BlockStore by - * destroying and re-creating SolidSyslog mid-run). Service holds the lock - * for one Service() call per iteration; the rebuild path holds it across - * Destroy → BlockStore_Create → Create. */ -static struct SolidSyslogMutex* lifecycleMutex = NULL; -static struct SolidSyslog* solidSyslog = NULL; -static volatile bool solidSyslogReady; -/* Signals Service to self-delete BEFORE Teardown destroys the lifecycle - * mutex. Without this, Service races against InteractiveTask: Teardown - * destroys lifecycleMutex and NULLs it, but Service's next iteration - * unconditionally locks lifecycleMutex — NULL deref or use-after-free. - * Set inside the lifecycle-mutex critical section so Service observes it - * atomically with the SolidSyslog destroy. */ -static volatile bool solidSyslogTeardown = false; - -/* File-backed store storage. Lives in .bss so it persists across the - * `set store file` rebuild; only populated when that command fires. - * STORE_PATH_PREFIX is "STORE" — sequence-numbered FAT filenames land as - * STORE00.log, STORE01.log, … which fit 8.3 short-filename mode (LFN=0 - * in our ffconf.h). */ -static const char STORE_PATH_PREFIX[] = "STORE"; - -/* FATFS object lives in .bss because f_mount stores its address inside the - * FatFs volume registry — the object must outlive every f_open / f_stat / - * f_unlink. One per volume (FF_VOLUMES = 1). */ -static FATFS fatfs; -static bool fatfsMounted = false; - -static struct SolidSyslogFile* storeFile = NULL; -static struct SolidSyslogBlockDevice* storeBlockDevice = NULL; -static struct SolidSyslogStore* currentStore = NULL; -static bool currentStoreIsFile = false; - -/* Pending values populated by the four `set max-blocks` / `max-block-size` - * / `discard-policy` / `halt-exit` commands and consumed by `set store - * file`. Defaults mirror Linux's BddTargetCommandLine (DEFAULT_MAX_BLOCKS=10, - * DEFAULT_MAX_BLOCK_SIZE=65536, discardPolicy="oldest", halt-exit=false). */ -enum -{ - DEFAULT_PENDING_MAX_BLOCKS = 10, - DEFAULT_PENDING_MAX_BLOCK_SIZE = 65536, -}; - -static size_t pendingMaxBlocks = DEFAULT_PENDING_MAX_BLOCKS; -static size_t pendingMaxBlockSize = DEFAULT_PENDING_MAX_BLOCK_SIZE; -static const char* pendingDiscardPolicy = "oldest"; -static volatile bool pendingHaltExit = false; -static size_t pendingCapacityThreshold = 0; -/* At-rest integrity policy for the file store: "crc16" (default), "hmac-sha256" - * (mbedTLS), "aes-256-gcm" (mbedTLS AEAD), or "null". Set before `store file`; - * consumed by RebuildWithFileStore. currentPolicy holds the created handle so - * DestroyCurrentStore can release it. */ -static const char* pendingSecurityPolicy = "crc16"; -static struct SolidSyslogSecurityPolicy* currentPolicy = NULL; -/* When true, SolidSyslog gets only the meta SD (sequenceId / sysUpTime / - * language) — timeQuality and origin are dropped. Mirrors Linux's - * --no-sd. Consumed by the initial Setup and by RebuildWithFileStore. */ -static volatile bool pendingNoSd = false; - -/* Holds the final SolidSyslog config so the rebuild path can rewrite - * .Store and pass the same struct back into SolidSyslog_Create. */ -static struct SolidSyslogConfig solidSyslogConfig; -static struct SolidSyslogStructuredData* sdList[3]; -static struct SolidSyslogAtomicCounter* atomicCounter = NULL; -static struct SolidSyslogStructuredData* metaSd = NULL; -static struct SolidSyslogStructuredData* timeQualitySd = NULL; -static struct SolidSyslogStructuredData* originSd = NULL; - -/* Resources allocated in InteractiveTask's Setup phase and released by - * TeardownAll. File-scope static so the two exit paths can reach the - * destroy chain: the interactive `quit` command (which returns from - * BddTargetInteractive_Run and falls through to TeardownAll in the same - * task), and `set shutdown 1` from the OnSet handler (which calls - * TeardownAll then SemihostingExit). */ +/* PlusTcp sender adapters — built by BuildSender on the interactive task, torn + * down by TeardownNetwork. */ static struct SolidSyslogResolver* resolver = NULL; static struct SolidSyslogDatagram* datagram = NULL; static struct SolidSyslogAddress* udpAddress = NULL; +static struct SolidSyslogSender* udpSender = NULL; static struct SolidSyslogStream* tcpStream = NULL; static struct SolidSyslogAddress* tcpAddress = NULL; static struct SolidSyslogSender* tcpSender = NULL; static struct SolidSyslogSender* tlsSender = NULL; -static struct SolidSyslogSender* udpSender = NULL; static struct SolidSyslogSender* switchingSender = NULL; -static struct SolidSyslogBuffer* buffer = NULL; -static struct SolidSyslogMutex* bufferMutex = NULL; -/* Ensures the interactive task is created exactly once even if the network - * goes down and back up. */ +/* Ensures the interactive + service tasks are created exactly once even if the + * network goes down and back up. */ static BaseType_t interactiveTaskCreated = pdFALSE; -/* Service task handle is captured at creation so the interactive task can - * report its peak stack usage alongside its own on `quit`. */ -static TaskHandle_t serviceTaskHandle = NULL; - -/* The task awaiting ServiceTask's exit during teardown. Set by TeardownAll - * (inside the lifecycle critical section, so ServiceTask reads it atomically - * with the teardown flag); ServiceTask notifies it the instant before it - * self-deletes, letting TeardownAll destroy the lifecycle mutex on a real - * "service stopped" signal instead of a fixed delay. */ -static TaskHandle_t serviceStopWaiter = NULL; - -/* Upper bound on the teardown wait for ServiceTask to self-delete. ServiceTask - * observes the flag within one ~1 ms iteration, so this is only ever reached if - * the Service task never started (xTaskCreate failure); it bounds teardown - * rather than timing it. */ -enum -{ - SERVICE_STOP_TIMEOUT_MS = 1000, -}; - extern NetworkInterface_t* pxMPS2_FillInterfaceDescriptor(BaseType_t xEMACIndex, NetworkInterface_t* pxInterface); static void SetEthernetIrqPriority(void); -static void InteractiveTask(void* argument); -static void ServiceTask(void* argument); - -static bool TryUpdateString(char* storage, size_t storageSize, const char* value); -static bool TryParseUInt(const char* value, unsigned long* out); -static bool RebuildWithFileStore(void); -static void DestroyCurrentStore(void); -static void TeardownAll(void); -static bool EnsureFatFsMounted(void); -static enum SolidSyslogDiscardPolicy MapDiscardPolicy(const char* policy); -static void OnStoreFull(void* context); -static size_t GetCapacityThreshold(void* context); -static void OnThresholdCrossed(void* context); -static void SemihostingExit(int status); - -static uint32_t MmioRead32(uintptr_t address) -{ - // NOLINTNEXTLINE(performance-no-int-to-ptr) -- mapping the CMSDK UART MMIO address into a 32-bit volatile pointer. - return *(volatile uint32_t*) address; -} - -static void MmioWrite32(uintptr_t address, uint32_t value) -{ - // NOLINTNEXTLINE(performance-no-int-to-ptr) -- mapping the CMSDK UART MMIO address into a 32-bit volatile pointer. - *(volatile uint32_t*) address = value; -} - -static void RtosSleep(int milliseconds) -{ - /* Round any non-zero millisecond request up to at least one tick so a - * sub-tick sleep (e.g. CmsdkUart's 1 ms yield against a 100 Hz tick, - * where pdMS_TO_TICKS(1) == 0) still blocks the task instead of falling - * through vTaskDelay(0) and busy-spinning the spin-and-yield loops. */ - TickType_t ticks = pdMS_TO_TICKS((TickType_t) milliseconds); - if ((milliseconds > 0) && (ticks == 0U)) - { - ticks = 1U; - } - vTaskDelay(ticks); -} - -static const CmsdkUartMemoryAccess MMIO_ACCESS = {MmioRead32, MmioWrite32, RtosSleep}; +static void GetHostname(struct SolidSyslogFormatter* formatter); +static struct SolidSyslogSender* BuildSender(void); +static void TeardownNetwork(void); + +static const struct BddTargetFreeRtosPipelineConfig PIPELINE_CONFIG = { + .DefaultHost = "10.0.2.2", + .BuildSender = BuildSender, + .GetHostname = GetHostname, + .TeardownNetwork = TeardownNetwork, +}; int main(void) { - CmsdkUart_Init(&MMIO_ACCESS, CMSDK_UART0_BASE_ADDRESS); + BddTargetFreeRtosPipeline_InitConsole(CMSDK_UART0_BASE_ADDRESS); + BddTargetFreeRtosPipeline_SetConfig(&PIPELINE_CONFIG); SetEthernetIrqPriority(); @@ -385,24 +140,36 @@ void vApplicationIPNetworkEventHook_Multi(eIPCallbackEvent_t eNetworkEvent, stru (void) pxEndPoint; if ((eNetworkEvent == eNetworkUp) && (interactiveTaskCreated == pdFALSE)) { + TaskHandle_t interactiveTask = NULL; if (xTaskCreate( - InteractiveTask, + BddTargetFreeRtosPipeline_InteractiveTask, "interactive", - INTERACTIVE_TASK_STACK_DEPTH, + configMINIMAL_STACK_SIZE * BDD_TARGET_INTERACTIVE_STACK_MULTIPLIER, NULL, tskIDLE_PRIORITY + 1, - NULL + &interactiveTask ) == pdPASS) { - (void) xTaskCreate( - ServiceTask, - "service", - SERVICE_TASK_STACK_DEPTH, - NULL, - tskIDLE_PRIORITY + 1, - &serviceTaskHandle - ); - interactiveTaskCreated = pdTRUE; + if (xTaskCreate( + BddTargetFreeRtosPipeline_ServiceTask, + "service", + configMINIMAL_STACK_SIZE * BDD_TARGET_SERVICE_STACK_MULTIPLIER, + NULL, + tskIDLE_PRIORITY + 1, + NULL + ) == pdPASS) + { + interactiveTaskCreated = pdTRUE; + } + else + { + /* Service create failed — undo the interactive task so the next + * eNetworkUp retries both cleanly instead of latching a + * half-started pipeline. Safe to delete: this hook runs on the + * higher-priority IP task, so the idle+1 interactive task has not + * been scheduled yet. */ + vTaskDelete(interactiveTask); + } } } } @@ -423,10 +190,9 @@ void vApplicationStackOverflowHook(TaskHandle_t task, char* taskName) } } -/* Plus-TCP requires the application to provide a per-endpoint random seed - * source for ARP / IP-ID generation. The QEMU mps2-an385 has no RNG; for a - * deterministic smoke test we return the run-time tick mixed with a fixed - * constant. */ +/* Plus-TCP requires the application to provide a per-endpoint random seed source + * for ARP / IP-ID generation. The QEMU mps2-an385 has no RNG; for a + * deterministic smoke test we return the run-time tick mixed with a constant. */ BaseType_t xApplicationGetRandomNumber(uint32_t* pulValue) { *pulValue = (uint32_t) xTaskGetTickCount() ^ 0xA5A5A5A5U; @@ -456,18 +222,9 @@ static void SetEthernetIrqPriority(void) static void GetHostname(struct SolidSyslogFormatter* formatter) { - /* RFC 5424 §6.2.4 walk for this reference target: - * 1. FQDN — n/a (no DNS resolver on the FreeRTOS reference) - * 2. Static IP address — present (queried from the IP-stack below) ← emit this - * 3. hostname — n/a (no integrator-supplied hostname) - * 4. Dynamic IP — n/a (no DHCP) - * 5. NILVALUE — fallthrough if none of the above are available - * - * Source of truth is TEST_IP_ADDRESS, which flows to FreeRTOS-Plus-TCP - * via FreeRTOS_FillEndPoint at boot. Read it back via the IP-stack so - * any future slice that swaps the static configuration for DHCP (rung - * 4) or supplies a hostname (rung 3) doesn't have to re-touch this - * function — same callback, different rung satisfied by the stack. */ + /* RFC 5424 §6.2.4 rung 2 (static IP address). Read back from the IP stack so + * a future DHCP / hostname slice satisfies the same rung without re-touching + * this callback. */ uint32_t ipAddress = 0U; char ipBuffer[16]; FreeRTOS_GetEndPointConfiguration(&ipAddress, NULL, NULL, NULL, &networkEndPoint); @@ -475,602 +232,15 @@ static void GetHostname(struct SolidSyslogFormatter* formatter) SolidSyslogFormatter_BoundedString(formatter, ipBuffer, strlen(ipBuffer)); } -static void GetAppName(struct SolidSyslogFormatter* formatter) -{ - SolidSyslogFormatter_BoundedString(formatter, appName, strlen(appName)); -} - -/* No RTC and no time-sync on this reference target — the example models an - * embedded device that has no concept of wall-clock time. RFC 5424 §6.2.3.1 - * mandates NILVALUE TIMESTAMP in that case, and the timeQuality SD reports - * tzKnown=0, isSynced=0. SolidSyslogConfig.Clock=NULL drops through to the - * library's NilClock; the resulting all-zero SolidSyslogTimestamp fails - * TimestampIsValid in Core/Source/SolidSyslog.c and emits "-" on the wire. */ -static void ErrorHandlerEx(void* context, const struct SolidSyslogErrorEvent* event) -{ - (void) context; - const char* sourceName = ""; - const struct SolidSyslogErrorSource* source = event->Source; - if (source != NULL) - { - sourceName = source->Name; - } - const char* message = BddTargetErrorText_Category(event->Category); - (void) printf( - "[solidsyslog] severity=%d [%s cat=%u detail=%ld] %s\n", - (int) event->Severity, - sourceName, - (unsigned) event->Category, - (long) event->Detail, - message - ); -} - -static void GetTimeQuality(struct SolidSyslogTimeQuality* timeQuality) -{ - timeQuality->TzKnown = false; - timeQuality->IsSynced = false; - timeQuality->SyncAccuracyMicroseconds = SOLIDSYSLOG_SYNC_ACCURACY_OMIT; -} - -static void GetEndpoint(struct SolidSyslogEndpoint* endpoint) -{ - SolidSyslogFormatter_BoundedString(endpoint->Host, host, strlen(host)); - endpoint->Port = port; -} - -static uint32_t GetEndpointVersion(void) -{ - return endpointVersion; -} - -static bool OnSet(const char* name, const char* value) -{ - if (strcmp(name, "appname") == 0) - { - return TryUpdateString(appName, sizeof(appName), value); - } - if (strcmp(name, "msgid") == 0) - { - return TryUpdateString(messageId, sizeof(messageId), value); - } - if (strcmp(name, "msg") == 0) - { - return TryUpdateString(msg, sizeof(msg), value); - } - if (strcmp(name, "host") == 0) - { - return TryUpdateString(host, sizeof(host), value); - } - if (strcmp(name, "port") == 0) - { - unsigned long parsed = 0U; - if (!TryParseUInt(value, &parsed) || parsed == 0U || parsed > UINT16_MAX) - { - return false; - } - port = (uint16_t) parsed; - endpointVersion++; - return true; - } - if (strcmp(name, "facility") == 0) - { - /* Mirrors the Linux example's atoi-and-cast (Bdd/Targets/Common/ - * BddTargetCommandLine.c): the example forwards the parsed value - * unchanged so the library is the single authority on what's - * valid (out-of-range facility/severity encodes as the - * internal-error PRIVAL 43). */ - unsigned long parsed = 0U; - if (!TryParseUInt(value, &parsed)) - { - return false; - } - testMessage.Facility = (enum SolidSyslogFacility) parsed; - return true; - } - if (strcmp(name, "severity") == 0) - { - unsigned long parsed = 0U; - if (!TryParseUInt(value, &parsed)) - { - return false; - } - testMessage.Severity = (enum SolidSyslogSeverity) parsed; - return true; - } - if (strcmp(name, "transport") == 0) - { - /* Switching sender selector — "udp" / "tcp" / "tls" / "mtls" route - * through BddTargetSwitchConfig (Bdd/Targets/Common). The FreeRTOS - * target only wires UDP and TCP inner senders today; selecting tls - * falls through to the SwitchingSender's nil sender, surfacing the - * gap as a send failure rather than a crash. */ - BddTargetSwitchConfig_SetByName(value); - return true; - } - if (strcmp(name, "max-blocks") == 0) - { - unsigned long parsed = 0U; - if (!TryParseUInt(value, &parsed)) - { - return false; - } - pendingMaxBlocks = (size_t) parsed; - return true; - } - if (strcmp(name, "max-block-size") == 0) - { - unsigned long parsed = 0U; - if (!TryParseUInt(value, &parsed)) - { - return false; - } - pendingMaxBlockSize = (size_t) parsed; - return true; - } - if (strcmp(name, "discard-policy") == 0) - { - if ((strcmp(value, "oldest") != 0) && (strcmp(value, "newest") != 0) && (strcmp(value, "halt") != 0)) - { - return false; - } - /* String literal storage — target_driver.py emits one of the three - * literals above so the pointer stays valid (no copy needed). */ - pendingDiscardPolicy = - (strcmp(value, "newest") == 0) ? "newest" : ((strcmp(value, "halt") == 0) ? "halt" : "oldest"); - return true; - } - if (strcmp(name, "security-policy") == 0) - { - if ((strcmp(value, "crc16") != 0) && (strcmp(value, "hmac-sha256") != 0) && - (strcmp(value, "aes-256-gcm") != 0) && (strcmp(value, "null") != 0)) - { - return false; - } - /* String literal storage — see discard-policy above. */ - if (strcmp(value, "hmac-sha256") == 0) - { - pendingSecurityPolicy = "hmac-sha256"; - } - else if (strcmp(value, "aes-256-gcm") == 0) - { - pendingSecurityPolicy = "aes-256-gcm"; - } - else if (strcmp(value, "null") == 0) - { - pendingSecurityPolicy = "null"; - } - else - { - pendingSecurityPolicy = "crc16"; - } - return true; - } - if (strcmp(name, "capacity-threshold") == 0) - { - unsigned long parsed = 0U; - if (!TryParseUInt(value, &parsed)) - { - return false; - } - pendingCapacityThreshold = (size_t) parsed; - return true; - } - if (strcmp(name, "halt-exit") == 0) - { - /* Harness emits `set halt-exit 1` (or `0`) because the FreeRTOS - * `set` protocol always carries a value; on Linux the equivalent - * is a bare --halt-exit flag (no_argument). Anything non-zero - * trips the halt path. */ - unsigned long parsed = 0U; - if (!TryParseUInt(value, &parsed)) - { - return false; - } - pendingHaltExit = (parsed != 0U); - return true; - } - if (strcmp(name, "no-sd") == 0) - { - /* `set no-sd 1` drops the SD list to only metaSd — mirrors - * Linux's --no-sd bare flag. The setting takes effect via - * SolidSyslog re-Create (initial Setup or `set store file` - * rebuild). */ - unsigned long parsed = 0U; - if (!TryParseUInt(value, &parsed)) - { - return false; - } - pendingNoSd = (parsed != 0U); - return true; - } - if (strcmp(name, "store") == 0) - { - /* "null" is the default state — accept it as an unconditional - * no-op so the harness can pass --store null without us needing - * to special-case it on the harness side. "file" triggers the - * rebuild (which is one-way for the lifetime of this QEMU - * instance — see RebuildWithFileStore). */ - if (strcmp(value, "null") == 0) - { - return true; - } - if (strcmp(value, "file") == 0) - { - return RebuildWithFileStore(); - } - return false; - } - if (strcmp(name, "shutdown") == 0) - { - /* Any non-zero value triggers a full teardown then exits QEMU. - * Same destroy chain as the `quit` path; only the final action - * differs (SemihostingExit here, vTaskDelete on `quit`). The BDD - * `the client is killed` step on freertos sends `set shutdown 1`. */ - unsigned long parsed = 0U; - if (!TryParseUInt(value, &parsed)) - { - return false; - } - if (parsed != 0U) - { - TeardownAll(); - SemihostingExit(0); - } - return true; - } - return false; -} - -static bool TryUpdateString(char* storage, size_t storageSize, const char* value) -{ - size_t length = strlen(value); - if ((length == 0U) || (length >= storageSize)) - { - return false; - } - memcpy(storage, value, length); - storage[length] = '\0'; - return true; -} - -static bool TryParseUInt(const char* value, unsigned long* out) -{ - if (*value == '\0') - { - return false; - } - char* end = NULL; - unsigned long parsed = strtoul(value, &end, 10); - /* strtoul accepts a leading '-' and wraps to a huge unsigned. The port - * call site bounds-checks against UINT16_MAX upstream; facility and - * severity intentionally don't, mirroring the Linux example's - * atoi-and-cast so wrapped values reach the library and encode as - * the internal-error PRIVAL. */ - if (*end != '\0') - { - return false; - } - *out = parsed; - return true; -} - -/* DEMO KEY ONLY. A real integrator supplies key material from a secure element, - * a KDF, or encrypted NVM via their own SolidSyslogKeyFunction — never a - * hard-coded constant. This exists so the BDD scenario can exercise the mbedTLS - * HMAC-SHA256 at-rest policy end-to-end with real crypto. */ -static bool BddDemoGetKey(void* context, uint8_t* keyOut, size_t capacity, size_t* keyLengthOut) -{ - enum - { - DEMO_KEY_SIZE = 32 - }; - - (void) context; - size_t written = (capacity < DEMO_KEY_SIZE) ? capacity : (size_t) DEMO_KEY_SIZE; - (void) memset(keyOut, 0x5A, written); - *keyLengthOut = written; - return true; -} - -static struct SolidSyslogSecurityPolicy* CreateSecurityPolicy(void) -{ - struct SolidSyslogSecurityPolicy* policy = NULL; - if (strcmp(pendingSecurityPolicy, "hmac-sha256") == 0) - { - static const struct SolidSyslogMbedTlsHmacSha256PolicyConfig hmacConfig = {BddDemoGetKey, NULL}; - policy = SolidSyslogMbedTlsHmacSha256Policy_Create(&hmacConfig); - } - else if (strcmp(pendingSecurityPolicy, "aes-256-gcm") == 0) - { - /* Reuse the TLS module's already-seeded CTR-DRBG as the AEAD nonce - * source — see BddTargetTlsSender_GetRng. Not static const: Rng is a - * runtime handle. */ - const struct SolidSyslogMbedTlsAesGcmPolicyConfig aesConfig = - {BddDemoGetKey, NULL, BddTargetTlsSender_GetRng()}; - policy = SolidSyslogMbedTlsAesGcmPolicy_Create(&aesConfig); - } - else if (strcmp(pendingSecurityPolicy, "null") == 0) - { - policy = SolidSyslogNullSecurityPolicy_Get(); - } - else - { - policy = SolidSyslogCrc16Policy_Create(); - } - return policy; -} - -static bool RebuildWithFileStore(void) -{ - /* Lifecycle mutex blocks the Service task from running SolidSyslog_Service - * across the Destroy → re-Create transition. Service waits on the next - * iteration's lock; rebuild releases when done. */ - SolidSyslogMutex_Lock(lifecycleMutex); - - /* FatFs does NOT auto-mount on first f_open — the integrator must call - * f_mount before any file operation, and f_mkfs when the volume is - * fresh. EnsureFatFsMounted handles both. Doing this BEFORE tearing down - * the existing store means a mount failure leaves the target running on - * the original NullStore (zero-disruption); we return false so OnSet - * reports the failure to the harness. */ - if (!EnsureFatFsMounted()) - { - SolidSyslogMutex_Unlock(lifecycleMutex); - return false; - } - - solidSyslogReady = false; - SolidSyslog_Destroy(solidSyslog); - solidSyslog = NULL; - DestroyCurrentStore(); - - /* Build a fresh FatFs-backed BlockStore. With the volume mounted above, - * BlockSequence_Open's f_stat / f_open calls now hit a live filesystem - * via disk_read / disk_write semihosting traps. */ - storeFile = SolidSyslogFatFsFile_Create(); - storeBlockDevice = SolidSyslogFileBlockDevice_Create(storeFile, STORE_PATH_PREFIX); - - struct SolidSyslogSecurityPolicy* policy = CreateSecurityPolicy(); - currentPolicy = policy; - struct SolidSyslogBlockStoreConfig storeConfig = { - .BlockDevice = storeBlockDevice, - .MaxBlockSize = pendingMaxBlockSize, - .MaxBlocks = pendingMaxBlocks, - .DiscardPolicy = MapDiscardPolicy(pendingDiscardPolicy), - .SecurityPolicy = policy, - .OnStoreFull = OnStoreFull, - .StoreFullContext = NULL, - .GetCapacityThreshold = GetCapacityThreshold, - .OnThresholdCrossed = OnThresholdCrossed, - .ThresholdContext = &pendingCapacityThreshold, - }; - currentStore = SolidSyslogBlockStore_Create(&storeConfig); - currentStoreIsFile = true; - - solidSyslogConfig.Store = currentStore; - /* Re-honour `set no-sd 1` if it arrived before this rebuild — the - * sort order in target_driver.py guarantees `set no-sd` comes before - * `set store file` so the value is final by the time we get here. */ - solidSyslogConfig.SdCount = pendingNoSd ? 1U : (sizeof(sdList) / sizeof(sdList[0])); - solidSyslog = SolidSyslog_Create(&solidSyslogConfig); - solidSyslogReady = true; - SolidSyslogMutex_Unlock(lifecycleMutex); - return true; -} - -/* Mount volume 0; format-on-first-use if the disk image has no FAT yet. - * Idempotent — subsequent calls short-circuit on fatfsMounted. The work - * buffer for f_mkfs is sized to FF_MAX_SS (512 B) which is the minimum - * f_mkfs accepts on a FAT12/16 volume. */ -static bool EnsureFatFsMounted(void) -{ - if (fatfsMounted) - { - return true; - } - FRESULT res = f_mount( - &fatfs, - "", - 1 - ); /* opt=1 → mount immediately, surface FR_NO_FILESYSTEM here rather than at first f_open */ - if (res == FR_NO_FILESYSTEM) - { - /* Fresh disk image — lay down a FAT and re-mount. FM_FAT keeps the - * formatter on FAT12/16; at the shared 8 MiB geometry auto cluster - * sizing clears the ~4085-cluster boundary, so this lands FAT16 (the - * geometry the later FreeRTOS-Plus-FAT formatter needs). */ - static BYTE workBuffer[FF_MAX_SS]; - const MKFS_PARM opts = {.fmt = FM_FAT | FM_SFD, .n_fat = 1, .align = 1, .n_root = 0, .au_size = 0}; - res = f_mkfs("", &opts, workBuffer, sizeof(workBuffer)); - if (res == FR_OK) - { - res = f_mount(&fatfs, "", 1); - } - } - if (res != FR_OK) - { - (void) printf("[solidsyslog] fatfs mount failed: FRESULT=%d\n", (int) res); - return false; - } - fatfsMounted = true; - return true; -} - -/* Tears down whichever store is currently installed (file-backed or null). - * Shared by RebuildWithFileStore (which then re-creates) and - * ShutdownGracefully (which then exits). FatFsFile_Destroy → Close → - * f_close flushes the underlying FIL's dir entry. */ -static void DestroySecurityPolicy(void) -{ - if (strcmp(pendingSecurityPolicy, "hmac-sha256") == 0) - { - SolidSyslogMbedTlsHmacSha256Policy_Destroy(currentPolicy); - } - else if (strcmp(pendingSecurityPolicy, "aes-256-gcm") == 0) - { - SolidSyslogMbedTlsAesGcmPolicy_Destroy(currentPolicy); - } - else if (strcmp(pendingSecurityPolicy, "crc16") == 0) - { - SolidSyslogCrc16Policy_Destroy(); - } - /* else "null": the shared NullSecurityPolicy is immutable — nothing to free. */ - currentPolicy = NULL; -} - -static void DestroyCurrentStore(void) -{ - if (currentStoreIsFile) - { - SolidSyslogBlockStore_Destroy(currentStore); - SolidSyslogFileBlockDevice_Destroy(storeBlockDevice); - DestroySecurityPolicy(); - SolidSyslogFatFsFile_Destroy(storeFile); - } - /* else: NullStore is shared and immutable — nothing to destroy. */ -} - -static enum SolidSyslogDiscardPolicy MapDiscardPolicy(const char* policy) -{ - if (strcmp(policy, "newest") == 0) - { - return SOLIDSYSLOG_DISCARD_POLICY_NEWEST; - } - if (strcmp(policy, "halt") == 0) - { - return SOLIDSYSLOG_DISCARD_POLICY_HALT; - } - return SOLIDSYSLOG_DISCARD_POLICY_OLDEST; -} - -static void OnStoreFull(void* context) -{ - (void) context; - if (pendingHaltExit) - { - /* Semihosting SYS_EXIT — terminates QEMU with the given status so - * the BDD harness sees the run end deterministically. Mirrors the - * Linux example's _exit(2) (Bdd/Targets/Linux/main.c::OnStoreFull). */ - SemihostingExit(2); - } -} - -static size_t GetCapacityThreshold(void* context) -{ - return *(const size_t*) context; -} - -/* Stdout-based marker the behave harness watches for. Linux's equivalent - * (Bdd/Targets/Linux/main.c::OnThresholdCrossed) writes a host file at - * /tmp/solidsyslog_threshold_marker.log — that file path isn't reachable - * from the QEMU guest. Instead we print a known token to the UART, which - * the captured-stdout reader in Bdd/features/steps/syslog_steps.py buffers - * and the threshold step then scans. The token is line-anchored so a stray - * substring inside a longer message body could not accidentally trip the - * assertion. */ -static void OnThresholdCrossed(void* context) -{ - (void) context; - (void) printf("[THRESHOLD-CROSSED]\r\n"); -} - -/* Full teardown of every resource InteractiveTask allocated during Setup. - * Two entry points — `quit` (falls through after BddTargetInteractive_Run - * returns) and `set shutdown 1` (OnSet handler) — both route through here - * so the destroy chain is single-source-of-truth, not duplicated. f_unmount - * fires regardless of how we got here; the BDD power_cycle_replay scenario - * relies on this so the next session's f_mount finds STORE*.log directory - * entries up-to-date. The lifecycle mutex held across the SolidSyslog + - * store destroy keeps Service from racing the teardown. */ -static void TeardownAll(void) -{ - SolidSyslogMutex_Lock(lifecycleMutex); - serviceStopWaiter = xTaskGetCurrentTaskHandle(); - solidSyslogTeardown = true; - solidSyslogReady = false; - SolidSyslog_Destroy(solidSyslog); - solidSyslog = NULL; - SolidSyslogOriginSd_Destroy(originSd); - SolidSyslogTimeQualitySd_Destroy(timeQualitySd); - SolidSyslogMetaSd_Destroy(metaSd); - SolidSyslogStdAtomicCounter_Destroy(atomicCounter); - DestroyCurrentStore(); - if (fatfsMounted) - { - (void) f_unmount(""); - fatfsMounted = false; - } - SolidSyslogMutex_Unlock(lifecycleMutex); - - /* Wait for Service to observe the teardown flag and vTaskDelete itself - * before we destroy the lifecycle mutex out from under it. ServiceTask - * notifies us from its self-delete path, so this returns the moment it - * has actually exited rather than after a fixed guess. Bounded so a - * Service task that never started (xTaskCreate failure → NULL handle) - * cannot wedge teardown. */ - if (serviceTaskHandle != NULL) - { - (void) ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(SERVICE_STOP_TIMEOUT_MS)); - } - serviceTaskHandle = NULL; - - SolidSyslogCircularBuffer_Destroy(buffer); - SolidSyslogFreeRtosMutex_Destroy(bufferMutex); - SolidSyslogFreeRtosMutex_Destroy(lifecycleMutex); - lifecycleMutex = NULL; - SolidSyslogSwitchingSender_Destroy(switchingSender); - /* BddTargetTlsSender owns the inner MbedTlsStream + PlusTcpTcpStream pool - * slots and the StreamSender slot, so it must be destroyed before tcpSender - * / tcpStream below — releasing slots in roughly reverse order of Create - * keeps the dependency graph clean even though the pool allocator itself - * is order-insensitive. */ - BddTargetTlsSender_Destroy(); - tlsSender = NULL; - SolidSyslogStreamSender_Destroy(tcpSender); - SolidSyslogPlusTcpAddress_Destroy(tcpAddress); - SolidSyslogPlusTcpTcpStream_Destroy(tcpStream); - SolidSyslogUdpSender_Destroy(udpSender); - SolidSyslogPlusTcpAddress_Destroy(udpAddress); - SolidSyslogPlusTcpDatagram_Destroy(datagram); - SolidSyslogPlusTcpResolver_Destroy(resolver); -} - -static void SemihostingExit(int status) -{ - /* SYS_EXIT_EXTENDED (0x20) — the only ARM Semihosting exit form on - * AArch32 that propagates a non-zero status: R1 points to a - * { reason, subcode } parameter block. The simpler SYS_EXIT (0x18) - * on AArch32 takes R1 as a *literal* reason code (ADP_Stopped_*), - * so passing a struct pointer there resolved to "unrecognised reason" - * → QEMU exit 1 regardless of subcode. Marked unreachable after the - * trap because QEMU terminates the VM; the for(;;) is defensive. */ - const struct - { - uint32_t reason; - uint32_t subcode; - } args = {0x20026U, (uint32_t) status}; - - register int r0 __asm("r0") = 0x20; - register const void* r1 __asm("r1") = &args; - __asm volatile("bkpt 0xAB" : "+r"(r0) : "r"(r1) : "memory"); - for (;;) - { - } -} - -static void InteractiveTask(void* argument) +/* Build the PlusTcp SwitchingSender: UDP datagram, octet-framed TCP, and a + * TLS/mTLS slot (mbedTLS over a second PlusTcp TCP stream). Default transport + * UDP so existing @udp scenarios stay green; `set transport ` + * flips it at runtime. Runs on the interactive task. */ +static struct SolidSyslogSender* BuildSender(void) { - (void) argument; - - /* Linux defaults its TLS / mTLS host to "syslog-ng" (the docker DNS - * alias) and overrides via SOLIDSYSLOG_BDD_{TLS,MTLS}_HOST env vars - * when needed. FreeRTOS has no env-var access in QEMU and slirp DNS - * doesn't reach the docker DNS alias in CI, so route the connection - * via the slirp gateway 10.0.2.2 directly — same path UDP / TCP - * take. ServerName for SNI / cert verification is pinned separately - * to "syslog-ng" (the cert's subject); without that the handshake - * fails because the syslog-ng cert isn't issued for 10.0.2.2. */ + /* Route TLS / mTLS via the slirp gateway 10.0.2.2 (same path UDP/TCP take — + * slirp DNS doesn't reach the docker alias in CI). ServerName is pinned to + * "syslog-ng" (the cert subject) so SNI / cert verification still pass. */ BddTargetTlsConfig_SetHost("10.0.2.2"); BddTargetTlsConfig_SetServerName("syslog-ng"); BddTargetMtlsConfig_SetHost("10.0.2.2"); @@ -1079,47 +249,28 @@ static void InteractiveTask(void* argument) resolver = SolidSyslogPlusTcpResolver_Create(); datagram = SolidSyslogPlusTcpDatagram_Create(); udpAddress = SolidSyslogPlusTcpAddress_Create(); - struct SolidSyslogUdpSenderConfig udpConfig = { .Resolver = resolver, .Datagram = datagram, .Address = udpAddress, - .Endpoint = GetEndpoint, - .EndpointVersion = GetEndpointVersion, + .Endpoint = BddTargetFreeRtosPipeline_GetEndpoint, + .EndpointVersion = BddTargetFreeRtosPipeline_GetEndpointVersion, }; udpSender = SolidSyslogUdpSender_Create(&udpConfig); - /* Plain TCP path via the new FreeRTOS Plus-TCP stream adapter. Shares the - * UDP endpoint callbacks because the BDD oracle (syslog-ng) listens on the - * same host:port for both transports — the syslog-ng config in - * Bdd/syslog-ng/syslog-ng.conf has a TCP listener on 5514 alongside UDP. */ tcpStream = SolidSyslogPlusTcpTcpStream_Create(NULL); tcpAddress = SolidSyslogPlusTcpAddress_Create(); struct SolidSyslogStreamSenderConfig tcpConfig = { .Resolver = resolver, .Stream = tcpStream, .Address = tcpAddress, - .Endpoint = GetEndpoint, - .EndpointVersion = GetEndpointVersion, + .Endpoint = BddTargetFreeRtosPipeline_GetEndpoint, + .EndpointVersion = BddTargetFreeRtosPipeline_GetEndpointVersion, }; tcpSender = SolidSyslogStreamSender_Create(&tcpConfig); - /* TLS slot: SolidSyslogMbedTlsStream over SolidSyslogPlusTcpTcpStream, - * with the demo CA / client cert / client key baked into the ELF. The - * same slot serves both @tls (port 6514) and @mtls (port 6515) - * scenarios — the wrapper wires the client cert unconditionally and - * its StreamSender Endpoint callback dispatches on - * BddTargetSwitchConfig_IsMtlsMode() at Connect time. The `false` - * argument here is honoured for cross-platform signature uniformity - * (Linux / Windows TLS senders read it at startup) but ignored on - * FreeRTOS, where the harness flips mode over the UART after the - * prompt is already up. */ tlsSender = BddTargetTlsSender_Create(resolver, false); - /* SwitchingSender lets `set transport ` flip the - * active transport at runtime. Default to UDP so existing UDP-tagged - * scenarios stay green; `--transport tcp|tls|mtls` flowing through the - * behave harness lands here as `set transport ` over the UART. */ static struct SolidSyslogSender* inners[BDD_TARGET_SWITCH_COUNT]; inners[BDD_TARGET_SWITCH_UDP] = udpSender; inners[BDD_TARGET_SWITCH_TCP] = tcpSender; @@ -1131,128 +282,22 @@ static void InteractiveTask(void* argument) }; BddTargetSwitchConfig_SetByName("udp"); switchingSender = SolidSyslogSwitchingSender_Create(&switchConfig); - struct SolidSyslogSender* sender = switchingSender; - - /* CircularBuffer drained by ServiceTask below, with a FreeRtosMutex - * gating concurrent producers (interactive task today; multi-task - * emission in S08.04 slice 3 will add more). The buffer's Read side - * is the Service task; its Write side is whichever task calls - * SolidSyslog_Log. */ - bufferMutex = SolidSyslogFreeRtosMutex_Create(); - buffer = SolidSyslogCircularBuffer_Create(bufferMutex, bufferRing, sizeof(bufferRing)); - - /* Lifecycle mutex created up front so the Service task can take it - * from its very first iteration without a NULL check. */ - lifecycleMutex = SolidSyslogFreeRtosMutex_Create(); - - /* Default store is NullStore — flipped to FatFs/BlockStore by - * `set store file` via RebuildWithFileStore(). */ - currentStore = SolidSyslogNullStore_Get(); - currentStoreIsFile = false; - - atomicCounter = SolidSyslogStdAtomicCounter_Create(); - struct SolidSyslogMetaSdConfig metaConfig = { - .Counter = atomicCounter, - .GetSysUpTime = SolidSyslogFreeRtosSysUpTime_Get, - .GetLanguage = BddTargetLanguage_Get, - }; - metaSd = SolidSyslogMetaSd_Create(&metaConfig); - timeQualitySd = SolidSyslogTimeQualitySd_Create(GetTimeQuality); - struct SolidSyslogOriginSdConfig originConfig = { - .Software = "SolidSyslogBddTarget", - .SwVersion = "0.7.0", - .EnterpriseId = BDD_TARGET_ENTERPRISE_ID, - .GetIpCount = BddTargetIps_Count, - .GetIpAt = BddTargetIps_At, - }; - originSd = SolidSyslogOriginSd_Create(&originConfig); - sdList[0] = metaSd; - sdList[1] = timeQualitySd; - sdList[2] = originSd; - - solidSyslogConfig = (struct SolidSyslogConfig) { - .Buffer = buffer, - .Sender = sender, - .Clock = NULL, - .GetHostname = GetHostname, - .GetAppName = GetAppName, - /* PROCID — RFC 5424 §6.2.6 NILVALUE: FreeRTOS QEMU has no - * process model. NULL drops through to the library's - * NilStringFunction which yields an empty field; FormatStringField - * (Core/Source/SolidSyslog.c) then emits "-" on the wire. */ - .GetProcessId = NULL, - .Store = currentStore, - .Sd = sdList, - /* pendingNoSd is normally false at this initial Setup call — - * the `set no-sd 1` translation runs over the UART AFTER the - * prompt is up. Slice 6's @store scenarios on FreeRTOS always - * couple --no-sd with --store file, so the rebuild path rewrites - * .SdCount with the up-to-date value. This initial value is - * defensive in case a future scenario sends `set no-sd 1` before - * any rebuild. */ - .SdCount = pendingNoSd ? 1U : (sizeof(sdList) / sizeof(sdList[0])), - }; - SolidSyslog_SetErrorHandler(ErrorHandlerEx, NULL); - solidSyslog = SolidSyslog_Create(&solidSyslogConfig); - solidSyslogReady = true; - - BddTargetInteractive_Run(solidSyslog, &testMessage, stdin, BddTargetSwitchConfig_SetByName, OnSet); - - /* Peak stack usage report on `quit`. Captured into every BDD run's QEMU - * console output so stack regressions surface in bdd-freertos-qemu logs - * and so E21 tunable changes (S21.02 onward) leave an empirical trail - * for the deferred stack-shrink optimisation to consume. Words, not - * bytes — FreeRTOS reports min free stack in StackType_t units (4 B on - * Cortex-M3). serviceTaskHandle is guarded because - * uxTaskGetStackHighWaterMark(NULL) means "the calling task" — passing a - * NULL handle after a Service xTaskCreate failure would silently - * double-report the interactive task's HWM. */ - const UBaseType_t interactiveHwm = uxTaskGetStackHighWaterMark(NULL); - const UBaseType_t serviceHwm = (serviceTaskHandle != NULL) ? uxTaskGetStackHighWaterMark(serviceTaskHandle) : 0U; - (void) printf( - "[stack-hwm] interactive=%lu words service=%lu words\n", - (unsigned long) interactiveHwm, - (unsigned long) serviceHwm - ); - - TeardownAll(); - vTaskDelete(NULL); + return switchingSender; } -static void ServiceTask(void* argument) +/* Reverse-order teardown of the PlusTcp sender stack. BddTargetTlsSender owns + * the inner MbedTlsStream + PlusTcpTcpStream + StreamSender pool slots, so it is + * released before the plain-TCP tcpSender / tcpStream. */ +static void TeardownNetwork(void) { - (void) argument; - /* Wait until the interactive task has finished initial Setup and - * created the lifecycle mutex / SolidSyslog. After that, the mutex is - * the source of truth — Setup, RebuildWithFileStore, and Teardown all - * hold it across their Destroy/Create transitions, so the ready flag - * is only checked after we win the lock. */ - while ((lifecycleMutex == NULL) || !solidSyslogReady) - { - vTaskDelay(pdMS_TO_TICKS(1)); - } - for (;;) - { - SolidSyslogMutex_Lock(lifecycleMutex); - if (solidSyslogTeardown) - { - /* Teardown set this flag inside the lifecycle critical section and - * is now blocked waiting for us to exit. Capture the waiter under - * the lock, release, notify it that Service has stopped, then - * self-delete before Teardown destroys the mutex. */ - TaskHandle_t waiter = serviceStopWaiter; - SolidSyslogMutex_Unlock(lifecycleMutex); - if (waiter != NULL) - { - (void) xTaskNotifyGive(waiter); - } - vTaskDelete(NULL); - } - if (solidSyslogReady) - { - SolidSyslog_Service(solidSyslog); - } - SolidSyslogMutex_Unlock(lifecycleMutex); - vTaskDelay(pdMS_TO_TICKS(1)); - } + SolidSyslogSwitchingSender_Destroy(switchingSender); + BddTargetTlsSender_Destroy(); + tlsSender = NULL; + SolidSyslogStreamSender_Destroy(tcpSender); + SolidSyslogPlusTcpAddress_Destroy(tcpAddress); + SolidSyslogPlusTcpTcpStream_Destroy(tcpStream); + SolidSyslogUdpSender_Destroy(udpSender); + SolidSyslogPlusTcpAddress_Destroy(udpAddress); + SolidSyslogPlusTcpDatagram_Destroy(datagram); + SolidSyslogPlusTcpResolver_Destroy(resolver); } diff --git a/Bdd/Targets/FreeRtosLwip/CMakeLists.txt b/Bdd/Targets/FreeRtosLwip/CMakeLists.txt index 50888131..eb053c69 100644 --- a/Bdd/Targets/FreeRtosLwip/CMakeLists.txt +++ b/Bdd/Targets/FreeRtosLwip/CMakeLists.txt @@ -250,6 +250,7 @@ add_executable(SolidSyslogBddTargetLwip ${CMAKE_SOURCE_DIR}/Platform/MbedTls/Source/SolidSyslogMbedTlsAesGcmPolicyStatic.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/diskio.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetErrorText.c + ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetFreeRtosPipeline.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetInteractive.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetIps.c ${CMAKE_SOURCE_DIR}/Bdd/Targets/Common/BddTargetLanguage.c diff --git a/Bdd/Targets/FreeRtosLwip/main.c b/Bdd/Targets/FreeRtosLwip/main.c index 42dc9106..6e26f035 100644 --- a/Bdd/Targets/FreeRtosLwip/main.c +++ b/Bdd/Targets/FreeRtosLwip/main.c @@ -1,83 +1,39 @@ /* FreeRTOS + lwIP (Raw API, NO_SYS=0) SolidSyslog BDD target for QEMU * mps2-an385. * - * S28.09: the worked NO_SYS=0 integration the lwIP guide promises. lwIP runs - * its own tcpip thread (tcpip_init); a hand-written LAN9118 netif - * (netif/EthernetIf.c) drives the wire; the SolidSyslog LwipRaw adapters reach - * the lwIP core through the tcpip_callback marshal (S28.06, - * SolidSyslogLwipRaw_SetMarshal). The pipeline mirrors the FreeRTOS-Plus-TCP - * target (Bdd/Targets/FreeRtos/main.c) with the network backend swapped - * PlusTcp -> LwipRaw: a CircularBuffer + FreeRtosMutex feed a Service task that - * drains over UDP, and BddTargetInteractive drives `send N` / `set ` / - * `quit` over the QEMU -serial stdio UART. + * The platform-independent pipeline — SolidSyslog lifecycle, FatFs-backed store + * + security policies, SD set, the interactive `set` handler, the Service drain + * task, and the console glue — lives in Bdd/Targets/Common/BddTargetFreeRtosPipeline + * (shared with the FreeRTOS-Plus-TCP target, S29.03). This file keeps only the + * lwIP network backend behind the pipeline seam: the tcpip thread + tcpip_callback + * marshal (S28.06), the hand-written LAN9118 netif (netif/EthernetIf.c), the + * static-IP bring-up + gateway ARP warm-up, the LwipRaw sender wiring (UDP + + * octet-framed TCP + TLS/mTLS via mbedTLS over a second LwipRaw TCP), and the + * RFC 5424 HOSTNAME read from the netif. * - * Scope is UDP (S28.09) + TCP (S28.10) + TLS/mTLS (S28.11). The SwitchingSender - * carries a real UDP sender (LwipRawDatagram), a real octet-framed TCP sender - * (StreamSender over LwipRawTcpStream), and a real TLS sender (StreamSender over - * SolidSyslogMbedTlsStream over a second LwipRawTcpStream) — see - * BddTargetTlsSender_MbedTls_LwipRawTcp.c. tls and mtls share the one TLS slot; - * the destination port (6514 vs 6515) is dispatched at Connect time. - * - * S29.01 adds the FatFs-backed store, mirroring the Plus-TCP target: `set store - * file` tears down the default NullStore and rebuilds with FatFsFile + - * FileBlockDevice + BlockStore over QEMU semihosting (shared diskio.c, 8 MiB - * FAT16). `set security-policy ` selects the - * at-rest integrity policy (hmac / aes reuse the TLS module's mbedTLS RNG). - * - * Static IPv4 (10.0.2.15) on the QEMU slirp network, host reachable at the - * slirp gateway 10.0.2.2. The oracle is addressed by name ("syslog-ng") via - * SolidSyslogLwipRawDnsResolver; lwIP's DNS_LOCAL_HOSTLIST (see lwipopts.h) - * maps that name statically to 10.0.2.2, since slirp cannot return a reachable - * address for the docker alias over real DNS. This is the by-name end-to-end - * path S28.08 delivers — host now equals the TLS serverName, so no numeric pin. */ + * Static IPv4 (10.0.2.15) on the QEMU slirp network, host reachable at the slirp + * gateway 10.0.2.2. The oracle is addressed by name ("syslog-ng") via + * SolidSyslogLwipRawDnsResolver; lwIP's DNS_LOCAL_HOSTLIST (see lwipopts.h) maps + * that name statically to 10.0.2.2 (slirp can't return a reachable address for + * the docker alias over real DNS). */ -#include "CmsdkUart.h" +#include "BddTargetFreeRtosPipeline.h" #include "EthernetIf.h" -#include "BddTargetEnterpriseId.h" -#include "BddTargetErrorText.h" -#include "BddTargetInteractive.h" -#include "BddTargetIps.h" -#include "BddTargetLanguage.h" #include "BddTargetSwitchConfig.h" #include "BddTargetTlsSender.h" -#include "SolidSyslog.h" -#include "SolidSyslogAtomicCounter.h" -#include "SolidSyslogBlockStore.h" -#include "SolidSyslogCircularBuffer.h" -#include "SolidSyslogConfig.h" -#include "SolidSyslogCrc16Policy.h" -#include "SolidSyslogEndpoint.h" -#include "SolidSyslogError.h" -#include "SolidSyslogFatFsFile.h" -#include "SolidSyslogFileBlockDevice.h" #include "SolidSyslogFormatter.h" -#include "SolidSyslogFreeRtosMutex.h" -#include "SolidSyslogFreeRtosSysUpTime.h" #include "SolidSyslogLwipRawAddress.h" #include "SolidSyslogLwipRawDatagram.h" #include "SolidSyslogLwipRawDnsResolver.h" #include "SolidSyslogLwipRawMarshal.h" #include "SolidSyslogLwipRawTcpStream.h" -#include "SolidSyslogMbedTlsAesGcmPolicy.h" -#include "SolidSyslogMbedTlsHmacSha256Policy.h" -#include "SolidSyslogMetaSd.h" -#include "SolidSyslogMutex.h" -#include "SolidSyslogNullSecurityPolicy.h" -#include "SolidSyslogNullStore.h" -#include "SolidSyslogOriginSd.h" -#include "SolidSyslogPrival.h" -#include "SolidSyslogStdAtomicCounter.h" +#include "SolidSyslogSender.h" #include "SolidSyslogStreamSender.h" #include "SolidSyslogSwitchingSender.h" -#include "SolidSyslogTimeQuality.h" -#include "SolidSyslogTimeQualitySd.h" -#include "SolidSyslogTunables.h" #include "SolidSyslogUdpSender.h" -#include "ff.h" /* f_mount / f_mkfs — eager mount-or-format on the `set store file` rebuild trigger. */ - #include "lwip/etharp.h" #include "lwip/init.h" #include "lwip/ip4_addr.h" @@ -89,111 +45,17 @@ #include #include -#include -#include #include #define CMSDK_UART0_BASE_ADDRESS UINT32_C(0x40004000) -/* Unprivileged mirror of SOLIDSYSLOG_UDP_DEFAULT_PORT (514) for BDD listeners. */ -#define BDD_TARGET_UDP_PORT 5514U - -/* Interactive task: BddTargetInteractive's 2048-byte line + name frames plus - * SolidSyslog_Log's two SOLIDSYSLOG_MAX_MESSAGE_SIZE formatter frames and - * newlib printf, *and* the one-shot mbedTLS init (DRBG seed, RSA key parse, - * x509 walks) that BddTargetTlsSender_Create drives on this task at startup. - * *48 (24 KB) matches the +TCP target's post-TLS budget for the same work; - * heap_4 (96 KB) absorbs it alongside the lwIP tcpip / RX tasks. */ -#define INTERACTIVE_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE * 48U) -#define SERVICE_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE * 16U) - -static char appName[49] = "SolidSyslogBddTarget"; -static char messageId[33] = "example"; -static char msg[SOLIDSYSLOG_MAX_MESSAGE_SIZE] = "Hello from FreeRTOS lwIP"; -static char host[16] = "syslog-ng"; -static uint16_t port = (uint16_t) BDD_TARGET_UDP_PORT; -static uint32_t endpointVersion = 0U; - -static struct SolidSyslogMessage testMessage = { - .Facility = SOLIDSYSLOG_FACILITY_LOCAL0, - .Severity = SOLIDSYSLOG_SEVERITY_INFORMATIONAL, - .MessageId = messageId, - .Msg = msg, -}; - /* lwIP netif descriptor — must outlive the tcpip thread. */ static struct netif networkInterface; -/* Gateway IP, kept at file scope so the ARP warm-up (which resolves it before - * the first datagram) can reach it after bring-up. */ +/* Gateway IP, kept at file scope so the ARP warm-up can reach it after bring-up. */ static ip4_addr_t gatewayAddress; -/* CircularBuffer + FreeRtosMutex for cross-task emission. 8 max-sized messages - * is comfortably above the 3-message BDD scenarios. */ -enum -{ - BDD_TARGET_BUFFER_MESSAGES = 8 -}; - -static uint8_t bufferRing[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(BDD_TARGET_BUFFER_MESSAGES)]; - -/* Lifecycle mutex serialises SolidSyslog_Service against the teardown path. - * solidSyslogTeardown is set inside the critical section so Service observes - * it atomically with the destroy and self-deletes before the mutex goes. */ -static struct SolidSyslogMutex* lifecycleMutex = NULL; -static struct SolidSyslog* solidSyslog = NULL; -static volatile bool solidSyslogReady = false; -static volatile bool solidSyslogTeardown = false; - -static struct SolidSyslogConfig solidSyslogConfig; -static struct SolidSyslogStructuredData* sdList[3]; -static struct SolidSyslogAtomicCounter* atomicCounter = NULL; -static struct SolidSyslogStructuredData* metaSd = NULL; -static struct SolidSyslogStructuredData* timeQualitySd = NULL; -static struct SolidSyslogStructuredData* originSd = NULL; - -/* File-backed store storage. Lives in .bss so it persists across the `set - * store file` rebuild; only populated when that command fires. STORE_PATH_PREFIX - * is "STORE" — sequence-numbered FAT filenames land as STORE00.log, STORE01.log, - * … which fit 8.3 short-filename mode (LFN=0 in our shared ffconf.h). */ -static const char STORE_PATH_PREFIX[] = "STORE"; - -/* FATFS object lives in .bss because f_mount stores its address inside the FatFs - * volume registry — the object must outlive every f_open / f_stat / f_unlink. - * One per volume (FF_VOLUMES = 1). */ -static FATFS fatfs; -static bool fatfsMounted = false; - -static struct SolidSyslogFile* storeFile = NULL; -static struct SolidSyslogBlockDevice* storeBlockDevice = NULL; -static struct SolidSyslogStore* currentStore = NULL; -static bool currentStoreIsFile = false; - -/* Pending values populated by the `set max-blocks` / `max-block-size` / - * `discard-policy` / `halt-exit` / `capacity-threshold` / `security-policy` / - * `no-sd` commands and consumed by `set store file`. Defaults mirror the - * Plus-TCP target (Bdd/Targets/FreeRtos/main.c). */ -enum -{ - DEFAULT_PENDING_MAX_BLOCKS = 10, - DEFAULT_PENDING_MAX_BLOCK_SIZE = 65536, -}; - -static size_t pendingMaxBlocks = DEFAULT_PENDING_MAX_BLOCKS; -static size_t pendingMaxBlockSize = DEFAULT_PENDING_MAX_BLOCK_SIZE; -static const char* pendingDiscardPolicy = "oldest"; -static volatile bool pendingHaltExit = false; -static size_t pendingCapacityThreshold = 0; -/* At-rest integrity policy for the file store: "crc16" (default), "hmac-sha256" - * (mbedTLS), "aes-256-gcm" (mbedTLS AEAD), or "null". Set before `store file`; - * consumed by RebuildWithFileStore. currentPolicy holds the created handle so - * DestroyCurrentStore can release it. */ -static const char* pendingSecurityPolicy = "crc16"; -static struct SolidSyslogSecurityPolicy* currentPolicy = NULL; -/* When true, SolidSyslog gets only the meta SD — timeQuality and origin are - * dropped. Mirrors Linux's --no-sd. Consumed by the initial Setup and by - * RebuildWithFileStore. */ -static volatile bool pendingNoSd = false; - +/* LwipRaw sender adapters — built by BuildSender on the interactive task, torn + * down by TeardownNetwork. */ static struct SolidSyslogResolver* resolver = NULL; static struct SolidSyslogDatagram* datagram = NULL; static struct SolidSyslogAddress* udpAddress = NULL; @@ -202,76 +64,26 @@ static struct SolidSyslogStream* tcpStream = NULL; static struct SolidSyslogAddress* tcpAddress = NULL; static struct SolidSyslogSender* tcpSender = NULL; static struct SolidSyslogSender* switchingSender = NULL; -static struct SolidSyslogBuffer* buffer = NULL; -static struct SolidSyslogMutex* bufferMutex = NULL; - -static TaskHandle_t serviceTaskHandle = NULL; - -/* The task awaiting ServiceTask's exit during teardown. Set by TeardownAll - * (inside the lifecycle critical section, so ServiceTask reads it atomically - * with the teardown flag); ServiceTask notifies it the instant before it - * self-deletes, letting TeardownAll destroy the lifecycle mutex on a real - * "service stopped" signal instead of a fixed delay. */ -static TaskHandle_t serviceStopWaiter = NULL; - -/* Upper bound on the teardown wait for ServiceTask to self-delete. ServiceTask - * observes the flag within one ~1 ms iteration, so this is only ever reached if - * the Service task never started (xTaskCreate failure); it bounds teardown - * rather than timing it. */ -enum -{ - SERVICE_STOP_TIMEOUT_MS = 1000, -}; -static void InteractiveTask(void* argument); -static void ServiceTask(void* argument); static void LwipTcpipMarshal(SolidSyslogLwipRawCallback callback, void* context); static void NetworkBringUp(void* context); static void WarmUpGatewayArp(void); static void GatewayResolvedQuery(void* context); -static bool TryUpdateString(char* storage, size_t storageSize, const char* value); -static bool TryParseUInt(const char* value, unsigned long* out); -static bool RebuildWithFileStore(void); -static void DestroyCurrentStore(void); -static bool EnsureFatFsMounted(void); -static struct SolidSyslogSecurityPolicy* CreateSecurityPolicy(void); -static void DestroySecurityPolicy(void); -static enum SolidSyslogDiscardPolicy MapDiscardPolicy(const char* policy); -static void OnStoreFull(void* context); -static size_t GetCapacityThreshold(void* context); -static void OnThresholdCrossed(void* context); -static void TeardownAll(void); -static void SemihostingExit(int status); - -static uint32_t MmioRead32(uintptr_t address) -{ - // NOLINTNEXTLINE(performance-no-int-to-ptr) -- mapping the CMSDK UART MMIO address into a 32-bit volatile pointer. - return *(volatile uint32_t*) address; -} - -static void MmioWrite32(uintptr_t address, uint32_t value) -{ - // NOLINTNEXTLINE(performance-no-int-to-ptr) -- mapping the CMSDK UART MMIO address into a 32-bit volatile pointer. - *(volatile uint32_t*) address = value; -} - -static void RtosSleep(int milliseconds) -{ - TickType_t ticks = pdMS_TO_TICKS((TickType_t) milliseconds); - if ((milliseconds > 0) && (ticks == 0U)) - { - ticks = 1U; - } - vTaskDelay(ticks); -} - -static const CmsdkUartMemoryAccess MMIO_ACCESS = {MmioRead32, MmioWrite32, RtosSleep}; +static void GetHostname(struct SolidSyslogFormatter* formatter); +static struct SolidSyslogSender* BuildSender(void); +static void TeardownNetwork(void); + +static const struct BddTargetFreeRtosPipelineConfig PIPELINE_CONFIG = { + .DefaultHost = "syslog-ng", + .BuildSender = BuildSender, + .GetHostname = GetHostname, + .TeardownNetwork = TeardownNetwork, +}; -/* lwIP randomness source (declared by arch/cc.h's LWIP_RAND). sys_now() is - * provided by the contrib FreeRTOS sys_arch under NO_SYS=0, so — unlike the - * S28.07 probe — we do not define it here. A self-contained xorshift32 keeps - * TCP ISN selection deterministic without newlib rand() / a real entropy - * backend; adequate for the BDD smoke test. */ +/* lwIP randomness source (declared by arch/cc.h's LWIP_RAND). sys_now() comes + * from the contrib FreeRTOS sys_arch under NO_SYS=0. A self-contained xorshift32 + * keeps TCP ISN selection deterministic without a real entropy backend; + * adequate for the BDD smoke test. */ unsigned int LwipPortRand(void) { static uint32_t state = 0x2545F491U; @@ -283,28 +95,39 @@ unsigned int LwipPortRand(void) int main(void) { - CmsdkUart_Init(&MMIO_ACCESS, CMSDK_UART0_BASE_ADDRESS); + BddTargetFreeRtosPipeline_InitConsole(CMSDK_UART0_BASE_ADDRESS); + BddTargetFreeRtosPipeline_SetConfig(&PIPELINE_CONFIG); /* Pin every LwipRaw adapter call onto the tcpip thread. */ SolidSyslogLwipRaw_SetMarshal(LwipTcpipMarshal); - /* Create the tcpip thread + mbox + core-lock mutex. Pre-scheduler safe - * (xTaskCreate / xQueueCreate / xSemaphoreCreate all work before - * vTaskStartScheduler); the thread itself only runs once the scheduler - * starts. The netif bring-up is deferred to NetworkBringUp on that thread - * — smsc9220_init() calls vTaskDelay, which would deref a NULL - * pxCurrentTCB if run before the scheduler. */ + /* Create the tcpip thread + mbox + core-lock mutex. Pre-scheduler safe; the + * thread runs once the scheduler starts. The netif bring-up is deferred to + * NetworkBringUp on that thread (smsc9220_init calls vTaskDelay, which would + * deref a NULL pxCurrentTCB before the scheduler). */ tcpip_init(NULL, NULL); - if (xTaskCreate(InteractiveTask, "interactive", INTERACTIVE_TASK_STACK_DEPTH, NULL, tskIDLE_PRIORITY + 1, NULL) != - pdPASS) + if (xTaskCreate( + BddTargetFreeRtosPipeline_InteractiveTask, + "interactive", + configMINIMAL_STACK_SIZE * BDD_TARGET_INTERACTIVE_STACK_MULTIPLIER, + NULL, + tskIDLE_PRIORITY + 1, + NULL + ) != pdPASS) { - SemihostingExit(1); + BddTargetFreeRtosPipeline_Exit(1); } - if (xTaskCreate(ServiceTask, "service", SERVICE_TASK_STACK_DEPTH, NULL, tskIDLE_PRIORITY + 1, &serviceTaskHandle) != - pdPASS) + if (xTaskCreate( + BddTargetFreeRtosPipeline_ServiceTask, + "service", + configMINIMAL_STACK_SIZE * BDD_TARGET_SERVICE_STACK_MULTIPLIER, + NULL, + tskIDLE_PRIORITY + 1, + NULL + ) != pdPASS) { - SemihostingExit(1); + BddTargetFreeRtosPipeline_Exit(1); } vTaskStartScheduler(); @@ -315,16 +138,16 @@ int main(void) return 0; } -/* Runs on the tcpip thread (dispatched via tcpip_callback once the scheduler - * is up) so netif_add, the link/up transitions, and smsc9220_init's vTaskDelay - * all execute in a valid task context with the lwIP core lock held. */ +/* Runs on the tcpip thread (dispatched via tcpip_callback once the scheduler is + * up) so netif_add, the link/up transitions, and smsc9220_init's vTaskDelay all + * execute in a valid task context with the lwIP core lock held. */ static void NetworkBringUp(void* context) { (void) context; ip4_addr_t ipAddress; ip4_addr_t netmask; - /* QEMU slirp default: 10.0.2.15 guest, 10.0.2.2 gateway (NATed to the - * QEMU host, where the syslog-ng oracle listens). */ + /* QEMU slirp default: 10.0.2.15 guest, 10.0.2.2 gateway (NATed to the QEMU + * host, where the syslog-ng oracle listens). */ IP4_ADDR(&ipAddress, 10, 0, 2, 15); IP4_ADDR(&netmask, 255, 255, 255, 0); IP4_ADDR(&gatewayAddress, 10, 0, 2, 2); @@ -335,11 +158,9 @@ static void NetworkBringUp(void* context) netif_set_link_up(&networkInterface); /* Kick off ARP resolution for the gateway now, so the cache is warm before - * the first datagram. SolidSyslogLwipRawDatagram sends PBUF_REF packets - * pointing at a transient buffer; if the first send hit an ARP miss the - * queued copy would reference freed memory and be lost (the documented - * first-packet drop). The reply is processed by this tcpip thread; the - * interactive task waits for the cache to populate via WarmUpGatewayArp. */ + * the first datagram (SolidSyslogLwipRawDatagram sends PBUF_REF packets; an + * ARP miss on the first send would drop it). The reply is processed by this + * tcpip thread; the interactive task waits via WarmUpGatewayArp. */ (void) etharp_request(&networkInterface, &gatewayAddress); } @@ -352,9 +173,9 @@ static void GatewayResolvedQuery(void* context) *(bool*) context = (etharp_find_addr(&networkInterface, &gatewayAddress, ðRet, &ipRet) >= 0); } -/* Blocks the calling (interactive) task — never the tcpip thread, which must - * stay free to process the ARP reply — until the gateway resolves or a bounded - * deadline passes. Generous deadline: QEMU is markedly slower than host. */ +/* Blocks the calling (interactive) task — never the tcpip thread — until the + * gateway resolves or a bounded deadline passes. Generous deadline: QEMU is + * markedly slower than host. */ static void WarmUpGatewayArp(void) { enum @@ -367,8 +188,7 @@ static void WarmUpGatewayArp(void) { bool resolved = false; /* Same synchronous marshal the LwipRaw adapters use: the query runs under - * the core lock and writes `resolved` before this returns, so there is no - * race between the queued callback and the next loop iteration. */ + * the core lock and writes `resolved` before this returns. */ LwipTcpipMarshal(GatewayResolvedQuery, &resolved); if (resolved) { @@ -382,10 +202,10 @@ static void LwipTcpipMarshal(SolidSyslogLwipRawCallback callback, void* context) { /* The synchronous-marshal contract (SolidSyslogLwipRawMarshal.h) requires the * callback's results to be ready when this returns. lwIP's tcpip_callback only - * blocks until the work is *queued*, so it cannot satisfy that by itself. + * blocks until the work is queued, so it cannot satisfy that alone. * LWIP_TCPIP_CORE_LOCKING is enabled, so we run the callback in the caller's * own task context under the core lock instead: unconditionally synchronous, - * independent of task priority, and no per-send mailbox message. The lock is + * independent of task priority, no per-send mailbox message. The lock is * recursive and our callbacks never re-marshal, so this cannot self-deadlock. */ LOCK_TCPIP_CORE(); callback(context); @@ -410,504 +230,51 @@ void vApplicationStackOverflowHook(TaskHandle_t task, char* taskName) static void GetHostname(struct SolidSyslogFormatter* formatter) { - /* RFC 5424 section 6.2.4 rung 2 (static IP address) — read back from the - * netif so a future DHCP slice satisfies the same rung without touching - * this callback. */ + /* RFC 5424 §6.2.4 rung 2 (static IP) — read back from the netif so a future + * DHCP slice satisfies the same rung without touching this callback. */ const char* address = ip4addr_ntoa(netif_ip4_addr(&networkInterface)); SolidSyslogFormatter_BoundedString(formatter, address, strlen(address)); } -static void GetAppName(struct SolidSyslogFormatter* formatter) -{ - SolidSyslogFormatter_BoundedString(formatter, appName, strlen(appName)); -} - -static void GetTimeQuality(struct SolidSyslogTimeQuality* timeQuality) -{ - timeQuality->TzKnown = false; - timeQuality->IsSynced = false; - timeQuality->SyncAccuracyMicroseconds = SOLIDSYSLOG_SYNC_ACCURACY_OMIT; -} - -static void GetEndpoint(struct SolidSyslogEndpoint* endpoint) -{ - SolidSyslogFormatter_BoundedString(endpoint->Host, host, strlen(host)); - endpoint->Port = port; -} - -static uint32_t GetEndpointVersion(void) -{ - return endpointVersion; -} - -static void ErrorHandlerEx(void* context, const struct SolidSyslogErrorEvent* event) -{ - (void) context; - const char* sourceName = ""; - const struct SolidSyslogErrorSource* source = event->Source; - if (source != NULL) - { - sourceName = source->Name; - } - const char* message = BddTargetErrorText_Category(event->Category); - (void) printf( - "[solidsyslog] severity=%d [%s cat=%u detail=%ld] %s\n", - (int) event->Severity, - sourceName, - (unsigned) event->Category, - (long) event->Detail, - message - ); -} - -static bool OnSet(const char* name, const char* value) -{ - bool handled = false; - if (strcmp(name, "appname") == 0) - { - handled = TryUpdateString(appName, sizeof(appName), value); - } - else if (strcmp(name, "msgid") == 0) - { - handled = TryUpdateString(messageId, sizeof(messageId), value); - } - else if (strcmp(name, "msg") == 0) - { - handled = TryUpdateString(msg, sizeof(msg), value); - } - else if (strcmp(name, "host") == 0) - { - handled = TryUpdateString(host, sizeof(host), value); - if (handled) - { - endpointVersion++; - } - } - else if (strcmp(name, "port") == 0) - { - unsigned long parsed = 0U; - if (TryParseUInt(value, &parsed) && (parsed != 0U) && (parsed <= UINT16_MAX)) - { - port = (uint16_t) parsed; - endpointVersion++; - handled = true; - } - } - else if (strcmp(name, "facility") == 0) - { - unsigned long parsed = 0U; - if (TryParseUInt(value, &parsed)) - { - testMessage.Facility = (enum SolidSyslogFacility) parsed; - handled = true; - } - } - else if (strcmp(name, "severity") == 0) - { - unsigned long parsed = 0U; - if (TryParseUInt(value, &parsed)) - { - testMessage.Severity = (enum SolidSyslogSeverity) parsed; - handled = true; - } - } - else if (strcmp(name, "transport") == 0) - { - BddTargetSwitchConfig_SetByName(value); - handled = true; - } - else if (strcmp(name, "max-blocks") == 0) - { - unsigned long parsed = 0U; - if (TryParseUInt(value, &parsed)) - { - pendingMaxBlocks = (size_t) parsed; - handled = true; - } - } - else if (strcmp(name, "max-block-size") == 0) - { - unsigned long parsed = 0U; - if (TryParseUInt(value, &parsed)) - { - pendingMaxBlockSize = (size_t) parsed; - handled = true; - } - } - else if (strcmp(name, "discard-policy") == 0) - { - if ((strcmp(value, "oldest") == 0) || (strcmp(value, "newest") == 0) || (strcmp(value, "halt") == 0)) - { - /* String literal storage — target_driver.py emits one of the three - * literals so the pointer stays valid (no copy needed). */ - pendingDiscardPolicy = - (strcmp(value, "newest") == 0) ? "newest" : ((strcmp(value, "halt") == 0) ? "halt" : "oldest"); - handled = true; - } - } - else if (strcmp(name, "security-policy") == 0) - { - if (strcmp(value, "hmac-sha256") == 0) - { - pendingSecurityPolicy = "hmac-sha256"; - handled = true; - } - else if (strcmp(value, "aes-256-gcm") == 0) - { - pendingSecurityPolicy = "aes-256-gcm"; - handled = true; - } - else if (strcmp(value, "null") == 0) - { - pendingSecurityPolicy = "null"; - handled = true; - } - else if (strcmp(value, "crc16") == 0) - { - pendingSecurityPolicy = "crc16"; - handled = true; - } - } - else if (strcmp(name, "capacity-threshold") == 0) - { - unsigned long parsed = 0U; - if (TryParseUInt(value, &parsed)) - { - pendingCapacityThreshold = (size_t) parsed; - handled = true; - } - } - else if (strcmp(name, "halt-exit") == 0) - { - /* Harness emits `set halt-exit 1` (or `0`); anything non-zero trips the - * halt path. Mirrors Linux's bare --halt-exit flag. */ - unsigned long parsed = 0U; - if (TryParseUInt(value, &parsed)) - { - pendingHaltExit = (parsed != 0U); - handled = true; - } - } - else if (strcmp(name, "no-sd") == 0) - { - /* `set no-sd 1` drops the SD list to only metaSd — mirrors Linux's - * --no-sd. Takes effect via SolidSyslog re-Create (initial Setup or the - * `set store file` rebuild). */ - unsigned long parsed = 0U; - if (TryParseUInt(value, &parsed)) - { - pendingNoSd = (parsed != 0U); - handled = true; - } - } - else if (strcmp(name, "store") == 0) - { - /* "null" is the default state — accept it as an unconditional no-op so - * the harness can pass --store null without special-casing. "file" - * triggers the rebuild (one-way for the lifetime of this QEMU - * instance — see RebuildWithFileStore). */ - if (strcmp(value, "null") == 0) - { - handled = true; - } - else if (strcmp(value, "file") == 0) - { - handled = RebuildWithFileStore(); - } - } - else if (strcmp(name, "shutdown") == 0) - { - unsigned long parsed = 0U; - if (TryParseUInt(value, &parsed)) - { - if (parsed != 0U) - { - TeardownAll(); - SemihostingExit(0); - } - handled = true; - } - } - return handled; -} - -static bool TryUpdateString(char* storage, size_t storageSize, const char* value) -{ - bool updated = false; - size_t length = strlen(value); - if ((length != 0U) && (length < storageSize)) - { - memcpy(storage, value, length); - storage[length] = '\0'; - updated = true; - } - return updated; -} - -static bool TryParseUInt(const char* value, unsigned long* out) -{ - bool parsed = false; - if (*value != '\0') - { - char* end = NULL; - unsigned long candidate = strtoul(value, &end, 10); - if (*end == '\0') - { - *out = candidate; - parsed = true; - } - } - return parsed; -} - -/* DEMO KEY ONLY. A real integrator supplies key material from a secure element, - * a KDF, or encrypted NVM via their own SolidSyslogKeyFunction — never a - * hard-coded constant. This exists so the BDD scenario can exercise the mbedTLS - * HMAC-SHA256 / AES-256-GCM at-rest policies end-to-end with real crypto. */ -static bool BddDemoGetKey(void* context, uint8_t* keyOut, size_t capacity, size_t* keyLengthOut) -{ - enum - { - DEMO_KEY_SIZE = 32 - }; - - (void) context; - size_t written = (capacity < DEMO_KEY_SIZE) ? capacity : (size_t) DEMO_KEY_SIZE; - (void) memset(keyOut, 0x5A, written); - *keyLengthOut = written; - return true; -} - -static struct SolidSyslogSecurityPolicy* CreateSecurityPolicy(void) -{ - struct SolidSyslogSecurityPolicy* policy = NULL; - if (strcmp(pendingSecurityPolicy, "hmac-sha256") == 0) - { - static const struct SolidSyslogMbedTlsHmacSha256PolicyConfig hmacConfig = {BddDemoGetKey, NULL}; - policy = SolidSyslogMbedTlsHmacSha256Policy_Create(&hmacConfig); - } - else if (strcmp(pendingSecurityPolicy, "aes-256-gcm") == 0) - { - /* Reuse the TLS module's already-seeded CTR-DRBG as the AEAD nonce - * source — see BddTargetTlsSender_GetRng. Not static const: Rng is a - * runtime handle. */ - const struct SolidSyslogMbedTlsAesGcmPolicyConfig aesConfig = - {BddDemoGetKey, NULL, BddTargetTlsSender_GetRng()}; - policy = SolidSyslogMbedTlsAesGcmPolicy_Create(&aesConfig); - } - else if (strcmp(pendingSecurityPolicy, "null") == 0) - { - policy = SolidSyslogNullSecurityPolicy_Get(); - } - else - { - policy = SolidSyslogCrc16Policy_Create(); - } - return policy; -} - -/* `set store file` trigger: swap the default NullStore for a FatFs-backed - * BlockStore. One-way for the lifetime of this QEMU instance. The lifecycle - * mutex blocks the Service task across the Destroy → re-Create transition. */ -static bool RebuildWithFileStore(void) +/* Bring up the netif on the tcpip thread, warm the gateway ARP, then build the + * LwipRaw SwitchingSender: UDP, octet-framed TCP, and a TLS/mTLS slot (mbedTLS + * over a second LwipRaw TCP stream). Default transport UDP. Runs on the + * interactive task — LwipRaw adapters touch a started lwIP core, which is now up. */ +static struct SolidSyslogSender* BuildSender(void) { - SolidSyslogMutex_Lock(lifecycleMutex); - - /* FatFs does NOT auto-mount on first f_open — mount (and format-on-first-use) - * before tearing down the existing store so a mount failure leaves the - * target running on the original NullStore (zero-disruption); return false - * so OnSet reports the failure to the harness. */ - if (!EnsureFatFsMounted()) - { - SolidSyslogMutex_Unlock(lifecycleMutex); - return false; - } - - solidSyslogReady = false; - SolidSyslog_Destroy(solidSyslog); - solidSyslog = NULL; - DestroyCurrentStore(); - - storeFile = SolidSyslogFatFsFile_Create(); - storeBlockDevice = SolidSyslogFileBlockDevice_Create(storeFile, STORE_PATH_PREFIX); - - struct SolidSyslogSecurityPolicy* policy = CreateSecurityPolicy(); - currentPolicy = policy; - struct SolidSyslogBlockStoreConfig storeConfig = { - .BlockDevice = storeBlockDevice, - .MaxBlockSize = pendingMaxBlockSize, - .MaxBlocks = pendingMaxBlocks, - .DiscardPolicy = MapDiscardPolicy(pendingDiscardPolicy), - .SecurityPolicy = policy, - .OnStoreFull = OnStoreFull, - .StoreFullContext = NULL, - .GetCapacityThreshold = GetCapacityThreshold, - .OnThresholdCrossed = OnThresholdCrossed, - .ThresholdContext = &pendingCapacityThreshold, - }; - currentStore = SolidSyslogBlockStore_Create(&storeConfig); - currentStoreIsFile = true; - - solidSyslogConfig.Store = currentStore; - /* Re-honour `set no-sd 1` if it arrived before this rebuild — target_driver.py - * sorts `set no-sd` before `set store file`, so the value is final here. */ - solidSyslogConfig.SdCount = pendingNoSd ? 1U : (sizeof(sdList) / sizeof(sdList[0])); - solidSyslog = SolidSyslog_Create(&solidSyslogConfig); - solidSyslogReady = true; - SolidSyslogMutex_Unlock(lifecycleMutex); - return true; -} - -/* Mount volume 0; format-on-first-use if the disk image has no FAT yet. - * Idempotent — subsequent calls short-circuit on fatfsMounted. The work buffer - * for f_mkfs is sized to FF_MAX_SS (512 B), the minimum f_mkfs accepts on a - * FAT12/16 volume. */ -static bool EnsureFatFsMounted(void) -{ - if (fatfsMounted) - { - return true; - } - FRESULT res = f_mount(&fatfs, "", 1); /* opt=1 → mount immediately, surface FR_NO_FILESYSTEM here */ - if (res == FR_NO_FILESYSTEM) - { - /* Fresh disk image — lay down a FAT and re-mount. FM_FAT keeps the - * formatter on FAT12/16; at the shared 8 MiB geometry auto cluster - * sizing clears the ~4085-cluster boundary, so this lands FAT16 (the - * geometry the later FreeRTOS-Plus-FAT formatter needs). */ - static BYTE workBuffer[FF_MAX_SS]; - const MKFS_PARM opts = {.fmt = FM_FAT | FM_SFD, .n_fat = 1, .align = 1, .n_root = 0, .au_size = 0}; - res = f_mkfs("", &opts, workBuffer, sizeof(workBuffer)); - if (res == FR_OK) - { - res = f_mount(&fatfs, "", 1); - } - } - if (res != FR_OK) - { - (void) printf("[solidsyslog] fatfs mount failed: FRESULT=%d\n", (int) res); - return false; - } - fatfsMounted = true; - return true; -} - -static void DestroySecurityPolicy(void) -{ - if (strcmp(pendingSecurityPolicy, "hmac-sha256") == 0) - { - SolidSyslogMbedTlsHmacSha256Policy_Destroy(currentPolicy); - } - else if (strcmp(pendingSecurityPolicy, "aes-256-gcm") == 0) - { - SolidSyslogMbedTlsAesGcmPolicy_Destroy(currentPolicy); - } - else if (strcmp(pendingSecurityPolicy, "crc16") == 0) - { - SolidSyslogCrc16Policy_Destroy(); - } - /* else "null": the shared NullSecurityPolicy is immutable — nothing to free. */ - currentPolicy = NULL; -} - -/* Tears down whichever store is currently installed (file-backed or null). - * FatFsFile_Destroy → Close → f_close flushes the underlying FIL's dir entry. */ -static void DestroyCurrentStore(void) -{ - if (currentStoreIsFile) - { - SolidSyslogBlockStore_Destroy(currentStore); - SolidSyslogFileBlockDevice_Destroy(storeBlockDevice); - DestroySecurityPolicy(); - SolidSyslogFatFsFile_Destroy(storeFile); - } - /* else: NullStore is shared and immutable — nothing to destroy. */ -} - -static enum SolidSyslogDiscardPolicy MapDiscardPolicy(const char* policy) -{ - if (strcmp(policy, "newest") == 0) - { - return SOLIDSYSLOG_DISCARD_POLICY_NEWEST; - } - if (strcmp(policy, "halt") == 0) - { - return SOLIDSYSLOG_DISCARD_POLICY_HALT; - } - return SOLIDSYSLOG_DISCARD_POLICY_OLDEST; -} - -static void OnStoreFull(void* context) -{ - (void) context; - if (pendingHaltExit) - { - /* Semihosting SYS_EXIT — terminates QEMU with status 2 so the BDD - * harness sees the run end deterministically. Mirrors the Linux - * example's _exit(2). */ - SemihostingExit(2); - } -} - -static size_t GetCapacityThreshold(void* context) -{ - return *(const size_t*) context; -} - -/* Stdout marker the behave harness watches for. The Linux equivalent writes a - * host file unreachable from the QEMU guest; instead we print a line-anchored - * token to the UART, which the captured-stdout reader in syslog_steps.py scans. */ -static void OnThresholdCrossed(void* context) -{ - (void) context; - (void) printf("[THRESHOLD-CROSSED]\r\n"); -} - -static void InteractiveTask(void* argument) -{ - (void) argument; - - /* Bring the netif up on the tcpip thread now the scheduler is running. - * Blocking callback — returns once the interface is added and link/up. */ + /* Bring the netif up on the tcpip thread now the scheduler is running, then + * wait for the gateway ARP so the first datagram is not lost to a cache miss. */ (void) tcpip_callback(NetworkBringUp, NULL); - - /* Wait for the gateway ARP to resolve before any datagram goes out, so the - * first send is not lost to a cache miss (see WarmUpGatewayArp). */ WarmUpGatewayArp(); - /* No host pin needed: SolidSyslogLwipRawDnsResolver resolves the oracle by - * name, and the DNS_LOCAL_HOSTLIST entry maps "syslog-ng" -> 10.0.2.2 on the - * guest. The shared BddTargetTlsConfig already defaults host to "syslog-ng" - * with serverName falling back to it, so the destination host now equals the - * TLS serverName / cert subject without any override. RtosSleep drives the - * resolver's bounded async-resolve spin (a local-hostlist hit returns - * synchronously, so it never actually spins here). */ + /* SolidSyslogLwipRawDnsResolver resolves the oracle by name; the + * DNS_LOCAL_HOSTLIST entry maps "syslog-ng" -> 10.0.2.2 on the guest, so the + * destination host equals the TLS serverName / cert subject without any + * numeric pin. The shared Sleep drives the bounded async-resolve spin (a + * local-hostlist hit returns synchronously, so it never actually spins). */ struct SolidSyslogLwipRawDnsResolverConfig dnsConfig = { - .Sleep = RtosSleep, + .Sleep = BddTargetFreeRtosPipeline_Sleep, }; resolver = SolidSyslogLwipRawDnsResolver_Create(&dnsConfig); datagram = SolidSyslogLwipRawDatagram_Create(); udpAddress = SolidSyslogLwipRawAddress_Create(); - struct SolidSyslogUdpSenderConfig udpConfig = { .Resolver = resolver, .Datagram = datagram, .Address = udpAddress, - .Endpoint = GetEndpoint, - .EndpointVersion = GetEndpointVersion, + .Endpoint = BddTargetFreeRtosPipeline_GetEndpoint, + .EndpointVersion = BddTargetFreeRtosPipeline_GetEndpointVersion, }; udpSender = SolidSyslogUdpSender_Create(&udpConfig); - /* Plain TCP path: RFC 6587 octet-framed StreamSender over the LwipRaw TCP - * stream adapter. Shares the resolver and the UDP endpoint callbacks because - * the syslog-ng oracle listens on the same host:port for both transports. - * RtosSleep drives the stream's bounded synchronous-connect spin; the - * connect timeout comes from the SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS tunable - * (GetConnectTimeoutMs NULL). */ + /* Plain TCP: RFC 6587 octet-framed StreamSender over the LwipRaw TCP stream. + * The connect timeout comes from the SOLIDSYSLOG_TCP_CONNECT_TIMEOUT_MS + * tunable (GetConnectTimeoutMs NULL); the shared Sleep drives the bounded + * synchronous-connect spin. */ struct SolidSyslogLwipRawTcpStreamConfig tcpStreamConfig = { .GetConnectTimeoutMs = NULL, .ConnectTimeoutContext = NULL, - .Sleep = RtosSleep, + .Sleep = BddTargetFreeRtosPipeline_Sleep, }; tcpStream = SolidSyslogLwipRawTcpStream_Create(&tcpStreamConfig); tcpAddress = SolidSyslogLwipRawAddress_Create(); @@ -915,23 +282,13 @@ static void InteractiveTask(void* argument) .Resolver = resolver, .Stream = tcpStream, .Address = tcpAddress, - .Endpoint = GetEndpoint, - .EndpointVersion = GetEndpointVersion, + .Endpoint = BddTargetFreeRtosPipeline_GetEndpoint, + .EndpointVersion = BddTargetFreeRtosPipeline_GetEndpointVersion, }; tcpSender = SolidSyslogStreamSender_Create(&tcpConfig); - /* TLS slot: SolidSyslogMbedTlsStream over a second LwipRawTcpStream, with - * the demo CA / client cert / client key baked into the ELF. The same slot - * serves both @tls (port 6514) and @mtls (port 6515) scenarios — the - * wrapper wires the client cert unconditionally and its StreamSender - * Endpoint callback dispatches on BddTargetSwitchConfig_IsMtlsMode() at - * Connect time. The `false` argument is honoured for cross-platform - * signature uniformity but ignored on FreeRTOS, where the harness flips - * mode over the UART after the prompt is already up. */ struct SolidSyslogSender* tlsSender = BddTargetTlsSender_Create(resolver, false); - /* SwitchingSender lets `set transport ` flip transport at - * runtime. UDP, TCP, and TLS/mTLS are all wired. */ static struct SolidSyslogSender* inners[BDD_TARGET_SWITCH_COUNT]; inners[BDD_TARGET_SWITCH_UDP] = udpSender; inners[BDD_TARGET_SWITCH_TCP] = tcpSender; @@ -943,144 +300,15 @@ static void InteractiveTask(void* argument) }; BddTargetSwitchConfig_SetByName("udp"); switchingSender = SolidSyslogSwitchingSender_Create(&switchConfig); - - bufferMutex = SolidSyslogFreeRtosMutex_Create(); - buffer = SolidSyslogCircularBuffer_Create(bufferMutex, bufferRing, sizeof(bufferRing)); - lifecycleMutex = SolidSyslogFreeRtosMutex_Create(); - - /* Default store is NullStore — flipped to FatFs/BlockStore by - * `set store file` via RebuildWithFileStore(). */ - currentStore = SolidSyslogNullStore_Get(); - currentStoreIsFile = false; - - atomicCounter = SolidSyslogStdAtomicCounter_Create(); - struct SolidSyslogMetaSdConfig metaConfig = { - .Counter = atomicCounter, - .GetSysUpTime = SolidSyslogFreeRtosSysUpTime_Get, - .GetLanguage = BddTargetLanguage_Get, - }; - metaSd = SolidSyslogMetaSd_Create(&metaConfig); - timeQualitySd = SolidSyslogTimeQualitySd_Create(GetTimeQuality); - struct SolidSyslogOriginSdConfig originConfig = { - .Software = "SolidSyslogBddTarget", - .SwVersion = "0.7.0", - .EnterpriseId = BDD_TARGET_ENTERPRISE_ID, - .GetIpCount = BddTargetIps_Count, - .GetIpAt = BddTargetIps_At, - }; - originSd = SolidSyslogOriginSd_Create(&originConfig); - sdList[0] = metaSd; - sdList[1] = timeQualitySd; - sdList[2] = originSd; - - solidSyslogConfig = (struct SolidSyslogConfig) { - .Buffer = buffer, - .Sender = switchingSender, - .Clock = NULL, - .GetHostname = GetHostname, - .GetAppName = GetAppName, - .GetProcessId = NULL, - .Store = currentStore, - .Sd = sdList, - /* pendingNoSd is normally false at this initial Setup — `set no-sd 1` - * arrives over the UART after the prompt is up, and the `set store file` - * rebuild rewrites .SdCount with the final value. Defensive here in case - * a scenario sends `set no-sd 1` before any rebuild. */ - .SdCount = pendingNoSd ? 1U : (sizeof(sdList) / sizeof(sdList[0])), - }; - SolidSyslog_SetErrorHandler(ErrorHandlerEx, NULL); - solidSyslog = SolidSyslog_Create(&solidSyslogConfig); - solidSyslogReady = true; - - BddTargetInteractive_Run(solidSyslog, &testMessage, stdin, BddTargetSwitchConfig_SetByName, OnSet); - - const UBaseType_t interactiveHwm = uxTaskGetStackHighWaterMark(NULL); - const UBaseType_t serviceHwm = (serviceTaskHandle != NULL) ? uxTaskGetStackHighWaterMark(serviceTaskHandle) : 0U; - (void) printf( - "[stack-hwm] interactive=%lu words service=%lu words\n", - (unsigned long) interactiveHwm, - (unsigned long) serviceHwm - ); - - TeardownAll(); - vTaskDelete(NULL); + return switchingSender; } -static void ServiceTask(void* argument) +/* Reverse-order teardown of the LwipRaw sender stack. BddTargetTlsSender owns + * the inner MbedTlsStream + LwipRawTcpStream + StreamSender pool slots, so it is + * released before the plain-TCP tcpSender / tcpStream. */ +static void TeardownNetwork(void) { - (void) argument; - while ((lifecycleMutex == NULL) || !solidSyslogReady) - { - vTaskDelay(pdMS_TO_TICKS(1)); - } - for (;;) - { - SolidSyslogMutex_Lock(lifecycleMutex); - if (solidSyslogTeardown) - { - /* Capture the waiter under the lock, release, notify it that - * Service has stopped, then self-delete before Teardown destroys - * the mutex. */ - TaskHandle_t waiter = serviceStopWaiter; - SolidSyslogMutex_Unlock(lifecycleMutex); - if (waiter != NULL) - { - (void) xTaskNotifyGive(waiter); - } - vTaskDelete(NULL); - } - if (solidSyslogReady) - { - SolidSyslog_Service(solidSyslog); - } - SolidSyslogMutex_Unlock(lifecycleMutex); - vTaskDelay(pdMS_TO_TICKS(1)); - } -} - -static void TeardownAll(void) -{ - SolidSyslogMutex_Lock(lifecycleMutex); - serviceStopWaiter = xTaskGetCurrentTaskHandle(); - solidSyslogTeardown = true; - solidSyslogReady = false; - SolidSyslog_Destroy(solidSyslog); - solidSyslog = NULL; - SolidSyslogOriginSd_Destroy(originSd); - SolidSyslogTimeQualitySd_Destroy(timeQualitySd); - SolidSyslogMetaSd_Destroy(metaSd); - SolidSyslogStdAtomicCounter_Destroy(atomicCounter); - DestroyCurrentStore(); - /* f_unmount regardless of how we got here so the next session's f_mount - * finds STORE*.log directory entries up-to-date — the power_cycle_replay - * scenario relies on this. */ - if (fatfsMounted) - { - (void) f_unmount(""); - fatfsMounted = false; - } - SolidSyslogMutex_Unlock(lifecycleMutex); - - /* Wait for Service to observe the teardown flag and vTaskDelete itself - * before the lifecycle mutex is destroyed under it. ServiceTask notifies - * us from its self-delete path, so this returns the moment it has actually - * exited rather than after a fixed guess. Bounded so a Service task that - * never started (xTaskCreate failure → NULL handle) cannot wedge teardown. */ - if (serviceTaskHandle != NULL) - { - (void) ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(SERVICE_STOP_TIMEOUT_MS)); - } - serviceTaskHandle = NULL; - - SolidSyslogCircularBuffer_Destroy(buffer); - SolidSyslogFreeRtosMutex_Destroy(bufferMutex); - SolidSyslogFreeRtosMutex_Destroy(lifecycleMutex); - lifecycleMutex = NULL; SolidSyslogSwitchingSender_Destroy(switchingSender); - /* BddTargetTlsSender owns the inner MbedTlsStream + LwipRawTcpStream pool - * slots and its own StreamSender slot, so release it before the plain-TCP - * tcpSender / tcpStream below — roughly reverse order of Create keeps the - * dependency graph clean even though the pool allocator is order-insensitive. */ BddTargetTlsSender_Destroy(); SolidSyslogUdpSender_Destroy(udpSender); SolidSyslogStreamSender_Destroy(tcpSender); @@ -1090,22 +318,3 @@ static void TeardownAll(void) SolidSyslogLwipRawDatagram_Destroy(datagram); SolidSyslogLwipRawDnsResolver_Destroy(resolver); } - -static void SemihostingExit(int status) -{ - /* SYS_EXIT_EXTENDED (0x20) — propagates a non-zero status on AArch32 via a - * { reason, subcode } block. QEMU terminates the VM; the for(;;) is - * defensive. */ - const struct - { - uint32_t reason; - uint32_t subcode; - } args = {0x20026U, (uint32_t) status}; - - register int r0 __asm("r0") = 0x20; - register const void* r1 __asm("r1") = &args; - __asm volatile("bkpt 0xAB" : "+r"(r0) : "r"(r1) : "memory"); - for (;;) - { - } -} diff --git a/DEVLOG.md b/DEVLOG.md index 7eb5e86c..ed821406 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -1,5 +1,46 @@ # Dev Log +## 2026-06-03 — S29.03 Extract the shared FreeRTOS BDD-target pipeline + +The DRY pass S29.01 deferred. The two FreeRTOS target `main.c` files +(FreeRTOS-Plus-TCP + lwIP) had drifted to ~600 lines of near-verbatim shared +code — the whole store/security-policy lifecycle, the `OnSet` handler, the SD +set, the Service drain task, the console glue, `ErrorHandlerEx`. Extracted all of +it into `Bdd/Targets/Common/BddTargetFreeRtosPipeline.{c,h}`; each `main.c` now +keeps only its network backend. + +### Design — shared pipeline + injection seam + +`BddTargetFreeRtosPipelineConfig` is the whole seam: `DefaultHost`, a `BuildSender` +thunk (platform brings up its network + builds the Switching sender, runs on the +interactive task), a `GetHostname` callback (reads the platform IP stack), and a +`TeardownNetwork` thunk. The pipeline owns the SolidSyslog lifecycle, store, +SD/config, both FreeRTOS tasks, and exposes `GetEndpoint`/`GetEndpointVersion` +(the sender configs in each `main.c` wire these — they read the shared host/port) +plus `Sleep` / `Exit` / `InitConsole`. The Service task self-registers its handle, +so neither task needs the caller to plumb anything. + +### Decisions + +- **One TU, compiled into each executable** — not a static lib. It's Tier-3 BDD + glue; the existing pattern is per-target source lists. +- **`BuildSender` is a thunk, not a pre-built pointer** — the lwIP adapters must + touch a *started* lwIP core, so the netif bring-up + ARP warm-up had to run on + the interactive task, inside the seam, not in `main()` before the scheduler. +- **`GetEndpoint`/`GetEndpointVersion` exported** — they read the shared + host/port but are consumed by the platform sender configs; leaving them static + would have been an unused-symbol warning *and* unreachable from `main.c`. + +### Result / validation + +- `FreeRtos/main.c` 1258 → 291, `FreeRtosLwip/main.c` 1111 → 320; the shared + surface (956 lines incl. header) now exists once. Net −800 lines. +- Both targets cross-build clean on the new `sha-0b93766` image (no warnings); + the lwIP ELF still has **zero `Platform/PlusTcp` symbols**; clang-format clean. +- Full BDD suite green on **both** targets — **44/44** scenarios each (UDP / TCP / + TLS / mTLS / the whole `@store` suite incl. `@hmac` / `@aesgcm` / capacity / + power-cycle / block-lifecycle). Pure behaviour preservation. + ## 2026-06-03 — S29.02 Re-provision FreeRTOS container images with FreeRTOS-Plus-FAT Container/infra prep for E29: brings FreeRTOS-Plus-FAT back into the