Skip to content

validate log_uniform_int_distribution stream input and harden preconditions - #2144

Open
nabhan06 wants to merge 2 commits into
abseil:masterfrom
nabhan06:log-uniform-nonfinite-cast
Open

nabhan06 wants to merge 2 commits into
abseil:masterfrom
nabhan06:log-uniform-nonfinite-cast

Conversation

@nabhan06

@nabhan06 nabhan06 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

log_uniform_int_distribution's param_type constructor computes log_range_ as static_cast(ceil((1/log(base)) * log(range))). A base of 0 or 1, or a negative base for a signed IntType, violates the base > 1 precondition and makes that math non-finite: 1/log(1) divides by zero, and the final cast of inf/NaN to int is undefined behavior (UBSan: "inf is outside the range of representable values of type 'int'"). It was also reachable through operator>>, which handed whatever it parsed straight to param_type.

Changes:

  • Check the param_type preconditions with ABSL_HARDENING_ASSERT.
  • Only take the log() branch for base_ > 2, so an out-of-contract base never reaches the floating-point math.
  • Have operator>> validate max >= min and base > 1 and set failbit on bad input instead of constructing an invalid param_type, with a test covering that rejection.

@derekmauro derekmauro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/cc @laramiel

There is a fundamental difference between C++ language-level UB (compiler optimizations exploiting float overflow, which we want to avoid) and API contract violations.

Contract violations should fail fast and loudly. It should be very uncomfortable to have such a violation, otherwise users will eventually come to depend on them (Hyrum's Law).

We should avoid the language UB so UBSan and optimizers remain happy, but we should not promote an out-of-contract state into a tested feature.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about instead we simply change this to base_ > 2? This avoids all language level UB.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, the else is now else if (base_ > 2) and the cast guard is gone.

//
// Thus a result which should equal K may equal K +/- epsilon,
// which can eliminate some values depending on where the bounds fall.
const double inv_log_base = 1.0 / std::log(static_cast<double>(base_));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this fix fails to handle base_ == 1, which will produce a divide by 0 here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the guard only covered the cast. With the branch gated on base_ > 2 the log() math is not reached for base 1 (or 0 / negative) at all. Verified with -fsanitize=float-divide-by-zero on top of the usual UBSan checks.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use ABSL_HARDENING_ASSERT to check preconditions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, both preconditions use ABSL_HARDENING_ASSERT now.

EXPECT_GE(sample, dist.min());
EXPECT_LE(sample, dist.max());

// The same bad base arriving through deserialization must also stay defined.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proper fix for this is for operator>> to validate input and set std::ios_base::failbit. The wrong fix is for param_type to quietly sanitize bad input and leave the distribution in a silently failing state.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. operator>> now checks max >= min and base > 1 on the parsed values and sets failbit without touching the distribution. The opt-mode test is gone; the replacement only checks that operator>> rejects that input.

…itions

Address review feedback: check the param_type preconditions with
ABSL_HARDENING_ASSERT, only take the log() branch for base_ > 2 so an
out-of-contract base never reaches floating-point undefined behavior
(including the divide by log(1) == 0), and have operator>> validate
max >= min and base > 1, setting failbit instead of constructing an
invalid param_type. Replace the opt-mode out-of-contract test with one
that checks operator>> rejects such input.
@nabhan06

Copy link
Copy Markdown
Contributor Author

Agreed on the contract vs. language UB distinction. Pushed a revision along those lines: ABSL_HARDENING_ASSERT for the preconditions, the log() branch gated on base_ > 2 with the cast guard dropped, and operator>> validating and setting failbit instead of handing bad input to param_type. The test no longer exercises out-of-contract construction, it just covers the operator>> rejection. Passes in fastbuild and -c opt with UBSan (including float-divide-by-zero).

@nabhan06 nabhan06 changed the title guard non-finite cast in log_uniform_int_distribution param_type validate log_uniform_int_distribution stream input and harden preconditions Sep 13, 2026
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.

3 participants