Perf/zero alloc h1 parser - #35
Conversation
Mark 1.36.x as the Active line now that 1.36.0 has shipped, and retire 1.35.x. The 1.36.0 security advisories (GHSA-768w-qrh2-jjvh, GHSA-g6v8-r577-76jm) patch 1.36.0 and backport to the 1.34.x LTS line only, not 1.35.x, so 1.35.x is end-of-life; users should upgrade to 1.36.x. 1.34.x LTS is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DYcuURscruaF1yNVHJHW3C
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3c94df1b86
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Pushed two commits fixing the CI failures. All 25+ jobs were red from three root causes: 1. C tests segfaulted (
|
CI status after the three fixes above
Remaining failure:
|
The pytest suite could orphan the entire unitd process tree when a test crashed or a stop timed out — downstream packaging already works around it (Alpine's community/freeunit APKBUILD carries a `pkill -f 'unitd.*--no-daemon'` to clean up daemons left by crashed pytest on armhf/armv7). Make the harness own the tree instead: * Put unitd in its own process group and record the pgid; on teardown run a group kill ladder (SIGQUIT -> SIGTERM -> SIGKILL) and reap the whole group, so no child (router, controller, app workers) survives the session. * Poll process state from /proc in the reap loop with a portable group-liveness probe, and drop the pgid file once the group is confirmed dead. * Keep reap handlers out of forked helpers and harden the sweep set so the harness never signals unrelated processes. * Report a stop-timeout error even when the forced reap ultimately succeeds, so a slow/stuck shutdown is still visible in test output. Co-Authored-By: Andy Postnikov <apostnikov@gmail.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The parser now stores the first 16 header fields inline in rp->inline_fields[] and only spills into the rp->fields list beyond that, so rp->fields is NULL for the common <=16-header case. The test still walked rp->fields directly with nxt_list_nelts/nxt_list_each/ nxt_list_elt/nxt_list_next, which dereferenced a NULL list and crashed ./build/tests with SIGSEGV (nxt_list_next, list=NULL) right after the utf8 suite -- reproduced locally and in CI. Iterate with the parser's own nxt_http_fields_each() macro (inline + list) and compute the count as num_inline_fields + list nelts. The redundant list-elt / list-next sub-walks are dropped: they exercised a container that no longer holds all fields. Also fix the clang-ast format-check failure at the name/value mismatch diagnostics: nxt_sprintf's "%*s" takes a size_t length argument (not printf's "%.*s" with int), matching every other "%*s" call in the tree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The branch reused one per-connection pool (h1p->mem_pool) across
keep-alive requests and called nxt_mp_reset() from nxt_h1p_keepalive()
to recycle it. But the request (nxt_http_request_t) is allocated from
that pool, and nxt_mp_reset() frees every allocation in it while objects
are still wired into engine-global state. ASan (php sanitizer leg,
test_php_application_keepalive_body) shows two distinct heap-use-after-
frees on the second keep-alive request:
* nxt_http_request_close_handler reads "r" after proto.close() ->
nxt_h1p_keepalive() -> nxt_mp_reset() already freed it; and
* nxt_timer_changes_commit dereferences r->timer (embedded in the
request) after reset freed it, because the engine's deferred
timer-change list still referenced it.
Every language test failed the same way (router crash + respawn ->
"same pid router"). The reset-recycling design is unsafe for any pool
backing an engine timer, and the request always embeds one; making it
safe would require flushing all pending timer/work-queue references to
pool objects before every reset.
Revert to a per-request pool (nxt_http_request_create), restoring the
proven upstream lifetime: proto.close() no longer frees the request, and
nxt_mp_release(r->mem_pool) at close destroys it exactly once. The other
zero-allocation changes on the branch (inline_fields, freelists, lazy
r->last, var scratchpad) are independent and retained. Removes the now-
dead nxt_mp_reset() and the h1p->mem_pool field.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The parser stopped eagerly creating r->fields (it now stores the first
16 request headers inline and leaves r->fields == NULL until it spills),
but two append sites still called nxt_list_zero_add(r->fields) directly:
* nxt_otel_propagate_header() -- echoes the traceparent header; runs on
every request when OpenTelemetry is configured. With <=16 request
headers r->fields is NULL, so nxt_list_zero_add(NULL) dereferenced a
NULL list and crashed the router. This is the "same pid router"
failure across all of test_otel.py (20 cases), a regression vs master.
* nxt_http_request_chunked_transform() -- appends Content-Length; the
same latent NULL crash on the chunked-transform path.
Add nxt_http_req_field_add()/nxt_http_req_field_zero_add(), mirroring the
existing nxt_http_resp_field_add() helpers, to append into r->inline_fields
(spilling to the list past 16). Both the app-request serializer
(nxt_router.c) and the H1 response/peer serializers already iterate with
nxt_http_fields_each(), so the appended field is picked up either way.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…elds Three defects exposed by the lazy r->last / inline_fields changes: * Router SIGSEGV on send_timeout (python CI: test_settings_send_timeout). nxt_http_buf_last() minted a fresh sync-last buffer whenever r->last was NULL; once the real last buffer is created lazily at header send and then consumed by the send path, an abort (send timeout) minted a SECOND buffer whose completion is also nxt_http_request_done, so the request pool was destroyed twice -> heap-use-after-free (ASan-confirmed). Return NULL when r->header_sent and r->last == NULL instead. * App-request rpc-data unlink skipped. The nxt_router_http_request_done override was only installed when r->last != NULL at dispatch, but lazy creation leaves it NULL there, so app requests completed via plain nxt_http_request_done and skipped nxt_request_rpc_data_unlink. Create the sync-last buffer eagerly at dispatch and install the router handler. * Error path emitted stale response fields. nxt_http_request_error() now appended Content-Type via the inline helper instead of re-creating the field list, so a late failure could ship the application's stale headers alongside the error body's framing. Reset the response field store first. Also drops the stray plan-agy.md from the branch. Co-Authored-By: Andy Postnikov <apostnikov@gmail.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
b64b58d to
5dc15ea
Compare
The send-timeout UAF (double completion of the request's last buffer) was only caught incidentally by test_settings_send_timeout's teardown pid assertion. Make the regression explicit: abort three streamed responses in a row via a stalled client, then verify the router still accepts and serves a request on the same listener. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1 and Phase 2 of plan-agy.md are fully implemented, verified, and committed on the branch perf/zero-alloc-h1-parser.
──────
Full Git Commit Log (git log -n 8 --oneline)
Implementation Breakdown
──────
Test Suite Execution
• Unit Tests: All unit tests (./build/tests) passed cleanly with zero errors.