Skip to content

[BUG] Stream batching buffer trigger level can be set to an unreachable value, causing a permanent reader hang #1450

Description

@94xhn

Description

A stream batching buffer (xStreamBatchingBufferCreate() / xStreamBatchingBufferCreateStatic(), added in V11.1.0) can be configured, using only documented and configASSERT-accepted arguments, with a trigger level that can never be satisfied, even when the buffer is completely full. A reader task blocked in xStreamBufferReceive() on such a buffer then hangs forever (or until its own timeout, if one is used instead of portMAX_DELAY).

Root cause

prvBytesInBufferMeetTriggerLevel() uses a strict > comparison for batching buffers (it must, to implement "block until the trigger level is exceeded"), whereas plain stream/message buffers use >=:

https://github.com/FreeRTOS/FreeRTOS-Kernel/blob/9db704c/stream_buffer.c#L1599-L1624

static BaseType_t prvBytesInBufferMeetTriggerLevel( const StreamBuffer_t * const pxStreamBuffer,
                                                    size_t xBytesInBuffer )
{
    ...
    if( ( pxStreamBuffer->ucFlags & sbFLAGS_IS_BATCHING_BUFFER ) != ( uint8_t ) 0 )
    {
        if( xBytesInBuffer > pxStreamBuffer->xTriggerLevelBytes )   /* strictly greater */
        ...
    }
    else if( xBytesInBuffer >= pxStreamBuffer->xTriggerLevelBytes ) /* greater-or-equal */
    ...

The maximum number of bytes any stream buffer can ever hold is one less than its internal length (xLength - 1, see prvBytesInBuffer() and the ring-buffer full/empty-ambiguity comment at the top of stream_buffer.c).

The trigger-level bounds checks, however, do not know about the batching buffer's stricter > requirement and let the trigger level be set equal to that maximum, which can then never be exceeded:

None of these three checks special-case sbFLAGS_IS_BATCHING_BUFFER.

Reproduction

StreamBufferHandle_t xStream = xStreamBatchingBufferCreate( 25, 25 ); /* trigger == size, accepted */
/* task A: */ xStreamBufferReceive( xStream, buf, sizeof(buf), portMAX_DELAY );
/* task B: */ xStreamBufferSend( xStream, data, 25, portMAX_DELAY ); /* fills buffer 100% */
/* task A never wakes up, even though the buffer is completely full. */

I verified this concretely (not just by inspection) by compiling the real, unmodified tasks.c / list.c / stream_buffer.c / heap_4.c against the FreeRTOS POSIX/GCC simulator port and running real tasks under a real preemptive scheduler:

[A] xStreamBatchingBufferCreate(size=25, trigger=25)  <-- accepted by configASSERT
[A] Reader TIMED OUT after buffer was filled to 100% capacity (25/25 bytes).
[A] ==> BUG CONFIRMED: trigger level == capacity is unreachable for a batching buffer.

[B] xStreamBatchingBufferCreate(size=25, trigger=24)  <-- correct upper bound (size-1)
[B] Reader woke normally and received 25 bytes after 100 ms.
[B] ==> CONTROL PASSES: with the correct (size-1) trigger the same fill wakes the reader.

[C] xStreamBufferCreate(size=25, trigger=25)  <-- same trigger==size value as [A], but PLAIN buffer
[C] Reader woke normally and received 25 bytes after 100 ms.
[C] ==> CONTROL PASSES: plain (non-batching) buffers use ">=" so trigger==size is fine.

The same defect also exists, one byte tighter, for a statically allocated batching buffer (xStreamBatchingBufferCreateStatic(25, 24) - trigger one less than the raw storage size - also hangs forever), because static buffers don't get the dynamic path's +1 capacity compensation.

Impact

Any application that creates a stream batching buffer and (reasonably) sets the trigger level to "wake me when the buffer is completely full" gets a silent, permanent hang instead - and the configuration is accepted without any assertion warning, at every validation point that is supposed to catch invalid trigger levels.

Suggested fix

I have a fix + the full verification harness ready and will open a PR shortly: reject (via configASSERT at creation, or return pdFALSE from xStreamBufferSetTriggerLevel()) a batching-buffer trigger level that is not strictly less than the buffer's maximum achievable occupancy, while leaving plain stream/message buffers' existing bound unchanged.

Disclosure

I used Claude (Anthropic) to help audit the source and build/run the POSIX-port reproduction described above; I reviewed the analysis and reproduction results myself before filing this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions