Skip to content

Guard against intermediate x*x overflow in Student's t pdf/cdf#1402

Open
a-leontyev wants to merge 7 commits into
boostorg:developfrom
a-leontyev:fix/students-t-overflow
Open

Guard against intermediate x*x overflow in Student's t pdf/cdf#1402
a-leontyev wants to merge 7 commits into
boostorg:developfrom
a-leontyev:fix/students-t-overflow

Conversation

@a-leontyev
Copy link
Copy Markdown

Closes #1401.

Problem

pdf() and cdf() of the Student's t distribution form x * x before any
division by the degrees of freedom. For finite but large |x| this overflows
to infinity, after which pdf() silently returns 0 and cdf() silently returns
the tail (0 or 1). The error-handling policy is bypassed entirely, so an
overflow policy of throw_on_error / errno_on_error has no effect.

Fix

Add an explicit guard in both functions: when fabs(x) > sqrt(max_value)
the squared deviate would overflow, so invoke raise_overflow_error<> with
the active Policy. This mirrors the existing use of raise_overflow_error in
quantile() for the p==0 / p==1 cases, so the style is consistent with the
surrounding code.

Behaviour

  • Default (throw_on_error): throws std::overflow_error.
  • errno_on_error: sets ERANGE, returns +infinity.
  • ignore_error: returns +infinity.
  • All in-range arguments: bit-for-bit unchanged — the guard only fires for
    deviates that would otherwise overflow xx. The existing isinf(x) fast-path
    is untouched (the guard sits below it, on the finite-but-huge path). The
    internal edgeworth-expansion helper, which also forms x
    x but only on a
    bounded normal deviate, is intentionally left unguarded.

Tests

Adds test/test_students_t_overflow.cpp covering, for float/double/long double:

  1. throw_on_error -> pdf/cdf throw on an overflowing-but-finite deviate;
  2. errno_on_error -> pdf/cdf set ERANGE and return non-finite;
  3. no regression -> cdf(0)==0.5, pdf(1.5) finite & positive, and a large
    but safe deviate (1e6) evaluates without firing the
    guard.

Both the new test and the existing test_students_t pass locally
(clang-17, arm64, C++14).

Notes for reviewers

The numerical core is intentionally left untouched — this is a guard, not a
reformulation of x*x, precisely to keep accuracy on in-range inputs identical.
Threshold sqrt(max_value) is conservative: any |x| above it overflows when
squared regardless of df.

LeantionX added 2 commits June 3, 2026 10:07
pdf() and cdf() form x*x before normalising by the degrees of freedom.
For finite but large |x| this overflows to infinity, after which pdf()
silently returns 0 and cdf() silently returns the tail, bypassing the
error-handling policy. Add an explicit guard that invokes
raise_overflow_error<> via the active Policy, mirroring quantile().

Adds test_students_t_overflow.cpp covering throw_on_error, errno_on_error
and a no-regression case for float/double/long double.
Comment thread include/boost/math/distributions/students_t.hpp Outdated
Comment thread test/test_students_t_overflow.cpp
Address review feedback:
- Revert std::fabs to an ADL fabs in the pdf/cdf guards and add
  BOOST_MATH_STD_USING to cdf, so the call resolves for non-standard
  types (real_concept, multiprecision).
- Extend test_students_t_overflow with real_concept and cpp_bin_float_50,
  exercising the throw policy and the no-regression path. The test's
  overflowing_deviate() now also uses an ADL sqrt for the same reason.
@mborland
Copy link
Copy Markdown
Member

mborland commented Jun 3, 2026

I've approved the CI run for this now since the changes look good.

@mborland
Copy link
Copy Markdown
Member

mborland commented Jun 3, 2026

Looks like you have a single failure when running with ISAN:

testing.capture-output ../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow.run
====== BEGIN OUTPUT ======
../../../boost/multiprecision/cpp_int/bitwise.hpp:441:48: runtime error: left shift of 7988312333241215 by 49 places cannot be represented in type 'unsigned long long'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:441:48 
../../../boost/multiprecision/cpp_int/bitwise.hpp:446:48: runtime error: left shift of 10353304230224386838 by 49 places cannot be represented in type 'unsigned long long'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:446:48 
../../../boost/multiprecision/cpp_int/bitwise.hpp:600:35: runtime error: left shift of 8544794553241845838 by 63 places cannot be represented in type 'unsigned long long'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:600:35 
/usr/bin/../lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/basic_string.h:490:51: runtime error: unsigned integer overflow: 3 - 9 cannot be represented in type 'size_type' (aka 'unsigned long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /usr/bin/../lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/basic_string.h:490:51 
../../../boost/multiprecision/cpp_int/bitwise.hpp:600:35: runtime error: left shift of 1099511627775 by 44 places cannot be represented in type 'limb_type' (aka 'unsigned long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:600:35 
../../../boost/multiprecision/cpp_int/bitwise.hpp:446:48: runtime error: left shift of 8796093022206 by 42 places cannot be represented in type 'limb_type' (aka 'unsigned long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:446:48 
../../../boost/multiprecision/cpp_int/bitwise.hpp:600:35: runtime error: left shift of 562949953421312 by 54 places cannot be represented in type 'limb_type' (aka 'unsigned long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:600:35 
Running 1 test case...
test_students_t_overflow.cpp(95): error: in "students_t_overflow_test": check (*__errno_location ()) == 0 has failed [25 != 0]
../../../boost/multiprecision/cpp_int/bitwise.hpp:441:48: runtime error: left shift of 17693815744170386243 by 1 places cannot be represented in type 'limb_type' (aka 'unsigned long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:441:48 
../../../boost/multiprecision/cpp_int/bitwise.hpp:446:48: runtime error: left shift of 9223372036854775807 by 41 places cannot be represented in type 'limb_type' (aka 'unsigned long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:446:48 
../../../boost/multiprecision/cpp_int/bitwise.hpp:441:48: runtime error: left shift of 14682102426013724755 by 2 places cannot be represented in type 'limb_type' (aka 'unsigned long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:441:48 
../../../boost/multiprecision/cpp_int/bitwise.hpp:600:35: runtime error: left shift of 16607023625928704 by 60 places cannot be represented in type 'limb_type' (aka 'unsigned long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:600:35 
../../../boost/multiprecision/cpp_int/bitwise.hpp:441:48: runtime error: left shift of 1043677052928000 by 15 places cannot be represented in type 'limb_type' (aka 'unsigned long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:441:48 
test_students_t_overflow.cpp(99): error: in "students_t_overflow_test": check (*__errno_location ()) == 0 has failed [25 != 0]

*** 2 failures are detected in the test module "Master Test Suite"

EXIT STATUS: 201
====== END OUTPUT ======

    
     "../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow"   > "../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow.output" 2>&1 < /dev/null
    status=$?
    echo >> "../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow.output"
    echo EXIT STATUS: $status >> "../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow.output"
    if test $status -eq 0 ; then
        cp "../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow.output" "../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow.run" >/dev/null
    fi
    verbose=0
    if test $status -ne 0 ; then
        verbose=1
    fi
    if test $verbose -eq 1 ; then
        echo ====== BEGIN OUTPUT ======
        cat "../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow.output"
        echo ====== END OUTPUT ======
    fi
    exit $status

...failed testing.capture-output ../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow.run...

@a-leontyev
Copy link
Copy Markdown
Author

Looks like you have a single failure when running with ISAN:

testing.capture-output ../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow.run
====== BEGIN OUTPUT ======
../../../boost/multiprecision/cpp_int/bitwise.hpp:441:48: runtime error: left shift of 7988312333241215 by 49 places cannot be represented in type 'unsigned long long'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:441:48 
../../../boost/multiprecision/cpp_int/bitwise.hpp:446:48: runtime error: left shift of 10353304230224386838 by 49 places cannot be represented in type 'unsigned long long'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:446:48 
../../../boost/multiprecision/cpp_int/bitwise.hpp:600:35: runtime error: left shift of 8544794553241845838 by 63 places cannot be represented in type 'unsigned long long'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:600:35 
/usr/bin/../lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/basic_string.h:490:51: runtime error: unsigned integer overflow: 3 - 9 cannot be represented in type 'size_type' (aka 'unsigned long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /usr/bin/../lib/gcc/x86_64-linux-gnu/13/../../../../include/c++/13/bits/basic_string.h:490:51 
../../../boost/multiprecision/cpp_int/bitwise.hpp:600:35: runtime error: left shift of 1099511627775 by 44 places cannot be represented in type 'limb_type' (aka 'unsigned long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:600:35 
../../../boost/multiprecision/cpp_int/bitwise.hpp:446:48: runtime error: left shift of 8796093022206 by 42 places cannot be represented in type 'limb_type' (aka 'unsigned long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:446:48 
../../../boost/multiprecision/cpp_int/bitwise.hpp:600:35: runtime error: left shift of 562949953421312 by 54 places cannot be represented in type 'limb_type' (aka 'unsigned long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:600:35 
Running 1 test case...
test_students_t_overflow.cpp(95): error: in "students_t_overflow_test": check (*__errno_location ()) == 0 has failed [25 != 0]
../../../boost/multiprecision/cpp_int/bitwise.hpp:441:48: runtime error: left shift of 17693815744170386243 by 1 places cannot be represented in type 'limb_type' (aka 'unsigned long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:441:48 
../../../boost/multiprecision/cpp_int/bitwise.hpp:446:48: runtime error: left shift of 9223372036854775807 by 41 places cannot be represented in type 'limb_type' (aka 'unsigned long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:446:48 
../../../boost/multiprecision/cpp_int/bitwise.hpp:441:48: runtime error: left shift of 14682102426013724755 by 2 places cannot be represented in type 'limb_type' (aka 'unsigned long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:441:48 
../../../boost/multiprecision/cpp_int/bitwise.hpp:600:35: runtime error: left shift of 16607023625928704 by 60 places cannot be represented in type 'limb_type' (aka 'unsigned long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:600:35 
../../../boost/multiprecision/cpp_int/bitwise.hpp:441:48: runtime error: left shift of 1043677052928000 by 15 places cannot be represented in type 'limb_type' (aka 'unsigned long long')
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ../../../boost/multiprecision/cpp_int/bitwise.hpp:441:48 
test_students_t_overflow.cpp(99): error: in "students_t_overflow_test": check (*__errno_location ()) == 0 has failed [25 != 0]

*** 2 failures are detected in the test module "Master Test Suite"

EXIT STATUS: 201
====== END OUTPUT ======

    
     "../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow"   > "../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow.output" 2>&1 < /dev/null
    status=$?
    echo >> "../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow.output"
    echo EXIT STATUS: $status >> "../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow.output"
    if test $status -eq 0 ; then
        cp "../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow.output" "../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow.run" >/dev/null
    fi
    verbose=0
    if test $status -ne 0 ; then
        verbose=1
    fi
    if test $verbose -eq 1 ; then
        echo ====== BEGIN OUTPUT ======
        cat "../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow.output"
        echo ====== END OUTPUT ======
    fi
    exit $status

...failed testing.capture-output ../../../bin.v2/libs/math/test/test_students_t_overflow.test/clang-linux-18/debug/x86_64/link-static/threading-multi/visibility-hidden/test_students_t_overflow.run...

Fixed. ISAN failures were the BOOST_CHECK_EQUAL(errno, 0) assertions on the success paths in test_no_regression(). Under UBSan the diagnostic printer calls isatty() on the redirected stderr, which sets errno to ENOTTY (25) right between our reset and the check - and strictly speaking C11 7.5/3 allows any library call to modify errno on success anyway, so those assertions were over-strict. I've removed them; the policy-driven errno semantics are still covered by test_errno() (reset → call → ERANGE).

@jzmaddock
Copy link
Copy Markdown
Collaborator

Many thanks for looking into this, however, I'm not sure this is the correct fix: overflow_errors are for when the result is non-finite, in this case we have an intermediate calculation which just happens to spuriously overflow, but the result is expected to be finite. ie 0 for the PDF and 1 or 0 for the CDF and CDF complement respectively. So we should prevent the overflow in the intermediate calculation so that FPU exceptions don't get clobbered, but IMO the result should be either 0 or 1 depending on the function, and there's no need to call the error handlers for this.

@a-leontyev
Copy link
Copy Markdown
Author

Many thanks for looking into this, however, I'm not sure this is the correct fix: overflow_errors are for when the result is non-finite, in this case we have an intermediate calculation which just happens to spuriously overflow, but the result is expected to be finite. ie 0 for the PDF and 1 or 0 for the CDF and CDF complement respectively. So we should prevent the overflow in the intermediate calculation so that FPU exceptions don't get clobbered, but IMO the result should be either 0 or 1 depending on the function, and there's no need to call the error handlers for this.

Thanks. I agreed, that's the cleaner semantic. I've reworked the guards to return the exact saturated values directly. 0 for the pdf, and 0/1 for the cdf depending on sign (the complement gets the mirrored values via the negation overload). No error handlers are involved any more. The test now checks the exact tail values under a throwing policy so any error-handler invocation would fail it and verifies via fetestexcept that FE_OVERFLOW is not raised for built-in types.

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.

students_t pdf/cdf: intermediate x*x overflow bypasses the error policy

4 participants