ybgwon96/sdcpp-pickle-5dim-heap-oob-poc
PoC — stable-diffusion.cpp pickle 5-dimension smuggling → heap OOB write
Proof-of-concept model file for a huntr Model File Vulnerabilities report against `leejet/stable-diffusion.cpp` @ 3590aa8.
This repository contains a deliberately crafted PyTorch checkpoint for authorized security research (responsible disclosure via huntr). It is not a usable model.
The bug (summary)
stable-diffusion.cpp defines SD_MAX_DIMS = 5 (src/model_io/tensor_storage.h) while ggml tensors have at most 4 dimensions. Its pickle loader accepts a 5-element shape tuple, and TensorStorage::nelements() multiplies all five ne[] slots — but the destination-shape check only compares ne[0..3]. An attacker declares a tensor whose first four dims match a real model weight (passing the shape check) while a bogus 5th dim inflates the element count. On the dtype-mismatch load path (src/model_loader.cpp:1123-1165, which lacks the size assertion that guards the dtype-match path), the inflated element count is used as the copy length into a destination buffer sized for only four dimensions → heap out-of-bounds write (convert_tensor, src/model_loader.cpp:176/:1165).
Dynamically validated (parse level)
poc_pickle_5dim.pt maps model.diffusion_model.input_blocks.0.0.weight to a genuine 5-D tensor (8, 320, 4, 3, 3). Driven through sd.cpp's real read_torch_legacy_file():
read_torch_legacy_file -> ok=1 tensors=1
tensor[0] name='...input_blocks.0.0.weight' n_dims=5 ne=[3,3,4,320,8] nelements=92160 type=0
>>> SMUGGLED: n_dims=5 > 4; ne[4]=8 inflates nelements past the 4-dim destinationnelements = 92160 = 3·3·4·320·8 — 8× a real 4-D destination ([3,3,4,320], 11520). See VALIDATED.md.
Files
Disclosure
Reported privately via huntr. Suggested fix: reject shapes with more real dimensions than ggml supports (or require ne[i]==1 for i>=4), and validate nbytes() against ggml_nbytes(dst) on all copy paths (including the dtype-mismatch branch), not only the dtype-match branch.
