Summary
read_safetensors_file (src/model_io/safetensors_io.cpp) accepts a crafted .safetensors file whose tensor shape overflows the element-count product to 0, leading to a NULL pointer dereference (denial of service) when the tensor is later accessed.
Root cause
Each tensor dimension is parsed with shape[i].get<int64_t>() and stored into ne[] with no range or overflow check. TensorStorage::nelements() (src/model_io/tensor_storage.h) multiplies the dims as a signed int64 product, and the reader uses nbytes() (derived from that product) for its only size check:
tensor_size_ok = (tensor_storage.nbytes() == tensor_data_size); // safetensors_io.cpp:346
// tensor_data_size = end - begin, bounded against the file at :255
shape = [4294967296, 4294967296] → int64 product 2^32 · 2^32 = 2^64 ≡ 0 → nbytes() == 0. Paired with an empty in-bounds data range (data_offsets [x, x]), 0 == 0 passes the check and the tensor is accepted with huge ne[] but nbytes() == 0. Negative dimensions are also accepted at parse time and reach the int64 product.
Impact
ggml_new_tensor computes its allocation from the same ne[] and also wraps to 0, returning a tensor with data == NULL. The first consumer access dereferences NULL → crash. A malicious model file causes a denial of service.
Reproduction
import struct, json
m = {"t":{"dtype":"I8","shape":[4294967296,4294967296],"data_offsets":[0,0]}}
j = json.dumps(m, separators=(",",":")).encode()
open("poc.safetensors","wb").write(struct.pack("<Q", len(j)) + j)
# load -> read_safetensors_file ACCEPTS; ggml_new_tensor -> data=NULL -> SEGV on access
Suggested fix
Validate each parsed dimension (reject <= 0) and compute the element-count product with __builtin_mul_overflow, rejecting the tensor up front on a non-positive dim or overflow. PR: #1875.
Summary
read_safetensors_file(src/model_io/safetensors_io.cpp) accepts a crafted.safetensorsfile whose tensor shape overflows the element-count product to 0, leading to a NULL pointer dereference (denial of service) when the tensor is later accessed.Root cause
Each tensor dimension is parsed with
shape[i].get<int64_t>()and stored intone[]with no range or overflow check.TensorStorage::nelements()(src/model_io/tensor_storage.h) multiplies the dims as a signedint64product, and the reader usesnbytes()(derived from that product) for its only size check:shape = [4294967296, 4294967296]→int64product2^32 · 2^32 = 2^64 ≡ 0→nbytes() == 0. Paired with an empty in-bounds data range (data_offsets [x, x]),0 == 0passes the check and the tensor is accepted with hugene[]butnbytes() == 0. Negative dimensions are also accepted at parse time and reach theint64product.Impact
ggml_new_tensorcomputes its allocation from the samene[]and also wraps to 0, returning a tensor withdata == NULL. The first consumer access dereferences NULL → crash. A malicious model file causes a denial of service.Reproduction
Suggested fix
Validate each parsed dimension (reject
<= 0) and compute the element-count product with__builtin_mul_overflow, rejecting the tensor up front on a non-positive dim or overflow. PR: #1875.