fix(port): cap queue item size at the slot data bound#27
Conversation
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.
There was a problem hiding this comment.
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.
| * concurrent peer write cannot make the bound-check and the memcpy | ||
| * disagree. | ||
| */ | ||
| size = qi->size; |
There was a problem hiding this comment.
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.
Summary
Bounds-check
qi->sizein the shared-memory queue used between Unitprocesses.
nxt_port_queue_recv()previously trusted the size wordread from the queue slot and used it as a
memcpylength into acaller-provided buffer sized for
NXT_PORT_QUEUE_MSG_SIZE(31 bytes).Because
qi->sizeis auint8_tand the queue lives in shared memorythat any worker can write, a buggy or compromised peer can set
qi->sizeup to 255 whileqi->datais only 31 bytes — yielding upto a 224-byte write past the receive buffer. On the main-process
side this receive buffer is stack-resident.
This PR caps
qi->sizeatNXT_PORT_QUEUE_MSG_SIZEbefore the memcpyand 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
sizeargument on the send side so abuggy 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
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