Skip to content

http: frame the response body by status and request method - #32

Open
yosuke-wolfssl wants to merge 1 commit into
wolfSSL:mainfrom
yosuke-wolfssl:fix/f_8020
Open

yosuke-wolfssl wants to merge 1 commit into
wolfSSL:mainfrom
yosuke-wolfssl:fix/f_8020

Conversation

@yosuke-wolfssl

@yosuke-wolfssl yosuke-wolfssl commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Problem

read_body() chose the response framing from Transfer-Encoding and Content-Length alone, falling through to a read-until-close loop when neither was present. The status line was parsed but never consulted, and the request method was never in scope.

RFC 9112 section 6.3 makes a response to HEAD, and any 1xx, 204 or 304, end at the header block regardless of those fields. On a keep-alive connection the peer correctly holds the socket open, so the blocking client stalled in conn_read() until timeout and the non-blocking session returned WOLFCERT_ERR_WANT_READ indefinitely.

This is reachable in deployment. EST /csrattrs returns 204 No Content (RFC 7030 section 4.5.2) and wolfcert_client_fetch_meta() already handles it. The in-tree test server happens to send Content-Length: 0, which is legal but not required — so every in-tree test passed while any third-party EST server omitting it hangs the client.

Two adjacent defects in the same path: an interim 1xx was returned as the final response, leaving the real one unread on the connection; and an empty body went through WOLFCERT_XMALLOC(0), which may return NULL under a static-memory pool and fail a valid response with WOLFCERT_ERR_MEMORY.

Closes f-8020.

Fix (src/http.c)

Response Framing
HEAD reply, 204, 304 No body; ends at the header block. The advertised length is what a GET would have returned, so it is not applied as a size bound.
Interim 1xx Block discarded, reader continues to the final response, capped at WOLFCERT_HTTP_MAX_INTERIM (8).
101 WOLFCERT_ERR_PROTOCOL — wolfCert never sends Upgrade.
Empty body body = NULL, body_len = 0, no allocation.
  • http_read_response() takes the request method, loops past interim blocks, and skips read_body() for a bodyless response. Its four scattered free-and-return exits become one cleanup block.
  • inspect_headers() mirrors it, setting sm_content_length = 0 so SM_READ_HEAD enters the existing SM_READ_BODY_CL case — no new state-machine state, and the surplus still lands in residual for the next request on the session.
  • The method is matched with strcmp, per review: RFC 9110 section 9.1 makes the method token case-sensitive, so a lowercase head is a different method whose response carries a body.

Tests (tests/unit/test_transport.c)

The scripted peer gains open_rc (stays connected instead of closing, so a mis-framed body shows up as a deterministic error rather than a hang) and stall (a WANT_READ between reads). 23 cases cover both framing paths for HEAD/204/304, a lowercase head, both sides of the interim cap, 101, an advertised length above max_response_bytes, interim feeds split byte-by-byte across 118 suspends, and a session reused after a bodyless response.

Verification

  • Clean build, no warnings; 29/29 under both CMake ctest and autotools make check.
  • ASan + UBSan clean.
  • All 23 new cases fail against the pre-fix source — none passes vacuously.
  • Six single-decision mutations (each interim cap, each HEAD comparison, the residual stash, the bodyless size bound) are each caught only by their own path's test.

Not in this PR

The interim cap and drop mechanics are duplicated between the blocking and non-blocking readers. Those are separate implementations throughout, so deduplicating properly means unifying them; tracked as follow-up work rather than widened here.

@yosuke-wolfssl yosuke-wolfssl self-assigned this Sep 17, 2026
Copilot AI lite review requested due to automatic review settings September 17, 2026 02:05

Copilot AI 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.

🔵 Needs a closer look

Use exact, case-sensitive HEAD comparisons in both blocking and non-blocking response handling.

Pull request overview

Updates HTTP response framing to handle request methods, status codes, interim responses, and empty bodies correctly.

Changes:

  • Suppresses bodies for HEAD, 204, and 304 responses.
  • Bounds interim 1xx handling and rejects 101 responses.
  • Handles empty bodies without allocation.
  • Expands transport tests and documentation.
File summaries
File Summary
wolfcert/http.h Documents HEAD support and nullable empty bodies.
tests/unit/test_transport.c Adds framing and interim-response coverage.
src/http.c Implements status- and method-aware response framing; exact HEAD comparison remains required in both paths.
docs/ARCHITECTURE.md Documents bodyless response behavior.
Review details

Suppressed comments (2)

src/http.c:1298

  • HTTP method names are case-sensitive, but ci_cmp classifies head, Head, etc. as HEAD. A compliant server may treat such a token as a different method and send a body; this reader would skip it, and on a keep-alive session those bytes would be parsed as the next response. Use an exact comparison for HEAD here (and in the non-blocking path below).
    int    head_request = (ci_cmp(method, "HEAD") == 0);

src/http.c:2030

  • This duplicates the same protocol issue in the non-blocking path: HTTP method names are case-sensitive, so treating mixed/lower-case head as HEAD can leave a real response body in sm_rx and corrupt the next keep-alive response. Use an exact comparison for HEAD.
        s->sm_head_request = (ci_cmp(req->method, "HEAD") == 0);
  • Files reviewed: 4/4 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-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.

Fenrir Automated Review — PR #32

Scan targets checked: wolfcert-src, wolfcert-bugs

Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Reported findings require changes before merge.

Comment thread src/http.c Outdated
- http_read_response() takes the request method, matched exactly, and
  skips read_body() for a HEAD reply and for a 204 or 304. It also
  drops interim 1xx header blocks until a final status arrives,
  bounded by the new WOLFCERT_HTTP_MAX_INTERIM, and rejects 101; its
  frees move to one block at the bottom. status_is_interim() and
  response_has_no_body() hold both conditions.
- inspect_headers() mirrors it: 101 and interim statuses return early,
  and a bodyless response sets sm_content_length to 0 so SM_READ_HEAD
  enters SM_READ_BODY_CL. WolfCertHttpSession gains sm_head_request
  and sm_interim.
- read_body() leaves *out NULL and *out_len 0 for an empty body
  instead of allocating.
- http.h documents HEAD and the NULL body.
- test_transport.c's Peer gains open_rc and stall, its fetch helpers
  take a method and a chunk size, and drive_nb() steps one request.
- Cases cover an empty body framed by Content-Length and by close,
  204, 304, HEAD, a lowercase "head" that keeps its body, a HEAD
  reply advertising more than max_response_bytes, an interim 1xx
  whole and split, the interim cap at its ceiling and past it, and
  101. The non-blocking path repeats those it shares and adds a
  byte-at-a-time feed across suspends, an interim block's own header
  fields, and a session reused after a 204.

Issue: F-8020
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.

4 participants