From 6760a1814f3f406a3c4a822b1595339fefd24599 Mon Sep 17 00:00:00 2001 From: kumagi Date: Sat, 5 Sep 2026 16:05:25 +0900 Subject: [PATCH 1/2] purego: validate callback inputs before touching reflect NewCallback (unix and windows) called ty.NumIn() before checking the kind, so non-function input died inside reflect (or with a bare nil dereference) instead of the intended purego error. RegisterFunc called Value.Elem() before checking for a function pointer with the same effect. Validate first so misuse fails with actionable messages. Closes #507 --- func.go | 6 +++++- syscall_unix.go | 3 +++ syscall_windows.go | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/func.go b/func.go index 5a3410b8..9f20ca20 100644 --- a/func.go +++ b/func.go @@ -128,7 +128,11 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) { // [Cgo rules]: https://pkg.go.dev/cmd/cgo#hdr-Go_references_to_C func RegisterFunc(fptr any, cfn uintptr) { const is32bit = unsafe.Sizeof(uintptr(0)) == 4 - fn := reflect.ValueOf(fptr).Elem() + rv := reflect.ValueOf(fptr) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + panic("purego: fptr must be a non-nil function pointer") + } + fn := rv.Elem() ty := fn.Type() if ty.Kind() != reflect.Func { panic("purego: fptr must be a function pointer") diff --git a/syscall_unix.go b/syscall_unix.go index 999f6967..43814b15 100644 --- a/syscall_unix.go +++ b/syscall_unix.go @@ -27,6 +27,9 @@ func syscall_syscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) { // provides similar functionality to windows.NewCallback it is distinct. func NewCallback(fn any) uintptr { ty := reflect.TypeOf(fn) + if ty == nil || ty.Kind() != reflect.Func { + panic("purego: the type must be a function but was not") + } for i := range ty.NumIn() { in := ty.In(i) if !in.AssignableTo(reflect.TypeFor[CDecl]()) { diff --git a/syscall_windows.go b/syscall_windows.go index a8e1b5b1..45e280cc 100644 --- a/syscall_windows.go +++ b/syscall_windows.go @@ -26,6 +26,9 @@ func syscall_syscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) { func NewCallback(fn any) uintptr { isCDecl := false ty := reflect.TypeOf(fn) + if ty == nil || ty.Kind() != reflect.Func { + panic("purego: the type must be a function but was not") + } for i := range ty.NumIn() { in := ty.In(i) if !in.AssignableTo(reflect.TypeFor[CDecl]()) { From cdc6f07ea10d564e58bf94f39a178908dc017ca5 Mon Sep 17 00:00:00 2001 From: kumagi Date: Fri, 11 Sep 2026 00:54:41 +0900 Subject: [PATCH 2/2] purego: add tests for callback input validation --- func_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/func_test.go b/func_test.go index de4df804..baf3c9ed 100644 --- a/func_test.go +++ b/func_test.go @@ -181,6 +181,60 @@ func TestRegisterLibFunc_Bool(t *testing.T) { } } +func TestNewCallback_NotAFunction(t *testing.T) { + for _, tc := range []struct { + name string + fn any + }{ + {"nil", nil}, + {"int", 42}, + {"string", "not a function"}, + {"pointer", new(int)}, + } { + t.Run(tc.name, func(t *testing.T) { + defer func() { + r := recover() + if r == nil { + t.Fatal("NewCallback did not panic") + } + const want = "purego: the type must be a function but was not" + if got := fmt.Sprint(r); got != want { + t.Fatalf("panic mismatch:\n got: %q\n want: %q", got, want) + } + }() + purego.NewCallback(tc.fn) + }) + } +} + +func TestRegisterFunc_InvalidFunctionPointer(t *testing.T) { + for _, tc := range []struct { + name string + fptr any + want string + }{ + {"nil", nil, "purego: fptr must be a non-nil function pointer"}, + {"non_pointer", 42, "purego: fptr must be a non-nil function pointer"}, + {"function_value", func() {}, "purego: fptr must be a non-nil function pointer"}, + {"nil_function_pointer", (*func())(nil), "purego: fptr must be a non-nil function pointer"}, + {"pointer_to_non_function", new(int), "purego: fptr must be a function pointer"}, + {"pointer_to_function_pointer", new(*func()), "purego: fptr must be a function pointer"}, + } { + t.Run(tc.name, func(t *testing.T) { + defer func() { + r := recover() + if r == nil { + t.Fatal("RegisterFunc did not panic") + } + if got := fmt.Sprint(r); got != tc.want { + t.Fatalf("panic mismatch:\n got: %q\n want: %q", got, tc.want) + } + }() + purego.RegisterFunc(tc.fptr, 1) + }) + } +} + func TestABI(t *testing.T) { libFileName := filepath.Join(t.TempDir(), "abitest.so") t.Logf("Build %v", libFileName)