Skip to content

Linearize only the header block of multi-segment input - #86

Merged
MDA2AV merged 2 commits into
mainfrom
fix/65-multiseg-copy
Nov 3, 2026
Merged

MDA2AV merged 2 commits into
mainfrom
fix/65-multiseg-copy

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Nov 2, 2026

Copy link
Copy Markdown
Member

Fixes #65

Why

The ReadOnlySequence entry points found the CRLFCRLF, then copied the whole sequence with ToArray() — any pipelined body included — and only then parsed, which is where MaxTotalHeaderBytes was checked. A tiny header followed by a large pipelined body forced an allocation the size of the body, per parse:

  • UltraHardenedParser.TryExtractFullHeaderValidated (requests)
  • UltraHardenedParser.TryExtractFullResponseHeaderValidated (responses)
  • FlexibleParser.TryExtractFullHeader

What changes

The SequenceReader that located the terminator already knows the header block's length: reader.Consumed, CRLFCRLF included. So:

  • the two hardened entry points reject a block over MaxTotalHeaderBytes with the same 431 ("Total header size exceeds limit.") before copying anything;
  • all three copy input.Slice(0, reader.Consumed) instead of the whole sequence.

The ROM parse never reads past the terminator, so parsed fields and bytesReadCount are unchanged; only the size of the copy changes. The XML summaries that said "linearized via ToArray()" now say what is copied.

Out of scope, possible follow-up: a header section that never terminates is still buffered by the caller and rescanned on each call (single- and multi-segment alike). Rejecting an unterminated buffer that has already reached MaxTotalHeaderBytes would bound that too, but it changes the incomplete path of both entry points, so it is not folded in here.

Overlap with sibling PRs

Tests

New tests/Tests/MultiSegment.HeaderCopy.cs (10 cases), for requests, responses and FlexibleParser:

  • an 8 MiB pipelined body behind a multi-segment header: the parse is correct, the array backing the parsed slices (MemoryMarshal.TryGetArray) is exactly the header length, and the thread allocates < 64 KiB for the call;
  • the terminator split at each position across two segments, and a body that starts in the terminator's own segment: the copy is still exactly the header block;
  • a header one byte over MaxTotalHeaderBytes: 431, with < 1 MiB allocated for the call (so not the 8 MiB body); exactly at the limit: parses.

9 of the 10 fail against main (the exact-limit case passes on both). 413 passing, 0 failing (net10.0); the solution builds for net8.0/net9.0/net10.0.

The ReadOnlySequence entry points (UltraHardenedParser requests and
responses, FlexibleParser) found the CRLFCRLF and then copied the whole
sequence with ToArray() - any pipelined body included - before
MaxTotalHeaderBytes was checked, which only happened inside the ROM parse.
A tiny header followed by a huge pipelined body therefore forced an
allocation the size of the body.

The reader that located the terminator already knows the header block's
length (reader.Consumed). The hardened entry points now reject a block over
MaxTotalHeaderBytes with the same 431 before copying anything, and all
three copy input.Slice(0, reader.Consumed) instead of the whole sequence.
The ROM parse never reads past the terminator, so the parsed fields and
bytesReadCount are unchanged.

Fixes #65.
# Conflicts:
#	src/Glyph11/Parser/FlexibleParser/FlexibleParser.TryExtractFullHeader.cs
#	src/Glyph11/Parser/UltraHardened/UltraHardenedParser.TryExtractFullHeader.cs
#	src/Glyph11/Parser/UltraHardened/UltraHardenedParser.TryExtractFullResponseHeader.cs
@MDA2AV
MDA2AV merged commit 828d221 into main Nov 3, 2026
1 of 2 checks passed
MDA2AV added a commit that referenced this pull request Nov 3, 2026
Describes the library as it is once #74-#86 are merged: requests and
responses through TryParseRequestHeader / TryParseResponseHeader, the
body-framing helpers, chunked decoding (partial chunks, the trailer
cap), every ParserLimits field with its default and status code
(including AllowBareLf and MaxTotalTrailerBytes), the two parser tiers,
a summary of the hardening with a link to SECURITY.md, the tests, and
what changes when migrating to 0.4.0. Glyph11.Native and Glyph11.Pico
are marked discontinued.

Also fixes the chunked example, which listed a ChunkResult.Error that
does not exist (malformed framing throws).
MDA2AV added a commit that referenced this pull request Nov 3, 2026
…89)

The harness called TryExtractFullHeaderROM and TryExtractFullHeaderValidated, obsolete
since #81, and cleared the request before each call, which the parser does itself since
#76. The multi-segment comment now says that only the header block is linearized (#86).
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.

Multi-segment input is fully copied (body included) before header limits are enforced

1 participant