rdma: pin session strings passed to the RC cgo ABI

The Prepare and ReadyTransfer wrappers embed string views built from
Go heap strings inside request structs passed to C by pointer. The
cgo pointer check rejects such requests when the string data is an
unpinned Go heap pointer, so any live PREPARE or READY call with
header-derived strings panicked at the call boundary and the route
returned a 500. Constant strings passed the check because their data
lives in read-only static storage, which is why standalone callers
kept working while the gateway did not.

Pin the string bytes with runtime.Pinner for the duration of the cgo
call and drop the now redundant KeepAlive calls in those two
wrappers. The other string-taking wrappers pass rc_str_in by value
and are unaffected.

Also add a deviceless cgo boundary regression test that calls the
real Prepare wrapper with heap-backed interior-pointer strings and an
invalid opcode, so C returns from argument validation before the
server handle is touched.
This commit is contained in:
Jihyeon Gim
2026-09-05 21:59:00 +09:00
parent 09bbe9ccbd
commit 34a3e4152e
2 changed files with 82 additions and 11 deletions
+58
View File
@@ -0,0 +1,58 @@
//go:build linux && cgo
package rcserver
import (
"errors"
"strings"
"testing"
"unsafe"
)
// heapSubstring returns a substring whose backing array is heap-allocated
// and whose data pointer is an interior pointer, mirroring how header
// strings reach the binding from the gateway routes.
func heapSubstring(value string) string {
const prefix = "prefix:"
backing := strings.Clone(prefix + value + ":suffix")
return backing[len(prefix) : len(prefix)+len(value)]
}
// TestPrepareHeapStringsReachCValidation calls the real Prepare wrapper
// with heap-backed strings. The request uses an invalid opcode so the C
// entrypoint returns RC_E_ARG from its argument validation before the
// server handle is dereferenced; the dummy handle below is never touched.
// Before the pinnedStrIn fix this call panics at the cgo pointer check;
// after it, the C argument validation runs and the error surfaces.
func TestPrepareHeapStringsReachCValidation(t *testing.T) {
const literalTarget = "/bucket1/obj1"
heapTarget := heapSubstring(literalTarget)
heapToken := heapSubstring(strings.Repeat("0", 88))
for _, tc := range []struct {
name, target, token string
}{
{"literal_control", literalTarget, ""},
{"heap_target", heapTarget, ""},
{"heap_token", literalTarget, heapToken},
{"both_heap", heapTarget, heapToken},
{"empty_control", "", ""},
} {
t.Run(tc.name, func(t *testing.T) {
var svc RCSvc
// Test-only opaque sentinel, not an initialized rc_server.
// Op=255 returns in C argument validation before the
// server pointer is dereferenced.
dummy := new(uint64)
*(*unsafe.Pointer)(unsafe.Pointer(&svc.srv)) = unsafe.Pointer(dummy)
resp, err := svc.Prepare(PrepareRequest{
Op: 255,
Size: 1,
Target: tc.target,
ClientToken: tc.token,
})
if resp != nil || !errors.Is(err, ErrArg) {
t.Fatalf("Prepare = (%v, %v), want (nil, ErrArg)", resp, err)
}
})
}
}
+24 -11
View File
@@ -371,18 +371,33 @@ func statusError(rc C.int) error {
}
}
// strIn borrows string bytes for a synchronous rc_str_in value argument.
// For a string view embedded in a request struct passed by pointer, use
// pinnedStrIn instead: the cgo pointer check rejects request structs that
// contain unpinned Go string data. C must not retain or modify the borrowed
// bytes.
func strIn(s string) C.rc_str_in {
if s == "" {
return C.rc_str_in{ptr: nil, len: 0}
}
// The pointer must stay alive across the cgo call; every
// wrapper keeps the string referenced until after the call.
return C.rc_str_in{
ptr: (*C.char)(unsafe.Pointer(unsafe.StringData(s))),
len: C.uint32_t(len(s)),
}
}
// pinnedStrIn builds a strIn view and pins the string bytes for the
// duration of the enclosing cgo call. The caller owns the Pinner and
// must defer Unpin before the first pinnedStrIn call. Pinning an
// interior pointer pins the whole backing allocation.
func pinnedStrIn(s string, pins *runtime.Pinner) C.rc_str_in {
in := strIn(s)
if in.ptr != nil {
pins.Pin(unsafe.Pointer(in.ptr))
}
return in
}
// cStr reads a fixed C char array of length n into a Go string.
func cStr(p *C.char, n C.uint32_t) string {
if n == 0 {
@@ -396,14 +411,13 @@ func (s *RCSvc) Prepare(req PrepareRequest) (*PrepareResponse, error) {
if s.srv == nil {
return nil, errors.New("rcserver: service closed")
}
var pins runtime.Pinner
defer pins.Unpin()
var principal C.rc_principal_id
copy((*[32]byte)(unsafe.Pointer(&principal.id[0]))[:], req.Principal[:])
var token C.rc_str_in
if req.ClientToken != "" {
token = strIn(req.ClientToken)
}
target := strIn(req.Target)
token := pinnedStrIn(req.ClientToken, &pins)
target := pinnedStrIn(req.Target, &pins)
creq := C.rc_prepare_req{
principal: principal,
op: C.uint8_t(req.Op),
@@ -415,8 +429,6 @@ func (s *RCSvc) Prepare(req PrepareRequest) (*PrepareResponse, error) {
client_token: token,
}
var cresp C.rc_prepare_resp
runtime.KeepAlive(req.ClientToken)
runtime.KeepAlive(req.Target)
rc := C.rc_prepare(s.srv, &creq, &cresp)
if rc != C.RC_OK {
return nil, statusError(rc)
@@ -510,9 +522,11 @@ func (s *RCSvc) ReadyTransfer(req ReadyRequest) (*ReadyResponse, error) {
if s.srv == nil {
return nil, errors.New("rcserver: service closed")
}
var pins runtime.Pinner
defer pins.Unpin()
var principal C.rc_principal_id
copy((*[32]byte)(unsafe.Pointer(&principal.id[0]))[:], req.Principal[:])
in := strIn(req.SessionID)
in := pinnedStrIn(req.SessionID, &pins)
creq := C.rc_ready_req{
principal: principal,
session_id: in,
@@ -523,7 +537,6 @@ func (s *RCSvc) ReadyTransfer(req ReadyRequest) (*ReadyResponse, error) {
}
var cresp C.rc_ready_resp
rc := C.rc_ready_transfer(s.srv, &creq, &cresp)
runtime.KeepAlive(req.SessionID)
if rc != C.RC_OK {
return nil, statusError(rc)
}