fix(security): bounds-check untrusted shmem offsets in port and libunit#21
fix(security): bounds-check untrusted shmem offsets in port and libunit#21andypost wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces several security hardening measures, including bounds-checking for shared-memory offsets, addressing a TOCTOU vulnerability in mmap handler refcounting, preventing integer overflows during response buffer allocation, and validating string pointers (sptrs) in incoming requests. A critical issue was identified in the nxt_unit_sptr_in_buf function, where the bounds check for the sptr struct itself is insufficient, potentially allowing an out-of-bounds read when accessing its members.
| if (sptr_off > buf_size) { | ||
| return 0; | ||
| } |
There was a problem hiding this comment.
The current check is insufficient to prevent an out-of-bounds read. It does not ensure that the entire nxt_unit_sptr_t struct is within the buffer before its members are accessed. For example, if sptr_off is equal to buf_size, this check passes, but sptr would point just beyond the buffer, and accessing sptr->offset would be an out-of-bounds read. A similar issue occurs if sptr is only partially inside the buffer.
To fix this, you must validate that the sptr struct itself is fully contained within the buffer.
if (sptr_off > buf_size - sizeof(nxt_unit_sptr_t)) {
return 0;
}Audit-driven hardening pass clustered around the trust boundary where
one process writes a struct into shared memory and another process
reads it. Five findings (security-audit.md V5/V10), one helper per
file, no public-API or libunit-ABI changes.
V5 [High] chunk_id no bounds check (src/nxt_port_memory.c:698 on
master @ 7b12696). Peer-supplied chunk_id was passed to
nxt_port_mmap_chunk_start() without checking against
PORT_MMAP_CHUNK_COUNT, yielding an OOB pointer. Reject in
nxt_port_mmap_get_incoming_buf() before pointer arithmetic.
V5 [High] chunk_id + nchunks past mapped region (line 701).
nchunks is computed from untrusted mmap_msg->size; a peer
could land b->mem.end past the mapped segment. Combined
with #1 above into a single overflow-safe range check
(chunk_id + nchunks <= PORT_MMAP_CHUNK_COUNT, written as
nchunks <= PORT_MMAP_CHUNK_COUNT - chunk_id after the
chunk_id bound holds).
V5 [Medium] TOCTOU on shmem mmap_id lookup (lines 676-678).
process->incoming.mutex was released between locating
mmap_handler and the caller's first dereference of
mmap_handler->hdr; a concurrent peer-side close could free
the handler in that window. nxt_port_get_port_incoming_mmap
now bumps the existing atomic refcount under the mutex; the
sole caller adopts the reference (the redundant explicit
handler_use(+1) at the buf-parent assignment site is
removed) and releases it on every error path.
V10 [Medium] max_fields_count * sizeof(field) + 2 overflow on response
(src/nxt_unit.c:2049-2051, 2128-2130). Application-supplied
counts/sizes were multiplied without overflow checking;
wrap-around produced an undersized buffer that subsequent
field memcpy()s overran. New file-static helper
nxt_unit_response_buf_size() computes the size with
UINT32_MAX-bounded checks at both multiplication and
addition; nxt_unit_response_init/realloc reject overflow
with a libunit error.
V10 [Low] sptr offset dereferenced without bounds (line 1329 etc.).
Every nxt_unit_request_t sptr is now validated at
request-arrival time inside nxt_unit_process_req_headers()
before any consumer dereferences it. New file-static
helper nxt_unit_sptr_in_buf(sptr, length, buf, size)
performs the underflow-safe check; one-shot validation
keeps the per-deref hot paths and the libunit ABI
unchanged.
Cross-cutting: file-static helpers nxt_port_mmap_chunk_range_valid()
in src/nxt_port_memory.c and nxt_unit_sptr_in_buf() /
nxt_unit_response_buf_size() in src/nxt_unit.c. None exported
through public headers; nxt_unit_sptr.h, nxt_unit_request.h,
nxt_unit_field.h, and nxt_unit.h struct layouts are unchanged.
Builds clean with ./configure --openssl && make under -Werror.
nxt_unit.c also compile-checked standalone with the stock CFLAGS.
pytest-3 --collect-only test/ collects 898 tests; root-required
runs were not exercised in the worktree.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
e855da4 to
d010e20
Compare
|
Gemini's [security-critical] finding addressed in |
|
Coming back to this with three coverage gaps surfaced by a parallel-POV review of the integration branch ( 1. Send-side:
|
Two follow-ups that sit squarely inside this PR's named scope — "bounds-check untrusted shmem offsets in port and libunit" — surfaced during integration audit on the post-merge codebase. 1. nxt_port_mmap_at() index cap (src/nxt_port_memory.c:47) When called from the receive path, the i parameter is hdr->id of a peer-mapped shared-memory region. A forged value of 0xFFFFFFFF wraps (i + 1) to 0 in both the initial cap assignment and the growth loop condition, skips realloc, and returns port_mmaps->elts + 0xFFFFFFFF — an out-of-array pointer the caller then writes through (nxt_port_get_port_incoming_mmap stores mmap_handler at that offset). Worker-to-main wild write at root. Cap i against a generous ceiling (PORT_MMAP_MAX_REGIONS = 1 << 20) at function entry. Symmetric with the (chunk_id, nchunks) helper this PR already introduces; the chunk_id helper covers the receive-path chunk arena, this cap covers the mmap-region index that flows through the same receive path one level up. 2. Response-side sptr validation in the router (src/nxt_router.c:4221) The router resolves response field sptrs (name / value) and the piggyback content sptr via nxt_unit_sptr_get() without bounds checking the embedded offset against the response buffer. A buggy or malicious worker can encode arbitrary offsets and have the router serialise bytes from outside the response buffer (within the same mmap region) onto the wire — a cross-request bleed inside the worker's own shmem area. This PR already added nxt_unit_sptr_in_buf() for the request-side sptrs on the libunit side. Move that helper from nxt_unit.c into nxt_unit_sptr.h so the router can use it too, then call it for each f->name / f->value / resp->piggyback_content sptr at the router's response-resolve site. No new helper: same shape, same underflow-safe pattern, same constant-side subtraction. The libunit write-side memset(0xA5) poison-fill site flagged alongside these by parallel review is intentionally not addressed here: it executes inside the worker process, with both source and length owned by the worker itself, so it does not cross a trust boundary. Filing as a hardening backlog note.
Summary
Audit-driven shared-memory bounds-check pass (PR-B from security-audit.md / PR #10). CVE-track — bundles five findings clustered around the "trust untrusted-input-from-shmem-peer" theme.
Findings addressed
Cross-cutting helper
File-static helper
nxt_port_mmap_chunk_range_validinsrc/nxt_port_memory.cand two siblings insrc/nxt_unit.c(nxt_unit_response_buf_size,nxt_unit_sptr_in_buf). None exported through public headers; libunit ABI unchanged (nxt_unit.h,nxt_unit_sptr.h,nxt_unit_request.h,nxt_unit_field.hstruct layouts and inline-helper bodies untouched).Notes on the TOCTOU fix
nxt_port_get_port_incoming_mmap()had a real lookup-vs-deref window: the function releasedprocess->incoming.mutexbefore returning, then the caller dereferencedmmap_handler->hdrand only afterwards bumped the existing atomic refcount. The fix bumps the (existing)use_countrefcount under the mutex inside the lookup — no new refcount field invented. The sole caller adopts that reference; the previously-explicithandler_use(+1)at the buf-parent assignment site is now removed, and every error path between lookup and buf creation drops the lookup ref.Test plan
./configure --openssl && makebuilds unitd clean under-Werror.nxt_unit.ccompile-checked standalone with stock CFLAGS (-Werror).pytest-3 --collect-only test/collects 898 tests (imports parse).sudo pytest-3 test/in a normal checkout.Upstream
Same fixes apply to
freeunitorg/freeunit; will forward after merge. Maintainer should consider disclosure timing — V5 chunk_id bounds is the closest to RCE-shaped finding in this bundle.Generated by Claude Code