From ddacdfacd53893249da37d40a822aba68283fd81 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 10 Apr 2026 12:15:58 +0200 Subject: [PATCH] compiler, runtime: fix SyscallN handling for Windows on Go 1.26+ Go 1.26 changed all Windows syscall wrappers in zsyscall_windows.go to use SyscallN instead of fixed-argument Syscall/Syscall6/etc. The SyscallN function now has a body that calls an unexported syscalln function (provided by runtime via //go:linkname). TinyGo's existing createSyscall compiler builtin used call.Args[2:] to extract syscall arguments, but for variadic SyscallN the SSA representation passes args as a slice value (not individual args), causing call.Args[2:] to be empty -- resulting in zero arguments being passed to Windows API calls and 0xc0000005 access violations. Fix this by: 1. Excluding syscall.SyscallN from builtin interception, letting Go 1.26's function body compile normally (it calls syscalln) 2. Adding a new createSyscalln compiler builtin that intercepts syscall.syscalln and correctly handles the variadic slice: - Generates a switch on the arg count n (0-18 cases) - Each case loads args from the slice via GEP/Load - Wraps calls with SetLastError(0)/GetLastError() as before - Handles i386 stdcall conventions 3. Adding runtime stubs for both Go versions: - go1.26: syscall.syscalln stub (body intercepted by compiler) - pre-go1.26: syscall.SyscallN stub (linker satisfaction) --- compiler/compiler.go | 6 +- compiler/syscall.go | 124 +++++++++++++++++++++++++++ src/runtime/runtime_windows_go125.go | 18 ++++ src/runtime/runtime_windows_go126.go | 17 ++++ 4 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 src/runtime/runtime_windows_go125.go create mode 100644 src/runtime/runtime_windows_go126.go diff --git a/compiler/compiler.go b/compiler/compiler.go index a4c2d2e8a..45e6c8a54 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -1980,10 +1980,14 @@ func (b *builder) createFunctionCall(instr *ssa.CallCommon) (llvm.Value, error) return b.emitSV64Call(instr.Args, getPos(instr)) case strings.HasPrefix(name, "(device/riscv.CSR)."): return b.emitCSROperation(instr) - case strings.HasPrefix(name, "syscall.Syscall") || strings.HasPrefix(name, "syscall.RawSyscall") || strings.HasPrefix(name, "golang.org/x/sys/unix.Syscall") || strings.HasPrefix(name, "golang.org/x/sys/unix.RawSyscall"): + case (strings.HasPrefix(name, "syscall.Syscall") || strings.HasPrefix(name, "syscall.RawSyscall") || strings.HasPrefix(name, "golang.org/x/sys/unix.Syscall") || strings.HasPrefix(name, "golang.org/x/sys/unix.RawSyscall")) && name != "syscall.SyscallN": if b.GOOS != "darwin" { return b.createSyscall(instr) } + case name == "syscall.syscalln": + if b.GOOS == "windows" { + return b.createSyscalln(instr) + } case strings.HasPrefix(name, "syscall.rawSyscallNoError") || strings.HasPrefix(name, "golang.org/x/sys/unix.RawSyscallNoError"): return b.createRawSyscallNoError(instr) case name == "runtime.supportsRecover": diff --git a/compiler/syscall.go b/compiler/syscall.go index 23a64f5b1..946acdecb 100644 --- a/compiler/syscall.go +++ b/compiler/syscall.go @@ -349,6 +349,130 @@ func (b *builder) createSyscall(call *ssa.CallCommon) (llvm.Value, error) { } } +// createSyscalln emits instructions for the syscall.syscalln function on +// Windows. This handles the variadic calling convention used in Go 1.26+: +// +// func syscalln(fn, n uintptr, args ...uintptr) (r1, r2 uintptr, err Errno) +// +// The function generates a switch on n to dispatch to the correct fixed-argument +// function pointer call with SetLastError(0)/GetLastError() wrapping. +func (b *builder) createSyscalln(call *ssa.CallCommon) (llvm.Value, error) { + const maxArgs = 18 // Windows syscalls support up to 18 args + + isI386 := strings.HasPrefix(b.Triple, "i386-") + + // Get the function pointer (call.Args[0]) and n (call.Args[1]). + fn := b.getValue(call.Args[0], getPos(call)) + fnPtr := b.CreateIntToPtr(fn, b.dataPtrType, "") + n := b.getValue(call.Args[1], getPos(call)) + + // Get the variadic args slice (call.Args[2]). + // In SSA, the variadic slice is the third argument. + var argsPtr llvm.Value + if len(call.Args) > 2 { + argsSlice := b.getValue(call.Args[2], getPos(call)) + argsPtr = b.CreateExtractValue(argsSlice, 0, "args.data") + } else { + argsPtr = llvm.ConstNull(b.dataPtrType) + } + + // Prepare SetLastError and GetLastError. + setLastError := b.mod.NamedFunction("SetLastError") + if setLastError.IsNil() { + llvmType := llvm.FunctionType(b.ctx.VoidType(), []llvm.Type{b.ctx.Int32Type()}, false) + setLastError = llvm.AddFunction(b.mod, "SetLastError", llvmType) + if isI386 { + setLastError.SetFunctionCallConv(llvm.X86StdcallCallConv) + } + } + getLastError := b.mod.NamedFunction("GetLastError") + if getLastError.IsNil() { + llvmType := llvm.FunctionType(b.ctx.Int32Type(), nil, false) + getLastError = llvm.AddFunction(b.mod, "GetLastError", llvmType) + if isI386 { + getLastError.SetFunctionCallConv(llvm.X86StdcallCallConv) + } + } + + retType := b.ctx.StructType([]llvm.Type{b.uintptrType, b.uintptrType, b.uintptrType}, false) + + // Create the merge block where all cases converge. + mergeBB := b.insertBasicBlock("syscalln.merge") + + // Create the default (panic) block. + panicBB := b.insertBasicBlock("syscalln.panic") + + // Create the switch on n. + sw := b.CreateSwitch(n, panicBB, maxArgs+1) + + // We'll collect blocks and values for the PHI node. + var incomingVals []llvm.Value + var incomingBlocks []llvm.BasicBlock + + // Generate a case for each arg count 0..maxArgs. + for i := 0; i <= maxArgs; i++ { + caseBB := b.insertBasicBlock("syscalln.case" + strconv.Itoa(i)) + sw.AddCase(llvm.ConstInt(b.uintptrType, uint64(i), false), caseBB) + b.SetInsertPointAtEnd(caseBB) + + // Load args[0] through args[i-1] from the slice data pointer. + var params []llvm.Value + var paramTypes []llvm.Type + for j := 0; j < i; j++ { + gep := b.CreateInBoundsGEP(b.uintptrType, argsPtr, []llvm.Value{ + llvm.ConstInt(b.ctx.Int32Type(), uint64(j), false), + }, "") + arg := b.CreateLoad(b.uintptrType, gep, "") + params = append(params, arg) + paramTypes = append(paramTypes, b.uintptrType) + } + + // SetLastError(0) + setCall := b.CreateCall(setLastError.GlobalValueType(), setLastError, []llvm.Value{llvm.ConstNull(b.ctx.Int32Type())}, "") + var sp llvm.Value + if isI386 { + setCall.SetInstructionCallConv(llvm.X86StdcallCallConv) + sp = b.readStackPointer() + } + + // Call fn(args...) + fnType := llvm.FunctionType(b.uintptrType, paramTypes, false) + syscallResult := b.CreateCall(fnType, fnPtr, params, "") + if isI386 { + syscallResult.SetInstructionCallConv(llvm.X86StdcallCallConv) + b.writeStackPointer(sp) + } + + // err = GetLastError() + errResult := b.CreateCall(getLastError.GlobalValueType(), getLastError, nil, "err") + if isI386 { + errResult.SetInstructionCallConv(llvm.X86StdcallCallConv) + } + if b.uintptrType != b.ctx.Int32Type() { + errResult = b.CreateZExt(errResult, b.uintptrType, "err.uintptr") + } + + // Build {r1, 0, err} + result := llvm.ConstNull(retType) + result = b.CreateInsertValue(result, syscallResult, 0, "") + result = b.CreateInsertValue(result, errResult, 2, "") + + incomingVals = append(incomingVals, result) + incomingBlocks = append(incomingBlocks, b.Builder.GetInsertBlock()) + b.CreateBr(mergeBB) + } + + // Panic block for n > maxArgs. + b.SetInsertPointAtEnd(panicBB) + b.CreateUnreachable() + + // Merge block: PHI node to select the result. + b.SetInsertPointAtEnd(mergeBB) + phi := b.CreatePHI(retType, "syscalln.result") + phi.AddIncoming(incomingVals, incomingBlocks) + return phi, nil +} + // createRawSyscallNoError emits instructions for the Linux-specific // syscall.rawSyscallNoError function. func (b *builder) createRawSyscallNoError(call *ssa.CallCommon) (llvm.Value, error) { diff --git a/src/runtime/runtime_windows_go125.go b/src/runtime/runtime_windows_go125.go new file mode 100644 index 000000000..593e3fd0f --- /dev/null +++ b/src/runtime/runtime_windows_go125.go @@ -0,0 +1,18 @@ +//go:build windows && !go1.26 + +package runtime + +// For Go < 1.26, SyscallN is declared without a body in +// syscall/dll_windows.go. The standard Go runtime provides its implementation +// via //go:linkname. TinyGo must provide one as well. +// +// Note: The TinyGo compiler cannot correctly handle SyscallN as a builtin +// because it uses variadic arguments represented as a slice in SSA, which +// the fixed-argument builtin mechanism cannot process. This implementation +// is provided to satisfy the linker for code paths that reference SyscallN +// (e.g., syscall.Proc.Call). + +//go:linkname syscall_SyscallN syscall.SyscallN +func syscall_SyscallN(trap uintptr, args ...uintptr) (r1, r2, err uintptr) { + panic("syscall.SyscallN is not yet supported in TinyGo for Go < 1.26") +} diff --git a/src/runtime/runtime_windows_go126.go b/src/runtime/runtime_windows_go126.go new file mode 100644 index 000000000..8918e49ef --- /dev/null +++ b/src/runtime/runtime_windows_go126.go @@ -0,0 +1,17 @@ +//go:build windows && go1.26 + +package runtime + +// Starting with Go 1.26, the syscall package on Windows defines function bodies +// for Syscall, SyscallN, etc., that all call syscalln (lowercase). In standard +// Go, syscalln is provided by the runtime via //go:linkname. TinyGo's compiler +// intercepts calls to syscall.syscalln and replaces them with inline LLVM IR +// (see compiler/syscall.go createSyscalln), so this function body is never +// actually called at runtime. However, the compiled function bodies in the +// syscall package still reference it, so we must provide a definition to +// satisfy the linker. + +//go:linkname syscall_syscalln syscall.syscalln +func syscall_syscalln(fn, n uintptr, args ...uintptr) (r1, r2, err uintptr) { + panic("unreachable: syscall.syscalln should be handled by the compiler") +}