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 Unverified
B5690EEEBB952194The key is unknown
14 files · +102 -10
M dlfcn_test.go +30 -0
| @@ -8,6 +8,7 @@ package purego_test | |||
| 8 | 8 | import ( | |
| 9 | 9 | "os" | |
| 10 | 10 | "path/filepath" | |
| 11 | + | "syscall" | |
| 11 | 12 | "testing" | |
| 12 | 13 | "unsafe" | |
| 13 | 14 | ||
| @@ -48,3 +49,32 @@ func TestSyscallN(t *testing.T) { | |||
| 48 | 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 | + | } | |
| @@ -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 | |||
| 129 | 129 | MOVL PTR_ADDRESS(SP), BX | |
| 130 | 130 | MOVL AX, syscallArgs_a1(BX) // return value r1 | |
| 131 | 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 | 134 | // Save x87 FPU return value (ST0) to f1 field | |
| 134 | 135 | // 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), BX | 129 | MOVL PTR_ADDRESS(SP), BX | ||
| 130 | MOVL AX, syscallArgs_a1(BX) // return value r1 | 130 | 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 field | 134 | // 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 | |||
| 127 | 127 | MOVQ PTR_ADDRESS(SP), DI // reload (DI clobbered by call) | |
| 128 | 128 | MOVQ (AX), AX | |
| 129 | 129 | MOVQ AX, syscallArgs_a3(DI) // save errno | |
| 130 | + | #else | |
| 131 | + | MOVQ $0, syscallArgs_a3(DI) // no errno here: clear the input argument | |
| 130 | 132 | #endif | |
| 131 | 133 | ||
| 132 | 134 | 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), AX | 128 | MOVQ (AX), AX | ||
| 129 | MOVQ AX, syscallArgs_a3(DI) // save errno | 129 | MOVQ AX, syscallArgs_a3(DI) // save errno | ||
| 130 | + | #else | |||
| 131 | + | MOVQ $0, syscallArgs_a3(DI) // no errno here: clear the input argument | |||
| 130 | #endif | 132 | #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: | |||
| 135 | 135 | // save R0, R1 | |
| 136 | 136 | MOVW R0, syscallArgs_a1(R8) | |
| 137 | 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 | 141 | MOVB runtime·goarmsoftfp(SB), R11 | |
| 140 | 142 | CMP $0, R11 | |
| @@ -135,6 +135,8 @@ skipfpload: | |||||
| 135 | // save R0, R1 | 135 | // 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), R11 | 141 | MOVB runtime·goarmsoftfp(SB), R11 | ||
| 140 | CMP $0, R11 | 142 | CMP $0, R11 | ||
M sys_arm64.s +2 -1
| @@ -127,6 +127,7 @@ TEXT syscallX(SB), NOSPLIT, $0 | |||
| 127 | 127 | BL purego_error(SB) | |
| 128 | 128 | MOVD (R0), R0 | |
| 129 | 129 | MOVD R0, syscallArgs_a3(R2) // save errno | |
| 130 | - | ||
| 130 | + | #else | |
| 131 | + | MOVD $0, syscallArgs_a3(R2) // no errno here: clear the input argument | |
| 131 | 132 | #endif | |
| 132 | 133 | RET | |
| @@ -127,6 +127,7 @@ TEXT syscallX(SB), NOSPLIT, $0 | |||||
| 127 | BL purego_error(SB) | 127 | BL purego_error(SB) | ||
| 128 | MOVD (R0), R0 | 128 | MOVD (R0), R0 | ||
| 129 | MOVD R0, syscallArgs_a3(R2) // save errno | 129 | MOVD R0, syscallArgs_a3(R2) // save errno | ||
| 130 | - | 130 | + | #else | |
| 131 | + | MOVD $0, syscallArgs_a3(R2) // no errno here: clear the input argument | |||
| 131 | #endif | 132 | #endif | ||
| 132 | RET | 133 | RET | ||
M sys_loong64.s +1 -0
| @@ -121,6 +121,7 @@ TEXT syscallX(SB), NOSPLIT, $0 | |||
| 121 | 121 | // save R4, R5 | |
| 122 | 122 | MOVV R4, syscallArgs_a1(R13) | |
| 123 | 123 | MOVV R5, syscallArgs_a2(R13) | |
| 124 | + | MOVV R0, syscallArgs_a3(R13) // no errno here: clear the input argument | |
| 124 | 125 | ||
| 125 | 126 | // save f0-f3 | |
| 126 | 127 | MOVD F0, syscallArgs_f1(R13) | |
| @@ -121,6 +121,7 @@ TEXT syscallX(SB), NOSPLIT, $0 | |||||
| 121 | // save R4, R5 | 121 | // 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-f3 | 126 | // 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 | |||
| 106 | 106 | // Store integer results back (R3, R4) | |
| 107 | 107 | MOVD R3, syscallArgs_a1(R11) | |
| 108 | 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 | 112 | // Store float return values F1-F8 (an HFA may span up to eight). | |
| 111 | 113 | 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 | |||
| 118 | 118 | // Store results back | |
| 119 | 119 | MOV X10, syscallArgs_a1(X9) | |
| 120 | 120 | MOV X11, syscallArgs_a2(X9) | |
| 121 | + | MOV X0, syscallArgs_a3(X9) // no errno here: clear the input argument | |
| 121 | 122 | ||
| 122 | 123 | // Store back float return regs if used by your ABI contract | |
| 123 | 124 | MOVD F10, syscallArgs_f1(X9) | |
| @@ -118,6 +118,7 @@ TEXT syscallX(SB), NOSPLIT, $0 | |||||
| 118 | // Store results back | 118 | // 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 contract | 123 | // 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 | |||
| 131 | 131 | // Store integer results back (R2, R3) | |
| 132 | 132 | MOVD R2, syscallArgs_a1(R9) | |
| 133 | 133 | MOVD R3, syscallArgs_a2(R9) | |
| 134 | + | MOVD $0, syscallArgs_a3(R9) // no errno here: clear the input argument | |
| 134 | 135 | ||
| 135 | 136 | // Store float return values (F0, F2, F4, F6) | |
| 136 | 137 | 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 ( | |||
| 18 | 18 | // syscallArgs is the argument block handed to the syscall15 trampoline. On | |
| 19 | 19 | // platforms that go through internal/cgo it is passed to C as a | |
| 20 | 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 | 25 | type syscallArgs struct { | |
| 22 | 26 | _ structs.HostLayout | |
| 23 | 27 | ||
| @@ -49,7 +53,8 @@ func syscall_SyscallN(fn uintptr, sysargs []uintptr, floats []uintptr, r8 uintpt | |||
| 49 | 53 | ||
| 50 | 54 | // SyscallN takes fn, a C function pointer and a list of arguments as uintptr. | |
| 51 | 55 | // 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 | 59 | // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer] | |
| 55 | 60 | // especially point 4. | |
| @@ -18,6 +18,10 @@ const ( | |||||
| 18 | // syscallArgs is the argument block handed to the syscall15 trampoline. On | 18 | // syscallArgs is the argument block handed to the syscall15 trampoline. On | ||
| 19 | // platforms that go through internal/cgo it is passed to C as a | 19 | // 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.HostLayout | 26 | _ 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 panics | 55 | // 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 ( | |||
| 18 | 18 | // syscallArgs is the argument block handed to the syscall15 trampoline. On | |
| 19 | 19 | // platforms that go through internal/cgo it is passed to C as a | |
| 20 | 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 | 25 | type syscallArgs struct { | |
| 22 | 26 | _ structs.HostLayout | |
| 23 | 27 | ||
| @@ -51,7 +55,8 @@ func syscall_SyscallN(fn uintptr, sysargs []uintptr, floats []uintptr, r8 uintpt | |||
| 51 | 55 | ||
| 52 | 56 | // SyscallN takes fn, a C function pointer and a list of arguments as uintptr. | |
| 53 | 57 | // 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 | 61 | // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer] | |
| 57 | 62 | // especially point 4. | |
| @@ -18,6 +18,10 @@ const ( | |||||
| 18 | // syscallArgs is the argument block handed to the syscall15 trampoline. On | 18 | // syscallArgs is the argument block handed to the syscall15 trampoline. On | ||
| 19 | // platforms that go through internal/cgo it is passed to C as a | 19 | // 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.HostLayout | 26 | _ 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 panics | 57 | // 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 | |||
| 37 | 37 | ||
| 38 | 38 | // SyscallN takes fn, a C function pointer and a list of arguments as uintptr. | |
| 39 | 39 | // 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 | 43 | // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer] | |
| 43 | 44 | // 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 panics | 39 | // 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 | |||
| 5 | 5 | ||
| 6 | 6 | import ( | |
| 7 | 7 | "os" | |
| 8 | + | "path/filepath" | |
| 8 | 9 | "runtime" | |
| 9 | 10 | "testing" | |
| 10 | - | "unsafe" | |
| 11 | 11 | ||
| 12 | 12 | "github.com/ebitengine/purego" | |
| 13 | 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 | 43 | func TestErrno(t *testing.T) { | |
| 29 | - | if runtime.GOOS != "darwin" { | |
| 44 | + | if !errnoIsCaptured() { | |
| 30 | 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 | 55 | if err != nil { | |
| 35 | 56 | t.Fatal(err) | |
| 36 | 57 | } | |
| 37 | 58 | ||
| 38 | - | openSym, err := load.OpenSymbol(libc, "open") | |
| 59 | + | setErrno, err := load.OpenSymbol(lib, "setErrno") | |
| 39 | 60 | if err != nil { | |
| 40 | 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 | 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 | 78 | 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) string | 78 | 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 | + | } | |||