detached 9ba4c40

purego: validate callback inputs before touching reflect (#512)

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
commit
9ba4c4002b4ec4da6b3d2585b0b4153ecadc5fde
author
Hiroki KUMAZAKI · · 2026-09-12 06:59 UTC
committer
GitHub · · 2026-09-12 06:59 UTC
parent
72b0dcd
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
4 files · +65 -1
M func.go +5 -1
@@ -130,7 +130,11 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) {
130130 // [Cgo rules]: https://pkg.go.dev/cmd/cgo#hdr-Go_references_to_C
131131 func RegisterFunc(fptr any, cfn uintptr) {
132132 const is32bit = unsafe.Sizeof(uintptr(0)) == 4
133- fn := reflect.ValueOf(fptr).Elem()
133+ rv := reflect.ValueOf(fptr)
134+ if rv.Kind() != reflect.Ptr || rv.IsNil() {
135+ panic("purego: fptr must be a non-nil function pointer")
136+ }
137+ fn := rv.Elem()
134138 ty := fn.Type()
135139 if ty.Kind() != reflect.Func {
136140 panic("purego: fptr must be a function pointer")
@@ -130,7 +130,11 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) {
130 // [Cgo rules]: https://pkg.go.dev/cmd/cgo#hdr-Go_references_to_C130 // [Cgo rules]: https://pkg.go.dev/cmd/cgo#hdr-Go_references_to_C
131 func RegisterFunc(fptr any, cfn uintptr) {131 func RegisterFunc(fptr any, cfn uintptr) {
132 const is32bit = unsafe.Sizeof(uintptr(0)) == 4132 const is32bit = unsafe.Sizeof(uintptr(0)) == 4
133- fn := reflect.ValueOf(fptr).Elem()133+ rv := reflect.ValueOf(fptr)
134+ if rv.Kind() != reflect.Ptr || rv.IsNil() {
135+ panic("purego: fptr must be a non-nil function pointer")
136+ }
137+ fn := rv.Elem()
134 ty := fn.Type()138 ty := fn.Type()
135 if ty.Kind() != reflect.Func {139 if ty.Kind() != reflect.Func {
136 panic("purego: fptr must be a function pointer")140 panic("purego: fptr must be a function pointer")
M func_test.go +54 -0
@@ -181,6 +181,60 @@ func TestRegisterLibFunc_Bool(t *testing.T) {
181181 }
182182 }
183183
184+func TestNewCallback_NotAFunction(t *testing.T) {
185+ for _, tc := range []struct {
186+ name string
187+ fn any
188+ }{
189+ {"nil", nil},
190+ {"int", 42},
191+ {"string", "not a function"},
192+ {"pointer", new(int)},
193+ } {
194+ t.Run(tc.name, func(t *testing.T) {
195+ defer func() {
196+ r := recover()
197+ if r == nil {
198+ t.Fatal("NewCallback did not panic")
199+ }
200+ const want = "purego: the type must be a function but was not"
201+ if got := fmt.Sprint(r); got != want {
202+ t.Fatalf("panic mismatch:\n got: %q\n want: %q", got, want)
203+ }
204+ }()
205+ purego.NewCallback(tc.fn)
206+ })
207+ }
208+}
209+
210+func TestRegisterFunc_InvalidFunctionPointer(t *testing.T) {
211+ for _, tc := range []struct {
212+ name string
213+ fptr any
214+ want string
215+ }{
216+ {"nil", nil, "purego: fptr must be a non-nil function pointer"},
217+ {"non_pointer", 42, "purego: fptr must be a non-nil function pointer"},
218+ {"function_value", func() {}, "purego: fptr must be a non-nil function pointer"},
219+ {"nil_function_pointer", (*func())(nil), "purego: fptr must be a non-nil function pointer"},
220+ {"pointer_to_non_function", new(int), "purego: fptr must be a function pointer"},
221+ {"pointer_to_function_pointer", new(*func()), "purego: fptr must be a function pointer"},
222+ } {
223+ t.Run(tc.name, func(t *testing.T) {
224+ defer func() {
225+ r := recover()
226+ if r == nil {
227+ t.Fatal("RegisterFunc did not panic")
228+ }
229+ if got := fmt.Sprint(r); got != tc.want {
230+ t.Fatalf("panic mismatch:\n got: %q\n want: %q", got, tc.want)
231+ }
232+ }()
233+ purego.RegisterFunc(tc.fptr, 1)
234+ })
235+ }
236+}
237+
184238 func TestABI(t *testing.T) {
185239 libFileName := filepath.Join(t.TempDir(), "abitest.so")
186240 t.Logf("Build %v", libFileName)
@@ -181,6 +181,60 @@ func TestRegisterLibFunc_Bool(t *testing.T) {
181 }181 }
182 }182 }
183 183
184+func TestNewCallback_NotAFunction(t *testing.T) {
185+ for _, tc := range []struct {
186+ name string
187+ fn any
188+ }{
189+ {"nil", nil},
190+ {"int", 42},
191+ {"string", "not a function"},
192+ {"pointer", new(int)},
193+ } {
194+ t.Run(tc.name, func(t *testing.T) {
195+ defer func() {
196+ r := recover()
197+ if r == nil {
198+ t.Fatal("NewCallback did not panic")
199+ }
200+ const want = "purego: the type must be a function but was not"
201+ if got := fmt.Sprint(r); got != want {
202+ t.Fatalf("panic mismatch:\n got: %q\n want: %q", got, want)
203+ }
204+ }()
205+ purego.NewCallback(tc.fn)
206+ })
207+ }
208+}
209+
210+func TestRegisterFunc_InvalidFunctionPointer(t *testing.T) {
211+ for _, tc := range []struct {
212+ name string
213+ fptr any
214+ want string
215+ }{
216+ {"nil", nil, "purego: fptr must be a non-nil function pointer"},
217+ {"non_pointer", 42, "purego: fptr must be a non-nil function pointer"},
218+ {"function_value", func() {}, "purego: fptr must be a non-nil function pointer"},
219+ {"nil_function_pointer", (*func())(nil), "purego: fptr must be a non-nil function pointer"},
220+ {"pointer_to_non_function", new(int), "purego: fptr must be a function pointer"},
221+ {"pointer_to_function_pointer", new(*func()), "purego: fptr must be a function pointer"},
222+ } {
223+ t.Run(tc.name, func(t *testing.T) {
224+ defer func() {
225+ r := recover()
226+ if r == nil {
227+ t.Fatal("RegisterFunc did not panic")
228+ }
229+ if got := fmt.Sprint(r); got != tc.want {
230+ t.Fatalf("panic mismatch:\n got: %q\n want: %q", got, tc.want)
231+ }
232+ }()
233+ purego.RegisterFunc(tc.fptr, 1)
234+ })
235+ }
236+}
237+
184 func TestABI(t *testing.T) {238 func TestABI(t *testing.T) {
185 libFileName := filepath.Join(t.TempDir(), "abitest.so")239 libFileName := filepath.Join(t.TempDir(), "abitest.so")
186 t.Logf("Build %v", libFileName)240 t.Logf("Build %v", libFileName)
M syscall_unix.go +3 -0
@@ -27,6 +27,9 @@ func syscall_syscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
2727 // provides similar functionality to windows.NewCallback it is distinct.
2828 func NewCallback(fn any) uintptr {
2929 ty := reflect.TypeOf(fn)
30+ if ty == nil || ty.Kind() != reflect.Func {
31+ panic("purego: the type must be a function but was not")
32+ }
3033 for i := range ty.NumIn() {
3134 in := ty.In(i)
3235 if !in.AssignableTo(reflect.TypeFor[CDecl]()) {
@@ -27,6 +27,9 @@ func syscall_syscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
27 // provides similar functionality to windows.NewCallback it is distinct.27 // provides similar functionality to windows.NewCallback it is distinct.
28 func NewCallback(fn any) uintptr {28 func NewCallback(fn any) uintptr {
29 ty := reflect.TypeOf(fn)29 ty := reflect.TypeOf(fn)
30+ if ty == nil || ty.Kind() != reflect.Func {
31+ panic("purego: the type must be a function but was not")
32+ }
30 for i := range ty.NumIn() {33 for i := range ty.NumIn() {
31 in := ty.In(i)34 in := ty.In(i)
32 if !in.AssignableTo(reflect.TypeFor[CDecl]()) {35 if !in.AssignableTo(reflect.TypeFor[CDecl]()) {
M syscall_windows.go +3 -0
@@ -26,6 +26,9 @@ func syscall_syscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
2626 func NewCallback(fn any) uintptr {
2727 isCDecl := false
2828 ty := reflect.TypeOf(fn)
29+ if ty == nil || ty.Kind() != reflect.Func {
30+ panic("purego: the type must be a function but was not")
31+ }
2932 for i := range ty.NumIn() {
3033 in := ty.In(i)
3134 if !in.AssignableTo(reflect.TypeFor[CDecl]()) {
@@ -26,6 +26,9 @@ func syscall_syscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
26 func NewCallback(fn any) uintptr {26 func NewCallback(fn any) uintptr {
27 isCDecl := false27 isCDecl := false
28 ty := reflect.TypeOf(fn)28 ty := reflect.TypeOf(fn)
29+ if ty == nil || ty.Kind() != reflect.Func {
30+ panic("purego: the type must be a function but was not")
31+ }
29 for i := range ty.NumIn() {32 for i := range ty.NumIn() {
30 in := ty.In(i)33 in := ty.In(i)
31 if !in.AssignableTo(reflect.TypeFor[CDecl]()) {34 if !in.AssignableTo(reflect.TypeFor[CDecl]()) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 102

↑↓ moveenter openesc close