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.

MLeap bundle-ml tree deserializers: uncontrolled recursion -> uncaught StackOverflowError (CWE-674 / CWE-400)

Target: combust/mleap (huntr.com MFF), specifically the Bundle.ML tree serializer components in the bundle-ml module (component category: "other serializer classes", not the previously-filed ArraySerializer OOM finding).

Affected code

  • bundle-ml/src/main/scala/ml/combust/bundle/tree/decision/TreeSerializer.scala
    • TreeSerializer.read(reader: FormatTreeReader): N (lines ~112-122) β€” unbounded recursion, one stack frame per tree node, no depth/size limit.
    • ProtoFormatTreeReader.read() (lines ~67-76) β€” allocates new Array[Byte](size) from an attacker-controlled readInt() with no bound check against remaining stream length (secondary issue, same file).
  • bundle-ml/src/main/scala/ml/combust/bundle/tree/cluster/NodeSerializer.scala
    • NodeSerializer.read(reader: FormatNodeReader): N (lines ~111-118) β€” the identical unbounded-recursion pattern, keyed off attacker-controlled numChildren, in the sibling clustering-tree (BisectingKMeans) reader. Confirms this is a systemic pattern, not a one-off.

Both are reached from real, exercised production call paths, e.g.:

// mleap-runtime/.../bundle/ops/classification/DecisionTreeClassifierOp.scala
override def load(model: Model)(implicit context: BundleContext[MleapContext]): DecisionTreeClassifierModel = {
  val rootNode = TreeSerializer[tree.Node](context.file("tree"), withImpurities = true).read().get
  ...

i.e. any application that loads an untrusted MLeap bundle (.zip) containing a DecisionTreeClassifier/DecisionTreeRegressor (and, by the identical pattern above, BisectingKMeans) component reaches this code.

The bug

read(reader) recursively reads a pre-order-encoded binary tree with no depth limit and no node-count limit:

def read(reader: FormatTreeReader): N = {
  val node = reader.read()
  if(node.n.isInternal) {
    ntc.internal(node.getInternal, read(reader), read(reader))   // <-- unbounded recursion
  } else if(node.n.isLeaf) {
    ntc.leaf(node.getLeaf, withImpurities)
  } else { throw new IllegalArgumentException("invalid tree") }
}

A degenerate/unbalanced tree (a long right-leaning chain of "internal" nodes) recurses to a depth proportional to the number of nodes. With the JVM's default 1 MB thread stack, a ~340 KB crafted tree.json (~3,500 chained nodes) is enough to blow the stack.

Worse: MLeap's own bundle-loading API is entirely built on scala.util.Try / scala.util.Using, seemingly to convert load errors into a handled Failure(...). But scala.util.control.NonFatal (see NonFatal_bytecode.txt) checks instanceof VirtualMachineError first and treats it as fatal (not caught). StackOverflowError extends VirtualMachineError. So the StackOverflowError is not converted into a Failure anywhere in the chain β€” it propagates as an uncaught Error clean through TreeSerializer.read(): Try[N], ModelSerializer.readWithModel(), NodeSerializer.read(), and BundleSerializer.read(): Try[Bundle[_]], crashing the thread that called the "safe", Try-typed bundle-load API.

This is confirmed empirically in evidence_log.txt item 2 and item 4 below (not just theorized from the Scala docs).

Real, unmodified-code proof

All four harnesses below are plain Scala programs compiled against the real, unmodified, published ml.combust.bundle:bundle-ml_2.13:0.24.0 jar from Maven Central β€” the exact release that HEAD of https://github.com/combust/mleap is tagged as (v0.24.0, commit 027342cac5c96d6cb4f56046634a5f0487849a67). No library source was patched or stubbed; only a trivial NodeWrapper[N] type-class instance is supplied (the same kind of glue MLeap's own mleap-runtime module supplies via MleapNodeWrapper), because that is a public extension point the library requires callers to implement.

  • TreeRecursionHarness.scala β€” crafts a malicious tree payload and calls the real ml.combust.bundle.tree.decision.TreeSerializer.read(reader) directly.
  • TreeRecursionHarness2.scala β€” same payload, but drives the real file-based, Try-returning public API TreeSerializer.read(): Try[N] (exactly what DecisionTreeClassifierOp.load() / DecisionTreeRegressionOp.load() call), to prove the crash escapes even through the "safe" API.
  • ClusterRecursionHarness.scala β€” same technique against the sibling ml.combust.bundle.tree.cluster.NodeSerializer (BisectingKMeans).
  • TrySoeCheck.scala β€” isolated, minimal proof that scala.util.Try does not catch StackOverflowError in this Scala version (2.13.16, the exact version bundle-ml_2.13-0.24.0 is compiled against).

See evidence_log.txt for full run output (StackOverflowError stack traces truncated for brevity; full traces were captured and inspected manually) and NonFatal_bytecode.txt for the decompiled proof of the NonFatal.apply fatal/non-fatal check ordering.

Reproduce

# fetch bundle-ml_2.13-0.24.0.jar + its runtime deps from Maven Central
# (scala-library/-compiler/-reflect 2.13.16, protobuf-java 3.21.7,
#  scalapb-runtime_2.13 0.11.13, lenses_2.13 0.11.13, config 1.4.2,
#  scala-collection-compat_2.13 2.8.1, spray-json_2.13 1.3.6)
CP=$(ls jars/*.jar | tr '\n' ':')
java -cp "$CP" scala.tools.nsc.Main -d classes -classpath "$CP" \
    TreeRecursionHarness.scala TreeRecursionHarness2.scala \
    ClusterRecursionHarness.scala TrySoeCheck.scala
java -cp "${CP}classes" TreeRecursionHarness2 4000

Impact

Denial of Service: a small (a few hundred KB), otherwise well-formed MLeap bundle containing a degenerate decision-tree / random-forest / GBT / BisectingKMeans component crashes the loading thread with an uncaught StackOverflowError in any application that embeds MLeap to load user-/client-supplied model bundles (e.g. mleap-serving, a custom scoring service, a model-management pipeline). This directly defeats the Try-based error-handling contract the library advertises for bundle loading β€” callers who correctly match on Success/Failure still see their thread die from an uncaught Error instead.

Dedup check

Searched combust/mleap GitHub issues/PRs for StackOverflow, TreeSerializer, recursion, OOM, security, denial β€” no existing report of this recursion/stack-exhaustion issue. The only related prior security fix found was PR #866 ("Fix - Add zip slip validation"), an unrelated path-traversal fix in the bundle .zip extractor (already merged, confirmed still fixed in current FileUtil.scala). This finding is distinct from the already-filed ArraySerializer OOM report: different files, a different bug class (uncontrolled recursion / stack exhaustion vs. unbounded heap allocation), and a different vulnerable component (tree/decision/TreeSerializer.scala + tree/cluster/NodeSerializer.scala vs. tensor/ArraySerializer.scala).

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