From 54998641f2afa6ebc9dd279071dcae44c44d8a27 Mon Sep 17 00:00:00 2001 From: Nityahapani Date: Fri, 31 Jul 2026 05:14:25 +0000 Subject: [PATCH 1/2] Escape feature name before using it in a regex in get_split_value_histogram get_split_value_histogram() parses the text tree dump looking for lines matching a feature's split condition, using a regex built via: regexp = re.compile(r"\[{0}<([\d.Ee+-]+)\]".format(feature)) The feature name is inserted directly into the pattern with no escaping. Feature names are ordinary user-controlled strings and can contain regex metacharacters -- parentheses, brackets, +, *, etc -- which is not an exotic edge case: real-world column names like "price ($)", "x[1]", "col+extra", or "col(1)" are all plausible. Concretely, for a feature named "price ($)" with an actual split on it in the dump text "[price ($)<5.5]", the current (unescaped) regex fails to match at all -- the parentheses are interpreted as a regex group rather than literal characters -- silently returning an empty histogram (or triggering the "doesn't support categorical split" error path) instead of the real split-value data. Fix: wrap the feature name in re.escape() before formatting it into the pattern. Verified standalone (import xgboost requires the compiled C++ core, which isn't buildable in this sandbox) against a range of realistic feature names ("price ($)", "a.b", "x[1]", "col+extra", "a*b", "col(1)", "50%_off", plus an ordinary "plain_feature"): - before the fix: 5 of 7 names with metacharacters silently fail to match a dump line that should match - after the fix: every name, including the plain one (no regression), matches correctly mypy and ruff both pass on the modified file. --- python-package/xgboost/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-package/xgboost/core.py b/python-package/xgboost/core.py index 07376a49a743..7c560f74e2ea 100644 --- a/python-package/xgboost/core.py +++ b/python-package/xgboost/core.py @@ -3338,7 +3338,7 @@ def get_split_value_histogram( xgdump = self.get_dump(fmap=fmap) values = [] # pylint: disable=consider-using-f-string - regexp = re.compile(r"\[{0}<([\d.Ee+-]+)\]".format(feature)) + regexp = re.compile(r"\[{0}<([\d.Ee+-]+)\]".format(re.escape(feature))) for val in xgdump: m = re.findall(regexp, val) values.extend([float(x) for x in m]) From b039b316450f177891152394d0578b01929b2e3c Mon Sep 17 00:00:00 2001 From: Nityahapani Date: Sat, 1 Aug 2026 07:32:53 +0000 Subject: [PATCH 2/2] Add regression test for regex-special feature names Per review feedback from trivialfis: add a test for the regex escape fix. test_split_value_histogram_regex_special_feature_name trains two otherwise-identical models on the same digits dataset -- one with default feature names, one where feature 28 (the same feature already exercised by the existing test_split_value_histograms test) is renamed to "f(28)[+.]", containing parentheses, brackets, a plus sign, and a dot -- all regex metacharacters. Since training params have no randomness sources (no subsample/ colsample, no seed needed for determinism), both models train identically except for the renamed feature, so their split-value histograms for that feature should come out byte-for-byte identical. The test asserts exactly that, plus a direct sanity check that the histogram is non-empty -- which is the actual symptom of the bug: the unescaped regex silently returned an empty histogram for this feature name instead of matching the real split data. --- tests/python/test_with_sklearn.py | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/python/test_with_sklearn.py b/tests/python/test_with_sklearn.py index 9f44024970f7..4fa7122d88a4 100644 --- a/tests/python/test_with_sklearn.py +++ b/tests/python/test_with_sklearn.py @@ -698,6 +698,47 @@ def test_split_value_histograms(): assert gbdt.get_split_value_histogram("f28", bins=None).shape[0] == 2 +def test_split_value_histogram_regex_special_feature_name(): + # Regression test: a feature name containing regex metacharacters used to + # silently break get_split_value_histogram's internal regex, since the + # feature name was substituted into the pattern without escaping. + from sklearn.datasets import load_digits + + digits_2class = load_digits(n_class=2) + X = digits_2class["data"] + y = digits_2class["target"] + + # Give the same feature ("f28" in the plain, unescaped test above) a name + # containing several regex metacharacters: parentheses, brackets, a plus + # sign, and a dot. + feature_names = [f"f{i}" for i in range(X.shape[1])] + special_name = "f(28)[+.]" + feature_names[28] = special_name + + dm_plain = xgb.DMatrix(X, label=y) + dm_special = xgb.DMatrix(X, label=y, feature_names=feature_names) + params = { + "max_depth": 6, + "eta": 0.01, + "objective": "binary:logistic", + "base_score": 0.5, + } + + gbdt_plain = xgb.train(params, dm_plain, num_boost_round=10) + gbdt_special = xgb.train(params, dm_special, num_boost_round=10) + + expected = gbdt_plain.get_split_value_histogram("f28", as_pandas=False) + actual = gbdt_special.get_split_value_histogram(special_name, as_pandas=False) + + # Same underlying data and training params (only the feature name differs), + # so the histogram for this feature should come out identical either way. + assert actual.shape[0] == expected.shape[0] + assert np.array_equal(actual, expected) + # And, as a sanity check on the bug itself: the unescaped regex used to + # return an empty histogram for this feature name instead. + assert actual.shape[0] > 0 + + def test_sklearn_random_state(): clf = xgb.XGBClassifier(random_state=402) assert clf.get_xgb_params()["random_state"] == 402