Range-check intEnum and float wire values; validate intEnum membership (#109) - #197
Conversation
A finite double beyond float range is UB to static_cast ([conv.double]); generated deserializers need the check in one tested place. NaN and ±Infinity pass through — they are legal Smithy float values on every wire. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014C7WdBD99mUFWGxMvGSjSU
intEnum merges into the bounded-integer serde case: its underlying type is int32, so a wire int64 of 2^32+2 silently aliased onto a valid enumerator. float rejects finite doubles beyond float range via smithy::FloatFromDouble on both the document-body path and the query/label/header text path — the raw cast was UB that UBSan's float-cast-overflow aborts on. Unknown in-range intEnum values still parse, matching string enums' tolerant reads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014C7WdBD99mUFWGxMvGSjSU
String enums already fail request validation outside the modeled value set; intEnum members were accepted silently. Same suite-exact ValidationException message with the value set spelled in ints (the smithy-rs convention), same @internal policy: wire-valid but unadvertised. The generated smoke and response suites promptly caught that a default-constructed intEnum (0) is not usually a member — minimal test values now pick the first modeled member, exactly as string enums always did. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014C7WdBD99mUFWGxMvGSjSU
ParseError truncated error.code to int before its 100-599 range test, so a peer sending 2^32+404 classified as HTTP 404 — and 5xx aliases came back retryable. The interop suite now pins a hand-rolled peer sending 21474837103: not a 503, lands in the 400 class, not retryable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014C7WdBD99mUFWGxMvGSjSU
) One suite driving the roundtrip REST fixture at three depths: the generated serde directly (hostile Documents), the generated server over the wire (SerializationException before the handler, ValidationException with the suite-exact message), and the generated client parsing hostile responses — plus the deliberate asymmetry: clients keep unknown-but-in-range intEnum values for forward compatibility, servers reject them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014C7WdBD99mUFWGxMvGSjSU
std::numeric_limits and std::vector without <limits>/<vector> — breaks on the libc++ matrix cell (SF.10). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014C7WdBD99mUFWGxMvGSjSU
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014C7WdBD99mUFWGxMvGSjSU
FloatFromDouble rejected at FLT_MAX, but [conv.double] only goes undefined once round-to-nearest overflows — at 2^128*(1-2^-25). The gap is not theoretical: shortest-round-trip float text, including FormatFloat's own output for FLT_MAX, parses to a double slightly above FLT_MAX that must keep narrowing to it. The boundary is now the true overflow bound, pinned by a FormatFloat->strtod->FloatFromDouble round-trip test and a wire-text serde case. Also from the panel: the jsonRpc2 truncation test used 21474837103, which truncates to 623 and classified as 400 under old and new code alike — a vacuous pin; 21474836983 truncates to 503 and discriminates. Generated jsonRpc2 clients now include <cstdint> for their std::int64_t local instead of borrowing it transitively (SF.10). The Describe test helper fails cleanly instead of dereferencing a failed Outcome. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014C7WdBD99mUFWGxMvGSjSU
|
CI status on during
I don't have permission to re-run the failed job (403 on Generated by Claude Code |
|
Resolved — no maintainer re-run needed: updating the branch onto current Generated by Claude Code |
ReviewLGTM — this closes the real #109 narrowing holes cleanly, and the prior panel pass already fixed the sharp edges (true float overflow bound vs What looks right
Nits (non-blocking)
No correctness issues found; happy to see this land. |
| (#109). Three holes in the otherwise-uniform range-check posture: an | ||
| `intEnum` member cast the raw wire int64 straight into its `int32`-backed | ||
| `enum class`, so 2^32+2 silently aliased onto a valid enumerator (byte / | ||
| short / integer members already rejected out-of-range values — intEnum now |
There was a problem hiding this comment.
Nit (non-blocking): “text bindings alike” describes the end state — on main the intEnum text path was already bounded via ParseInt64Text + int64Bounds(INT_ENUM). The hole this PR closes for intEnum is the document-body cast in SerdeCodeGen (plus server membership). Harmless, but a reader bisecting will look here first expecting a text-path change too.
Review-bot nits on #197, both verified: the intEnum text-binding path was already bounded on main (ParseInt64Text + int64Bounds) — the hole this PR closes is the document-body cast, so the changelog now says which path changed; and model-evolution.md's add-enum-value row now covers intEnums (clients keep unknown in-range values, servers reject outside the modeled set) alongside string enums. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014C7WdBD99mUFWGxMvGSjSU
What
Closes the two highest-impact open items on the #109 C++ Core Guidelines tracker (the remaining serde narrowing holes, per the tracker's triage), plus the related smaller findings from the same narrowing family. #110 was consolidated into #109 and closed as its duplicate before this PR.
int32-backedenum class— a wire int64 of 2^32+2 silently aliased onto a valid enumerator. intEnum now merges into the bounded-integer serde case and shares byte/short/integer's "value out of range" rejection, on document bodies and text bindings alike (SerdeCodeGen.java).1e300) was undefined behavior per [conv.double], which UBSan'sfloat-cast-overflowaborts on. Both the document-body path (SerdeCodeGen.java) and the query/label/header text path (HttpBindingCodeGen.java) now narrow through a new runtime helper,smithy::FloatFromDouble, which rejects finite overflow while NaN/±Infinity still pass (they're legal Smithy float values and narrow losslessly).ValidationGenerator.java), matching string enums: same suite-exactValidationExceptionmessage with the value set spelled in ints (the smithy-rs convention), same@internalpolicy (wire-valid but unadvertised). Clients deliberately keep unknown-but-in-range values for forward compatibility — the asymmetry is pinned by tests. The generated smoke/response suites immediately caught that a default-constructed intEnum (0) usually isn't a member, so minimal test values now pick the first modeled member, as string enums always did (NodeLiteralGenerator.java).error.codetointbefore its 100–599 range test, so 2^32+404 classified as HTTP 404 and 5xx aliases came back retryable — codes are now classified on the full int64 (JsonRpc2Protocol.java); andrandom_document.hgets the<limits>/<vector>includes it was borrowing transitively (SF.10).Checked-in goldens are regenerated; regeneration is byte-stable. CHANGELOG,
docs/server-guide.md, anddocs/generated-types.mdupdated for the behavior changes.Testing
Beyoncé Rule throughout — everything touched now has something pinning it:
FloatFromDoubleindocument_serde_test.cc— in-range, exactFLT_MAXedge, finite overflow rejection (including one double-ULP past the edge), NaN/±Inf pass-through, subnormal rounding.SerdeGeneratorTestpins the intEnum range check preceding the cast and the float narrowing (double stays unchecked);HttpJsonBindingProtocolTestpins the text-binding float path (no fixture binds a float to a text position, so goldens can't);ValidationGeneratorTestpins intEnum membership incl. the@internalpolicy.examples/roundtrip/rest/numeric_bounds_wire_test.ccdrives the roundtrip fixture at three depths — generated serde directly with hostile Documents, generated server over the wire (SerializationExceptionbefore the handler;ValidationExceptionwith the suite-exact message and fieldList path), and generated client parsing hostile responses — plus the client/server asymmetry pin.examples/jsonrpc2/interop_wire_test.ccadds a hand-rolled peer sending error code 21474837103 (must not classify as a retryable 503). The hand-written malformed suite (protocol-tests/simplerestjson/malformed) pins the alloy fixture's intEnum label: out-of-int32 →SerializationException, in-range-unknown →ValidationException.bazel test //... --config=werror— 126/126 pass;(cd codegen && gradle build spotlessCheck)green; golden regeneration byte-identical; UBSan pass (gcc,-fsanitize=undefined -fno-sanitize-recover=all) green over the affected suites (this environment's clang lacks the sanitizer runtime libs, so the clang asan/ubsan cells are left to CI).Checklist
bazel test //...and(cd codegen && gradle build spotlessCheck)pass locally🤖 Generated with Claude Code
https://claude.ai/code/session_014C7WdBD99mUFWGxMvGSjSU
Generated by Claude Code