detached 19b4768

purego: clear uncaptured errno in syscall trampolines (#510)

Trampolines that do not capture errno leave the third input argument
in the error slot, causing SyscallN to return it as an error code.
Clear that slot after the call while preserving errno from Darwin
and the Linux C fallback.

Clarify the platform-dependent error return and add regression tests
for both captured errno and a nonzero third argument.

Closes #505
commit
19b476841cb4011e7841a7bea209d223431c337a
author
Hiroki KUMAZAKI · · 2026-09-18 20:26 UTC
committer
GitHub · · 2026-09-18 20:26 UTC
parent
9f4a5c8
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
14 files · +102 -10
M dlfcn_test.go +30 -0
@@ -8,6 +8,7 @@ package purego_test
88 import (
99 "os"
1010 "path/filepath"
11+ "syscall"
1112 "testing"
1213 "unsafe"
1314
@@ -48,3 +49,32 @@ func TestSyscallN(t *testing.T) {
4849 t.Fatalf("SyscallN didn't return the same result as purego.Dlsym: %d", err2)
4950 }
5051 }
52+
53+func TestSyscallNErrnoIsNotAnInputArgument(t *testing.T) {
54+ if errnoIsCaptured() {
55+ t.Skip("this platform saves errno; see TestErrno")
56+ }
57+
58+ openSym, err := purego.Dlsym(purego.RTLD_DEFAULT, "open")
59+ if err != nil {
60+ t.Fatal(err)
61+ }
62+
63+ path, err := syscall.BytePtrFromString("_file_that_does_not_exist_")
64+ if err != nil {
65+ t.Fatal(err)
66+ }
67+
68+ // The third argument is non-zero so that echoing it back as errno is visible.
69+ const third = uintptr(0o600)
70+ r1, _, errno := purego.SyscallN(openSym,
71+ uintptr(unsafe.Pointer(path)),
72+ uintptr(os.O_RDWR),
73+ third)
74+ if int32(r1) != -1 {
75+ t.Fatalf("open returned %d, wanted -1", r1)
76+ }
77+ if errno != 0 {
78+ t.Errorf("SyscallN returned %d as errno where errno is not captured, wanted 0", errno)
79+ }
80+}
@@ -8,6 +8,7 @@ package purego_test
8 import (8 import (
9 "os"9 "os"
10 "path/filepath"10 "path/filepath"
11+ "syscall"
11 "testing"12 "testing"
12 "unsafe"13 "unsafe"
13 14
@@ -48,3 +49,32 @@ func TestSyscallN(t *testing.T) {
48 t.Fatalf("SyscallN didn't return the same result as purego.Dlsym: %d", err2)49 t.Fatalf("SyscallN didn't return the same result as purego.Dlsym: %d", err2)
49 }50 }
50 }51 }
52+
53+func TestSyscallNErrnoIsNotAnInputArgument(t *testing.T) {
54+ if errnoIsCaptured() {
55+ t.Skip("this platform saves errno; see TestErrno")
56+ }
57+
58+ openSym, err := purego.Dlsym(purego.RTLD_DEFAULT, "open")
59+ if err != nil {
60+ t.Fatal(err)
61+ }
62+
63+ path, err := syscall.BytePtrFromString("_file_that_does_not_exist_")
64+ if err != nil {
65+ t.Fatal(err)
66+ }
67+
68+ // The third argument is non-zero so that echoing it back as errno is visible.
69+ const third = uintptr(0o600)
70+ r1, _, errno := purego.SyscallN(openSym,
71+ uintptr(unsafe.Pointer(path)),
72+ uintptr(os.O_RDWR),
73+ third)
74+ if int32(r1) != -1 {
75+ t.Fatalf("open returned %d, wanted -1", r1)
76+ }
77+ if errno != 0 {
78+ t.Errorf("SyscallN returned %d as errno where errno is not captured, wanted 0", errno)
79+ }
80+}
M sys_386.s +1 -0
@@ -129,6 +129,7 @@ TEXT syscallX(SB), NOSPLIT|NOFRAME, $0-0
129129 MOVL PTR_ADDRESS(SP), BX
130130 MOVL AX, syscallArgs_a1(BX) // return value r1
131131 MOVL DX, syscallArgs_a2(BX) // return value r2 (for 64-bit returns)
132+ MOVL $0, syscallArgs_a3(BX) // no errno here: clear the input argument
132133
133134 // Save x87 FPU return value (ST0) to f1 field
134135 // On i386 System V ABI, float/double returns are in ST(0)
@@ -129,6 +129,7 @@ TEXT syscallX(SB), NOSPLIT|NOFRAME, $0-0
129 MOVL PTR_ADDRESS(SP), BX129 MOVL PTR_ADDRESS(SP), BX
130 MOVL AX, syscallArgs_a1(BX) // return value r1130 MOVL AX, syscallArgs_a1(BX) // return value r1
131 MOVL DX, syscallArgs_a2(BX) // return value r2 (for 64-bit returns)131 MOVL DX, syscallArgs_a2(BX) // return value r2 (for 64-bit returns)
132+ MOVL $0, syscallArgs_a3(BX) // no errno here: clear the input argument
132 133
133 // Save x87 FPU return value (ST0) to f1 field134 // Save x87 FPU return value (ST0) to f1 field
134 // On i386 System V ABI, float/double returns are in ST(0)135 // On i386 System V ABI, float/double returns are in ST(0)
M sys_amd64.s +2 -0
@@ -127,6 +127,8 @@ TEXT syscallX(SB), NOSPLIT, $STACK_SIZE
127127 MOVQ PTR_ADDRESS(SP), DI // reload (DI clobbered by call)
128128 MOVQ (AX), AX
129129 MOVQ AX, syscallArgs_a3(DI) // save errno
130+#else
131+ MOVQ $0, syscallArgs_a3(DI) // no errno here: clear the input argument
130132 #endif
131133
132134 XORL AX, AX // no error (it's ignored anyway)
@@ -127,6 +127,8 @@ TEXT syscallX(SB), NOSPLIT, $STACK_SIZE
127 MOVQ PTR_ADDRESS(SP), DI // reload (DI clobbered by call)127 MOVQ PTR_ADDRESS(SP), DI // reload (DI clobbered by call)
128 MOVQ (AX), AX128 MOVQ (AX), AX
129 MOVQ AX, syscallArgs_a3(DI) // save errno129 MOVQ AX, syscallArgs_a3(DI) // save errno
130+#else
131+ MOVQ $0, syscallArgs_a3(DI) // no errno here: clear the input argument
130 #endif132 #endif
131 133
132 XORL AX, AX // no error (it's ignored anyway)134 XORL AX, AX // no error (it's ignored anyway)
M sys_arm.s +2 -0
@@ -135,6 +135,8 @@ skipfpload:
135135 // save R0, R1
136136 MOVW R0, syscallArgs_a1(R8)
137137 MOVW R1, syscallArgs_a2(R8)
138+ MOVW $0, R1
139+ MOVW R1, syscallArgs_a3(R8) // no errno here: clear the input argument
138140
139141 MOVB runtime·goarmsoftfp(SB), R11
140142 CMP $0, R11
@@ -135,6 +135,8 @@ skipfpload:
135 // save R0, R1135 // save R0, R1
136 MOVW R0, syscallArgs_a1(R8)136 MOVW R0, syscallArgs_a1(R8)
137 MOVW R1, syscallArgs_a2(R8)137 MOVW R1, syscallArgs_a2(R8)
138+ MOVW $0, R1
139+ MOVW R1, syscallArgs_a3(R8) // no errno here: clear the input argument
138 140
139 MOVB runtime·goarmsoftfp(SB), R11141 MOVB runtime·goarmsoftfp(SB), R11
140 CMP $0, R11142 CMP $0, R11
M sys_arm64.s +2 -1
@@ -127,6 +127,7 @@ TEXT syscallX(SB), NOSPLIT, $0
127127 BL purego_error(SB)
128128 MOVD (R0), R0
129129 MOVD R0, syscallArgs_a3(R2) // save errno
130-
130+#else
131+ MOVD $0, syscallArgs_a3(R2) // no errno here: clear the input argument
131132 #endif
132133 RET
@@ -127,6 +127,7 @@ TEXT syscallX(SB), NOSPLIT, $0
127 BL purego_error(SB)127 BL purego_error(SB)
128 MOVD (R0), R0128 MOVD (R0), R0
129 MOVD R0, syscallArgs_a3(R2) // save errno129 MOVD R0, syscallArgs_a3(R2) // save errno
130-130+#else
131+ MOVD $0, syscallArgs_a3(R2) // no errno here: clear the input argument
131 #endif132 #endif
132 RET133 RET
M sys_loong64.s +1 -0
@@ -121,6 +121,7 @@ TEXT syscallX(SB), NOSPLIT, $0
121121 // save R4, R5
122122 MOVV R4, syscallArgs_a1(R13)
123123 MOVV R5, syscallArgs_a2(R13)
124+ MOVV R0, syscallArgs_a3(R13) // no errno here: clear the input argument
124125
125126 // save f0-f3
126127 MOVD F0, syscallArgs_f1(R13)
@@ -121,6 +121,7 @@ TEXT syscallX(SB), NOSPLIT, $0
121 // save R4, R5121 // save R4, R5
122 MOVV R4, syscallArgs_a1(R13)122 MOVV R4, syscallArgs_a1(R13)
123 MOVV R5, syscallArgs_a2(R13)123 MOVV R5, syscallArgs_a2(R13)
124+ MOVV R0, syscallArgs_a3(R13) // no errno here: clear the input argument
124 125
125 // save f0-f3126 // save f0-f3
126 MOVD F0, syscallArgs_f1(R13)127 MOVD F0, syscallArgs_f1(R13)
M sys_ppc64le.s +2 -0
@@ -106,6 +106,8 @@ TEXT syscallX(SB), NOSPLIT, $0
106106 // Store integer results back (R3, R4)
107107 MOVD R3, syscallArgs_a1(R11)
108108 MOVD R4, syscallArgs_a2(R11)
109+ MOVD $0, R3
110+ MOVD R3, syscallArgs_a3(R11) // no errno here: clear the input argument
109111
110112 // Store float return values F1-F8 (an HFA may span up to eight).
111113 FMOVD F1, syscallArgs_f1(R11)
@@ -106,6 +106,8 @@ TEXT syscallX(SB), NOSPLIT, $0
106 // Store integer results back (R3, R4)106 // Store integer results back (R3, R4)
107 MOVD R3, syscallArgs_a1(R11)107 MOVD R3, syscallArgs_a1(R11)
108 MOVD R4, syscallArgs_a2(R11)108 MOVD R4, syscallArgs_a2(R11)
109+ MOVD $0, R3
110+ MOVD R3, syscallArgs_a3(R11) // no errno here: clear the input argument
109 111
110 // Store float return values F1-F8 (an HFA may span up to eight).112 // Store float return values F1-F8 (an HFA may span up to eight).
111 FMOVD F1, syscallArgs_f1(R11)113 FMOVD F1, syscallArgs_f1(R11)
M sys_riscv64.s +1 -0
@@ -118,6 +118,7 @@ TEXT syscallX(SB), NOSPLIT, $0
118118 // Store results back
119119 MOV X10, syscallArgs_a1(X9)
120120 MOV X11, syscallArgs_a2(X9)
121+ MOV X0, syscallArgs_a3(X9) // no errno here: clear the input argument
121122
122123 // Store back float return regs if used by your ABI contract
123124 MOVD F10, syscallArgs_f1(X9)
@@ -118,6 +118,7 @@ TEXT syscallX(SB), NOSPLIT, $0
118 // Store results back118 // Store results back
119 MOV X10, syscallArgs_a1(X9)119 MOV X10, syscallArgs_a1(X9)
120 MOV X11, syscallArgs_a2(X9)120 MOV X11, syscallArgs_a2(X9)
121+ MOV X0, syscallArgs_a3(X9) // no errno here: clear the input argument
121 122
122 // Store back float return regs if used by your ABI contract123 // Store back float return regs if used by your ABI contract
123 MOVD F10, syscallArgs_f1(X9)124 MOVD F10, syscallArgs_f1(X9)
M sys_s390x.s +1 -0
@@ -131,6 +131,7 @@ TEXT syscallX(SB), NOSPLIT, $0
131131 // Store integer results back (R2, R3)
132132 MOVD R2, syscallArgs_a1(R9)
133133 MOVD R3, syscallArgs_a2(R9)
134+ MOVD $0, syscallArgs_a3(R9) // no errno here: clear the input argument
134135
135136 // Store float return values (F0, F2, F4, F6)
136137 FMOVD F0, syscallArgs_f1(R9)
@@ -131,6 +131,7 @@ TEXT syscallX(SB), NOSPLIT, $0
131 // Store integer results back (R2, R3)131 // Store integer results back (R2, R3)
132 MOVD R2, syscallArgs_a1(R9)132 MOVD R2, syscallArgs_a1(R9)
133 MOVD R3, syscallArgs_a2(R9)133 MOVD R3, syscallArgs_a2(R9)
134+ MOVD $0, syscallArgs_a3(R9) // no errno here: clear the input argument
134 135
135 // Store float return values (F0, F2, F4, F6)136 // Store float return values (F0, F2, F4, F6)
136 FMOVD F0, syscallArgs_f1(R9)137 FMOVD F0, syscallArgs_f1(R9)
M syscall.go +6 -1
@@ -18,6 +18,10 @@ const (
1818 // syscallArgs is the argument block handed to the syscall15 trampoline. On
1919 // platforms that go through internal/cgo it is passed to C as a
2020 // struct syscallArgs, so its layout must match the C ABI.
21+//
22+// The trampoline reports its results through this block: the return values go
23+// to a1 and a2 and, where the platform captures errno, the error code goes to
24+// a3. A trampoline that cannot capture errno must clear a3.
2125 type syscallArgs struct {
2226 _ structs.HostLayout
2327
@@ -49,7 +53,8 @@ func syscall_SyscallN(fn uintptr, sysargs []uintptr, floats []uintptr, r8 uintpt
4953
5054 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
5155 // There is an internal maximum number of arguments that SyscallN can take. It panics
52-// when the maximum is exceeded. It returns the result and the libc error code if there is one.
56+// when the maximum is exceeded. It returns the result and GetLastError on Windows or the errno on
57+// platforms that capture it, otherwise 0.
5358 //
5459 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]
5560 // especially point 4.
@@ -18,6 +18,10 @@ const (
18 // syscallArgs is the argument block handed to the syscall15 trampoline. On18 // syscallArgs is the argument block handed to the syscall15 trampoline. On
19 // platforms that go through internal/cgo it is passed to C as a19 // platforms that go through internal/cgo it is passed to C as a
20 // struct syscallArgs, so its layout must match the C ABI.20 // struct syscallArgs, so its layout must match the C ABI.
21+//
22+// The trampoline reports its results through this block: the return values go
23+// to a1 and a2 and, where the platform captures errno, the error code goes to
24+// a3. A trampoline that cannot capture errno must clear a3.
21 type syscallArgs struct {25 type syscallArgs struct {
22 _ structs.HostLayout26 _ structs.HostLayout
23 27
@@ -49,7 +53,8 @@ func syscall_SyscallN(fn uintptr, sysargs []uintptr, floats []uintptr, r8 uintpt
49 53
50 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.54 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
51 // There is an internal maximum number of arguments that SyscallN can take. It panics55 // There is an internal maximum number of arguments that SyscallN can take. It panics
52-// when the maximum is exceeded. It returns the result and the libc error code if there is one.56+// when the maximum is exceeded. It returns the result and GetLastError on Windows or the errno on
57+// platforms that capture it, otherwise 0.
53 //58 //
54 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]59 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]
55 // especially point 4.60 // especially point 4.
M syscall_32bit.go +6 -1
@@ -18,6 +18,10 @@ const (
1818 // syscallArgs is the argument block handed to the syscall15 trampoline. On
1919 // platforms that go through internal/cgo it is passed to C as a
2020 // struct syscallArgs, so its layout must match the C ABI.
21+//
22+// The trampoline reports its results through this block: the return values go
23+// to a1 and a2 and, where the platform captures errno, the error code goes to
24+// a3. A trampoline that cannot capture errno must clear a3.
2125 type syscallArgs struct {
2226 _ structs.HostLayout
2327
@@ -51,7 +55,8 @@ func syscall_SyscallN(fn uintptr, sysargs []uintptr, floats []uintptr, r8 uintpt
5155
5256 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
5357 // There is an internal maximum number of arguments that SyscallN can take. It panics
54-// when the maximum is exceeded. It returns the result and the libc error code if there is one.
58+// when the maximum is exceeded. It returns the result and GetLastError on Windows or the errno on
59+// platforms that capture it, otherwise 0.
5560 //
5661 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]
5762 // especially point 4.
@@ -18,6 +18,10 @@ const (
18 // syscallArgs is the argument block handed to the syscall15 trampoline. On18 // syscallArgs is the argument block handed to the syscall15 trampoline. On
19 // platforms that go through internal/cgo it is passed to C as a19 // platforms that go through internal/cgo it is passed to C as a
20 // struct syscallArgs, so its layout must match the C ABI.20 // struct syscallArgs, so its layout must match the C ABI.
21+//
22+// The trampoline reports its results through this block: the return values go
23+// to a1 and a2 and, where the platform captures errno, the error code goes to
24+// a3. A trampoline that cannot capture errno must clear a3.
21 type syscallArgs struct {25 type syscallArgs struct {
22 _ structs.HostLayout26 _ structs.HostLayout
23 27
@@ -51,7 +55,8 @@ func syscall_SyscallN(fn uintptr, sysargs []uintptr, floats []uintptr, r8 uintpt
51 55
52 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.56 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
53 // There is an internal maximum number of arguments that SyscallN can take. It panics57 // There is an internal maximum number of arguments that SyscallN can take. It panics
54-// when the maximum is exceeded. It returns the result and the libc error code if there is one.58+// when the maximum is exceeded. It returns the result and GetLastError on Windows or the errno on
59+// platforms that capture it, otherwise 0.
55 //60 //
56 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]61 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]
57 // especially point 4.62 // especially point 4.
M syscall_ppc64le.go +2 -1
@@ -37,7 +37,8 @@ func syscall_SyscallN(fn uintptr, sysargs []uintptr, floats []uintptr, r8 uintpt
3737
3838 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
3939 // There is an internal maximum number of arguments that SyscallN can take. It panics
40-// when the maximum is exceeded. It returns the result and the libc error code if there is one.
40+// when the maximum is exceeded. It returns the result. This file builds only for
41+// linux/ppc64le, where the trampoline does not capture errno, so err is always 0.
4142 //
4243 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]
4344 // especially point 4.
@@ -37,7 +37,8 @@ func syscall_SyscallN(fn uintptr, sysargs []uintptr, floats []uintptr, r8 uintpt
37 37
38 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.38 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
39 // There is an internal maximum number of arguments that SyscallN can take. It panics39 // There is an internal maximum number of arguments that SyscallN can take. It panics
40-// when the maximum is exceeded. It returns the result and the libc error code if there is one.40+// when the maximum is exceeded. It returns the result. This file builds only for
41+// linux/ppc64le, where the trampoline does not capture errno, so err is always 0.
41 //42 //
42 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]43 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]
43 // especially point 4.44 // especially point 4.
M syscall_test.go +36 -6
@@ -5,9 +5,9 @@ package purego_test
55
66 import (
77 "os"
8+ "path/filepath"
89 "runtime"
910 "testing"
10- "unsafe"
1111
1212 "github.com/ebitengine/purego"
1313 "github.com/ebitengine/purego/internal/load"
@@ -25,24 +25,54 @@ func TestOS(t *testing.T) {
2525 }
2626 }
2727
28+// errnoIsCaptured reports whether SyscallN returns the libc errno as its third
29+// value on this platform.
30+func errnoIsCaptured() bool {
31+ switch runtime.GOOS {
32+ case "darwin":
33+ return true
34+ case "linux":
35+ switch runtime.GOARCH {
36+ case "mips", "mipsle", "mips64", "mips64le", "ppc64":
37+ return true
38+ }
39+ }
40+ return false
41+}
42+
2843 func TestErrno(t *testing.T) {
29- if runtime.GOOS != "darwin" {
44+ if !errnoIsCaptured() {
3045 t.Skip("platform does not support returning errno from syscall")
3146 }
3247
33- libc, err := load.OpenLibrary("/usr/lib/libSystem.B.dylib")
48+ libFileName := filepath.Join(t.TempDir(), "liberrnotest.so")
49+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "liberrnotest", "errno_test.c")); err != nil {
50+ t.Fatal(err)
51+ }
52+ defer os.Remove(libFileName)
53+
54+ lib, err := load.OpenLibrary(libFileName)
3455 if err != nil {
3556 t.Fatal(err)
3657 }
3758
38- openSym, err := load.OpenSymbol(libc, "open")
59+ setErrno, err := load.OpenSymbol(lib, "setErrno")
3960 if err != nil {
4061 t.Fatal(err)
4162 }
4263
43- r1, _, errno := purego.SyscallN(openSym, uintptr(unsafe.Pointer(&[]byte("_file_that_does_not_exist_\x00")[0])), uintptr(os.O_RDWR))
64+ r1, _, errno := purego.SyscallN(setErrno)
4465 if int32(r1) != -1 {
45- t.Errorf("open returned %d, wanted -1", r1)
66+ t.Errorf("setErrno returned %d, wanted -1", r1)
67+ }
68+
69+ libcName, err := getSystemLibrary()
70+ if err != nil {
71+ t.Fatal(err)
72+ }
73+ libc, err := load.OpenLibrary(libcName)
74+ if err != nil {
75+ t.Fatal(err)
4676 }
4777
4878 var strerror func(int32) string
@@ -5,9 +5,9 @@ package purego_test
5 5
6 import (6 import (
7 "os"7 "os"
8+ "path/filepath"
8 "runtime"9 "runtime"
9 "testing"10 "testing"
10- "unsafe"
11 11
12 "github.com/ebitengine/purego"12 "github.com/ebitengine/purego"
13 "github.com/ebitengine/purego/internal/load"13 "github.com/ebitengine/purego/internal/load"
@@ -25,24 +25,54 @@ func TestOS(t *testing.T) {
25 }25 }
26 }26 }
27 27
28+// errnoIsCaptured reports whether SyscallN returns the libc errno as its third
29+// value on this platform.
30+func errnoIsCaptured() bool {
31+ switch runtime.GOOS {
32+ case "darwin":
33+ return true
34+ case "linux":
35+ switch runtime.GOARCH {
36+ case "mips", "mipsle", "mips64", "mips64le", "ppc64":
37+ return true
38+ }
39+ }
40+ return false
41+}
42+
28 func TestErrno(t *testing.T) {43 func TestErrno(t *testing.T) {
29- if runtime.GOOS != "darwin" {44+ if !errnoIsCaptured() {
30 t.Skip("platform does not support returning errno from syscall")45 t.Skip("platform does not support returning errno from syscall")
31 }46 }
32 47
33- libc, err := load.OpenLibrary("/usr/lib/libSystem.B.dylib")48+ libFileName := filepath.Join(t.TempDir(), "liberrnotest.so")
49+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "liberrnotest", "errno_test.c")); err != nil {
50+ t.Fatal(err)
51+ }
52+ defer os.Remove(libFileName)
53+
54+ lib, err := load.OpenLibrary(libFileName)
34 if err != nil {55 if err != nil {
35 t.Fatal(err)56 t.Fatal(err)
36 }57 }
37 58
38- openSym, err := load.OpenSymbol(libc, "open")59+ setErrno, err := load.OpenSymbol(lib, "setErrno")
39 if err != nil {60 if err != nil {
40 t.Fatal(err)61 t.Fatal(err)
41 }62 }
42 63
43- r1, _, errno := purego.SyscallN(openSym, uintptr(unsafe.Pointer(&[]byte("_file_that_does_not_exist_\x00")[0])), uintptr(os.O_RDWR))64+ r1, _, errno := purego.SyscallN(setErrno)
44 if int32(r1) != -1 {65 if int32(r1) != -1 {
45- t.Errorf("open returned %d, wanted -1", r1)66+ t.Errorf("setErrno returned %d, wanted -1", r1)
67+ }
68+
69+ libcName, err := getSystemLibrary()
70+ if err != nil {
71+ t.Fatal(err)
72+ }
73+ libc, err := load.OpenLibrary(libcName)
74+ if err != nil {
75+ t.Fatal(err)
46 }76 }
47 77
48 var strerror func(int32) string78 var strerror func(int32) string
A testdata/liberrnotest/errno_test.c +10 -0
@@ -0,0 +1,10 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+#include <errno.h>
5+
6+// setErrno fails the way a C library call does: it returns -1 and sets errno.
7+int setErrno(void) {
8+ errno = ENOENT;
9+ return -1;
10+}
@@ -0,0 +1,10 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+#include <errno.h>
5+
6+// setErrno fails the way a C library call does: it returns -1 and sets errno.
7+int setErrno(void) {
8+ errno = ENOENT;
9+ return -1;
10+}

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