Skip to content

Reject non-representable floats when reading a JSONB float into an integer#2645

Merged
stephenberry merged 2 commits into
mainfrom
fix/jsonb-float-to-int-ub
Jun 18, 2026
Merged

Reject non-representable floats when reading a JSONB float into an integer#2645
stephenberry merged 2 commits into
mainfrom
fix/jsonb-float-to-int-ub

Conversation

@stephenberry

Copy link
Copy Markdown
Owner

Summary

Fixes undefined behavior when reading a JSONB FLOAT/FLOAT5 payload into an integer field.

The integral from<JSONB, T> reader decodes the payload into a double and then casts it straight to the target with static_cast<T>(tmp). When the decoded value is NaN, ±Inf (e.g. a JSON5 NaN/Infinity sentinel), or finite but outside T's range, that float→integer cast is undefined behavior ([conv.fpint]).

A fuzzer reached it through a reflected struct with an integer field:

include/glaze/jsonb/read.hpp:297:36: runtime error: nan is outside the range of representable values of type 'int'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior

Fix

Guard the conversion: reject any value not representable as T before the cast, returning error_code::parse_number_failure. Genuinely integral floats still convert (the reader's existing "accept floats that happen to be integral" behavior, including truncation of fractional values, is preserved).

The upper bound is exclusive at max(T) + 1, computed as (max(T) / 2 + 1) * 2.0, which is an exact power of two in double. This avoids the classic pitfall where static_cast<double>(max(T)) rounds up past the true maximum for 64-bit integer types: a naive tmp <= static_cast<double>(max) check would admit exactly 2^63 for int64_t and then invoke the very UB we're guarding against. NaN compares false against both bounds, so it is rejected by the same check.

Only JSONB is affected. CBOR errors on a float payload read into an integer, and BEVE explicitly disallows float→integer cross-conversion, so neither coerces and neither has this issue.

Tests

Adds a jsonb_test regression suite (float_to_int_coercion_tests) covering:

  • NaN / +Inf / -Inf payloads into an integer → error (not UB)
  • finite out-of-range (1e300) into int32/int64/uint64 → error
  • the 64-bit boundary: exactly 2^63 into int64_t rejected without UB; the largest representable double < 2^63 still converts
  • exact extremes (INT32_MAX, INT32_MIN) round-trip through the float payload
  • fractional floats still truncate toward zero (well-defined behavior preserved)

Verified locally under -fsanitize=undefined,address: pre-fix aborts at jsonb/read.hpp:297, post-fix all cases pass with no sanitizer diagnostics. jsonb_test passes (148 tests, 567 asserts).

…teger

A JSONB FLOAT/FLOAT5 payload read into an integer field was cast straight to
the target type via static_cast<T>(tmp). When the decoded double is NaN, +/-Inf
(e.g. a JSON5 "NaN"/"Infinity" sentinel), or finite but outside T's range,
that cast is undefined behavior. A fuzzer reached it through a reflected struct
with an integer field (UBSan: "nan is outside the range of representable values
of type 'int'" at jsonb/read.hpp).

Guard the conversion: reject any value not representable as T before casting.
The upper bound is exclusive at max(T) + 1 (an exact power of two in double) so
the 64-bit boundary, where static_cast<double>(max(T)) rounds up past the true
maximum, cannot slip through. Genuinely integral floats still convert.

Adds a jsonb_test regression suite covering NaN/Inf, out-of-range, the 64-bit
boundary, exact extremes, and truncation of fractional values.
@stephenberry stephenberry merged commit 607ae05 into main Jun 18, 2026
53 checks passed
@stephenberry stephenberry deleted the fix/jsonb-float-to-int-ub branch June 18, 2026 18:22
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.

1 participant