diff --git a/internal/cuda/purego_darwin.go b/internal/cuda/purego_darwin.go index 507924b..660b2d0 100644 --- a/internal/cuda/purego_darwin.go +++ b/internal/cuda/purego_darwin.go @@ -7,13 +7,21 @@ 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) @@ -21,27 +29,25 @@ func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err u //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, ) @@ -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, @@ -61,7 +67,7 @@ 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) @@ -69,7 +75,7 @@ func dlcloseImpl(handle uintptr) int { func dlerrorImpl() string { r1, _, _ := syscall_syscall6( - funcPC(libc_dlerror_trampoline), + libc_dlerror_trampoline_addr, 0, 0, 0, 0, 0, 0, ) if r1 == 0 { diff --git a/internal/cuda/purego_darwin_amd64.s b/internal/cuda/purego_darwin_amd64.s index 54643b8..d2509f4 100644 --- a/internal/cuda/purego_darwin_amd64.s +++ b/internal/cuda/purego_darwin_amd64.s @@ -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) diff --git a/internal/cuda/purego_darwin_arm64.s b/internal/cuda/purego_darwin_arm64.s index 54643b8..d2509f4 100644 --- a/internal/cuda/purego_darwin_arm64.s +++ b/internal/cuda/purego_darwin_arm64.s @@ -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) diff --git a/internal/cuda/purego_darwin_test.go b/internal/cuda/purego_darwin_test.go new file mode 100644 index 0000000..e3767b4 --- /dev/null +++ b/internal/cuda/purego_darwin_test.go @@ -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") + } +}