From a8bfa09bfc0b017ee8fa0410a66b6275e5c0ef69 Mon Sep 17 00:00:00 2001 From: Ricardo Branco Date: Mon, 15 Dec 2025 21:33:52 +0100 Subject: [PATCH] unix: add O_CLOFORK and related constants, support it in Dup3 POSIX.1-2024 adds close-on-fork: O_CLOFORK, FD_CLOFORK, F_DUPFD_CLOFORK, SOCK_CLOFORK and MSG_CMSG_CLOFORK. Add them, along with the system-specific F_DUP2FD_CLOFORK, F_DUP3FD and F_DUP3FD_SHIFT, on the systems that implement them: FreeBSD 15.0, NetBSD 11.0, OpenBSD 7.8, DragonFly BSD and illumos. The zerrors files were edited by hand. mkerrors.sh already matches these prefixes, so no generator change is needed. Regenerating would require running each new release on every architecture and would pull in many unrelated changes. The values were taken from these sources, and are the same on every architecture of an OS: FreeBSD release/15.0.0 sys/sys/fcntl.h, sys/sys/socket.h NetBSD netbsd-11 sys/sys/fcntl.h, sys/sys/socket.h OpenBSD 42a7be81bef7 sys/sys/fcntl.h, sys/sys/socket.h DragonFly dea249a60d18 sys/sys/fcntl.h, sys/sys/socket.h illumos 3581f14f537c uts/common/sys/fcntl.h, socket.h The OpenBSD commit is the one that added close-on-fork, ahead of 7.8. On FreeBSD and DragonFly, libc implements dup3 with F_DUP3FD and the flags shifted by F_DUP3FD_SHIFT. Do the same in Dup3 when O_CLOFORK is requested. Keep using F_DUP2FD and F_DUP2FD_CLOEXEC otherwise, so Dup3 keeps working on kernels without F_DUP3FD. Signed-off-by: Ricardo Branco --- unix/dup3_clofork_test.go | 66 +++++++++++++++++++++++++++++++++ unix/syscall_dragonfly.go | 18 +++++++-- unix/syscall_freebsd.go | 17 +++++++-- unix/zerrors_dragonfly_amd64.go | 8 ++++ unix/zerrors_freebsd_386.go | 7 ++++ unix/zerrors_freebsd_amd64.go | 7 ++++ unix/zerrors_freebsd_arm.go | 7 ++++ unix/zerrors_freebsd_arm64.go | 7 ++++ unix/zerrors_freebsd_riscv64.go | 7 ++++ unix/zerrors_netbsd_386.go | 5 +++ unix/zerrors_netbsd_amd64.go | 5 +++ unix/zerrors_netbsd_arm.go | 5 +++ unix/zerrors_netbsd_arm64.go | 5 +++ unix/zerrors_openbsd_386.go | 5 +++ unix/zerrors_openbsd_amd64.go | 5 +++ unix/zerrors_openbsd_arm.go | 5 +++ unix/zerrors_openbsd_arm64.go | 5 +++ unix/zerrors_openbsd_mips64.go | 5 +++ unix/zerrors_openbsd_ppc64.go | 5 +++ unix/zerrors_openbsd_riscv64.go | 5 +++ unix/zerrors_solaris_amd64.go | 6 +++ 21 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 unix/dup3_clofork_test.go diff --git a/unix/dup3_clofork_test.go b/unix/dup3_clofork_test.go new file mode 100644 index 000000000..cdbd7d832 --- /dev/null +++ b/unix/dup3_clofork_test.go @@ -0,0 +1,66 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build dragonfly || freebsd + +package unix_test + +import ( + "os" + "path/filepath" + "testing" + + "golang.org/x/sys/unix" +) + +func TestDup3Flags(t *testing.T) { + f, err := os.Create(filepath.Join(t.TempDir(), "TestDup3Flags")) + if err != nil { + t.Fatal(err) + } + defer f.Close() + oldFd := int(f.Fd()) + + tests := []struct { + name string + flags int + fdflag int + }{ + {"none", 0, 0}, + {"O_CLOEXEC", unix.O_CLOEXEC, unix.FD_CLOEXEC}, + {"O_CLOFORK", unix.O_CLOFORK, unix.FD_CLOFORK}, + {"O_CLOEXEC|O_CLOFORK", unix.O_CLOEXEC | unix.O_CLOFORK, unix.FD_CLOEXEC | unix.FD_CLOFORK}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + newFd, err := unix.Open(os.DevNull, unix.O_RDONLY, 0) + if err != nil { + t.Fatal(err) + } + defer unix.Close(newFd) + + err = unix.Dup3(oldFd, newFd, tt.flags) + if err == unix.EINVAL && tt.flags&unix.O_CLOFORK != 0 { + t.Skip("kernel does not support F_DUP3FD") + } + if err != nil { + t.Fatalf("Dup3(%d, %d, %#x): %v", oldFd, newFd, tt.flags, err) + } + got, err := unix.FcntlInt(uintptr(newFd), unix.F_GETFD, 0) + if err != nil { + t.Fatal(err) + } + if want := tt.fdflag; got != want { + t.Errorf("F_GETFD = %#x, want %#x", got, want) + } + }) + } + + if got, want := unix.Dup3(oldFd, oldFd, 0), unix.EINVAL; got != want { + t.Errorf("Dup3 with equal fds = %v, want %v", got, want) + } + if got, want := unix.Dup3(oldFd, 1000, unix.O_APPEND), unix.EINVAL; got != want { + t.Errorf("Dup3 with invalid flags = %v, want %v", got, want) + } +} diff --git a/unix/syscall_dragonfly.go b/unix/syscall_dragonfly.go index be8c00207..55a68db64 100644 --- a/unix/syscall_dragonfly.go +++ b/unix/syscall_dragonfly.go @@ -247,14 +247,24 @@ func Sendfile(outfd int, infd int, offset *int64, count int) (written int, err e } func Dup3(oldfd, newfd, flags int) error { - if oldfd == newfd || flags&^O_CLOEXEC != 0 { + if oldfd == newfd || flags&^(O_CLOEXEC|O_CLOFORK) != 0 { return EINVAL } - how := F_DUP2FD + if flags&O_CLOFORK == 0 { + // F_DUP3FD is not available on older kernels, so avoid it + // when possible. + how := F_DUP2FD + if flags&O_CLOEXEC != 0 { + how = F_DUP2FD_CLOEXEC + } + _, err := fcntl(oldfd, how, newfd) + return err + } + fdflags := FD_CLOFORK if flags&O_CLOEXEC != 0 { - how = F_DUP2FD_CLOEXEC + fdflags |= FD_CLOEXEC } - _, err := fcntl(oldfd, how, newfd) + _, err := fcntl(oldfd, F_DUP3FD|fdflags<