Skip to content

Eval bug: llama-adapter: 5 of 6 accessors crash on a NULL adapter #29704

Closed as not planned

Description

@west-ice-g

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=1

Reproduced 3/3 runs:

run1 EXIT=1
run2 EXIT=1
run3 EXIT=1

The 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

  1. wissamikol97 commented on Sep 30, 2026

    @wissamikol97
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions