LightGBM is no longer owned or maintained by Microsoft. Ownership of the project has transitioned to an external maintainer that operates its own security reporting process. Because the affected component falls outside Microsoft ownership, the report does not meet the bar for action.
Summary
The LightGBM text model parser validates array lengths against the number of leaves but never validates the parsed values. As a result, an untrusted text model can drive two memory-safety faults during normal use:
-
An out-of-bounds write with an attacker-controlled offset. A left_child or right_child entry can be any 32-bit integer. A negative value is treated as a leaf reference and bit-inverted into an array index, which is then used as a write subscript into the leaf_depth_ buffer. Calling predict(pred_contrib=True) (SHAP feature contributions, a documented feature) stores the recursion depth at leaf_depth_ + (attacker_index) * 4, an out-of-bounds write whose target address the attacker controls.
-
An out-of-bounds read. A split_feature entry is used directly as an index into the caller's feature buffer during ordinary predict, faulting on a large value.
Both stem from the same defect: parsed node values (left_child, right_child, split_feature, categorical indices) are used as array subscripts without any bounds validation. Models are shared, downloaded, and stored, so loading and predicting with one is a common, deliberate action.
Details
Out-of-bounds write (primary). The parser reads left_child and right_child with only a length check (src/io/tree.cpp:756-765):
left_child_ = CommonC::StringToArrayFast<int>(key_vals["left_child"], num_leaves_ - 1);
right_child_ = CommonC::StringToArrayFast<int>(key_vals["right_child"], num_leaves_ - 1);
The values are never checked to be valid node or leaf references. When SHAP contributions are requested, GBDT::PredictContrib initializes each tree via models_[i]->RecomputeMaxDepth() (src/boosting/gbdt.h:445, guarded by is_pred_contrib && !models_initialized_). RecomputeMaxDepth calls RecomputeLeafDepths, which recurses over the child arrays and writes using a bit-inverted child value as the index (include/LightGBM/tree.h):
inline void Tree::RecomputeLeafDepths(int node, int depth) {
if (node == 0) leaf_depth_.resize(num_leaves());
if (node < 0) {
leaf_depth_[~node] = depth; // ~node is attacker-controlled; no bounds check
} else {
RecomputeLeafDepths(left_child_[node], depth + 1);
RecomputeLeafDepths(right_child_[node], depth + 1);
}
}
A child value of -1000000000 yields ~(-1000000000) = 999999999, so leaf_depth_[999999999] = depth writes about 4 GB past a buffer sized to the number of leaves. The write offset is fully attacker-controlled through the child value (any negative int maps to an index across the positive int range), and the written value is the recursion depth, which the attacker influences by nesting depth. This is a constrained write-what-where primitive (controlled address, small controlled value), not merely a crash.
Confirmed under a debugger on lightgbm 4.6.0. The faulting instruction is a store, not a load:
mov %r15d,(%rdx,%rax,4) in LightGBM::Tree::RecomputeLeafDepths(int, int)
rax = 0x3b9ac9ff = 999999999 (= ~(-1000000000), the attacker index)
rdx = leaf_depth_ base ; %r15d = depth (value stored)
fault address = 0xef6d392c (base + index*4, about 4 GB past the buffer)
Out-of-bounds read (sibling). split_feature is read with only a length check (src/io/tree.cpp:769) and used as an index in the hot prediction path (include/LightGBM/tree.h:710, numerical; :706 categorical):
node = Decision(feature_values[split_feature_[node]], node);
split_feature_[node] indexes the caller's feature_values buffer; a value such as 2000000000 reads far out of bounds and faults during ordinary predict.
The same unchecked-value pattern affects related sinks: tree.h:386-387 uses cat_idx = static_cast<int>(threshold_[node]) then indexes cat_boundaries_[cat_idx] unchecked; tree.cpp:865 sizes cat_threshold_ from the attacker-controlled cat_boundaries_.back(); and out-of-range left_child_/right_child_ values also enable out-of-range node indices and cycles. No published GHSA covers the LightGBM C++ text parser; CVE-2024-37056 is an unrelated MLflow pickle issue.
PoC
shared in the GHSA - available here upon request
Impact
A malicious LightGBM text model, when loaded and used, corrupts memory in the loading process. The contribution path (predict(pred_contrib=True)) yields an out-of-bounds write whose target address is attacker-controlled and whose value is a small attacker-influenced integer, which is a memory-corruption primitive: depending on heap layout it overwrites adjacent allocations and is a plausible building block toward control-flow hijack, beyond the reliable crash. The ordinary prediction path yields an out-of-bounds read and crash. Both are reachable through the standard workflow of loading a shared or downloaded model and predicting with it. Code execution was not demonstrated; the out-of-bounds write with a controlled offset is.
Remediation
In the text model parser (Tree::Tree from string), validate every parsed node value before use: require split_feature entries to be within the model's feature count, require left_child/right_child to reference valid in-range nodes or leaves and to form an acyclic tree, and validate categorical indices (threshold_ as cat_idx, cat_boundaries_ entries) against their array bounds. Reject the model at load time when any value is out of range, rather than indexing with it during recompute or prediction.
Summary
The LightGBM text model parser validates array lengths against the number of leaves but never validates the parsed values. As a result, an untrusted text model can drive two memory-safety faults during normal use:
An out-of-bounds write with an attacker-controlled offset. A
left_childorright_childentry can be any 32-bit integer. A negative value is treated as a leaf reference and bit-inverted into an array index, which is then used as a write subscript into theleaf_depth_buffer. Callingpredict(pred_contrib=True)(SHAP feature contributions, a documented feature) stores the recursion depth atleaf_depth_ + (attacker_index) * 4, an out-of-bounds write whose target address the attacker controls.An out-of-bounds read. A
split_featureentry is used directly as an index into the caller's feature buffer during ordinarypredict, faulting on a large value.Both stem from the same defect: parsed node values (
left_child,right_child,split_feature, categorical indices) are used as array subscripts without any bounds validation. Models are shared, downloaded, and stored, so loading and predicting with one is a common, deliberate action.Details
Out-of-bounds write (primary). The parser reads
left_childandright_childwith only a length check (src/io/tree.cpp:756-765):The values are never checked to be valid node or leaf references. When SHAP contributions are requested,
GBDT::PredictContribinitializes each tree viamodels_[i]->RecomputeMaxDepth()(src/boosting/gbdt.h:445, guarded byis_pred_contrib && !models_initialized_).RecomputeMaxDepthcallsRecomputeLeafDepths, which recurses over the child arrays and writes using a bit-inverted child value as the index (include/LightGBM/tree.h):A child value of
-1000000000yields~(-1000000000) = 999999999, soleaf_depth_[999999999] = depthwrites about 4 GB past a buffer sized to the number of leaves. The write offset is fully attacker-controlled through the child value (any negativeintmaps to an index across the positiveintrange), and the written value is the recursion depth, which the attacker influences by nesting depth. This is a constrained write-what-where primitive (controlled address, small controlled value), not merely a crash.Confirmed under a debugger on lightgbm 4.6.0. The faulting instruction is a store, not a load:
Out-of-bounds read (sibling).
split_featureis read with only a length check (src/io/tree.cpp:769) and used as an index in the hot prediction path (include/LightGBM/tree.h:710, numerical;:706categorical):split_feature_[node]indexes the caller'sfeature_valuesbuffer; a value such as 2000000000 reads far out of bounds and faults during ordinarypredict.The same unchecked-value pattern affects related sinks:
tree.h:386-387usescat_idx = static_cast<int>(threshold_[node])then indexescat_boundaries_[cat_idx]unchecked;tree.cpp:865sizescat_threshold_from the attacker-controlledcat_boundaries_.back(); and out-of-rangeleft_child_/right_child_values also enable out-of-range node indices and cycles. No published GHSA covers the LightGBM C++ text parser; CVE-2024-37056 is an unrelated MLflow pickle issue.PoC
shared in the GHSA - available here upon request
Impact
A malicious LightGBM text model, when loaded and used, corrupts memory in the loading process. The contribution path (
predict(pred_contrib=True)) yields an out-of-bounds write whose target address is attacker-controlled and whose value is a small attacker-influenced integer, which is a memory-corruption primitive: depending on heap layout it overwrites adjacent allocations and is a plausible building block toward control-flow hijack, beyond the reliable crash. The ordinary prediction path yields an out-of-bounds read and crash. Both are reachable through the standard workflow of loading a shared or downloaded model and predicting with it. Code execution was not demonstrated; the out-of-bounds write with a controlled offset is.Remediation
In the text model parser (
Tree::Treefrom string), validate every parsed node value before use: requiresplit_featureentries to be within the model's feature count, requireleft_child/right_childto reference valid in-range nodes or leaves and to form an acyclic tree, and validate categorical indices (threshold_ascat_idx,cat_boundaries_entries) against their array bounds. Reject the model at load time when any value is out of range, rather than indexing with it during recompute or prediction.