Skip to content

fix(port): cap queue item size at the slot data bound#27

Draft
andypost wants to merge 2 commits into
pre-1.35.6from
fix/port-queue-msg-size-cap
Draft

fix(port): cap queue item size at the slot data bound#27
andypost wants to merge 2 commits into
pre-1.35.6from
fix/port-queue-msg-size-cap

Conversation

@andypost

Copy link
Copy Markdown
Owner

Summary

Bounds-check qi->size in the shared-memory queue used between Unit
processes. nxt_port_queue_recv() previously trusted the size word
read from the queue slot and used it as a memcpy length into a
caller-provided buffer sized for NXT_PORT_QUEUE_MSG_SIZE (31 bytes).
Because qi->size is a uint8_t and the queue lives in shared memory
that any worker can write, a buggy or compromised peer can set
qi->size up to 255 while qi->data is only 31 bytes — yielding up
to a 224-byte write past the receive buffer. On the main-process
side this receive buffer is stack-resident.

This PR caps qi->size at NXT_PORT_QUEUE_MSG_SIZE before the memcpy
and reads it into a local first so a concurrent peer write cannot make
the bound-check and the memcpy disagree (read-once on shared-memory
inputs). It also bounds the size argument on the send side so a
buggy caller that exceeds the per-item budget cannot clobber the
adjacent queue slot.

No behavioural change on the well-formed path; all in-tree callers
already pass valid sizes.

Files

  • src/nxt_port_queue.h — two single-hunk bounds checks.

Tests

./configure --debug --tests --openssl
make -j$(nproc)                                  # clean build, no warnings
python3 -m pytest test/test_idle_close_wait.py \
                  test/test_static.py            # 18 pass, 1 skip

The trigger requires a compromised peer; a deterministic test needs
fault injection into the shared-memory queue. Tracked separately.

Independence

Single file, single concern, no dependency on other in-flight PRs.


Generated by Claude Code

nxt_port_queue_recv() reads qi->size out of shared memory and uses it
verbatim as the memcpy length into a caller-provided buffer sized for
NXT_PORT_QUEUE_MSG_SIZE.  qi->size is uint8_t, so a peer that can write
to the queue (any worker, in the standard threat model for a
multi-tenant app server) can set qi->size up to 255 while qi->data is
31 bytes — yielding up to a 224-byte write past the receive buffer.
On the main process path this receive buffer lives on the stack of the
privileged process.

Cap qi->size at NXT_PORT_QUEUE_MSG_SIZE before the memcpy and read into
a local first so a concurrent peer write cannot make the bound-check
and the memcpy disagree (read-once invariant on shared-memory inputs).

Also bound size on nxt_port_queue_send() so a buggy caller that exceeds
the per-item budget cannot overwrite the adjacent queue slot.  All
in-tree callers already pass valid sizes; this is defensive.

No behavioural change on the well-formed path.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces bounds checking on queue item sizes in nxt_port_queue_send and nxt_port_queue_recv to prevent buffer overflows when handling messages from shared memory. A critical security issue was identified in nxt_port_queue_recv where a TOCTOU (Time-of-Check to Time-of-Use) vulnerability remains because qi is not declared volatile. The compiler could optimize the code to re-read qi->size from shared memory after the bounds check, potentially bypassing the check. It is recommended to force a volatile read of qi->size to ensure true read-once semantics.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/nxt_port_queue.h Outdated
* concurrent peer write cannot make the bound-check and the memcpy
* disagree.
*/
size = qi->size;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

Because qi is a non-volatile pointer, the compiler is allowed to assume that the memory it points to does not change concurrently. Under this assumption, the compiler can optimize the bounds check and the subsequent copy by re-reading qi->size directly from memory (e.g., transforming the code into a conditional branch where the else branch reads qi->size again). If a concurrent peer process modifies qi->size between the check and the second read, the bounds check is bypassed, leading to a stack buffer overflow. To prevent this compiler optimization and ensure a true "read-once" semantic, we must force a volatile read of qi->size by casting qi to a volatile pointer.

    size = ((volatile nxt_port_queue_item_t *) qi)->size;

Per Gemini review on PR #27: a non-volatile read of qi->size lets the
compiler legally re-load the field from shared memory when generating
the memcpy length, defeating the read-once invariant the cap relies on.
A concurrent peer write between the bound-check and the second load
would bypass the cap and put the stack overflow back.

Force the read through a volatile cast so the value is materialised
into the local once and the subsequent comparison + memcpy length use
that materialised value.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants