Runtime hardening: contain handler exceptions and range-check timestamp decoding - #50
Merged
Merged
Conversation
A handler (or Observe callback) that threw propagated out of the transport's I/O thread and called std::terminate, taking down every in-flight request on all threads — the interface only documented "handlers must not throw" (issue #41). - Add smithy::http::InvokeHandlerGuarded (smithy/http/server_dispatch.h): invokes a RequestHandler, converting any escaped exception into a 500 carrying a generated x-correlation-id header (and a minimal JSON body repeating it), with the id + what() written to std::clog so an otherwise-silent crash leaves one greppable server-side line. An empty handler yields 503. - Route the Beast, socket, and loopback transports through it so the guard holds regardless of which handler a consumer installs. - Guard the Observe middleware callback locally so a throwing metrics/log sink neither discards the built response nor unwinds the transport thread. - Tests: unit coverage of the guard (success passthrough, std/non-std exceptions -> 500 + distinct correlation ids, empty -> 503), a real end-to-end socket test proving a throwing handler yields 500 and the server survives the next request, and a middleware test for the throwing-callback case. - Docs: transport.h contract and server-guide describe the safety net. Refs #41 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyQAo21Pv6GYhHrkbQj8xQ
Complements the transport-level socket test with one that drives the generated WeatherServer over loopback: a handler subclass whose GetCity throws must be contained as a 500 (with an x-correlation-id header) through the full generated stack — routing, validation, then the handler — and the server must remain usable afterward. Refs #41 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyQAo21Pv6GYhHrkbQj8xQ
Decoding a CBOR tag-1 timestamp did `as_int() * 1000`, which overflows int64 on a ~9-byte payload — undefined behavior on untrusted input (issue #42). The epoch-seconds paths (CBOR double, JSON number, and the epoch-seconds string parser) likewise cast an unbounded double through `llround(seconds * 1000.0)` into int64, and extreme instants formatted to 5+ digit / negative years that no conformant peer can parse back. - Add Timestamp::FromEpochSecondsChecked / FromEpochMillisecondsChecked: Outcome-returning factories that reject non-finite values and any instant whose civil year falls outside the RFC 3339 / IMF-fixdate representable window (0000-9999), bounding the input before the scale and cast so neither can overflow. - Route the three untrusted-input paths through them: CBOR tag-1 decode (both integer and double content), TimestampFromDocument (JSON/CBOR numbers), and the epoch-seconds string parser. The unchecked FromEpochSeconds/FromEpochMilliseconds remain for internal callers with known-good values. - Tests: checked-factory unit tests (in-range, exact 0000/9999 boundaries, out-of-range and non-finite rejection) and a CBOR regression decoding the overflow payloads (huge tag-1 int, huge negative int, near-DBL_MAX double) as clean errors rather than UB. Closes #42 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyQAo21Pv6GYhHrkbQj8xQ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two top-tier findings from the whole-project review, bundled.
#41 — Contain handler/middleware exceptions instead of terminating the server
A handler (or
Observecallback) that threw propagated out of the transport's I/O thread and calledstd::terminate, taking down every in-flight request on all threads. The interface only documented "handlers must not throw."smithy::http::InvokeHandlerGuarded(smithy/http/server_dispatch.h): invokes aRequestHandler, converting any escaped exception into a 500 with a generatedx-correlation-idheader (and a minimal JSON body repeating it); the id +what()is written tostd::clogso an otherwise-silent crash leaves one greppable server-side line. An empty handler yields 503.Observemiddleware callback is guarded locally so a throwing metrics/log sink neither discards the built response nor unwinds the transport thread.transport.hcontract andserver-guide.mddescribe the safety net.#42 — Range-check timestamp conversions from untrusted numbers
Decoding a CBOR tag-1 timestamp did
as_int() * 1000, overflowingint64on a ~9-byte payload — undefined behavior on untrusted input. The epoch-seconds paths (CBOR double, JSON number, epoch-seconds string parser) cast an unbounded double throughllround(seconds * 1000.0)intoint64, and extreme instants formatted to 5+ digit / negative years no conformant peer can parse back.Timestamp::FromEpochSecondsChecked/FromEpochMillisecondsChecked:Outcome-returning factories that reject non-finite values and any instant whose civil year is outside the RFC 3339 / IMF-fixdate window (0000-9999), bounding the input before the scale and cast so neither can overflow.TimestampFromDocument, the epoch-seconds string parser); the unchecked factories remain for internal callers with known-good values.Testing
WeatherServerover loopback (routing → validation → throwing handler → contained 500 + correlation header); and a middleware test for the throwing-callback case.DBL_MAXdouble) as clean errors.bazel test //runtime/... //examples/... //protocol-tests/...(minus Beast targets, whose Boost/BoringSSL archives this sandbox's proxy can't fetch — the documented exclusion): 54/54 pass, including all three protocol conformance suites. The UB elimination will be confirmed by CI's clang asan+ubsan job (local sanitizer runtime libs aren't installed here); the new tests assert the observable error-return behavior regardless.Checklist
bazel test //...and(cd codegen && gradle build spotlessCheck)pass locally (Beast/benchmark targets excluded per docs — proxy blocks their archive fetches; sanitizer job deferred to CI)Outcome/error idiom)Closes #41
Closes #42
🤖 Generated with Claude Code
https://claude.ai/code/session_01SyQAo21Pv6GYhHrkbQj8xQ
Generated by Claude Code