Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 27 additions & 21 deletions internal/cuda/purego_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,47 @@ import (
_ "unsafe"
)

// On Darwin, we use syscall.syscall6 and syscall.syscall9 to call C
// library functions via libSystem. These do NOT go through runtime.cgocall
// so they are true zero-CGo calls.
// On Darwin, we call C library functions in libSystem via syscall.syscall6
// and syscall.syscall9. These do NOT go through runtime.cgocall, so they are
// true zero-CGo calls.
//
// For dlopen/dlsym, we import them dynamically from libSystem.B.dylib
// and call through assembly trampolines (defined in purego_darwin_amd64.s
// or purego_darwin_arm64.s).
// dlopen/dlsym/dlclose/dlerror are imported dynamically from libSystem.B.dylib
// and reached through assembly JMP trampolines. Crucially, syscall.syscall6
// must be handed the raw ABI0 entry point of each trampoline. We obtain that
// address from an assembly DATA directive (see purego_darwin_{amd64,arm64}.s),
// mirroring the golang.org/x/sys/unix darwin idiom.
//
// The previous implementation derived the trampoline address by taking the
// trampoline as a func() value and double-dereferencing it (funcPC). On
// darwin/amd64 that yields the compiler-generated ABIInternal wrapper, not the
// ABI0 trampoline, so syscall.syscall6 transferred control to a bad PC and the
// process SIGSEGV'd inside dlopen during package init (zerfoo T137.1, #171).

//go:linkname syscall_syscall6 syscall.syscall6
func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err uintptr)

//go:linkname syscall_syscall9 syscall.syscall9
func syscall_syscall9(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9 uintptr) (r1, r2 uintptr, err uintptr)

// Assembly trampolines for dlopen/dlsym/dlclose/dlerror.
// These are JMP stubs to the dynamically imported symbols.
func libc_dlopen_trampoline()
func libc_dlsym_trampoline()
func libc_dlclose_trampoline()
func libc_dlerror_trampoline()
// Raw ABI0 entry points of the assembly JMP trampolines. Each is populated by
// a DATA directive in the matching per-arch assembly file. These must be the
// trampoline addresses themselves, not func-value PCs.
var (
libc_dlopen_trampoline_addr uintptr
libc_dlsym_trampoline_addr uintptr
libc_dlclose_trampoline_addr uintptr
libc_dlerror_trampoline_addr uintptr
)

//go:cgo_import_dynamic libc_dlopen dlopen "/usr/lib/libSystem.B.dylib"
//go:cgo_import_dynamic libc_dlsym dlsym "/usr/lib/libSystem.B.dylib"
//go:cgo_import_dynamic libc_dlclose dlclose "/usr/lib/libSystem.B.dylib"
//go:cgo_import_dynamic libc_dlerror dlerror "/usr/lib/libSystem.B.dylib"

//go:nosplit
func funcPC(fn func()) uintptr {
return **(**uintptr)(unsafe.Pointer(&fn))
}

func dlopenImpl(path string, mode int) uintptr {
p := append([]byte(path), 0)
r1, _, _ := syscall_syscall6(
funcPC(libc_dlopen_trampoline),
libc_dlopen_trampoline_addr,
uintptr(unsafe.Pointer(&p[0])),
uintptr(mode), 0, 0, 0, 0,
)
Expand All @@ -51,7 +57,7 @@ func dlopenImpl(path string, mode int) uintptr {
func dlsymImpl(handle uintptr, name string) uintptr {
n := append([]byte(name), 0)
r1, _, _ := syscall_syscall6(
funcPC(libc_dlsym_trampoline),
libc_dlsym_trampoline_addr,
handle,
uintptr(unsafe.Pointer(&n[0])),
0, 0, 0, 0,
Expand All @@ -61,15 +67,15 @@ func dlsymImpl(handle uintptr, name string) uintptr {

func dlcloseImpl(handle uintptr) int {
r1, _, _ := syscall_syscall6(
funcPC(libc_dlclose_trampoline),
libc_dlclose_trampoline_addr,
handle, 0, 0, 0, 0, 0,
)
return int(r1)
}

func dlerrorImpl() string {
r1, _, _ := syscall_syscall6(
funcPC(libc_dlerror_trampoline),
libc_dlerror_trampoline_addr,
0, 0, 0, 0, 0, 0,
)
if r1 == 0 {
Expand Down
22 changes: 16 additions & 6 deletions internal/cuda/purego_darwin_amd64.s
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,27 @@

// Assembly trampolines for dynamically imported C library functions.
// Each trampoline jumps to the corresponding symbol resolved by
// //go:cgo_import_dynamic. This is the standard Go pattern for
// calling C library functions without CGo (used by syscall package).
// //go:cgo_import_dynamic. This mirrors the golang.org/x/sys/unix darwin
// idiom: the trampoline itself is a file-local (<>) symbol, and its raw
// ABI0 address is exported to Go through a DATA directive so that
// syscall.syscall6 receives the true entry point (not a func-value PC).

TEXT ·libc_dlopen_trampoline(SB),NOSPLIT,$0-0
TEXT libc_dlopen_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_dlopen(SB)
GLOBL ·libc_dlopen_trampoline_addr(SB), RODATA, $8
DATA ·libc_dlopen_trampoline_addr(SB)/8, $libc_dlopen_trampoline<>(SB)

TEXT ·libc_dlsym_trampoline(SB),NOSPLIT,$0-0
TEXT libc_dlsym_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_dlsym(SB)
GLOBL ·libc_dlsym_trampoline_addr(SB), RODATA, $8
DATA ·libc_dlsym_trampoline_addr(SB)/8, $libc_dlsym_trampoline<>(SB)

TEXT ·libc_dlclose_trampoline(SB),NOSPLIT,$0-0
TEXT libc_dlclose_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_dlclose(SB)
GLOBL ·libc_dlclose_trampoline_addr(SB), RODATA, $8
DATA ·libc_dlclose_trampoline_addr(SB)/8, $libc_dlclose_trampoline<>(SB)

TEXT ·libc_dlerror_trampoline(SB),NOSPLIT,$0-0
TEXT libc_dlerror_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_dlerror(SB)
GLOBL ·libc_dlerror_trampoline_addr(SB), RODATA, $8
DATA ·libc_dlerror_trampoline_addr(SB)/8, $libc_dlerror_trampoline<>(SB)
22 changes: 16 additions & 6 deletions internal/cuda/purego_darwin_arm64.s
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,27 @@

// Assembly trampolines for dynamically imported C library functions.
// Each trampoline jumps to the corresponding symbol resolved by
// //go:cgo_import_dynamic. This is the standard Go pattern for
// calling C library functions without CGo (used by syscall package).
// //go:cgo_import_dynamic. This mirrors the golang.org/x/sys/unix darwin
// idiom: the trampoline itself is a file-local (<>) symbol, and its raw
// ABI0 address is exported to Go through a DATA directive so that
// syscall.syscall6 receives the true entry point (not a func-value PC).

TEXT ·libc_dlopen_trampoline(SB),NOSPLIT,$0-0
TEXT libc_dlopen_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_dlopen(SB)
GLOBL ·libc_dlopen_trampoline_addr(SB), RODATA, $8
DATA ·libc_dlopen_trampoline_addr(SB)/8, $libc_dlopen_trampoline<>(SB)

TEXT ·libc_dlsym_trampoline(SB),NOSPLIT,$0-0
TEXT libc_dlsym_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_dlsym(SB)
GLOBL ·libc_dlsym_trampoline_addr(SB), RODATA, $8
DATA ·libc_dlsym_trampoline_addr(SB)/8, $libc_dlsym_trampoline<>(SB)

TEXT ·libc_dlclose_trampoline(SB),NOSPLIT,$0-0
TEXT libc_dlclose_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_dlclose(SB)
GLOBL ·libc_dlclose_trampoline_addr(SB), RODATA, $8
DATA ·libc_dlclose_trampoline_addr(SB)/8, $libc_dlclose_trampoline<>(SB)

TEXT ·libc_dlerror_trampoline(SB),NOSPLIT,$0-0
TEXT libc_dlerror_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_dlerror(SB)
GLOBL ·libc_dlerror_trampoline_addr(SB), RODATA, $8
DATA ·libc_dlerror_trampoline_addr(SB)/8, $libc_dlerror_trampoline<>(SB)
67 changes: 67 additions & 0 deletions internal/cuda/purego_darwin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//go:build darwin

package cuda

import "testing"

// These tests guard the darwin dlopen probe path against the regression fixed
// in zerfoo T137.1 / issue #171: syscall.syscall6 was handed a func-value PC
// (via funcPC) instead of the raw ABI0 trampoline entry, so probing CUDA at
// package-init time SIGSEGV'd and took down every test binary importing the
// device path. If the mechanism regresses, this test binary crashes at startup
// and CI on darwin goes red.

// TestDarwinTrampolineAddrsResolved verifies the assembly DATA directives
// populated each trampoline address. A zero here means the linker did not wire
// the raw ABI0 entry point, which is the precise defect behind #171.
func TestDarwinTrampolineAddrsResolved(t *testing.T) {
cases := []struct {
name string
addr uintptr
}{
{"dlopen", libc_dlopen_trampoline_addr},
{"dlsym", libc_dlsym_trampoline_addr},
{"dlclose", libc_dlclose_trampoline_addr},
{"dlerror", libc_dlerror_trampoline_addr},
}
for _, tc := range cases {
if tc.addr == 0 {
t.Errorf("%s trampoline address is 0; DATA directive did not resolve", tc.name)
}
}
}

// TestDarwinProbeDoesNotCrash exercises the exact init-time probe path from
// #171 (device.init -> GetDeviceCount -> Open -> dlopenImpl). On a macOS host
// without CUDA it must return a clean "not available" error rather than crash.
func TestDarwinProbeDoesNotCrash(t *testing.T) {
count, err := GetDeviceCount()
if err == nil {
t.Fatalf("expected GetDeviceCount to report CUDA unavailable on darwin, got count=%d", count)
}
if count != 0 {
t.Fatalf("expected 0 devices on darwin, got %d", count)
}
if Available() {
t.Fatal("expected Available() = false on darwin without CUDA")
}
}

// TestDarwinDlopenLibSystem confirms the corrected call mechanism actually
// works: dlopen resolves a real library, dlsym finds a symbol, and the symbol
// is callable through the zero-CGo ccall path.
func TestDarwinDlopenLibSystem(t *testing.T) {
h := dlopenImpl("/usr/lib/libSystem.B.dylib", rtldLazy)
if h == 0 {
t.Fatalf("dlopen(libSystem.B.dylib) failed: %s", dlerrorImpl())
}
defer dlcloseImpl(h)

getpid := dlsymImpl(h, "getpid")
if getpid == 0 {
t.Fatalf("dlsym(getpid) failed: %s", dlerrorImpl())
}
if pid := ccall(getpid); pid == 0 {
t.Fatal("expected getpid() to return a non-zero pid")
}
}
Loading