use a trampoline to get to the spicy _x functions

This commit is contained in:
JackDoan committed 2026-09-25 13:01:50 -05:00
1 parent 2ff4852551
commit fd29ebdacd
9 files changed
+227 -62

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
// Package msgx calls xnu's private batched socket calls, sendmsg_x and recvmsg_x, through libSystem.
//
// The calls are not public API, so the package never links them: it looks them up with dlsym on first use.
// The package is darwin-only, and on iOS Available is always false, so App Store builds keep to public API.
package msgx
+16
View File
@@ -0,0 +1,16 @@
package msgx
import "golang.org/x/sys/unix"
// Hdr mirrors xnu's struct msghdr_x (bsd/sys/socket_private.h), which the SDK doesn't ship.
// recvmsg_x reports each message's length in Datalen rather than in its return value.
type Hdr struct {
Name *byte
Namelen uint32
Iov *unix.Iovec
Iovlen int32
Control *byte
Controllen uint32
Flags int32
Datalen uint64
}
+77
View File
@@ -0,0 +1,77 @@
//go:build !ios
package msgx
import (
"sync"
"syscall"
"unsafe"
)
// dlsym is public libSystem API, imported the way x/sys/unix imports its libSystem stubs. The local name is
// distinct from runtime/race's dlsym import.
//
//go:cgo_import_dynamic msgx_dlsym dlsym "/usr/lib/libSystem.B.dylib"
// dlsymTrampolineAddr is the address of msgx_darwin.s's jump to dlsym.
var dlsymTrampolineAddr uintptr
// syscall6X calls the C function at fn on the system stack and returns errno when it returns a 64-bit -1.
// It is how x/sys/unix calls libSystem, and the runtime keeps it linkable for that.
//
//go:linkname syscall6X syscall.syscall6X
func syscall6X(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
// rtldDefault is dlfcn.h's RTLD_DEFAULT, ((void *)-2): search every image loaded in the process.
const rtldDefault = ^uintptr(1)
var (
sendmsgXName = []byte("sendmsg_x\x00")
recvmsgXName = []byte("recvmsg_x\x00")
)
type funcs struct {
sendmsgX uintptr
recvmsgX uintptr
}
var resolve = sync.OnceValue(func() funcs {
return funcs{sendmsgX: lookup(sendmsgXName), recvmsgX: lookup(recvmsgXName)}
})
// lookup returns the address of the NUL-terminated C symbol name, or 0 when no loaded image exports it.
func lookup(name []byte) uintptr {
p, _, _ := syscall6X(dlsymTrampolineAddr, rtldDefault, uintptr(unsafe.Pointer(&name[0])), 0, 0, 0, 0)
return p
}
// Available reports whether libSystem exports both sendmsg_x and recvmsg_x. When it is false, Send and Recv
// return ENOSYS.
func Available() bool {
f := resolve()
return f.sendmsgX != 0 && f.recvmsgX != 0
}
// Send passes hdrs to sendmsg_x on fd and returns how many messages the kernel took.
func Send(fd uintptr, hdrs []Hdr, flags int) (int, syscall.Errno) {
return call(resolve().sendmsgX, fd, hdrs, flags)
}
// Recv passes hdrs to recvmsg_x on fd and returns how many messages it filled.
func Recv(fd uintptr, hdrs []Hdr, flags int) (int, syscall.Errno) {
return call(resolve().recvmsgX, fd, hdrs, flags)
}
func call(fn, fd uintptr, hdrs []Hdr, flags int) (int, syscall.Errno) {
if fn == 0 {
return 0, syscall.ENOSYS
}
if len(hdrs) == 0 {
return 0, syscall.EINVAL
}
r, _, errno := syscall6X(fn, fd, uintptr(unsafe.Pointer(&hdrs[0])), uintptr(len(hdrs)), uintptr(flags), 0, 0)
if errno != 0 {
return 0, errno
}
return int(r), 0
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !ios
#include "textflag.h"
// The same trampoline shape x/sys/unix uses for its libSystem stubs; it assembles for amd64 and arm64 alike.
TEXT msgx_dlsym_trampoline<>(SB),NOSPLIT,$0-0
JMP msgx_dlsym(SB)
GLOBL ·dlsymTrampolineAddr(SB), RODATA, $8
DATA ·dlsymTrampolineAddr(SB)/8, $msgx_dlsym_trampoline<>(SB)
+69
View File
@@ -0,0 +1,69 @@
//go:build !ios
package msgx
import (
"bytes"
"syscall"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)
// TestAvailable pins that dlsym finds both calls in this macOS's libSystem, so the batched paths are live.
func TestAvailable(t *testing.T) {
assert.True(t, Available())
}
// TestSendRecvRoundTrip pins the libSystem calling path end to end: a batch goes out through sendmsg_x in one
// call and comes back through recvmsg_x in one call, each message whole, in order, with Datalen set.
func TestSendRecvRoundTrip(t *testing.T) {
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_DGRAM, 0)
require.NoError(t, err)
t.Cleanup(func() {
_ = unix.Close(fds[0])
_ = unix.Close(fds[1])
})
// A dropped datagram fails the receive instead of hanging it.
require.NoError(t, unix.SetsockoptTimeval(fds[1], unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: 2}))
const n = 5
var want [n][]byte
var iovs [n]unix.Iovec
hdrs := make([]Hdr, n)
for i := range n {
want[i] = bytes.Repeat([]byte{byte(i + 1)}, 10+i)
iovs[i] = unix.Iovec{Base: &want[i][0]}
iovs[i].SetLen(len(want[i]))
hdrs[i] = Hdr{Iov: &iovs[i], Iovlen: 1}
}
sent, errno := Send(uintptr(fds[0]), hdrs, 0)
require.Zero(t, errno)
require.Equal(t, n, sent)
var bufs [n + 1][64]byte
var riovs [n + 1]unix.Iovec
rhdrs := make([]Hdr, n+1)
for i := range rhdrs {
riovs[i] = unix.Iovec{Base: &bufs[i][0]}
riovs[i].SetLen(len(bufs[i]))
rhdrs[i] = Hdr{Iov: &riovs[i], Iovlen: 1}
}
got, errno := Recv(uintptr(fds[1]), rhdrs, 0)
require.Zero(t, errno)
require.Equal(t, n, got)
for i := range n {
l := int(rhdrs[i].Datalen)
assert.Equal(t, want[i], bufs[i][:l], "message %d", i)
}
}
// TestEmptyBatch pins that an empty batch is refused before it reaches the kernel.
func TestEmptyBatch(t *testing.T) {
_, errno := Send(0, nil, 0)
assert.Equal(t, syscall.EINVAL, errno)
_, errno = Recv(0, nil, 0)
assert.Equal(t, syscall.EINVAL, errno)
}
+12
View File
@@ -0,0 +1,12 @@
package msgx
import "syscall"
// Available is always false on iOS, so App Store builds never look up private symbols.
func Available() bool { return false }
// Send always returns ENOSYS on iOS.
func Send(uintptr, []Hdr, int) (int, syscall.Errno) { return 0, syscall.ENOSYS }
// Recv always returns ENOSYS on iOS.
func Recv(uintptr, []Hdr, int) (int, syscall.Errno) { return 0, syscall.ENOSYS }