runtime: fix Darwin syscall return value truncation and lost arguments

Go 1.26 replaced the individual Darwin syscall entry points (syscall,
syscallX, syscallPtr, syscall6, syscall6X) with two variadic functions:
syscalln and rawsyscalln. The old wrappers now have Go bodies that call
these, then use errno/errnoX/errnoPtr to interpret the result.

This caused two bugs in our rawsyscalln implementation:

1. Return value truncation: We used call_syscall/call_syscall6 which
   return int32, truncating 64-bit results (pointers from fdopendir,
   offsets from lseek, addresses from mmap). For example, a DIR* pointer
   returned by fdopendir would lose its upper 32 bits on arm64, causing
   SIGSEGV when later accessed.

2. Lost 4th argument: The case 4/5/6 fallthrough had 'a3 = args[3]'
   instead of 'a4 = args[3]', and a3 was immediately overwritten by
   'a1, a2, a3 = args[0], args[1], args[2]'. This lost the 4th syscall
   argument entirely, breaking pread (offset=0) and causing wrong data
   to be read from files.

Fix by using call_syscallX/call_syscall6X (returning full uintptr),
always reading errno (letting the Go wrappers decide what to do with
it), and correcting the argument assignment.
This commit is contained in:
deadprogram
2026-04-10 16:13:07 +02:00
committed by Ron Evans
parent 9d29971755
commit 618c689246
+18 -23
View File
@@ -2,6 +2,17 @@
package runtime
// Go 1.26 replaced the individual Darwin syscall functions (syscall, syscallX,
// syscallPtr, syscall6, syscall6X) with two variadic entry points: syscalln and
// rawsyscalln. All the old wrappers now have Go bodies that call these and then
// interpret the result (errno, errnoX, errnoPtr). Because the callers decide
// how to check for errors, we must:
// - always return the full register-width result (use call_syscallX /
// call_syscall6X which return uintptr, not call_syscall / call_syscall6
// which truncate to int32), and
// - always read errno so that the Go wrappers have the value available when
// the error condition is met.
// syscall_syscalln is a wrapper around the libc call with variable arguments.
//
//go:nosplit
@@ -30,7 +41,9 @@ func syscall_rawsyscalln(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
a1 = args[0]
fallthrough
case 0:
return runtime_syscall(fn, a1, a2, a3)
r1 = call_syscallX(fn, a1, a2, a3)
err = uintptr(*libc_errno_location())
return
case 6:
a6 = args[5]
@@ -39,31 +52,13 @@ func syscall_rawsyscalln(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
a5 = args[4]
fallthrough
case 4:
a3 = args[3]
a4 = args[3]
a1, a2, a3 = args[0], args[1], args[2]
return runtime_syscall6(fn, a1, a2, a3, a4, a5, a6)
r1 = call_syscall6X(fn, a1, a2, a3, a4, a5, a6)
err = uintptr(*libc_errno_location())
return
}
panic("syscall args not handled")
}
func runtime_syscall(fn, a1, a2, a3 uintptr) (r1, r2, err uintptr) {
result := call_syscall(fn, a1, a2, a3)
r1 = uintptr(result)
if result == -1 {
// Syscall returns -1 on failure.
err = uintptr(*libc_errno_location())
}
return
}
func runtime_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr) {
result := call_syscall6(fn, a1, a2, a3, a4, a5, a6)
r1 = uintptr(result)
if result == -1 {
// Syscall returns -1 on failure.
err = uintptr(*libc_errno_location())
}
return
}