compiler,runtime: call fcntl through a C wrapper on darwin - #5632
Open
yohimik wants to merge 1 commit into
Open
Conversation
This was referenced Aug 30, 2026
The third parameter of fcntl is variadic, and on darwin/arm64 a variadic argument goes on the stack and not in a register. A call to libc fcntl through a plain three-argument function pointer thus makes the callee read that argument from an unrelated stack slot. open() already has a C wrapper for the same reason. The symptom is quiet. fcntl(fd, F_SETFD, FD_CLOEXEC) sets the flag or does not, which depends on the stack contents, so the result is the same for one binary and different between binaries. syscall.CloseOnExec is the main caller, so when it fails, every descriptor of the program goes into every process that it starts. A child that holds a copy of the write end of a pipe keeps that pipe from a report of EOF, which is how os/exec collects the output of a command. Measured on macOS 26.6 arm64 before this change, fcntl(fd, F_DUPFD, 100) returns EINVAL, and three F_SETFL calls with 0x4, 0x0 and 0x8 all leave F_GETFL with 0x48. The wrapper takes the argument as a uintptr_t so that the pointer commands reached through syscall.fcntlPtr use it too. Both spellings go through libc_fcntl_trampoline, and on a little-endian target the int commands read the low half of the same stack slot. The new tests in src/os cover both shapes. TestFcntlSetNonblock fails on darwin before this change and passes after it.
yohimik
force-pushed
the
upstream-pr/darwin-fcntl
branch
from
September 2, 2026 08:50
c3bdc58 to
9161a8c
Compare
Author
|
Rebased on dev after the 0.42.0 release. The problem is present in v0.42.0 as |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
compiler,runtime: call fcntl through a C wrapper on darwin
What this does
The third parameter of
fcntlis variadic, and on darwin/arm64 a variadicargument goes on the stack and not in a register. A call to libc
fcntlthrougha plain three-argument function pointer thus makes the callee read that argument
from an unrelated stack slot.
open()already has a C wrapper insrc/runtime/os_darwin.cfor the same reason, and this adds the matching onefor
fcntl.The wrapper takes the argument as a
uintptr_tso that the pointer commandsreached through
syscall.fcntlPtruse it too. Both spellings go throughlibc_fcntl_trampoline, and on a little-endian target the int commands read thelow half of the same stack slot.
Why it matters
The failure is quiet.
fcntl(fd, F_SETFD, FD_CLOEXEC)sets the flag or doesnot, which depends on the stack contents, so the result is the same for one
binary and different between binaries.
syscall.CloseOnExecis the main caller,so when it fails, every descriptor of the program goes into every process that
it starts, and a child that holds a copy of the write end of a pipe keeps that
pipe from a report of EOF.
Evidence
Measured on macOS 26.6 arm64 before this change.
fcntl(fd, F_DUPFD, 100)returns EINVAL.F_SETFLcalls with 0x4, 0x0 and 0x8 all leaveF_GETFLreading 0x48.Two tests are added to
src/os/fcntl_test.go. Theospackage is already inTEST_PACKAGES_FAST, so both run in the linux and the macOS CI jobs with nomakefile change.
TestFcntlSetNonblockF_SETFLround trip. A read on an empty non-blocking pipe must return EAGAIN. It fails on darwin before this change.TestFcntlGetLockF_GETLKon an unlocked file must reportF_UNLCK.Both pass on macOS 26.6 arm64 with this change. Linux is unaffected, because it
does not use the darwin trampoline path, and the tests are a regression guard
there.
A downstream product also ships binaries built with a fork that carries this
change, in the published release dispat v1.4.0.
https://github.com/yohimik/dispat/releases/tag/services%2Fdispat%2Fv1.4.0
Scope
compiler/syscall.goonly changescreateDarwinFuncPCABI0Call, so no othertarget sees a difference.
Related
This is a prerequisite for correct process creation on darwin.
os.Pipeondarwin has no
pipe2, so it must mark both ends close-on-exec withfcntl.Related pull requests
This change is part of one body of work. Together the changes make programs that use the network and child processes work on hosted linux and macOS. A full CLI was tested end to end with all of them and ships binaries built this way, see dispat v1.4.0 in the evidence section.
In this repository
In tinygo-org/net
A merge order that works. The three bug fixes are independent. #5633 goes before #5635. HTTPS on linux needs only #5633 and #5635. Full darwin support also needs #5636, the net changes and a new src/net submodule pin.