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.

Core ML β€” Two .mlpackage Load-Time Bugs in Apple coremltools: (A) Path Traversal via Unsanitized Manifest.json Item Path, and (B) Native Stack-Overflow DoS via Deeply-Nested Manifest.json

This report bundles two independent, cold, load-time defects in the same .mlpackage parser, both reachable from the default ct.models.MLModel(path) / ct.utils.load_spec(path) APIs. (A) CWE-22 path traversal (existence oracle + out-of-package open + forced-read DoS). (B) CWE-674 uncontrolled recursion β†’ native SIGSEGV that is uncatchable from Python. A deep-dive also confirmed a lower-reachability arbitrary-file-delete variant (Β§9) and refuted a write-side RCE escalation (documented honestly in Β§10).

Target: the Core ML model format (huntr "Core ML") β€” the .mlpackage loader in Apple coremltools. Repository: github.com/apple/coremltools (native module modelpackage/src). Version / pin: confirmed on coremltools 9.0 (PyPI, Linux manylinux wheel β€” the native libmodelpackage.so ships in the Linux wheel; only on-device execution libcoremlpython is macOS-only, and it is not on this load path). Sink present on current main. Vulnerable file: modelpackage/src/ModelPackage.cpp β€” ModelPackageImpl::findItem() and validate(). CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted Directory / Path Traversal). CVSS 3.1: AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:H = 6.1 (Medium) for a victim who loads an attacker-supplied .mlpackage (arbitrary-path existence oracle + forced-read DoS); higher for a service/CI that auto-loads uploaded .mlpackage files (AV:N). Status: POC_EXECUTED β€” traversal, existence-oracle, absolute-path escape, and a forced-read DoS all confirmed on the shipped 9.0 wheel.


1. Summary

A .mlpackage is an on-disk directory containing a Manifest.json that maps model items (by UUID) to on-disk paths under Data/. When a victim loads such a package, the native ModelPackageImpl::findItem() builds the item's filesystem path by joining the package's data directory with the attacker-controlled "path" string straight from Manifest.json, with no containment:

// modelpackage/src/ModelPackage.cpp  (ModelPackageImpl::findItem)
auto path = m_packageDataDirPath / itemInfoEntry->getString(kModelPackageItemInfoPathKey); // "path"

std::filesystem::operator/ performs no normalization, and if the right-hand side is absolute it replaces the base entirely. There is no .. rejection and no realpath-within-root check β€” so a "path" of ../../../../etc/hostname or /dev/zero escapes the package directory. validate() (run in the constructor) does the same unguarded join and then if (!exists(path)) throw, which is itself an arbitrary-path existence oracle. The resolved path flows up to Python load_spec β†’ open(specfile, "rb").read(). Merely loading an untrusted .mlpackage reads an attacker-chosen path β€” exactly the huntr Model File Vulnerabilities threat model.

2. Vulnerable code (modelpackage/src/ModelPackage.cpp, coremltools @ 9.0 / main)

  • Read join (load-critical), findItem(identifier): auto path = m_packageDataDirPath / itemInfoEntry->getString("path"); β€” no sanitization.
  • Existence oracle, validate() (ctor): same join, then if (false == exists(path)) throw "Item does not exist…";.
  • Write twin, getItemPath / createFile: getItemPath = std::filesystem::path(author) / name; β†’ dstPath = m_packageDataDirPath / filePath; std::ofstream(dstPath); β€” the same missing sanitization on the write side (reachable on package round-trip/save; lower reachability, higher impact).
  • Reachability (Python), coremltools/models/utils.py::load_spec:
    if _os.path.isdir(model_path):
        specfile = _ModelPackage(model_path).getRootModel().path()   # native findItem -> escaped path
    with open(specfile, "rb") as f:                                  # arbitrary path OPEN + READ
        spec.ParseFromString(f.read())
    
    Reachable from the public ct.utils.load_spec(dir) and from ct.models.MLModel("x.mlpackage") β€” the latter with zero special flags (the natural default call).

3. Proof of Concept

poc/ runs against the real shipped 9.0 wheel (coreml_traversal_poc.py, full transcript in poc/poc_run.log / poc/evidence/traversal_poc.log). Four malicious packages, each just a Manifest.json + empty Data/:

  1. attacker_traversal.mlpackage β€” out-of-package open (strongest). A sentinel .mlmodel (metadata shortDescription = "SENTINEL_REAL_MODEL_MARKER_1234") is planted at /tmp/outside_secret.mlmodel, outside any package; the manifest "path": "../../../../../../../../tmp/outside_secret.mlmodel". ct.utils.load_spec("attacker_traversal.mlpackage") returns a spec whose shortDescription matches the sentinel β€” proving the native join escaped the package root and opened/parsed a file outside it. Also fires via ct.models.MLModel("attacker_traversal.mlpackage") with no flags.
    • Negative control: deleting the sentinel makes the identical package throw Item does not exist β€” proving a genuine live dereference, not caching.
  2. attacker_exists / attacker_missing β€” existence oracle. "path" β†’ /etc/hostname (exists) loads past validate() (no exception); "path" β†’ a nonexistent path throws a distinct RuntimeError('Item does not exist…'). The differential is a reliable blind arbitrary-path existence probe.
  3. attacker_absolute.mlpackage β€” absolute-path replacement. "path": "/etc/hostname" (no ..) also succeeds, confirming operator/ absolute-RHS-replaces-base.
  4. attacker_dos.mlpackage β€” forced-read DoS. "path": "/dev/zero"; loading it forces an unbounded read of /dev/zero into ParseFromString, exhausting memory (poc/evidence/dos_devzero.log):
    [dos] RESULT=MemoryError : forced unbounded read of /dev/zero via traversal = DoS CONFIRMED
    
python3 poc/coreml_traversal_poc.py    # needs: pip install coremltools  (and `pip uninstall executorch` if an unrelated torch-version import error blocks `import coremltools`)

4. Impact (claim exactly this β€” honestly scoped)

  • (a) Forced open/read of an arbitrary attacker-chosen path outside the package root β€” demonstrated (out-of-package spec load); with a FIFO/device/huge-file target this is an unbounded-read DoS (demonstrated via /dev/zero).
  • (b) Arbitrary-path existence-oracle information disclosure β€” demonstrated (success/RuntimeError differential).
  • (c) Exfiltration of any file that is itself a parseable Model protobuf β€” demonstrated (sentinel).
  • Honest caveat (state verbatim): because the bytes always flow through Model_pb2.ParseFromString(), general plaintext exfiltration of arbitrary non-protobuf files is NOT demonstrated and must not be claimed β€” protobuf's lenient parser typically "succeeds" with a garbage/partial object rather than returning file contents. The sharp real-world scenario is a service/CI that loads uploaded .mlpackage files, where the oracle and forced-read become attacker-observable.

5. Control test

A benign .mlpackage whose manifest "path" stays within Data/ loads normally with no escape β€” the traversal is attributable specifically to the unsanitized manifest path, not to normal model loading.

6. Remediation

In ModelPackageImpl::findItem()/validate()/createFile(), reject any manifest "path" that is absolute or that, after lexically normalizing the join against m_packageDataDirPath, does not remain within it (canonicalize + prefix-check, or reject any .. component).

FINDING B β€” Native stack-overflow (SIGSEGV) via deeply-nested Manifest.json (CWE-674)

CWE: CWE-674 (Uncontrolled Recursion) β†’ CWE-770 stack exhaustion β†’ native SIGSEGV. CVSS 3.1: AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H = 6.1 (Medium) local; higher (AV:N) for a service/CI that ingests uploaded .mlpackage files. Status: POC_EXECUTED β€” MLModel() and load_spec() both crash with exit code -11 (SIGSEGV).

The .mlpackage Manifest.json is parsed by modelpackage/src/utils/JsonMap.cpp:103 (stream >> m_jsonObject, nlohmann::json recursive-descent parser) with no nesting-depth guard, before any schema validation. A Manifest.json whose itemInfoEntries value is a deeply-nested JSON array chain (β‰ˆ150 000 levels, a single ~293 KB file with zero valid model content) exhausts the native stack and crashes the process. Empirically bisected: no crash at depth 104 687 (clean RuntimeError), crash at 106 250 β†’ a ~210 KB file suffices; confirmed genuine with no RLIMIT_AS set.

Unlike Finding A's Python-catchable exceptions, this is an uncatchable native process kill β€” try/except around the load does not protect the caller (poc/evidence/dos_stackoverflow.log):

--- coremltools.models.MLModel(attacker_stackoverflow.mlpackage) ---   child exit code = -11  (SIGSEGV)
--- coremltools.utils.load_spec(attacker_stackoverflow.mlpackage) ---   child exit code = -11  (SIGSEGV)
{ "MLModel_segv": true, "load_spec_segv": true }

Self-contained PoC (builds the malicious package + crashes both entry points): poc/dos/coreml_manifest_stackoverflow_poc.py. Fix: parse the manifest with a depth-limited / iterative (SAX) JSON parse, or reject manifests exceeding a sane nesting bound before stream >> m_jsonObject.

Confidence gate (P1): POC_EXECUTED, REACHED_FROM_INPUT, REACH_FIRST_PARTY_ARTIFACT, SHALLOW_CALL_CHAIN, PIN_HEAD_MATCH β†’ overall 81 (high) β†’ act. Honest ceiling: availability-only DoS (no memory-write / control-flow), not RCE.

7. Dedup / upstream status

Checked NVD/CVE, the GitHub Advisory Database (?query=coremltools β†’ no results, i.e. zero GHSA for coremltools at all), and huntr (apple/coremltools has only two unrelated Insecure Temporary File reports β€” lyutoon 2024, srikanthprathi 2022 β€” no path-traversal/.mlpackage/Manifest.json report). Both Finding A (traversal) and Finding B (stack-overflow) are cold β€” a dedicated search for coremltools/JsonMap/Manifest.json deeply-nested-JSON / stack-overflow / recursion DoS also returned only unrelated libraries (json-smart, jackson, jq, yaml). Not a duplicate. (Re-confirm on the huntr models board immediately before submitting.)

8. Reproduction environment

Linux, Python 3.10, pip install coremltools (9.0). No macOS, no GPU, no build β€” the native libmodelpackage.so ships in the Linux wheel. If import coremltools raises an unrelated executorch/torch-version error, pip uninstall executorch (optional dependency, unused here) unblocks it.

9. Related variant (Finding C) β€” arbitrary file DELETE, CONFIRMED but narrow reachability

The same unsanitized manifest path also reaches a delete sink: ModelPackage.cpp:516-517 β€” auto path = m_packageDataDirPath / itemInfoEntry->getString("path"); std::remove(path.c_str()) β€” reached via removeItem(), which is called by replaceRootModel() (it removes the loaded rootModelIdentifier's entry before installing the replacement). An attacker .mlpackage whose root item's path is /home/victim/.ssh/id_rsa or ../../…/target therefore causes an arbitrary file delete of a pre-existing victim file.

POC_EXECUTED / VERIFIED on the shipped 9.0 wheel (poc/delete/poc_trigger_delete.py, deterministic oracle in poc/evidence/delete_results.json):

victim_exists_before_trigger: true  β†’  victim_exists_after_trigger: false  β†’  delete_confirmed: true
abspath_variant (path="/tmp/…"):     before: true β†’ after: false  (confirmed)
negctrl_loadonly_survived: true      (pure ModelPackage(pkg)+getRootModel, NO edit β†’ victim intact)
negctrl_unrelated_benign_survived + negctrl_intree_root_removed_as_expected: true  (replace-semantics correct, delete is manifest-path-driven not indiscriminate)

Confidence-gate: 77 (high), weakest dim REACHABILITY=60.

Honest reachability caveat (state verbatim): the delete sink is reachable only through the low-level coremltools.libmodelpackage.ModelPackage.replaceRootModel native binding β€” the sole Python-exposed method that hits removeItem(). Stock coremltools never calls it (zero in-tree call sites): MLModel.save() copytrees to a fresh dir, ct.utils.save_spec()/_create_mlpackage() require an empty target and build a fresh package, and MLModel(hostile).save() was empirically refuted (the constructor throws DecodeError parsing the poisoned root path before any delete runs). So there is no natural high-level victim flow β€” the practical victim is third-party tooling that drives the libmodelpackage API directly on untrusted .mlpackage files (inspect/edit/repackage tools). Precondition: the target path must exist (the loader validates every manifest entry resolves on-disk) β€” attacker needs the victim's existence, not its contents. This bounds severity to LOW; it is filed here as a corroborating variant of the same root cause + fix, not as a headline.

10. Scope honesty β€” write-side escalation REFUTED (no RCE)

We explicitly tested whether a hostile .mlpackage can achieve an arbitrary file write with attacker content (→ RCE): it cannot on the realistic load / load→save threat model. The content-writing sinks (ModelPackage.cpp:399 ofstream, :419 std::filesystem::copy, via getItemPath = Data/author/name) are unsanitized, but their destination is built from the direct caller arguments of the public addItem/createFile APIs — they never read name/author/path back out of a loaded manifest. MLModel(attacker).save(out) does shutil.copytree + only hardcoded-constant addItem/setRootModel calls, so a loaded manifest's fields never reach an out-of-tree write; empirically the /tmp write-marker never appeared after load-alone or after save (poc/dos/write_contained_control.py). The write sink would only be attacker-reachable under a different threat model where a first-party tool forwards untrusted name/author into add_item. We state this plainly so the report is not over-claimed: the confirmed impact is Finding A (traversal read/oracle/DoS) + Finding B (native DoS), not RCE.

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