Repository navigation
Eval bug: llama-adapter: 5 of 6 accessors crash on a NULL adapter #29704
Description
Name and Version
./llama-cli --version
version: 0.5.0-dev (build 11277, commit ac52310c4)
built with GNU 15.2.0 for Linux x86_64
Operating systems
Linux
GGML backends
CPU
Hardware
Model name: Intel(R) Core(TM) i7-14650HX
Architecture: x86_64
CPU(s): 4 (2 sockets x 2 cores, 1 thread/core)
Hypervisor: VMware (fully virtualized)
Compiler: gcc 15.2.0
Models
Qwen2.5-0.5B-Instruct Q2_K GGUF
sha256 9ee36184e616dfc76df4f5dd66f908dbde6979524ae36e6cefb67f532f798cb8
Any model works. The reproducer passes a non-existent adapter path to
llama_adapter_lora_init, which returns NULL before it touches model tensors, so
the model is only needed to build a context.
Problem description & steps to reproduce
Summary
llama_adapter_lora_init() is documented to return NULL on failure, and two of the
six adapter accessors explicitly handle a NULL adapter. The other five dereference
it without a check: the four meta_* accessors segfault, and
llama_adapter_get_alora_invocation_tokens aborts through GGML_ASSERT.
A caller that follows the documented contract - check the return value of
llama_adapter_lora_init, then pass it to the accessors - crashes in 5 of those 6
cases.
Environment
- llama.cpp: 19e28a2 (version b11277)
- OS: Ubuntu 26.04.1 LTS, x86_64, kernel 7.0.0-34-generic
- CPU: Intel(R) Core(TM) i7-14650HX (VMware guest), 4 logical CPUs
- Compiler: gcc 15.2.0
- Model: Qwen2.5-0.5B-Instruct Q2_K GGUF
sha256 9ee36184e616dfc76df4f5dd66f908dbde6979524ae36e6cefb67f532f798cb8
Build - Release, asserts off:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -j$(nproc)
Steps to reproduce
The reproducer is pasted inline below (3635 bytes) because GitHub does not accept
a .cpp attachment. It forks once per accessor and reports the result of each. Note
the model argument uses the --model=PATH form:
g++ -O1 -g -std=c++17 repro_min.cpp \
-I<llama.cpp> -I<llama.cpp>/include -I<llama.cpp>/ggml/include \
-L<llama.cpp>/build/bin -lllama -o /tmp/v20
/tmp/v20 --model=qwen2.5-0.5b-instruct-q2_k.gguf
// Minimal repro for CLUSTER-6 adapter finding (FINDING 20, EXPANDED):
// llama_adapter_lora_init() is documented to return NULL on failure (a legitimate,
// expected input), yet 5 of the 6 adapter accessors dereference the adapter without a
// NULL check -> SIGABRT (debug/ASan) or SEGV (release):
// llama_adapter_meta_count (llama-adapter.cpp:449) CRASH
// llama_adapter_meta_key_by_index (llama-adapter.cpp:453) CRASH
// llama_adapter_meta_val_str (llama-adapter.cpp:438) CRASH
// llama_adapter_meta_val_str_by_index (llama-adapter.cpp:465) CRASH
// llama_adapter_get_alora_invocation_tokens(llama-adapter.cpp:498) CRASH (GGML_ASSERT)
// while two siblings DO NULL-guard, proving the API intends NULL tolerance:
// llama_adapter_lora_free (llama-adapter.cpp:470) SURVIVES (if null return)
// llama_adapter_get_alora_n_invocation_tokens(llama-adapter.cpp:490) SURVIVES (if null return 0)
//
// Each accessor runs in a child process (fork+exec) so one abort doesn't mask the rest.
#include "llama.h"
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
#include <cstring>
enum { COUNT=0, KEY, VALSTR, VALSTR_I, ALORA_N, ALORA_T, NFN };
static int run_one(int which, const char * model){
llama_model_params mp = llama_model_default_params();
mp.vocab_only = true;
struct llama_model * m = llama_model_load_from_file(model, mp);
if(!m) return 3;
struct llama_adapter_lora * a = llama_adapter_lora_init(m, "/nonexistent/lora.gguf");
char buf[64];
switch(which){
case COUNT: (void)llama_adapter_meta_count(a); break;
case KEY: (void)llama_adapter_meta_key_by_index(a, 0, buf, sizeof buf); break;
case VALSTR: (void)llama_adapter_meta_val_str(a, "key", buf, sizeof buf); break;
case VALSTR_I: (void)llama_adapter_meta_val_str_by_index(a, 0, buf, sizeof buf); break;
case ALORA_N: (void)llama_adapter_get_alora_n_invocation_tokens(a); break;
case ALORA_T: (void)llama_adapter_get_alora_invocation_tokens(a); break;
}
llama_model_free(m);
return 0;
}
int main(int argc, char **argv){
for(int i=1;i<argc;i++){
if(!strncmp(argv[i],"--which=",8)){
int which=atoi(argv[i]+8);
const char * model=nullptr;
for(int j=1;j<argc;j++) if(!strncmp(argv[j],"--model=",8)) model=argv[j]+8;
if(!model) return 2;
return run_one(which, model);
}
}
const char * model=nullptr;
for(int i=1;i<argc;i++) if(!strncmp(argv[i],"--model=",8)) model=argv[i]+8;
if(!model){ fprintf(stderr,"usage: %s --model PATH\n", argv[0]); return 2; }
const char * names[NFN] = {
"meta_count","meta_key_by_index","meta_val_str","meta_val_str_by_index",
"get_alora_n_invocation_tokens","get_alora_invocation_tokens"
};
int any=0;
int n_crash=0;
for(int w=0; w<NFN; w++){
pid_t p=fork();
if(p==0){
char wbuf[16], mbuf[1024];
snprintf(wbuf,sizeof wbuf,"--which=%d",w);
snprintf(mbuf,sizeof mbuf,"--model=%s",model);
execl(argv[0],argv[0],wbuf,mbuf,(char*)nullptr);
_exit(99);
}
int st=0; waitpid(p,&st,0);
int code=WIFEXITED(st)?WEXITSTATUS(st):(WIFSIGNALED(st)?128+WTERMSIG(st):-1);
fprintf(stderr,"%-8s %-28s (code=%d)\n",(code==0?"SURVIVED":"CRASH"),names[w],code);
if(code!=0){ any=1; n_crash++; }
}
fprintf(stderr,"\n%d/6 accessors crash on NULL adapter\n", n_crash);
return any?1:0;
}
sha256 of the file above:
c1bc207fad160f7ae4194d17874e1d8e4edfd938b2a2cfc5bae2f383b24191a5
Observed result
CRASH meta_count (code=139)
CRASH meta_key_by_index (code=139)
CRASH meta_val_str (code=139)
CRASH meta_val_str_by_index (code=139)
SURVIVED get_alora_n_invocation_tokens (code=0)
CRASH get_alora_invocation_tokens (code=134)
5/6 accessors crash on NULL adapter
exit code: 1
The four meta_* accessors are SIGSEGV; get_alora_invocation_tokens is a GGML_ASSERT
abort. Deterministic: 3/3 runs.
The abort path is visible in the backtrace:
#4 ggml_abort () libggml-base.so.0
#5 llama_adapter_get_alora_invocation_tokens () libllama.so.0
#6 run_one (which=5, ...) repro_min.cpp:37
#7 main (argc=..., argv=...) repro_min.cpp:50
Root cause
llama_adapter_lora_init returns NULL on failure (src/llama-adapter.cpp):
425: llama_adapter_lora * llama_adapter_lora_init(llama_model * model, const char * path_lora) {
428: FILE * file = ggml_fopen(path_lora, "rb");
429: if (!file) {
430: LLAMA_LOG_ERROR("%s: failed to open '%s': %s\n", __func__, path_lora, strerror(errno));
431: return nullptr;
432: }
...
457: return nullptr;
Five accessors then use the pointer directly (line numbers in the current source;
the file shifts by roughly 22 lines from the older numbering):
460: int32_t llama_adapter_meta_val_str(const llama_adapter_lora * adapter, const char * key, char * buf, size_t buf_size) {
461: const auto & it = adapter->gguf_kv.find(key); // NULL -> crash
471: int32_t llama_adapter_meta_count(const llama_adapter_lora * adapter) {
472: return (int)adapter->gguf_kv.size(); // NULL -> crash
475: int32_t llama_adapter_meta_key_by_index(const llama_adapter_lora * adapter, int i, char * buf, size_t buf_size) {
476: if (i < 0 || i >= (int)adapter->gguf_kv.size()) { // NULL -> crash
487: int32_t llama_adapter_meta_val_str_by_index(const llama_adapter_lora * adapter, int32_t i, char * buf, size_t buf_size) {
488: if (i < 0 || i >= (int)adapter->gguf_kv.size()) { // NULL -> crash
519: const llama_token * llama_adapter_get_alora_invocation_tokens(const llama_adapter_lora * adapter) {
// GGML_ASSERT on the adapter
Two siblings in the same file do guard it, which shows NULL tolerance is the
intended contract:
499: void llama_adapter_lora_free(llama_adapter_lora * adapter) {
500: if (adapter == nullptr) {
501: return;
502: }
512: uint64_t llama_adapter_get_alora_n_invocation_tokens(const struct llama_adapter_lora * adapter) {
513: if (!adapter) {
return 0;
Impact
A caller that checks the documented NULL return of llama_adapter_lora_init and
then calls any of these five accessors loses the process instead of getting an
error. This is a robustness problem in a public C API: two accessors accept NULL,
five do not, and nothing in the API tells the caller which is which.
Suggested fix
Add a NULL check to the five unguarded accessors, mirroring
llama_adapter_lora_free and llama_adapter_get_alora_n_invocation_tokens - return
-1 / 0 / nullptr as appropriate.
First Bad Commit
No response
Relevant log output
Logs
$ /tmp/v20 --model=qwen2.5-0.5b-instruct-q2_k.gguf
CRASH meta_count (code=139)
CRASH meta_key_by_index (code=139)
CRASH meta_val_str (code=139)
CRASH meta_val_str_by_index (code=139)
SURVIVED get_alora_n_invocation_tokens (code=0)
CRASH get_alora_invocation_tokens (code=134)
5/6 accessors crash on NULL adapter
EXIT=1Reproduced 3/3 runs:
run1 EXIT=1
run2 EXIT=1
run3 EXIT=1The only two NULL checks in the file:
$ grep -n 'if (!adapter)\|if (adapter == nullptr)' src/llama-adapter.cpp
500: if (adapter == nullptr) {
513: if (!adapter) {
Activity
wissamikol97 commentedon Sep 30, 2026