| # CoreML (apple/coremltools) MILBlob Span::Slice integer-overflow bounds-check bypass -> OOB read |
|
|
| PoC package for a heap/mapped out-of-bounds read in the CoreML weight-blob parser |
| (`MILBlob`) shipped in `apple/coremltools`. A crafted `.mlmodelc` / `weights.bin` |
| weight-blob file with attacker-controlled `blob_metadata.offset` and |
| `blob_metadata.sizeInBytes` (both `uint64`) triggers a 64-bit integer overflow in |
| the bounds check inside `Span<T>::Slice(index, size)`, returning a `Span` whose |
| `Size()` is enormous (here `2^64 - 50`). A consumer reading the weight bytes then |
| reads far past the mapped file. |
|
|
| ## Files |
|
|
| - `weights.bin` - the malicious CoreML weight-blob file (384 bytes) |
| - `craft.py` - script that generates `weights.bin` (documents the byte layout) |
| - `harness.cpp` - ASan harness that drives the REAL `StorageReader` / `MMapFileReader` / `Span` |
| - `build.sh` - build command (clang++ + ASan) |
| - `asan_crash.txt` - the captured AddressSanitizer crash trace |
|
|
| ## Reproduce |
|
|
| git clone --filter=blob:none --sparse https://github.com/apple/coremltools.git |
| cd coremltools |
| git sparse-checkout set mlmodel/src/MILBlob |
| # commit pinned for this PoC: 3fb88603a6af3d12bf72ca2dcc0a3724a631317b |
| |
| clang++ -std=c++17 -fsanitize=address -g -O0 -Imlmodel/src \ |
| harness.cpp \ |
| mlmodel/src/MILBlob/Blob/StorageReader.cpp \ |
| mlmodel/src/MILBlob/Blob/MMapFileReader.cpp \ |
| mlmodel/src/MILBlob/Blob/MMapFileReaderFactory.cpp \ |
| -o harness |
| |
| python3 craft.py |
| ASAN_OPTIONS=detect_leaks=0 ./harness weights.bin |
| |
| ## Root cause |
|
|
| `mlmodel/src/MILBlob/Util/Span.hpp` (~line 325): |
|
|
| Span<T> Slice(size_t index, size_t size) const |
| { |
| MILVerifyIsTrue(size > 0 && index < Size() && index + size <= Size(), std::range_error, "index out of bounds"); |
| return Span<T>(Data() + index, size); |
| } |
| |
| `index + size` is an unchecked 64-bit addition. With `index = 100` and |
| `size = 2^64 - 50`, `index + size` wraps to `50`, which is `<= Size()`, so the |
| check passes and an oversized `Span` is returned. |
|
|
| Both operands are attacker-controlled: `StorageReader::Impl::GetRawDataView` |
| (`StorageReader.cpp`) calls `m_reader->ReadData(metadata.offset, metadata.sizeInBytes)`, |
| and `MMapFileReader::ReadData` (`MMapFileReader.cpp`) forwards straight to |
| `m_dataSpan.Slice(offset, length)`. The only validation on the metadata is the |
| `sentinel == 0xDEADBEEF` check in `GetMetadata` - `offset` and `sizeInBytes` are |
| never range-checked before reaching `Slice`. |
|
|
| ## Source availability |
|
|
| 100% open source, BSD-3-Clause: https://github.com/apple/coremltools |
| Vulnerable file: `mlmodel/src/MILBlob/Util/Span.hpp`. |
|
|