Skip to content

Runtime hardening: contain handler exceptions and range-check timestamp decoding - #50

Merged
aaylward merged 3 commits into
mainfrom
claude/issue-27-fix-42z1s0
Jul 8, 2026
Merged

aaylward merged 3 commits into
mainfrom
claude/issue-27-fix-42z1s0

Conversation

@aaylward

@aaylward aaylward commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Two top-tier findings from the whole-project review, bundled.

#41 — Contain handler/middleware exceptions instead of terminating the server

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."

  • smithy::http::InvokeHandlerGuarded (smithy/http/server_dispatch.h): invokes a RequestHandler, converting any escaped exception into a 500 with a generated x-correlation-id header (and a minimal JSON body repeating it); the id + what() is written to std::clog so an otherwise-silent crash leaves one greppable server-side line. An empty handler yields 503.
  • The Beast, socket, and loopback transports route through it, so the guard holds regardless of which handler a consumer installs.
  • The Observe middleware callback is guarded locally so a throwing metrics/log sink neither discards the built response nor unwinds the transport thread.
  • Docs: transport.h contract and server-guide.md describe the safety net.

#42 — Range-check timestamp conversions from untrusted numbers

Decoding a CBOR tag-1 timestamp did as_int() * 1000, overflowing int64 on 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 through llround(seconds * 1000.0) into int64, 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.
  • The three untrusted-input paths route through them (CBOR tag-1 integer + double, TimestampFromDocument, the epoch-seconds string parser); the unchecked factories remain for internal callers with known-good values.

Testing

  • Uncaught handler (or middleware) exception terminates the whole server process #41: unit coverage of the guard (success passthrough, std/non-std exceptions → 500 + distinct correlation ids, empty → 503); a real end-to-end socket test — a throwing handler on the transport's own thread yields 500 and the server survives the next request; a generated-dispatch integration test driving the generated WeatherServer over loopback (routing → validation → throwing handler → contained 500 + correlation header); and a middleware test for the throwing-callback case.
  • CBOR tag-1 timestamp: signed-integer-overflow UB on untrusted input #42: checked-factory unit tests (in-range, exact 0000/9999 boundaries verified against the date-time parser, out-of-range + 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.
  • 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.
  • clang-format / clang-tidy / buildifier clean on the changed files.

Checklist

  • Tests added/updated for the change
  • 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)
  • Formatting clean (clang-format, clang-tidy, buildifier)
  • Architectural decisions recorded as an ADR (not applicable — bug fixes within existing conventions; the one new runtime type follows the Outcome/error idiom)

Closes #41
Closes #42

🤖 Generated with Claude Code

https://claude.ai/code/session_01SyQAo21Pv6GYhHrkbQj8xQ


Generated by Claude Code

claude added 3 commits July 8, 2026 04:01
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
@aaylward
aaylward merged commit 8162ca4 into main Jul 8, 2026
12 checks passed
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.

CBOR tag-1 timestamp: signed-integer-overflow UB on untrusted input Uncaught handler (or middleware) exception terminates the whole server process

2 participants