diff --git a/compiler/syscall.go b/compiler/syscall.go index 5172e78380..e77e675b8c 100644 --- a/compiler/syscall.go +++ b/compiler/syscall.go @@ -529,6 +529,11 @@ func (b *builder) createDarwinFuncPCABI0Call(instr *ssa.CallCommon) llvm.Value { // in C. name = "syscall_libc_open" } + if name == "fcntl" { + // Same for fcntl(), whose third parameter is variadic. See + // src/runtime/os_darwin.c for what goes wrong without the wrapper. + name = "syscall_libc_fcntl" + } if b.GOARCH == "amd64" { if name == "fdopendir" || name == "readdir_r" { // Hack to support amd64, which needs the $INODE64 suffix. diff --git a/src/os/fcntl_test.go b/src/os/fcntl_test.go new file mode 100644 index 0000000000..16258ecd1d --- /dev/null +++ b/src/os/fcntl_test.go @@ -0,0 +1,72 @@ +//go:build (darwin || linux) && !baremetal && !tinygo.wasm && !nintendoswitch + +package os_test + +import ( + . "os" + "syscall" + "testing" + "time" +) + +// fcntl takes its third parameter through a variadic list, so the call needs +// the C wrapper in src/runtime/os_darwin.c on darwin. + +// F_SETFL must reach fcntl with the value that the caller gave it. A read on +// an empty pipe returns EAGAIN when the descriptor is non-blocking, and blocks +// when the flag did not arrive. +func TestFcntlSetNonblock(t *testing.T) { + var fds [2]int + if err := syscall.Pipe(fds[:]); err != nil { + t.Fatalf("Pipe failed: %v", err) + } + defer syscall.Close(fds[0]) + defer syscall.Close(fds[1]) + + if err := syscall.SetNonblock(fds[0], true); err != nil { + t.Fatalf("SetNonblock failed: %v", err) + } + + type result struct { + n int + err error + } + done := make(chan result, 1) + go func() { + buf := make([]byte, 1) + n, err := syscall.Read(fds[0], buf) + done <- result{n, err} + }() + + select { + case r := <-done: + if r.err != syscall.EAGAIN { + t.Errorf("wanted EAGAIN from a read on an empty non-blocking pipe, got %d, %v", r.n, r.err) + } + case <-time.After(10 * time.Second): + t.Fatal("the read blocked, so the descriptor is still blocking") + } +} + +// The pointer commands go through the same wrapper as the int commands. A +// F_GETLK on a file that nobody locked reports F_UNLCK. +func TestFcntlGetLock(t *testing.T) { + f, err := CreateTemp(t.TempDir(), "fcntl") + if err != nil { + t.Fatalf("CreateTemp failed: %v", err) + } + defer f.Close() + + lk := syscall.Flock_t{ + Type: syscall.F_RDLCK, + Whence: 0, + Start: 0, + Len: 0, + } + if err := syscall.FcntlFlock(f.Fd(), syscall.F_GETLK, &lk); err != nil { + t.Fatalf("FcntlFlock(F_GETLK) failed: %v", err) + } + if lk.Type != syscall.F_UNLCK { + t.Errorf("wanted F_UNLCK on an unlocked file, got %d", lk.Type) + } +} diff --git a/src/runtime/os_darwin.c b/src/runtime/os_darwin.c index 5d7cd7c71d..5aa022e2e4 100644 --- a/src/runtime/os_darwin.c +++ b/src/runtime/os_darwin.c @@ -3,6 +3,7 @@ // This file is included in the build, despite the //go:build line above. #include +#include // Wrapper function because 'open' is a variadic function and variadic functions // use a different (incompatible) calling convention on darwin/arm64. @@ -12,6 +13,18 @@ int syscall_libc_open(const char *pathname, int flags, mode_t mode) { return open(pathname, flags, mode); } +// Wrapper function for 'fcntl', whose third parameter is variadic as well. +// A call through a plain three-argument function pointer makes the callee read +// that argument from the stack, which holds an unrelated value. +// +// The argument is a uintptr_t so that the pointer commands reached through +// syscall.fcntlPtr use the same wrapper. Both spellings share +// libc_fcntl_trampoline, and on a little-endian target the int commands read +// the low half of the same stack slot. +int syscall_libc_fcntl(int fd, int cmd, uintptr_t arg) { + return fcntl(fd, cmd, arg); +} + // The following functions are called by the runtime because Go can't call // function pointers directly.