Skip to content

Fix integer overflow in dump for split thresholds beyond int32 range - #12289

Open
momomuchu wants to merge 2 commits into
dmlc:masterfrom
momomuchu:fix/dump-int-threshold-overflow-10035
Open

Fix integer overflow in dump for split thresholds beyond int32 range#12289
momomuchu wants to merge 2 commits into
dmlc:masterfrom
momomuchu:fix/dump-int-threshold-overflow-10035

Conversation

@momomuchu

Copy link
Copy Markdown

Fixes #10035.

get_dump() and trees_to_dataframe() report a garbage threshold (INT32_MIN or INT32_MAX) instead of the real split condition when a kInteger feature's threshold exceeds the int32 range. For example a model split at ~3e10 dumps 2147483647. predict() is unaffected: the predictor compares against the raw float SplitCond and never goes through the dump formatter, so only the text/JSON/dataframe dumps were wrong.

Root cause: the two Integer() dump formatters in src/tree/tree_model.cc (TextGenerator and JsonGenerator) do an unchecked static_cast<int32_t> on the floored split condition, which wraps around (and is UB for out-of-range values).

This widens the cast to int64_t so the real threshold is reported, and clamps to the int64 range before the cast so a value beyond int64 (only reachable if a >9.2e18 float feature is mislabeled as int) saturates deterministically rather than being UB. The clamp is a no-op for every value a genuine int feature can hold. Both dump paths and trees_to_dataframe now report the true threshold.

Added tests for the int64 overflow, the in-range boundary (no regression), float thresholds being unaffected, and the beyond-int64 saturation. plot_tree/graphviz was checked and never had this bug (it renders the raw float, not the integer formatter), so it is left unchanged.

One open question for you: I widened to int64 to preserve the value; the alternative is clamping to int32 with a warning if you would rather keep a strict 32-bit dump range for downstream consumers. Happy to switch if you prefer.

TextGenerator::Integer and JsonGenerator::Integer round a kInteger
feature's split condition and cast it to an integer with no range
check. The original int32 cast was undefined behavior for thresholds
outside int32; widening to int64 only moved the boundary to ~9.22e18,
since static_cast to int64 of an out-of-range double is still UB.

Clamp the rounded value to [INT64_MIN, INT64_MAX] before the cast, so
an out-of-range threshold saturates deterministically instead of
emitting platform-dependent garbage. This is unreachable for real
models (genuine integer features stay well within int64) but keeps
the dump deterministic for a mislabeled >int64 float feature.
predict() and the float (kQuantitive) path are untouched, and
reachable-range dump output is byte-identical.

Closes dmlc#10035
@trivialfis

Copy link
Copy Markdown
Member

I think we can fix it by simply using int64_t or uint64_t since we don't support larger integers for training anyway.

Per review, drop the double based saturating clamp and just widen the
split threshold cast in TextGenerator::Integer and JsonGenerator::Integer
from int32_t to int64_t. Larger integers are not supported for training,
so the extra out-of-int64 saturation path is unnecessary.

Removes the now-obsolete beyond-int64 saturation test; the other
regression tests for the int32 overflow (dmlc#10035) are unchanged and still
pass.
@trivialfis
trivialfis self-requested a review July 29, 2026 09:44
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.

Integer overflow in get_dump and trees_to_dataframe

2 participants