From 4b617540a0b3efdb3b938e5492031a8c5ec8a75a Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 24 May 2026 18:43:09 +0000 Subject: [PATCH 01/11] chore(tests): introduce MqFake and migrate PMQB tests onto it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PMQB was the only Platform/Posix adapter unit-testing against real Linux POSIX message queues — violated the "fake the platform API for adapter unit tests" convention (S08.05 FatFsFake precedent; existing SocketFake/WinsockFake/OpenSslFake siblings). Add Tests/Support/MqFake.{h,c} exporting mq_open / mq_send / mq_receive / mq_close / mq_unlink with an in-memory queue registry and one-shot failure-injection knobs mirroring SocketFake. Migrate the 18 existing PMQB tests to call MqFake_Reset() in setup(). Behaviour-preserving — no new error codes yet. Add MqFakeTest.cpp self-tests pinning the fake's contracts. Foundation for S12.07 error-path slices. Real mq_* continues to exercise PMQB end-to-end at the BDD layer. --- Tests/CMakeLists.txt | 1 + Tests/MqFakeTest.cpp | 145 ++++++ ...SolidSyslogPosixMessageQueueBufferTest.cpp | 7 + Tests/Support/CMakeLists.txt | 1 + Tests/Support/MqFake.c | 423 ++++++++++++++++++ Tests/Support/MqFake.h | 51 +++ 6 files changed, 628 insertions(+) create mode 100644 Tests/MqFakeTest.cpp create mode 100644 Tests/Support/MqFake.c create mode 100644 Tests/Support/MqFake.h diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index bb4a720f..ae547a6d 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -122,6 +122,7 @@ if(SOLIDSYSLOG_POSIX) SolidSyslogGetAddrInfoResolverTest.cpp SolidSyslogPosixDatagramTest.cpp SolidSyslogPosixTcpStreamTest.cpp + MqFakeTest.cpp SocketFakeTest.cpp ) endif() diff --git a/Tests/MqFakeTest.cpp b/Tests/MqFakeTest.cpp new file mode 100644 index 00000000..76cd857c --- /dev/null +++ b/Tests/MqFakeTest.cpp @@ -0,0 +1,145 @@ +#include +#include +#include +#include +#include + +#include "MqFake.h" +#include "TestUtils.h" +#include "CppUTest/TestHarness.h" + +using namespace CososoTesting; // NOLINT(google-build-using-namespace) -- test-file scope only; brings NEVER/ONCE/TWICE/THRICE into scope for the CALLED_* + // macros + +static mqd_t OpenTestQueue(const char* name, long maxMessages = 4, size_t maxMessageSize = 64) +{ + struct mq_attr attr = {0, maxMessages, static_cast(maxMessageSize), 0, {0}}; + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg) -- POSIX mq_open is a varargs function when O_CREAT is set + return mq_open(name, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR, &attr); +} + +// clang-format off +TEST_GROUP(MqFake) +{ + void setup() override { MqFake_Reset(); } +}; +// clang-format on + +TEST(MqFake, OpenIncrementsCount) +{ + OpenTestQueue("/test"); + CALLED_FAKE(MqFake_Open, ONCE); +} + +TEST(MqFake, OpenCapturesName) +{ + OpenTestQueue("/captured"); + STRCMP_EQUAL("/captured", MqFake_LastOpenName()); +} + +TEST(MqFake, OpenCapturesAttrFields) +{ + OpenTestQueue("/test", 7, 128); + LONGS_EQUAL(7, MqFake_LastOpenMaxMessages()); + LONGS_EQUAL(128, MqFake_LastOpenMaxMessageSize()); +} + +TEST(MqFake, OpenReturnsDistinctMqdPerCall) +{ + mqd_t first = OpenTestQueue("/first"); + mqd_t second = OpenTestQueue("/second"); + CHECK(first != second); + CHECK(first != (mqd_t) -1); + CHECK(second != (mqd_t) -1); +} + +TEST(MqFake, FailNextOpenReturnsMinusOneAndSetsErrno) +{ + MqFake_FailNextOpen(EINVAL); + mqd_t result = OpenTestQueue("/test"); + LONGS_EQUAL(-1, (long) result); + LONGS_EQUAL(EINVAL, errno); +} + +TEST(MqFake, FailNextOpenIsOneShot) +{ + MqFake_FailNextOpen(EINVAL); + (void) OpenTestQueue("/first"); + mqd_t second = OpenTestQueue("/second"); + CHECK(second != (mqd_t) -1); +} + +TEST(MqFake, OpenNameHistoryReturnsNameByIndex) +{ + OpenTestQueue("/alpha"); + OpenTestQueue("/beta"); + STRCMP_EQUAL("/alpha", MqFake_OpenNameAt(0)); + STRCMP_EQUAL("/beta", MqFake_OpenNameAt(1)); +} + +TEST(MqFake, SendStoresMessageForReceive) +{ + mqd_t mqd = OpenTestQueue("/test"); + LONGS_EQUAL(0, mq_send(mqd, "payload", 7, 0)); + + char buf[16] = {}; + ssize_t received = mq_receive(mqd, buf, sizeof(buf), nullptr); + LONGS_EQUAL(7, received); + STRCMP_EQUAL("payload", buf); +} + +TEST(MqFake, ReceiveFromEmptyQueueReturnsMinusOneEagain) +{ + mqd_t mqd = OpenTestQueue("/test"); + + char buf[16] = {}; + ssize_t result = mq_receive(mqd, buf, sizeof(buf), nullptr); + LONGS_EQUAL(-1, result); + LONGS_EQUAL(EAGAIN, errno); +} + +TEST(MqFake, FailNextSendReturnsMinusOneAndSetsErrno) +{ + mqd_t mqd = OpenTestQueue("/test"); + MqFake_FailNextSend(EMSGSIZE); + LONGS_EQUAL(-1, mq_send(mqd, "x", 1, 0)); + LONGS_EQUAL(EMSGSIZE, errno); +} + +TEST(MqFake, FailNextReceiveReturnsMinusOneAndSetsErrno) +{ + mqd_t mqd = OpenTestQueue("/test"); + mq_send(mqd, "x", 1, 0); + MqFake_FailNextReceive(EBADMSG); + char buf[16] = {}; + LONGS_EQUAL(-1, mq_receive(mqd, buf, sizeof(buf), nullptr)); + LONGS_EQUAL(EBADMSG, errno); +} + +TEST(MqFake, CloseIncrementsCount) +{ + mqd_t mqd = OpenTestQueue("/test"); + mq_close(mqd); + CALLED_FAKE(MqFake_Close, ONCE); + LONGS_EQUAL((long) mqd, (long) MqFake_LastClosedMqd()); +} + +TEST(MqFake, UnlinkIncrementsCountAndCapturesName) +{ + mq_unlink("/gone"); + CALLED_FAKE(MqFake_Unlink, ONCE); + STRCMP_EQUAL("/gone", MqFake_LastUnlinkName()); +} + +TEST(MqFake, IsolatedQueuesDoNotShareMessages) +{ + mqd_t alpha = OpenTestQueue("/alpha"); + mqd_t beta = OpenTestQueue("/beta"); + + mq_send(alpha, "from-alpha", 10, 0); + + char buf[16] = {}; + ssize_t result = mq_receive(beta, buf, sizeof(buf), nullptr); + LONGS_EQUAL(-1, result); + LONGS_EQUAL(EAGAIN, errno); +} diff --git a/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp b/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp index d254ef79..bad8c8b1 100644 --- a/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp +++ b/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp @@ -7,6 +7,7 @@ using namespace CososoTesting; // NOLINT(google-build-using-namespace) -- test-f // macros #include "ConfigLockFake.h" #include "ErrorHandlerFake.h" +#include "MqFake.h" #include "SolidSyslogBuffer.h" #include "SolidSyslogBufferDefinition.h" #include "SolidSyslogPosixMessageQueueBuffer.h" @@ -33,6 +34,7 @@ TEST_GROUP(SolidSyslogPosixMessageQueueBuffer) void setup() override { + MqFake_Reset(); // cppcheck-suppress unreadVariable -- used across TEST_GROUP methods; cppcheck does not model CppUTest macros buffer = SolidSyslogPosixMessageQueueBuffer_Create(SOLIDSYSLOG_MAX_MESSAGE_SIZE, 10); readSize = 0; @@ -157,6 +159,11 @@ TEST_GROUP(SolidSyslogPosixMessageQueueBufferPool) struct SolidSyslogBuffer* pooled[SOLIDSYSLOG_POSIX_MESSAGE_QUEUE_BUFFER_POOL_SIZE] = {}; struct SolidSyslogBuffer* overflow = nullptr; + void setup() override + { + MqFake_Reset(); + } + void teardown() override { for (auto* handle : pooled) diff --git a/Tests/Support/CMakeLists.txt b/Tests/Support/CMakeLists.txt index 85925c9e..8d8ff36f 100644 --- a/Tests/Support/CMakeLists.txt +++ b/Tests/Support/CMakeLists.txt @@ -9,6 +9,7 @@ target_link_libraries(ConfigLockFake PUBLIC ${PROJECT_NAME}) if(SOLIDSYSLOG_POSIX) add_library(PosixFakes STATIC ClockFake.c + MqFake.c SafeStringStandard.c SocketFake.c ) diff --git a/Tests/Support/MqFake.c b/Tests/Support/MqFake.c new file mode 100644 index 00000000..bc4fb232 --- /dev/null +++ b/Tests/Support/MqFake.c @@ -0,0 +1,423 @@ +#include "MqFake.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "SafeString.h" +#include "SolidSyslogTunables.h" + +enum +{ + MQFAKE_MAX_QUEUES = 4, + MQFAKE_MAX_MESSAGES_PER_QUEUE = 8, + MQFAKE_MAX_OPEN_HISTORY = 8, + MQFAKE_MAX_NAME_LEN = 64, + MQFAKE_HANDLE_BASE = 100, + MQFAKE_SEND_BUF_SIZE = SOLIDSYSLOG_MAX_MESSAGE_SIZE + 1U +}; + +struct MqFakeQueue +{ + long maxMessages; + size_t maxMessageSize; + size_t messageLens[MQFAKE_MAX_MESSAGES_PER_QUEUE]; + int head; + int tail; + int count; + bool inUse; + char messages[MQFAKE_MAX_MESSAGES_PER_QUEUE][SOLIDSYSLOG_MAX_MESSAGE_SIZE]; +}; + +static struct MqFakeQueue queues[MQFAKE_MAX_QUEUES]; + +static bool nextOpenShouldFail; +static int nextOpenErrno; +static int openCallCount; +static char openNameHistory[MQFAKE_MAX_OPEN_HISTORY][MQFAKE_MAX_NAME_LEN]; +static char lastOpenName[MQFAKE_MAX_NAME_LEN]; +static int lastOpenOflag; +static long lastOpenMaxMessages; +static size_t lastOpenMaxMessageSize; + +static bool nextSendShouldFail; +static int nextSendErrno; +static int sendCallCount; +static mqd_t lastSendMqd; +static char lastSendBufCopy[MQFAKE_SEND_BUF_SIZE]; +static size_t lastSendLen; + +static bool nextReceiveShouldFail; +static int nextReceiveErrno; +static int receiveCallCount; +static mqd_t lastReceiveMqd; +static size_t lastReceiveMaxLen; + +static int closeCallCount; +static mqd_t lastClosedMqd; + +static int unlinkCallCount; +static char lastUnlinkName[MQFAKE_MAX_NAME_LEN]; + +void MqFake_Reset(void) +{ + /* Reset errno so a stale value from a prior test cannot leak. */ + errno = 0; + + for (size_t i = 0; i < (size_t) MQFAKE_MAX_QUEUES; i++) + { + queues[i].inUse = false; + queues[i].maxMessages = 0; + queues[i].maxMessageSize = 0; + queues[i].head = 0; + queues[i].tail = 0; + queues[i].count = 0; + for (size_t m = 0; m < (size_t) MQFAKE_MAX_MESSAGES_PER_QUEUE; m++) + { + queues[i].messageLens[m] = 0; + } + } + + nextOpenShouldFail = false; + nextOpenErrno = 0; + openCallCount = 0; + for (size_t i = 0; i < (size_t) MQFAKE_MAX_OPEN_HISTORY; i++) + { + openNameHistory[i][0] = '\0'; + } + lastOpenName[0] = '\0'; + lastOpenOflag = 0; + lastOpenMaxMessages = 0; + lastOpenMaxMessageSize = 0; + + nextSendShouldFail = false; + nextSendErrno = 0; + sendCallCount = 0; + lastSendMqd = (mqd_t) -1; + lastSendBufCopy[0] = '\0'; + lastSendLen = 0; + + nextReceiveShouldFail = false; + nextReceiveErrno = 0; + receiveCallCount = 0; + lastReceiveMqd = (mqd_t) -1; + lastReceiveMaxLen = 0; + + closeCallCount = 0; + lastClosedMqd = (mqd_t) -1; + + unlinkCallCount = 0; + lastUnlinkName[0] = '\0'; +} + +void MqFake_FailNextOpen(int errnoValue) +{ + nextOpenShouldFail = true; + nextOpenErrno = errnoValue; +} + +int MqFake_OpenCallCount(void) +{ + return openCallCount; +} + +const char* MqFake_LastOpenName(void) +{ + return lastOpenName; +} + +int MqFake_LastOpenOflag(void) +{ + return lastOpenOflag; +} + +long MqFake_LastOpenMaxMessages(void) +{ + return lastOpenMaxMessages; +} + +size_t MqFake_LastOpenMaxMessageSize(void) +{ + return lastOpenMaxMessageSize; +} + +const char* MqFake_OpenNameAt(int callIndex) +{ + const char* result = ""; + if ((callIndex >= 0) && (callIndex < MQFAKE_MAX_OPEN_HISTORY)) + { + result = openNameHistory[callIndex]; + } + return result; +} + +void MqFake_FailNextSend(int errnoValue) +{ + nextSendShouldFail = true; + nextSendErrno = errnoValue; +} + +int MqFake_SendCallCount(void) +{ + return sendCallCount; +} + +mqd_t MqFake_LastSendMqd(void) +{ + return lastSendMqd; +} + +const char* MqFake_LastSendBufAsString(void) +{ + return lastSendBufCopy; +} + +size_t MqFake_LastSendLen(void) +{ + return lastSendLen; +} + +void MqFake_FailNextReceive(int errnoValue) +{ + nextReceiveShouldFail = true; + nextReceiveErrno = errnoValue; +} + +int MqFake_ReceiveCallCount(void) +{ + return receiveCallCount; +} + +mqd_t MqFake_LastReceiveMqd(void) +{ + return lastReceiveMqd; +} + +size_t MqFake_LastReceiveMaxLen(void) +{ + return lastReceiveMaxLen; +} + +int MqFake_CloseCallCount(void) +{ + return closeCallCount; +} + +mqd_t MqFake_LastClosedMqd(void) +{ + return lastClosedMqd; +} + +int MqFake_UnlinkCallCount(void) +{ + return unlinkCallCount; +} + +const char* MqFake_LastUnlinkName(void) +{ + return lastUnlinkName; +} + +static int MqFake_AllocateQueue(void) +{ + int result = -1; + for (int i = 0; i < MQFAKE_MAX_QUEUES; i++) + { + if (!queues[i].inUse) + { + queues[i].inUse = true; + queues[i].head = 0; + queues[i].tail = 0; + queues[i].count = 0; + result = i; + break; + } + } + return result; +} + +static struct MqFakeQueue* MqFake_QueueFromMqd(mqd_t mqd) +{ + struct MqFakeQueue* result = NULL; + int index = (int) mqd - MQFAKE_HANDLE_BASE; + if ((index >= 0) && (index < MQFAKE_MAX_QUEUES) && queues[index].inUse) + { + result = &queues[index]; + } + return result; +} + +/* NOLINTNEXTLINE(cert-dcl50-cpp,readability-inconsistent-declaration-parameter-name) -- POSIX API; varargs and parameter names differ from glibc internal names */ +mqd_t mq_open(const char* name, int oflag, ...) +{ + mqd_t result = (mqd_t) -1; + + if (nextOpenShouldFail) + { + nextOpenShouldFail = false; + errno = nextOpenErrno; + } + else + { + long requestedMaxMessages = 0; + size_t requestedMaxMessageSize = 0; + if ((oflag & O_CREAT) != 0) + { + va_list ap; + va_start(ap, oflag); + (void) va_arg(ap, mode_t); /* mode — unused by fake */ + const struct mq_attr* attr = va_arg(ap, struct mq_attr*); + va_end(ap); + if (attr != NULL) + { + requestedMaxMessages = attr->mq_maxmsg; + requestedMaxMessageSize = (size_t) attr->mq_msgsize; + } + } + + int index = MqFake_AllocateQueue(); + if (index >= 0) + { + queues[index].maxMessages = requestedMaxMessages; + queues[index].maxMessageSize = requestedMaxMessageSize; + result = (mqd_t) (index + MQFAKE_HANDLE_BASE); + } + else + { + errno = EMFILE; + } + + lastOpenOflag = oflag; + lastOpenMaxMessages = requestedMaxMessages; + lastOpenMaxMessageSize = requestedMaxMessageSize; + } + + SafeString_Copy(lastOpenName, sizeof(lastOpenName), name); + if (openCallCount < MQFAKE_MAX_OPEN_HISTORY) + { + SafeString_Copy(openNameHistory[openCallCount], sizeof(openNameHistory[openCallCount]), name); + } + openCallCount++; + return result; +} + +// NOLINTNEXTLINE(readability-inconsistent-declaration-parameter-name) -- POSIX API; parameter names differ from glibc internal names +int mq_close(mqd_t mqd) +{ + closeCallCount++; + lastClosedMqd = mqd; + struct MqFakeQueue* queue = MqFake_QueueFromMqd(mqd); + if (queue != NULL) + { + queue->inUse = false; + } + return 0; +} + +int mq_unlink(const char* name) +{ + unlinkCallCount++; + SafeString_Copy(lastUnlinkName, sizeof(lastUnlinkName), name); + return 0; +} + +// NOLINTNEXTLINE(readability-inconsistent-declaration-parameter-name,bugprone-easily-swappable-parameters) -- POSIX API; parameter names and signature differ from glibc internal names +int mq_send(mqd_t mqd, const char* msg, size_t msgLen, unsigned int msgPrio) +{ + (void) msgPrio; + + sendCallCount++; + lastSendMqd = mqd; + lastSendLen = msgLen; + size_t copyLen = (msgLen < (sizeof(lastSendBufCopy) - 1U)) ? msgLen : (sizeof(lastSendBufCopy) - 1U); + if (copyLen > 0U) + { + memcpy(lastSendBufCopy, msg, copyLen); + } + lastSendBufCopy[copyLen] = '\0'; + + int result = -1; + if (nextSendShouldFail) + { + nextSendShouldFail = false; + errno = nextSendErrno; + } + else + { + struct MqFakeQueue* queue = MqFake_QueueFromMqd(mqd); + if (queue == NULL) + { + errno = EBADF; + } + else if (queue->count >= MQFAKE_MAX_MESSAGES_PER_QUEUE) + { + errno = EAGAIN; + } + else + { + size_t storeLen = (msgLen < sizeof(queue->messages[0])) ? msgLen : sizeof(queue->messages[0]); + if (storeLen > 0U) + { + memcpy(queue->messages[queue->tail], msg, storeLen); + } + queue->messageLens[queue->tail] = storeLen; + queue->tail = (queue->tail + 1) % MQFAKE_MAX_MESSAGES_PER_QUEUE; + queue->count++; + result = 0; + } + } + return result; +} + +// NOLINTNEXTLINE(readability-inconsistent-declaration-parameter-name,bugprone-easily-swappable-parameters,readability-non-const-parameter) -- POSIX API; parameter names and signature differ from glibc internal names +ssize_t mq_receive(mqd_t mqd, char* msg, size_t msgLen, unsigned int* msgPrio) +{ + (void) msgPrio; + + receiveCallCount++; + lastReceiveMqd = mqd; + lastReceiveMaxLen = msgLen; + + ssize_t result = -1; + if (nextReceiveShouldFail) + { + nextReceiveShouldFail = false; + errno = nextReceiveErrno; + } + else + { + struct MqFakeQueue* queue = MqFake_QueueFromMqd(mqd); + if (queue == NULL) + { + errno = EBADF; + } + else if (queue->count == 0) + { + errno = EAGAIN; + } + else + { + size_t pending = queue->messageLens[queue->head]; + if (pending > msgLen) + { + errno = EMSGSIZE; + } + else + { + if (pending > 0U) + { + memcpy(msg, queues[(int) mqd - MQFAKE_HANDLE_BASE].messages[queue->head], pending); + } + queue->head = (queue->head + 1) % MQFAKE_MAX_MESSAGES_PER_QUEUE; + queue->count--; + result = (ssize_t) pending; + } + } + } + return result; +} diff --git a/Tests/Support/MqFake.h b/Tests/Support/MqFake.h new file mode 100644 index 00000000..2aa56ac5 --- /dev/null +++ b/Tests/Support/MqFake.h @@ -0,0 +1,51 @@ +#ifndef MQFAKE_H +#define MQFAKE_H + +#include +#include + +#include "ExternC.h" + +EXTERN_C_BEGIN + + void MqFake_Reset(void); + + /* open configuration — one-shot, consumed by the next mq_open call */ + void MqFake_FailNextOpen(int errnoValue); + + /* open accessors */ + int MqFake_OpenCallCount(void); + const char* MqFake_LastOpenName(void); + int MqFake_LastOpenOflag(void); + long MqFake_LastOpenMaxMessages(void); + size_t MqFake_LastOpenMaxMessageSize(void); + const char* MqFake_OpenNameAt(int callIndex); + + /* send configuration — one-shot, consumed by the next mq_send call */ + void MqFake_FailNextSend(int errnoValue); + + /* send accessors */ + int MqFake_SendCallCount(void); + mqd_t MqFake_LastSendMqd(void); + const char* MqFake_LastSendBufAsString(void); + size_t MqFake_LastSendLen(void); + + /* receive configuration — one-shot, consumed by the next mq_receive call */ + void MqFake_FailNextReceive(int errnoValue); + + /* receive accessors */ + int MqFake_ReceiveCallCount(void); + mqd_t MqFake_LastReceiveMqd(void); + size_t MqFake_LastReceiveMaxLen(void); + + /* close accessors */ + int MqFake_CloseCallCount(void); + mqd_t MqFake_LastClosedMqd(void); + + /* unlink accessors */ + int MqFake_UnlinkCallCount(void); + const char* MqFake_LastUnlinkName(void); + +EXTERN_C_END + +#endif /* MQFAKE_H */ From 6bbc9920eee99d2e991982ff1e3462346dd4fd03 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 24 May 2026 20:50:56 +0000 Subject: [PATCH 02/11] feat(pmqb): emit MQ_OPEN_FAILED and release slot on mq_open failure PMQB Create previously stored the mq_open result without checking it. On failure (e.g. invalid maxMessages, kernel rlimit), Create returned a broken handle whose subsequent mq_send / mq_receive silently failed with EBADF, and the acquired pool slot leaked. Thread the mq_open result back from Initialise. On failure, Create releases the slot (so the pool stays accurate for the next caller) and returns the shared SolidSyslogNullBuffer fallback, emitting the new POSIXMESSAGEQUEUEBUFFER_ERROR_MQ_OPEN_FAILED error. Two tests pin the behaviour: error emission and slot release. Slice 1 of #118. --- ...SolidSyslogPosixMessageQueueBufferErrors.h | 1 + .../SolidSyslogPosixMessageQueueBuffer.c | 3 +- ...lidSyslogPosixMessageQueueBufferMessages.c | 2 ++ ...olidSyslogPosixMessageQueueBufferPrivate.h | 3 +- ...SolidSyslogPosixMessageQueueBufferStatic.c | 21 +++++++++++-- ...SolidSyslogPosixMessageQueueBufferTest.cpp | 31 +++++++++++++++++++ 6 files changed, 57 insertions(+), 4 deletions(-) diff --git a/Platform/Posix/Interface/SolidSyslogPosixMessageQueueBufferErrors.h b/Platform/Posix/Interface/SolidSyslogPosixMessageQueueBufferErrors.h index 10e3b8a4..0f50f943 100644 --- a/Platform/Posix/Interface/SolidSyslogPosixMessageQueueBufferErrors.h +++ b/Platform/Posix/Interface/SolidSyslogPosixMessageQueueBufferErrors.h @@ -11,6 +11,7 @@ EXTERN_C_BEGIN { POSIXMESSAGEQUEUEBUFFER_ERROR_POOL_EXHAUSTED, POSIXMESSAGEQUEUEBUFFER_ERROR_UNKNOWN_DESTROY, + POSIXMESSAGEQUEUEBUFFER_ERROR_MQ_OPEN_FAILED, POSIXMESSAGEQUEUEBUFFER_ERROR_MAX }; diff --git a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c index 91342f5a..395c4208 100644 --- a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c +++ b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c @@ -28,7 +28,7 @@ static inline struct SolidSyslogPosixMessageQueueBuffer* PosixMessageQueueBuffer static inline const char* PosixMessageQueueBuffer_QueueName(struct SolidSyslogPosixMessageQueueBuffer* self); // NOLINTBEGIN(bugprone-easily-swappable-parameters) -- distinct semantic meaning; mirrors the public _Create signature plus a per-slot discriminator -void PosixMessageQueueBuffer_Initialise( +bool PosixMessageQueueBuffer_Initialise( struct SolidSyslogBuffer* base, size_t maxMessageSize, long maxMessages, @@ -58,6 +58,7 @@ void PosixMessageQueueBuffer_Initialise( self->MaxMessageSize = maxMessageSize; self->Base.Write = PosixMessageQueueBuffer_Write; self->Base.Read = PosixMessageQueueBuffer_Read; + return self->Mq != (mqd_t) -1; } void PosixMessageQueueBuffer_Cleanup(struct SolidSyslogBuffer* base) diff --git a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferMessages.c b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferMessages.c index 83c136af..bf4fb643 100644 --- a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferMessages.c +++ b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferMessages.c @@ -10,6 +10,8 @@ static const char* PosixMessageQueueBufferError_AsString(uint8_t code) "SolidSyslogPosixMessageQueueBuffer_Create pool exhausted; returning fallback buffer", [POSIXMESSAGEQUEUEBUFFER_ERROR_UNKNOWN_DESTROY] = "SolidSyslogPosixMessageQueueBuffer_Destroy called with a handle not issued by this pool", + [POSIXMESSAGEQUEUEBUFFER_ERROR_MQ_OPEN_FAILED] = + "SolidSyslogPosixMessageQueueBuffer_Create mq_open failed; returning fallback buffer", }; const char* result = "unknown"; if (code < (uint8_t) POSIXMESSAGEQUEUEBUFFER_ERROR_MAX) diff --git a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferPrivate.h b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferPrivate.h index 43a2272d..f1be91bd 100644 --- a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferPrivate.h +++ b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferPrivate.h @@ -2,6 +2,7 @@ #define SOLIDSYSLOGPOSIXMESSAGEQUEUEBUFFERPRIVATE_H #include +#include #include #include "SolidSyslogBufferDefinition.h" @@ -21,7 +22,7 @@ struct SolidSyslogPosixMessageQueueBuffer size_t MaxMessageSize; }; -void PosixMessageQueueBuffer_Initialise( +bool PosixMessageQueueBuffer_Initialise( struct SolidSyslogBuffer* base, size_t maxMessageSize, long maxMessages, diff --git a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferStatic.c b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferStatic.c index 4a87d6fe..31b249c9 100644 --- a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferStatic.c +++ b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferStatic.c @@ -37,13 +37,30 @@ struct SolidSyslogBuffer* SolidSyslogPosixMessageQueueBuffer_Create(size_t maxMe struct SolidSyslogBuffer* handle = SolidSyslogNullBuffer_Get(); if (SolidSyslogPoolAllocator_IndexIsValid(&PosixMessageQueueBuffer_Allocator, index) == true) { - PosixMessageQueueBuffer_Initialise( + bool opened = PosixMessageQueueBuffer_Initialise( &PosixMessageQueueBuffer_Pool[index].Base, maxMessageSize, maxMessages, index ); - handle = &PosixMessageQueueBuffer_Pool[index].Base; + if (opened) + { + handle = &PosixMessageQueueBuffer_Pool[index].Base; + } + else + { + (void) SolidSyslogPoolAllocator_FreeIfInUse( + &PosixMessageQueueBuffer_Allocator, + index, + PosixMessageQueueBuffer_CleanupAtIndex, + NULL + ); + SolidSyslog_Error( + SOLIDSYSLOG_SEVERITY_ERROR, + &PosixMessageQueueBufferErrorSource, + (uint8_t) POSIXMESSAGEQUEUEBUFFER_ERROR_MQ_OPEN_FAILED + ); + } } else { diff --git a/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp b/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp index bad8c8b1..6271adc0 100644 --- a/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp +++ b/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp @@ -1,3 +1,4 @@ +#include #include #include "TestUtils.h" @@ -219,6 +220,36 @@ TEST(SolidSyslogPosixMessageQueueBufferPool, ExhaustedCreateReportsError) UNSIGNED_LONGS_EQUAL(POSIXMESSAGEQUEUEBUFFER_ERROR_POOL_EXHAUSTED, ErrorHandlerFake_LastCode()); } +TEST(SolidSyslogPosixMessageQueueBufferPool, CreateOnMqOpenFailureReportsError) +{ + ErrorHandlerFake_Install(nullptr); + MqFake_FailNextOpen(EINVAL); + + overflow = MakeBuffer(); + + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_ERROR, ErrorHandlerFake_LastSeverity()); + POINTERS_EQUAL(&PosixMessageQueueBufferErrorSource, ErrorHandlerFake_LastSource()); + UNSIGNED_LONGS_EQUAL(POSIXMESSAGEQUEUEBUFFER_ERROR_MQ_OPEN_FAILED, ErrorHandlerFake_LastCode()); +} + +TEST(SolidSyslogPosixMessageQueueBufferPool, CreateOnMqOpenFailureReleasesSlot) +{ + MqFake_FailNextOpen(EINVAL); + + overflow = MakeBuffer(); + + // Fill the pool *after* the failed Create; if the failed Create had leaked + // its acquired slot, the pool would overflow into the fallback one slot + // sooner — and FillPool's last MakeBuffer would return the same NullBuffer + // singleton as `overflow`, since both Creates would have run out of slots. + FillPool(); + for (auto* slot : pooled) + { + CHECK_TEXT(slot != overflow, "Pool slot collided with the failed-Create fallback handle"); + } +} + TEST(SolidSyslogPosixMessageQueueBufferPool, FallbackReadAndWriteAreNoOps) { FillPool(); From d291de311bff1b8f0ed74e6e5fb4a21671504dd6 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 24 May 2026 21:03:19 +0000 Subject: [PATCH 03/11] feat(pmqb): emit SEND_FAILED when mq_send fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PMQB Write previously discarded the mq_send return value, so any failure (queue full, oversized message, EBADF on a broken handle) was silent — the record was dropped with no visibility to the caller's error handler. Check the mq_send result and emit POSIXMESSAGEQUEUEBUFFER_ERROR_SEND_FAILED on failure. Write's contract stays fire-and-forget (the record is still dropped on the floor); honesty surfaces via the error handler rather than a return code, matching StreamSender. Test pins the EAGAIN (queue full) path. EMSGSIZE (oversized) goes through the same uniform check and lands in slice 3 as a regression guard. Slice 2 of #118. --- .../SolidSyslogPosixMessageQueueBufferErrors.h | 1 + .../Source/SolidSyslogPosixMessageQueueBuffer.c | 12 +++++++++++- .../SolidSyslogPosixMessageQueueBufferMessages.c | 2 ++ Tests/SolidSyslogPosixMessageQueueBufferTest.cpp | 13 +++++++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Platform/Posix/Interface/SolidSyslogPosixMessageQueueBufferErrors.h b/Platform/Posix/Interface/SolidSyslogPosixMessageQueueBufferErrors.h index 0f50f943..75d9767c 100644 --- a/Platform/Posix/Interface/SolidSyslogPosixMessageQueueBufferErrors.h +++ b/Platform/Posix/Interface/SolidSyslogPosixMessageQueueBufferErrors.h @@ -12,6 +12,7 @@ EXTERN_C_BEGIN POSIXMESSAGEQUEUEBUFFER_ERROR_POOL_EXHAUSTED, POSIXMESSAGEQUEUEBUFFER_ERROR_UNKNOWN_DESTROY, POSIXMESSAGEQUEUEBUFFER_ERROR_MQ_OPEN_FAILED, + POSIXMESSAGEQUEUEBUFFER_ERROR_SEND_FAILED, POSIXMESSAGEQUEUEBUFFER_ERROR_MAX }; diff --git a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c index 395c4208..9913233a 100644 --- a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c +++ b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c @@ -8,10 +8,13 @@ #include #include "SolidSyslogBufferDefinition.h" +#include "SolidSyslogError.h" #include "SolidSyslogFormatter.h" #include "SolidSyslogNullBuffer.h" +#include "SolidSyslogPosixMessageQueueBufferErrors.h" #include "SolidSyslogPosixMessageQueueBufferPrivate.h" #include "SolidSyslogPosixProcessId.h" +#include "SolidSyslogPrival.h" enum { @@ -90,7 +93,14 @@ static bool PosixMessageQueueBuffer_Read(struct SolidSyslogBuffer* base, void* d static void PosixMessageQueueBuffer_Write(struct SolidSyslogBuffer* base, const void* data, size_t size) { struct SolidSyslogPosixMessageQueueBuffer* self = PosixMessageQueueBuffer_SelfFromBase(base); - mq_send(self->Mq, data, size, 0); + if (mq_send(self->Mq, data, size, 0) != 0) + { + SolidSyslog_Error( + SOLIDSYSLOG_SEVERITY_ERROR, + &PosixMessageQueueBufferErrorSource, + (uint8_t) POSIXMESSAGEQUEUEBUFFER_ERROR_SEND_FAILED + ); + } } static inline struct SolidSyslogPosixMessageQueueBuffer* PosixMessageQueueBuffer_SelfFromBase( diff --git a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferMessages.c b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferMessages.c index bf4fb643..e6cbb372 100644 --- a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferMessages.c +++ b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferMessages.c @@ -12,6 +12,8 @@ static const char* PosixMessageQueueBufferError_AsString(uint8_t code) "SolidSyslogPosixMessageQueueBuffer_Destroy called with a handle not issued by this pool", [POSIXMESSAGEQUEUEBUFFER_ERROR_MQ_OPEN_FAILED] = "SolidSyslogPosixMessageQueueBuffer_Create mq_open failed; returning fallback buffer", + [POSIXMESSAGEQUEUEBUFFER_ERROR_SEND_FAILED] = + "SolidSyslogPosixMessageQueueBuffer_Write mq_send failed; record dropped", }; const char* result = "unknown"; if (code < (uint8_t) POSIXMESSAGEQUEUEBUFFER_ERROR_MAX) diff --git a/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp b/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp index 6271adc0..a00008d4 100644 --- a/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp +++ b/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp @@ -105,6 +105,19 @@ TEST(SolidSyslogPosixMessageQueueBuffer, SecondReadAfterSingleWriteReturnsFalse) CHECK_FALSE(Read()); } +TEST(SolidSyslogPosixMessageQueueBuffer, WriteWhenMqSendFailsReportsError) +{ + ErrorHandlerFake_Install(nullptr); + MqFake_FailNextSend(EAGAIN); + + Write(); + + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_ERROR, ErrorHandlerFake_LastSeverity()); + POINTERS_EQUAL(&PosixMessageQueueBufferErrorSource, ErrorHandlerFake_LastSource()); + UNSIGNED_LONGS_EQUAL(POSIXMESSAGEQUEUEBUFFER_ERROR_SEND_FAILED, ErrorHandlerFake_LastCode()); +} + TEST(SolidSyslogPosixMessageQueueBuffer, ServiceSendsMessageWrittenViaLog) { struct SolidSyslogSender* fakeSender = SenderFake_Create(); From 3b63d2b03f49ff1db22972d816d856ef51b15361 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 24 May 2026 21:04:11 +0000 Subject: [PATCH 04/11] test(pmqb): pin SEND_FAILED also fires on oversized message (EMSGSIZE) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regression guard. The production check in PosixMessageQueueBuffer_Write is errno-agnostic — `if (mq_send(...) != 0)` catches EAGAIN, EMSGSIZE, EBADF, etc. uniformly — so this test passes without a production change. A future contributor who adds errno-specific handling has to deliberately drop one of these cases. Slice 3 of #118. --- Tests/SolidSyslogPosixMessageQueueBufferTest.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp b/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp index a00008d4..166faa43 100644 --- a/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp +++ b/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp @@ -118,6 +118,21 @@ TEST(SolidSyslogPosixMessageQueueBuffer, WriteWhenMqSendFailsReportsError) UNSIGNED_LONGS_EQUAL(POSIXMESSAGEQUEUEBUFFER_ERROR_SEND_FAILED, ErrorHandlerFake_LastCode()); } +/* Regression guard: the production check is errno-agnostic + * (`if (mq_send(...) != 0)`), so EAGAIN and EMSGSIZE flow through + * the same branch today. A future contributor who adds errno-specific + * handling has to deliberately drop one of these cases. */ +TEST(SolidSyslogPosixMessageQueueBuffer, WriteWithOversizedMessageReportsError) +{ + ErrorHandlerFake_Install(nullptr); + MqFake_FailNextSend(EMSGSIZE); + + Write(); + + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); + UNSIGNED_LONGS_EQUAL(POSIXMESSAGEQUEUEBUFFER_ERROR_SEND_FAILED, ErrorHandlerFake_LastCode()); +} + TEST(SolidSyslogPosixMessageQueueBuffer, ServiceSendsMessageWrittenViaLog) { struct SolidSyslogSender* fakeSender = SenderFake_Create(); From 3e4c94e94234d6b5c3c0c9a1d24d3f816ad403d3 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 24 May 2026 21:06:12 +0000 Subject: [PATCH 05/11] feat(pmqb): emit RECEIVE_FAILED on mq_receive failure (except EAGAIN) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PMQB Read previously folded every mq_receive failure into a silent `return false`. EAGAIN (empty queue) is part of the polling happy path and rightly stays silent, but other errnos (EMSGSIZE on a short caller buffer, EBADF on a broken handle, EINTR, etc.) deserve to surface to the error handler. Classify the mq_receive errno: EAGAIN stays silent; any other failure emits the new POSIXMESSAGEQUEUEBUFFER_ERROR_RECEIVE_FAILED. Read's return contract is unchanged — still false on any failure. Two tests: EMSGSIZE-via-MqFake pins the new error emit; empty-queue poll pins that EAGAIN stays silent (regression guard). Slice 4 of #118. --- ...SolidSyslogPosixMessageQueueBufferErrors.h | 1 + .../SolidSyslogPosixMessageQueueBuffer.c | 12 ++++++++++ ...lidSyslogPosixMessageQueueBufferMessages.c | 2 ++ ...SolidSyslogPosixMessageQueueBufferTest.cpp | 24 +++++++++++++++++++ 4 files changed, 39 insertions(+) diff --git a/Platform/Posix/Interface/SolidSyslogPosixMessageQueueBufferErrors.h b/Platform/Posix/Interface/SolidSyslogPosixMessageQueueBufferErrors.h index 75d9767c..835f2f4b 100644 --- a/Platform/Posix/Interface/SolidSyslogPosixMessageQueueBufferErrors.h +++ b/Platform/Posix/Interface/SolidSyslogPosixMessageQueueBufferErrors.h @@ -13,6 +13,7 @@ EXTERN_C_BEGIN POSIXMESSAGEQUEUEBUFFER_ERROR_UNKNOWN_DESTROY, POSIXMESSAGEQUEUEBUFFER_ERROR_MQ_OPEN_FAILED, POSIXMESSAGEQUEUEBUFFER_ERROR_SEND_FAILED, + POSIXMESSAGEQUEUEBUFFER_ERROR_RECEIVE_FAILED, POSIXMESSAGEQUEUEBUFFER_ERROR_MAX }; diff --git a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c index 9913233a..466b435c 100644 --- a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c +++ b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c @@ -1,5 +1,6 @@ #include "SolidSyslogPosixMessageQueueBuffer.h" +#include #include #include #include @@ -85,6 +86,17 @@ static bool PosixMessageQueueBuffer_Read(struct SolidSyslogBuffer* base, void* d ssize_t received = mq_receive(self->Mq, data, maxSize, NULL); bool success = received >= 0; + /* EAGAIN is the empty-queue poll signal — part of the happy path and must + * stay silent. Any other errno is a real failure worth surfacing. */ + if (!success && (errno != EAGAIN)) + { + SolidSyslog_Error( + SOLIDSYSLOG_SEVERITY_ERROR, + &PosixMessageQueueBufferErrorSource, + (uint8_t) POSIXMESSAGEQUEUEBUFFER_ERROR_RECEIVE_FAILED + ); + } + *bytesRead = success ? (size_t) received : 0U; return success; diff --git a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferMessages.c b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferMessages.c index e6cbb372..c49e48c8 100644 --- a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferMessages.c +++ b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferMessages.c @@ -14,6 +14,8 @@ static const char* PosixMessageQueueBufferError_AsString(uint8_t code) "SolidSyslogPosixMessageQueueBuffer_Create mq_open failed; returning fallback buffer", [POSIXMESSAGEQUEUEBUFFER_ERROR_SEND_FAILED] = "SolidSyslogPosixMessageQueueBuffer_Write mq_send failed; record dropped", + [POSIXMESSAGEQUEUEBUFFER_ERROR_RECEIVE_FAILED] = + "SolidSyslogPosixMessageQueueBuffer_Read mq_receive failed", }; const char* result = "unknown"; if (code < (uint8_t) POSIXMESSAGEQUEUEBUFFER_ERROR_MAX) diff --git a/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp b/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp index 166faa43..3f5fafa9 100644 --- a/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp +++ b/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp @@ -133,6 +133,30 @@ TEST(SolidSyslogPosixMessageQueueBuffer, WriteWithOversizedMessageReportsError) UNSIGNED_LONGS_EQUAL(POSIXMESSAGEQUEUEBUFFER_ERROR_SEND_FAILED, ErrorHandlerFake_LastCode()); } +TEST(SolidSyslogPosixMessageQueueBuffer, ReadWhenMqReceiveFailsReportsError) +{ + ErrorHandlerFake_Install(nullptr); + MqFake_FailNextReceive(EMSGSIZE); + + Read(); + + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_ERROR, ErrorHandlerFake_LastSeverity()); + POINTERS_EQUAL(&PosixMessageQueueBufferErrorSource, ErrorHandlerFake_LastSource()); + UNSIGNED_LONGS_EQUAL(POSIXMESSAGEQUEUEBUFFER_ERROR_RECEIVE_FAILED, ErrorHandlerFake_LastCode()); +} + +/* Regression guard: empty queue (EAGAIN) is part of the happy poll loop + * and must NOT emit. The error handler must remain untouched. */ +TEST(SolidSyslogPosixMessageQueueBuffer, ReadFromEmptyQueueDoesNotEmitError) +{ + ErrorHandlerFake_Install(nullptr); + + CHECK_FALSE(Read()); + + CALLED_FAKE(ErrorHandlerFake_Handle, NEVER); +} + TEST(SolidSyslogPosixMessageQueueBuffer, ServiceSendsMessageWrittenViaLog) { struct SolidSyslogSender* fakeSender = SenderFake_Create(); From 0193068ed4bea0289876e941c5bdf3a3dd53346e Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 24 May 2026 21:07:26 +0000 Subject: [PATCH 06/11] fix(pmqb): guard NULL bytesRead in Read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PosixMessageQueueBuffer_Read previously dereferenced bytesRead unconditionally (`*bytesRead = ...`), so passing NULL would segfault. Early-return false if bytesRead is NULL. No new error code — this is invalid caller usage, not a runtime failure. The Read contract still says "false on failure, bytesRead holds the count on success". Slice 5 of #118. --- .../SolidSyslogPosixMessageQueueBuffer.c | 33 ++++++++++--------- ...SolidSyslogPosixMessageQueueBufferTest.cpp | 9 +++++ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c index 466b435c..e87363da 100644 --- a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c +++ b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c @@ -82,23 +82,26 @@ static inline const char* PosixMessageQueueBuffer_QueueName(struct SolidSyslogPo static bool PosixMessageQueueBuffer_Read(struct SolidSyslogBuffer* base, void* data, size_t maxSize, size_t* bytesRead) { - struct SolidSyslogPosixMessageQueueBuffer* self = PosixMessageQueueBuffer_SelfFromBase(base); - ssize_t received = mq_receive(self->Mq, data, maxSize, NULL); - bool success = received >= 0; - - /* EAGAIN is the empty-queue poll signal — part of the happy path and must - * stay silent. Any other errno is a real failure worth surfacing. */ - if (!success && (errno != EAGAIN)) + bool success = false; + if (bytesRead != NULL) { - SolidSyslog_Error( - SOLIDSYSLOG_SEVERITY_ERROR, - &PosixMessageQueueBufferErrorSource, - (uint8_t) POSIXMESSAGEQUEUEBUFFER_ERROR_RECEIVE_FAILED - ); + struct SolidSyslogPosixMessageQueueBuffer* self = PosixMessageQueueBuffer_SelfFromBase(base); + ssize_t received = mq_receive(self->Mq, data, maxSize, NULL); + success = received >= 0; + + /* EAGAIN is the empty-queue poll signal — part of the happy path and must + * stay silent. Any other errno is a real failure worth surfacing. */ + if (!success && (errno != EAGAIN)) + { + SolidSyslog_Error( + SOLIDSYSLOG_SEVERITY_ERROR, + &PosixMessageQueueBufferErrorSource, + (uint8_t) POSIXMESSAGEQUEUEBUFFER_ERROR_RECEIVE_FAILED + ); + } + + *bytesRead = success ? (size_t) received : 0U; } - - *bytesRead = success ? (size_t) received : 0U; - return success; } diff --git a/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp b/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp index 3f5fafa9..32acef13 100644 --- a/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp +++ b/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp @@ -157,6 +157,15 @@ TEST(SolidSyslogPosixMessageQueueBuffer, ReadFromEmptyQueueDoesNotEmitError) CALLED_FAKE(ErrorHandlerFake_Handle, NEVER); } +/* A NULL bytesRead* would crash on `*bytesRead = 0`. Guard at the + * Read entry — invalid caller usage, not a runtime failure, so no + * error code is emitted; just a defensive false return. */ +TEST(SolidSyslogPosixMessageQueueBuffer, ReadWithNullBytesReadDoesNotCrash) +{ + char data[16] = {}; + CHECK_FALSE(SolidSyslogBuffer_Read(buffer, data, sizeof(data), nullptr)); +} + TEST(SolidSyslogPosixMessageQueueBuffer, ServiceSendsMessageWrittenViaLog) { struct SolidSyslogSender* fakeSender = SenderFake_Create(); From 22b501bc00d9ed2a3bc1227230ae405275494c93 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 24 May 2026 21:08:44 +0000 Subject: [PATCH 07/11] test(pmqb): pin use-after-Destroy and Destroy(NULL) crash-safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both behaviours are already safe by construction: - After Destroy, the abstract-base vtable is overwritten with the shared SolidSyslogNullBuffer's, so stale-handle Write/Read is a no-op rather than a NULL-fn-pointer crash. Mirrors the equivalent PassthroughBuffer test. - Destroy(NULL) routes via IndexFromHandle → POOL_SIZE → IndexIsValid false → UNKNOWN_DESTROY warning. The existing DestroyOfUnknownHandleReportsWarning test exercises a stack-allocated stranger; the new test pins literal NULL specifically. Both pass first time — regression guards for an integrator who strays into either case. Slice 6 of #118. --- ...SolidSyslogPosixMessageQueueBufferTest.cpp | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp b/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp index 32acef13..42fe196b 100644 --- a/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp +++ b/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp @@ -166,6 +166,22 @@ TEST(SolidSyslogPosixMessageQueueBuffer, ReadWithNullBytesReadDoesNotCrash) CHECK_FALSE(SolidSyslogBuffer_Read(buffer, data, sizeof(data), nullptr)); } +/* After Destroy the slot's abstract-base vtable is the shared NullBuffer's, so + * Write/Read through the stale handle is a safe no-op rather than a NULL-fn-pointer + * crash. NullBuffer.Write swallows; NullBuffer.Read returns false with bytesRead=0. */ +TEST(SolidSyslogPosixMessageQueueBuffer, UseAfterDestroyIsCrashSafeViaNullBufferVtable) +{ + SolidSyslogPosixMessageQueueBuffer_Destroy(buffer); + + SolidSyslogBuffer_Write(buffer, "x", 1); + char data[16] = {}; + size_t bytesRead = 99; + CHECK_FALSE(SolidSyslogBuffer_Read(buffer, data, sizeof(data), &bytesRead)); + LONGS_EQUAL(0, bytesRead); + + buffer = SolidSyslogPosixMessageQueueBuffer_Create(SOLIDSYSLOG_MAX_MESSAGE_SIZE, 10); // for teardown +} + TEST(SolidSyslogPosixMessageQueueBuffer, ServiceSendsMessageWrittenViaLog) { struct SolidSyslogSender* fakeSender = SenderFake_Create(); @@ -381,6 +397,22 @@ TEST(SolidSyslogPosixMessageQueueBufferPool, DestroyOfUnknownHandleReportsWarnin UNSIGNED_LONGS_EQUAL(POSIXMESSAGEQUEUEBUFFER_ERROR_UNKNOWN_DESTROY, ErrorHandlerFake_LastCode()); } +/* Destroy(NULL) is reachable from any integrator who keeps a NullBuffer-fallback + * handle and later releases it. The IndexFromHandle search returns POOL_SIZE + * (no slot matches NULL), IndexIsValid returns false, so the FreeIfInUse branch + * is skipped — caller sees an UNKNOWN_DESTROY warning, no crash. */ +TEST(SolidSyslogPosixMessageQueueBufferPool, DestroyOfNullHandleReportsWarningWithoutCrashing) +{ + ErrorHandlerFake_Install(nullptr); + + SolidSyslogPosixMessageQueueBuffer_Destroy(nullptr); + + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_WARNING, ErrorHandlerFake_LastSeverity()); + POINTERS_EQUAL(&PosixMessageQueueBufferErrorSource, ErrorHandlerFake_LastSource()); + UNSIGNED_LONGS_EQUAL(POSIXMESSAGEQUEUEBUFFER_ERROR_UNKNOWN_DESTROY, ErrorHandlerFake_LastCode()); +} + TEST(SolidSyslogPosixMessageQueueBufferPool, DestroyOfStaleHandleReportsWarning) { pooled[0] = MakeBuffer(); From 105b3afb55b9342414a15e39566fc1b64b3083d9 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 25 May 2026 07:32:50 +0000 Subject: [PATCH 08/11] chore(pmqb): remove IGNORE_TEST HappyPathOnly placeholder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every test case enumerated in the PMQB IGNORE_TEST block is now either pinned by a real test (slices 1–6) or explicitly out of scope (NULL-buffer dispatcher guards belong to SolidSyslog_Log, not PMQB; the blocking-mode pointer at "S4.5 or later" was stale and deserves its own story if still wanted). Also re-flow a Messages.c line that clang-format coalesced onto a single line after RECEIVE_FAILED landed in slice 4. Slice 7 of #118 — closes the story. --- ...olidSyslogPosixMessageQueueBufferMessages.c | 3 +-- .../SolidSyslogPosixMessageQueueBufferTest.cpp | 18 ------------------ 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferMessages.c b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferMessages.c index c49e48c8..437beddb 100644 --- a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferMessages.c +++ b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferMessages.c @@ -14,8 +14,7 @@ static const char* PosixMessageQueueBufferError_AsString(uint8_t code) "SolidSyslogPosixMessageQueueBuffer_Create mq_open failed; returning fallback buffer", [POSIXMESSAGEQUEUEBUFFER_ERROR_SEND_FAILED] = "SolidSyslogPosixMessageQueueBuffer_Write mq_send failed; record dropped", - [POSIXMESSAGEQUEUEBUFFER_ERROR_RECEIVE_FAILED] = - "SolidSyslogPosixMessageQueueBuffer_Read mq_receive failed", + [POSIXMESSAGEQUEUEBUFFER_ERROR_RECEIVE_FAILED] = "SolidSyslogPosixMessageQueueBuffer_Read mq_receive failed", }; const char* result = "unknown"; if (code < (uint8_t) POSIXMESSAGEQUEUEBUFFER_ERROR_MAX) diff --git a/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp b/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp index 42fe196b..32bf0a86 100644 --- a/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp +++ b/Tests/SolidSyslogPosixMessageQueueBufferTest.cpp @@ -198,24 +198,6 @@ TEST(SolidSyslogPosixMessageQueueBuffer, ServiceSendsMessageWrittenViaLog) SenderFake_Destroy(fakeSender); } -IGNORE_TEST(SolidSyslogPosixMessageQueueBuffer, HappyPathOnly) - -{ - // Error handling not yet implemented — see Epic #31 - // Create with zero maxMessageSize or maxMessages - // Create when mq_open fails returns NULL - // Write with NULL buffer does not crash - // Write with NULL data does not crash - // Read with NULL buffer does not crash - // Read with NULL data does not crash - // Read with NULL bytesRead does not crash - // Destroy with NULL buffer does not crash - // Write when queue is full (back-pressure / overflow) - // - // Blocking mode not yet implemented — see S4.5 or later - // Read blocks waiting for a message (O_NONBLOCK removed) -} - // NOLINTBEGIN(cppcoreguidelines-macro-usage,cppcoreguidelines-avoid-do-while) #define CHECK_IS_FALLBACK(handle, pool) \ do \ From e4fa4be0e2e43b0714ae6aaad8b15d8b4cb4152e Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 25 May 2026 07:36:34 +0000 Subject: [PATCH 09/11] fix(tests): include in MqFakeTest for ssize_t MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-of-story IWYU sweep caught a transitive include — MqFakeTest references ssize_t (return type of mq_receive) but only got it through 's implementation. Add the direct include. Slice 7 follow-up of #118. --- Tests/MqFakeTest.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/MqFakeTest.cpp b/Tests/MqFakeTest.cpp index 76cd857c..089cc643 100644 --- a/Tests/MqFakeTest.cpp +++ b/Tests/MqFakeTest.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "MqFake.h" #include "TestUtils.h" From 0dbe112da111d8af4df4c8beb67c657732fe0aa9 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 25 May 2026 08:07:51 +0000 Subject: [PATCH 10/11] =?UTF-8?q?fix(pmqb):=20satisfy=20MISRA=20cppcheck?= =?UTF-8?q?=20=E2=80=94=20errno=20capture=20+=20line=20shifts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's analyze-cppcheck job runs a second MISRA-addon invocation that exits non-zero on unsuppressed violations; my local sweep only covered the plain cppcheck invocation. Four violations slipped through: 1. PMQB.c errno test after mq_receive triggered Rule 22.10 — the predicate's value could conceivably depend on errno being set by something other than the immediately preceding library call. Captured errno into a local right after mq_receive, matching the project's existing pattern in PosixTcpStream.c:190 and PosixDatagram.c:101. No suppression needed. 2-4. Three pre-existing line-pinned suppressions (misra-c2012-11.3 PMQB.c, 5.7 PMQB.c, 5.7 PMQBPrivate.h) shifted line numbers because slices 1-5 added includes and grew the functions. Updated the pinned line numbers in misra_suppressions.txt — no new suppressions added. Local cppcheck-misra now exits 0 across all 147 production files. --- .../Posix/Source/SolidSyslogPosixMessageQueueBuffer.c | 10 +++++++--- misra_suppressions.txt | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c index e87363da..51996e3a 100644 --- a/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c +++ b/Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c @@ -89,9 +89,13 @@ static bool PosixMessageQueueBuffer_Read(struct SolidSyslogBuffer* base, void* d ssize_t received = mq_receive(self->Mq, data, maxSize, NULL); success = received >= 0; - /* EAGAIN is the empty-queue poll signal — part of the happy path and must - * stay silent. Any other errno is a real failure worth surfacing. */ - if (!success && (errno != EAGAIN)) + /* Capture errno immediately after mq_receive so the EAGAIN test below + * stays a pure predicate and is decoupled from errno's lifetime + * between the errno-setting call and the read (MISRA C:2012 Rule 22.10). + * EAGAIN is the empty-queue poll signal — part of the happy path and + * must stay silent. Any other errno is a real failure worth surfacing. */ + int receiveErrno = success ? 0 : errno; + if (!success && (receiveErrno != EAGAIN)) { SolidSyslog_Error( SOLIDSYSLOG_SEVERITY_ERROR, diff --git a/misra_suppressions.txt b/misra_suppressions.txt index 09225d8d..d0bd263f 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -53,7 +53,7 @@ misra-c2012-11.3:Platform/Posix/Source/SolidSyslogPosixAddressStatic.c:34 misra-c2012-11.3:Platform/Posix/Source/SolidSyslogPosixAddressStatic.c:54 misra-c2012-11.3:Platform/Posix/Source/SolidSyslogPosixDatagram.c:55 misra-c2012-11.3:Platform/Posix/Source/SolidSyslogPosixFile.c:66 -misra-c2012-11.3:Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c:99 +misra-c2012-11.3:Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c:129 misra-c2012-11.3:Platform/Posix/Source/SolidSyslogPosixMutex.c:47 misra-c2012-11.3:Platform/Posix/Source/SolidSyslogPosixTcpStream.c:75 misra-c2012-11.3:Platform/Windows/Source/SolidSyslogWinsockAddress.c:9 @@ -148,8 +148,8 @@ misra-c2012-5.7:Core/Source/SolidSyslogCrc16Policy.c:10 misra-c2012-5.7:Core/Source/SolidSyslogOriginSdPrivate.h:9 misra-c2012-5.7:Platform/FreeRtos/Source/SolidSyslogFreeRtosSysUpTime.c:7 misra-c2012-5.7:Platform/Posix/Source/SolidSyslogPosixHostname.c:10 -misra-c2012-5.7:Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c:17 -misra-c2012-5.7:Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferPrivate.h:11 +misra-c2012-5.7:Platform/Posix/Source/SolidSyslogPosixMessageQueueBuffer.c:21 +misra-c2012-5.7:Platform/Posix/Source/SolidSyslogPosixMessageQueueBufferPrivate.h:12 misra-c2012-5.7:Platform/Posix/Source/SolidSyslogPosixSysUpTime.c:6 misra-c2012-5.7:Platform/Windows/Source/SolidSyslogWindowsClock.c:21 misra-c2012-5.7:Platform/Windows/Source/SolidSyslogWindowsHostname.c:19 From e8110ce1b647d6192b009b46b2bafd6230a562e7 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Mon, 25 May 2026 08:08:04 +0000 Subject: [PATCH 11/11] fix(tests): MqFake mq_send enforces per-queue limits per POSIX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit caught that MqFake's mq_send ignored the per-queue attributes captured at mq_open: - Queue-full path used the fake-wide MQFAKE_MAX_MESSAGES_PER_QUEUE (8) instead of queue->maxMessages, so a queue opened with maxMessages=2 would accept 8 sends before EAGAIN — diverging from the real kernel. - Oversized messages were silently truncated via storeLen clamping rather than failing with -1/EMSGSIZE per POSIX. Both gaps would mask real-kernel behaviour from tests that exercise natural overflow rather than MqFake_FailNext* injection. Per the "fakes must model the API faithfully" principle from feedback_fake_platform_apis (the precedent that drove introducing MqFake in S12.07 slice 0), fix while the fake is fresh. Enforcement order matches POSIX (mq_send specification): 1. EBADF — bad descriptor 2. EMSGSIZE — msgLen > mq_attr.mq_msgsize 3. EAGAIN — queue at mq_attr.mq_maxmsg Storage cap MQFAKE_MAX_MESSAGES_PER_QUEUE bumped 8 → 16 so it remains >= any maxMessages value tests pass (PMQB tests use 10). Two new MqFakeTest cases pin the natural-overflow and natural-oversize paths. Addresses CodeRabbit comment on PR #443. --- Tests/MqFakeTest.cpp | 20 ++++++++++++++++++++ Tests/Support/MqFake.c | 25 +++++++++++++++++++------ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/Tests/MqFakeTest.cpp b/Tests/MqFakeTest.cpp index 089cc643..22ddc94e 100644 --- a/Tests/MqFakeTest.cpp +++ b/Tests/MqFakeTest.cpp @@ -107,6 +107,26 @@ TEST(MqFake, FailNextSendReturnsMinusOneAndSetsErrno) LONGS_EQUAL(EMSGSIZE, errno); } +/* Natural overflow — sending past mq_attr.mq_maxmsg must surface as + * -1/EAGAIN per POSIX, not silently succeed. */ +TEST(MqFake, SendBeyondMaxMessagesReturnsMinusOneEagain) +{ + mqd_t mqd = OpenTestQueue("/test", /*maxMessages=*/2, /*maxMessageSize=*/64); + LONGS_EQUAL(0, mq_send(mqd, "a", 1, 0)); + LONGS_EQUAL(0, mq_send(mqd, "b", 1, 0)); + + LONGS_EQUAL(-1, mq_send(mqd, "c", 1, 0)); + LONGS_EQUAL(EAGAIN, errno); +} + +/* Natural oversize — POSIX requires -1/EMSGSIZE, not truncation. */ +TEST(MqFake, SendLargerThanMaxMessageSizeReturnsMinusOneEmsgsize) +{ + mqd_t mqd = OpenTestQueue("/test", /*maxMessages=*/4, /*maxMessageSize=*/4); + LONGS_EQUAL(-1, mq_send(mqd, "fivefive", 8, 0)); + LONGS_EQUAL(EMSGSIZE, errno); +} + TEST(MqFake, FailNextReceiveReturnsMinusOneAndSetsErrno) { mqd_t mqd = OpenTestQueue("/test"); diff --git a/Tests/Support/MqFake.c b/Tests/Support/MqFake.c index bc4fb232..42816953 100644 --- a/Tests/Support/MqFake.c +++ b/Tests/Support/MqFake.c @@ -15,7 +15,11 @@ enum { MQFAKE_MAX_QUEUES = 4, - MQFAKE_MAX_MESSAGES_PER_QUEUE = 8, + /* Storage cap for the in-memory ring per queue. Per-call enforcement + * uses the caller's `mq_attr.mq_maxmsg` instead, so this value just + * needs to be at least as large as the largest `maxMessages` any test + * passes to `mq_open`. Bumping it is cheap (BSS only). */ + MQFAKE_MAX_MESSAGES_PER_QUEUE = 16, MQFAKE_MAX_OPEN_HISTORY = 8, MQFAKE_MAX_NAME_LEN = 64, MQFAKE_HANDLE_BASE = 100, @@ -354,18 +358,27 @@ int mq_send(mqd_t mqd, const char* msg, size_t msgLen, unsigned int msgPrio) { errno = EBADF; } - else if (queue->count >= MQFAKE_MAX_MESSAGES_PER_QUEUE) + else if (msgLen > queue->maxMessageSize) { + /* POSIX: mq_send must return -1/EMSGSIZE when msgLen exceeds the + * queue's per-message size. Truncating instead would mask real-kernel + * behaviour from tests. */ + errno = EMSGSIZE; + } + else if (queue->count >= (int) queue->maxMessages) + { + /* POSIX: with O_NONBLOCK and the queue at capacity, mq_send must + * return -1/EAGAIN. Use the per-queue limit captured at mq_open, + * not the fake's storage cap. */ errno = EAGAIN; } else { - size_t storeLen = (msgLen < sizeof(queue->messages[0])) ? msgLen : sizeof(queue->messages[0]); - if (storeLen > 0U) + if (msgLen > 0U) { - memcpy(queue->messages[queue->tail], msg, storeLen); + memcpy(queue->messages[queue->tail], msg, msgLen); } - queue->messageLens[queue->tail] = storeLen; + queue->messageLens[queue->tail] = msgLen; queue->tail = (queue->tail + 1) % MQFAKE_MAX_MESSAGES_PER_QUEUE; queue->count++; result = 0;