Patch release: one OpenTelemetry-friendly access-log field plus an HTTP/2 memory-amplification DoS mitigation. Additive only — the new ProtobufAccessLog.start_time field and the h2_max_header_fields listener knob both carry safe defaults, so existing configurations and sozu-command-lib consumers can bump from ^2.0.1 to ^2.0.2 without code changes.
✨ Added
fix(otel): wall-clockstart_timefield inProtobufAccessLog(field 30, optionalUint128). Access-log consumers reconstructing OpenTelemetry spans no longer need to computetime - request_time— a subtraction that mixedCLOCK_REALTIMEandCLOCK_MONOTONICand produced unreliable start timestamps on short-lived requests. The new field is captured at request start viaSessionMetrics::mark_request_start()and should be preferred whenever present. Old consumers ignore the unknown field; new consumers with old producers seeNoneand can fall back to the subtraction.
🐛 Fixed
fix(ci/release): release-notes extractor matches the## X.Y.Z - DATEchangelog heading.release.yml'sawkextractor only recognized bracketed## [VERSION]headings, but every released section uses## X.Y.Z - DATE, so since 2.0.0 the draft release body silently fell back to a placeholder (a non-fatal::warning::). The extractor now matches the actual heading format (a bracketed## [X.Y.Z]is still accepted) and reads until the next##heading, so the GitHub release body is populated fromCHANGELOG.mdat tag time.RELEASE.mdis updated to match.
🔒 Security
fix(mux): mitigate the HTTP/2 "bomb" — HPACK header-field cap + window-stall reaping. Closes both halves of a memory-amplification DoS (the calif.io HTTP/2-bomb class; same family as Apache CVE-2026-49975):- HPACK header bomb. Thousands of 1-byte HPACK indexed references — or a single
cookieheader split into many crumbs (RFC 9113 §8.2.3) — each materialize aPairof per-entry bookkeeping, amplifying wire bytes into allocation. TheSETTINGS_MAX_HEADER_LIST_SIZEbudget now counts the RFC 9113 §6.5.2 mandated 32 octets per field (the per-field overhead bounds the field count), and a new per-listenerh2_max_header_fieldsknob (default 128, counting cookie crumbs individually) caps the number of materialized fields. Over-limit HEADERS / CONTINUATION / trailer blocks are rejected withGOAWAY/RST_STREAM(ENHANCE_YOUR_CALM). - Window-stall. A peer that holds its receive window shut while a response is buffered can no longer pin the stream and its
MAX_CONCURRENT_STREAMSslot. A dedicated per-stream flow-control-stall deadline (governed by the existingh2_stream_idle_timeout_seconds) reaps it withRST_STREAM(CANCEL). It is never refreshed by inbound activity, and a connection-scoped cumulative-stall budget means the deadline clears only on a genuinely open send window or once real outbound progress reaches one max DATA frame (16 KiB) — so neither a 1-byte inbound DATA drip nor aWINDOW_UPDATE(+1)drip can keep a window-stalled stream alive, while legitimate slow-but-steady transfers (above ~0.5 KiB/s at the 30 s default) are unaffected. The guard is bidirectional: a stalled request upload to a slow H2 backend is reaped too (returned to the client as a502). The reaper runs from both the read path and the connection-timeout path — flushing theRST_STREAM(CANCEL)even to a fully-silent peer — so the slot is freed rather than lingering until the zombie-session reaper. Newh2.streams.reaped.{idle_timeout,window_stall,stall_budget}andh2.headers.rejected.{header_list_size,header_fields}counters make the mitigations observable.