diff --git a/builder/darwin-libsystem.go b/builder/darwin-libsystem.go index e47b54230e..75d1efcc25 100644 --- a/builder/darwin-libsystem.go +++ b/builder/darwin-libsystem.go @@ -1,6 +1,7 @@ package builder import ( + "os" "path/filepath" "strings" @@ -8,6 +9,29 @@ import ( "github.com/tinygo-org/tinygo/goenv" ) +// Symbols that libSystem exports but the minimal macOS SDK in +// lib/macos-minimal-sdk does not declare. Its generator reads a fixed list of +// headers that does not contain , so these names are absent from the +// generated libSystem.s. The stubs below have the same form as the generated +// ones, because the linker only needs to know that the names are in +// libSystem.B.dylib. +var darwinExtraLibSystemSymbols = []string{ + // The posix_spawn family, which src/os uses to start processes. + // posix_spawn_file_actions_addchdir_np came with macOS 10.15, so a binary + // from this toolchain needs at least that release. + "posix_spawn", + "posix_spawn_file_actions_addchdir_np", + "posix_spawn_file_actions_addclose", + "posix_spawn_file_actions_adddup2", + "posix_spawn_file_actions_destroy", + "posix_spawn_file_actions_init", + "posix_spawnattr_destroy", + "posix_spawnattr_init", + "posix_spawnattr_setflags", + "posix_spawnattr_setpgroup", + "posix_spawnattr_setsigmask", +} + // Create a job that builds a Darwin libSystem.dylib stub library. This library // contains all the symbols needed so that we can link against it, but it // doesn't contain any real symbol implementations. @@ -36,7 +60,34 @@ func makeDarwinLibSystemJob(config *compileopts.Config, tmpdir string) *compileJ return err } - // Link object file to dynamic library. + // Compile the extra stubs into a second object file, so that the + // generated one stays as it is. + extrapath := filepath.Join(tmpdir, "libSystem-extra.s") + extraobjpath := filepath.Join(tmpdir, "libSystem-extra.o") + var extra strings.Builder + extra.WriteString("// Stubs for symbols exported by libSystem but not declared in lib/macos-minimal-sdk.\n") + for _, symbol := range darwinExtraLibSystemSymbols { + extra.WriteString("\n.global _" + symbol + "\n_" + symbol + ":\n") + } + if err := os.WriteFile(extrapath, []byte(extra.String()), 0o666); err != nil { + return err + } + flags = []string{ + "-nostdlib", + "--target=" + config.Triple(), + "-c", + "-o", extraobjpath, + extrapath, + } + if config.Options.PrintCommands != nil { + config.Options.PrintCommands("clang", flags...) + } + err = runCCompiler(flags...) + if err != nil { + return err + } + + // Link object files to dynamic library. platformVersion := strings.TrimPrefix(strings.Split(config.Triple(), "-")[2], "macosx") flags = []string{ "-flavor", "darwin", @@ -48,6 +99,7 @@ func makeDarwinLibSystemJob(config *compileopts.Config, tmpdir string) *compileJ "-install_name", "/usr/lib/libSystem.B.dylib", "-o", job.result, objpath, + extraobjpath, } if config.Options.PrintCommands != nil { config.Options.PrintCommands("ld.lld", flags...) 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/exec.go b/src/os/exec.go index 28406f916b..cc00a10e0e 100644 --- a/src/os/exec.go +++ b/src/os/exec.go @@ -5,6 +5,9 @@ import ( "syscall" ) +// Errors StartProcess returns for a ProcAttr that it cannot honour. On a +// hosted OS only ErrNotImplementedSys is reachable. The other two stay because +// they are part of the exported API of this package. var ( ErrNotImplementedDir = errors.New("directory setting not implemented") ErrNotImplementedSys = errors.New("sys setting not implemented") @@ -36,35 +39,12 @@ type ProcAttr struct { // ErrProcessDone indicates a Process has finished. var ErrProcessDone = errors.New("os: process already finished") -type ProcessState struct { -} - -func (p *ProcessState) String() string { - return "" // TODO -} -func (p *ProcessState) Success() bool { - return false // TODO -} - -// Sys returns system-dependent exit information about -// the process. Convert it to the appropriate underlying -// type, such as syscall.WaitStatus on Unix, to access its contents. -func (p *ProcessState) Sys() interface{} { - return nil // TODO -} - -func (p *ProcessState) Exited() bool { - return false // TODO -} - -// ExitCode returns the exit code of the exited process, or -1 -// if the process hasn't exited or was terminated by a signal. -func (p *ProcessState) ExitCode() int { - return -1 // TODO -} - type Process struct { Pid int + + // done reports whether Wait reaped this process. A signal to a reaped pid + // is unsafe, because the number can belong to an unrelated process. + done int32 } // StartProcess starts a new process with the program, arguments and attributes specified by name, argv and attr. @@ -73,21 +53,6 @@ func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error) return startProcess(name, argv, attr) } -func (p *Process) Wait() (*ProcessState, error) { - if p.Pid == -1 { - return nil, syscall.EINVAL - } - return nil, ErrNotImplemented -} - -func (p *Process) Kill() error { - return ErrNotImplemented -} - -func (p *Process) Signal(sig Signal) error { - return ErrNotImplemented -} - func Ignore(sig ...Signal) { // leave all the signals unaltered return diff --git a/src/os/exec_linux.go b/src/os/exec_linux.go deleted file mode 100644 index 6914a2c285..0000000000 --- a/src/os/exec_linux.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2009 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 linux && !baremetal && !tinygo.wasm && !nintendoswitch - -package os - -import ( - "errors" - "runtime" - "syscall" -) - -// The only signal values guaranteed to be present in the os package on all -// systems are os.Interrupt (send the process an interrupt) and os.Kill (force -// the process to exit). On Windows, sending os.Interrupt to a process with -// os.Process.Signal is not implemented; it will return an error instead of -// sending a signal. -var ( - Interrupt Signal = syscall.SIGINT - Kill Signal = syscall.SIGKILL -) - -// Keep compatible with golang and always succeed and return new proc with pid on Linux. -func findProcess(pid int) (*Process, error) { - return &Process{Pid: pid}, nil -} - -func (p *Process) release() error { - // NOOP for unix. - p.Pid = -1 - // no need for a finalizer anymore - runtime.SetFinalizer(p, nil) - return nil -} - -// This function is a wrapper around the forkExec function, which is a wrapper around the fork and execve system calls. -// The StartProcess function creates a new process by forking the current process and then calling execve to replace the current process with the new process. -// It thereby replaces the newly created process with the specified command and arguments. -// Differences to upstream golang implementation (https://cs.opensource.google/go/go/+/master:src/syscall/exec_unix.go;l=143): -// * No setting of Process Attributes -// * Ignoring Ctty -// * No ForkLocking (might be introduced by #4273) -// * No parent-child communication via pipes (TODO) -// * No waiting for crashes child processes to prohibit zombie process accumulation / Wait status checking (TODO) -func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error) { - if argv == nil { - return 0, errors.New("exec: no argv") - } - - if len(argv) == 0 { - return 0, errors.New("exec: no argv") - } - - if attr == nil { - attr = new(ProcAttr) - } - - p, err := fork() - pid = int(p) - - if err != nil { - return 0, err - } - - // else code runs in child, which then should exec the new process - err = execve(argv0, argv, attr.Env) - if err != nil { - // exec failed - return 0, err - } - // 3. TODO: use pipes to communicate back child status - return pid, nil -} - -// In Golang, the idiomatic way to create a new process is to use the StartProcess function. -// Since the Model of operating system processes in tinygo differs from the one in Golang, we need to implement the StartProcess function differently. -// The startProcess function is a wrapper around the forkExec function, which is a wrapper around the fork and execve system calls. -// The StartProcess function creates a new process by forking the current process and then calling execve to replace the current process with the new process. -// It thereby replaces the newly created process with the specified command and arguments. -func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err error) { - if attr != nil { - if attr.Dir != "" { - return nil, ErrNotImplementedDir - } - - if attr.Sys != nil { - return nil, ErrNotImplementedSys - } - - if len(attr.Files) != 0 { - return nil, ErrNotImplementedFiles - } - } - - pid, err := forkExec(name, argv, attr) - if err != nil { - return nil, err - } - - return findProcess(pid) -} diff --git a/src/os/exec_linux_test.go b/src/os/exec_linux_test.go deleted file mode 100644 index 34f1fef983..0000000000 --- a/src/os/exec_linux_test.go +++ /dev/null @@ -1,78 +0,0 @@ -//go:build linux && !baremetal && !tinygo.wasm - -package os_test - -import ( - "errors" - . "os" - "runtime" - "syscall" - "testing" -) - -// Test the functionality of the forkExec function, which is used to fork and exec a new process. -// This test is not run on Windows, as forkExec is not supported on Windows. -// This test is not run on Plan 9, as forkExec is not supported on Plan 9. -func TestForkExec(t *testing.T) { - if runtime.GOOS != "linux" { - t.Logf("skipping test on %s", runtime.GOOS) - return - } - - proc, err := StartProcess("/bin/echo", []string{"hello", "world"}, &ProcAttr{}) - if !errors.Is(err, nil) { - t.Fatalf("forkExec failed: %v", err) - } - - if proc == nil { - t.Fatalf("proc is nil") - } - - if proc.Pid == 0 { - t.Fatalf("forkExec failed: new process has pid 0") - } -} - -func TestForkExecErrNotExist(t *testing.T) { - proc, err := StartProcess("invalid", []string{"invalid"}, &ProcAttr{}) - if !errors.Is(err, ErrNotExist) { - t.Fatalf("wanted ErrNotExist, got %s\n", err) - } - - if proc != nil { - t.Fatalf("wanted nil, got %v\n", proc) - } -} - -func TestForkExecProcDir(t *testing.T) { - proc, err := StartProcess("/bin/echo", []string{"hello", "world"}, &ProcAttr{Dir: "dir"}) - if !errors.Is(err, ErrNotImplementedDir) { - t.Fatalf("wanted ErrNotImplementedDir, got %v\n", err) - } - - if proc != nil { - t.Fatalf("wanted nil, got %v\n", proc) - } -} - -func TestForkExecProcSys(t *testing.T) { - proc, err := StartProcess("/bin/echo", []string{"hello", "world"}, &ProcAttr{Sys: &syscall.SysProcAttr{}}) - if !errors.Is(err, ErrNotImplementedSys) { - t.Fatalf("wanted ErrNotImplementedSys, got %v\n", err) - } - - if proc != nil { - t.Fatalf("wanted nil, got %v\n", proc) - } -} - -func TestForkExecProcFiles(t *testing.T) { - proc, err := StartProcess("/bin/echo", []string{"hello", "world"}, &ProcAttr{Files: []*File{}}) - if !errors.Is(err, ErrNotImplementedFiles) { - t.Fatalf("wanted ErrNotImplementedFiles, got %v\n", err) - } - - if proc != nil { - t.Fatalf("wanted nil, got %v\n", proc) - } -} diff --git a/src/os/exec_other.go b/src/os/exec_other.go index b05e2830db..05fcc39988 100644 --- a/src/os/exec_other.go +++ b/src/os/exec_other.go @@ -1,4 +1,4 @@ -//go:build (!aix && !android && !freebsd && !linux && !netbsd && !openbsd && !plan9 && !solaris) || baremetal || tinygo.wasm || nintendoswitch +//go:build (!aix && !android && !darwin && !freebsd && !linux && !netbsd && !openbsd && !plan9 && !solaris) || baremetal || tinygo.wasm || nintendoswitch package os @@ -18,6 +18,49 @@ func (p *Process) release() error { return nil } +// ProcessState is a placeholder on targets that have no process model. +type ProcessState struct { +} + +func (p *ProcessState) String() string { + return "" // TODO +} +func (p *ProcessState) Success() bool { + return false // TODO +} + +// Sys returns system-dependent exit information about +// the process. Convert it to the appropriate underlying +// type, such as syscall.WaitStatus on Unix, to access its contents. +func (p *ProcessState) Sys() interface{} { + return nil // TODO +} + +func (p *ProcessState) Exited() bool { + return false // TODO +} + +// ExitCode returns the exit code of the exited process, or -1 +// if the process hasn't exited or was terminated by a signal. +func (p *ProcessState) ExitCode() int { + return -1 // TODO +} + +func (p *Process) Wait() (*ProcessState, error) { + if p.Pid == -1 { + return nil, syscall.EINVAL + } + return nil, ErrNotImplemented +} + +func (p *Process) Kill() error { + return ErrNotImplemented +} + +func (p *Process) Signal(sig Signal) error { + return ErrNotImplemented +} + func forkExec(_ string, _ []string, _ *ProcAttr) (pid int, err error) { return 0, ErrNotImplemented } diff --git a/src/os/exec_posix_spawn.go b/src/os/exec_posix_spawn.go new file mode 100644 index 0000000000..f5a51617c9 --- /dev/null +++ b/src/os/exec_posix_spawn.go @@ -0,0 +1,408 @@ +// Copyright 2009 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 (linux || darwin) && !baremetal && !tinygo.wasm && !nintendoswitch + +package os + +import ( + "errors" + "internal/itoa" + "runtime" + "sync/atomic" + "syscall" + _ "unsafe" // for go:linkname +) + +// Process creation on a hosted OS uses posix_spawn(3) and not fork(2) plus +// execve(2). These targets run the threads scheduler and collect with Boehm, +// so a fork from Go gives the child one thread that holds the locks of the +// other threads, and the stop-the-world signal of the collector can arrive +// between the fork and the exec. posix_spawn does the clone and the exec +// inside libc, where no Go code runs. +// +// posix_spawn takes two POSIX objects whose shape is different for each OS, so +// the types are in exec_posix_spawn_linux.go and exec_posix_spawn_darwin.go. + +// The only signal values guaranteed to be present in the os package on all +// systems are os.Interrupt (send the process an interrupt) and os.Kill (force +// the process to exit). On Windows, sending os.Interrupt to a process with +// os.Process.Signal is not implemented; it will return an error instead of +// sending a signal. +var ( + Interrupt Signal = syscall.SIGINT + Kill Signal = syscall.SIGKILL +) + +// Give the child an empty signal mask. A blocked mask survives an exec, and +// the spawning thread can carry the signal of the collector blocked. +const _POSIX_SPAWN_SETSIGMASK = 0x08 + +// POSIX_SPAWN_SETPGROUP puts the child in the process group of +// posix_spawnattr_setpgroup. The value is 2 in lib/musl/include/spawn.h and in +// the of Darwin. +const _POSIX_SPAWN_SETPGROUP = 0x02 + +// Keep compatible with golang and always succeed and return new proc with pid. +func findProcess(pid int) (*Process, error) { + return &Process{Pid: pid}, nil +} + +func (p *Process) release() error { + // NOOP for unix. + p.Pid = -1 + // no need for a finalizer anymore + runtime.SetFinalizer(p, nil) + return nil +} + +// ProcessState stores information about a process, as reported by Wait. +type ProcessState struct { + pid int // The process's id. + status syscall.WaitStatus // System-dependent status info. + rusage *syscall.Rusage +} + +// Pid returns the process id of the exited process. +func (p *ProcessState) Pid() int { + return p.pid +} + +func (p *ProcessState) String() string { + if p == nil { + return "" + } + status := p.status + res := "" + switch { + case status.Exited(): + res = "exit status " + itoa.Itoa(status.ExitStatus()) + case status.Signaled(): + res = "signal: " + status.Signal().String() + case status.Stopped(): + res = "stop signal: " + status.StopSignal().String() + if status.StopSignal() == syscall.SIGTRAP && status.TrapCause() != 0 { + res += " (trap " + itoa.Itoa(status.TrapCause()) + ")" + } + case status.Continued(): + res = "continued" + } + if status.CoreDump() { + res += " (core dumped)" + } + return res +} + +func (p *ProcessState) Success() bool { + return p.status.ExitStatus() == 0 +} + +// Sys returns system-dependent exit information about +// the process. Convert it to the appropriate underlying +// type, such as syscall.WaitStatus on Unix, to access its contents. +func (p *ProcessState) Sys() interface{} { + return p.status +} + +// SysUsage returns system-dependent resource usage information about +// the exited process. Convert it to the appropriate underlying +// type, such as *syscall.Rusage on Unix, to access its contents. +func (p *ProcessState) SysUsage() interface{} { + return p.rusage +} + +func (p *ProcessState) Exited() bool { + return p.status.Exited() +} + +// ExitCode returns the exit code of the exited process, or -1 +// if the process hasn't exited or was terminated by a signal. +func (p *ProcessState) ExitCode() int { + // return -1 if the process hasn't started. + if p == nil || !p.status.Exited() { + return -1 + } + return p.status.ExitStatus() +} + +// Wait waits for the Process to exit, and then returns a ProcessState +// describing its status and an error, if any. +func (p *Process) Wait() (*ProcessState, error) { + if p.Pid == -1 { + return nil, syscall.EINVAL + } + var status syscall.WaitStatus + var rusage syscall.Rusage + var wpid int + var err error + for { + wpid, err = syscall.Wait4(p.Pid, &status, 0, &rusage) + // The collector stops the world with a signal, so a thread in wait4 + // gets EINTR as a matter of course. + if err != syscall.EINTR { + break + } + } + if err != nil { + return nil, NewSyscallError("wait", err) + } + atomic.StoreInt32(&p.done, 1) + return &ProcessState{pid: wpid, status: status, rusage: &rusage}, nil +} + +// Signal sends a signal to the Process. Sending Interrupt on Windows is not +// implemented. +func (p *Process) Signal(sig Signal) error { + if p.Pid == -1 { + return errors.New("os: process already released") + } + if p.Pid == 0 { + return errors.New("os: process not initialized") + } + if atomic.LoadInt32(&p.done) != 0 { + return ErrProcessDone + } + s, ok := sig.(syscall.Signal) + if !ok { + return errors.New("os: unsupported signal type") + } + if err := syscall.Kill(p.Pid, s); err != nil { + // Another goroutine can reap the process between the check above and + // the kill. exec.CommandContext expects ErrProcessDone here. + if err == syscall.ESRCH { + return ErrProcessDone + } + return err + } + return nil +} + +// Kill causes the Process to exit immediately. Kill does not wait until the +// Process has actually exited. This only kills the Process itself, not any +// other processes it may have started. +func (p *Process) Kill() error { + return p.Signal(Kill) +} + +// startProcess creates the child with posix_spawn instead of a fork and exec +// pair. +func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err error) { + if attr == nil { + attr = new(ProcAttr) + } + if attr.Sys != nil { + // Refuse by name every field that posix_spawn cannot express. Only + // Setpgid and Pgid are honoured. + if err := checkSysProcAttr(attr.Sys); err != nil { + return nil, err + } + } + + pid, err := forkExec(name, argv, attr) + if err != nil { + return nil, err + } + + return &Process{Pid: pid}, nil +} + +// forkExec spawns the program at argv0 and returns its pid. It does not fork. +// posix_spawn reports a failed exec as its return value, so no status pipe is +// necessary. +func forkExec(argv0 string, argv []string, attr *ProcAttr) (pid int, err error) { + if len(argv) == 0 { + return 0, errors.New("exec: no argv") + } + if attr == nil { + attr = new(ProcAttr) + } + + argv0p, err := syscall.BytePtrFromString(argv0) + if err != nil { + return 0, err + } + argvp, err := syscall.SlicePtrFromStrings(argv) + if err != nil { + return 0, err + } + env := attr.Env + if env == nil { + // A nil Env means the environment of the parent. + env = Environ() + } + envp, err := syscall.SlicePtrFromStrings(env) + if err != nil { + return 0, err + } + + var fa spawnFileActions + if errno := posix_spawn_file_actions_init(&fa); errno != 0 { + return 0, syscall.Errno(errno) + } + defer posix_spawn_file_actions_destroy(&fa) + + var sa spawnAttr + if errno := posix_spawnattr_init(&sa); errno != 0 { + return 0, syscall.Errno(errno) + } + defer posix_spawnattr_destroy(&sa) + + var mask sigset + if errno := posix_spawnattr_setsigmask(&sa, &mask); errno != 0 { + return 0, syscall.Errno(errno) + } + + flags := int16(_POSIX_SPAWN_SETSIGMASK) + + // Setpgid is the one SysProcAttr field that posix_spawn can express. A + // Pgid of 0 makes a new group whose id is the pid of the child. + if attr.Sys != nil && attr.Sys.Setpgid { + if errno := posix_spawnattr_setpgroup(&sa, int32(attr.Sys.Pgid)); errno != 0 { + return 0, syscall.Errno(errno) + } + flags |= _POSIX_SPAWN_SETPGROUP + } + + if errno := posix_spawnattr_setflags(&sa, flags); errno != 0 { + return 0, syscall.Errno(errno) + } + + if attr.Dir != "" { + dirp, err := syscall.BytePtrFromString(attr.Dir) + if err != nil { + return 0, err + } + // Darwin stores the pointer and not a copy of the path, and the + // collector cannot see it. Keep the Go bytes alive until the spawn. + defer runtime.KeepAlive(dirp) + if errno := posix_spawn_file_actions_addchdir_np(&fa, dirp); errno != 0 { + return 0, syscall.Errno(errno) + } + } + + // Entry i becomes descriptor i in the child, and a missing entry means + // that the descriptor is closed. + for i, f := range attr.Files { + fd := ^uintptr(0) + if f != nil { + fd = f.Fd() + } + if fd == ^uintptr(0) { + if errno := posix_spawn_file_actions_addclose(&fa, int32(i)); errno != 0 { + return 0, syscall.Errno(errno) + } + continue + } + // A dup2 onto the same descriptor clears FD_CLOEXEC and is not a + // no-op, which is what an inherited os.Stdin needs. + if errno := posix_spawn_file_actions_adddup2(&fa, int32(fd), int32(i)); errno != 0 { + return 0, syscall.Errno(errno) + } + } + + // Close the standard descriptors that ProcAttr.Files does not name, which + // is what syscall.forkAndExecInChild does in the standard library. + for i := len(attr.Files); i < 3; i++ { + if errno := posix_spawn_file_actions_addclose(&fa, int32(i)); errno != 0 { + return 0, syscall.Errno(errno) + } + } + + var childPid int32 + // ForkLock keeps a descriptor made without O_CLOEXEC out of a child that + // another goroutine spawns at the same time. + syscall.ForkLock.Lock() + errno := posix_spawn(&childPid, argv0p, &fa, &sa, &argvp[0], &envp[0]) + syscall.ForkLock.Unlock() + runtime.KeepAlive(argv0p) + runtime.KeepAlive(argvp) + runtime.KeepAlive(envp) + if errno != 0 { + return 0, syscall.Errno(errno) + } + + return int(childPid), nil +} + +// Bindings for the posix_spawn family. They use //go:linkname and not +// //export, because //export promises that pointer arguments do not escape, +// and the objects here outlive the call. + +//go:linkname posix_spawn posix_spawn +func posix_spawn(pid *int32, path *byte, fa *spawnFileActions, sa *spawnAttr, argv **byte, envp **byte) int32 + +//go:linkname posix_spawn_file_actions_init posix_spawn_file_actions_init +func posix_spawn_file_actions_init(fa *spawnFileActions) int32 + +//go:linkname posix_spawn_file_actions_destroy posix_spawn_file_actions_destroy +func posix_spawn_file_actions_destroy(fa *spawnFileActions) int32 + +//go:linkname posix_spawn_file_actions_adddup2 posix_spawn_file_actions_adddup2 +func posix_spawn_file_actions_adddup2(fa *spawnFileActions, fildes, newfildes int32) int32 + +//go:linkname posix_spawn_file_actions_addclose posix_spawn_file_actions_addclose +func posix_spawn_file_actions_addclose(fa *spawnFileActions, fildes int32) int32 + +// Present in musl since 1.1.24 and in macOS since 10.15. +// +//go:linkname posix_spawn_file_actions_addchdir_np posix_spawn_file_actions_addchdir_np +func posix_spawn_file_actions_addchdir_np(fa *spawnFileActions, path *byte) int32 + +//go:linkname posix_spawnattr_init posix_spawnattr_init +func posix_spawnattr_init(sa *spawnAttr) int32 + +//go:linkname posix_spawnattr_destroy posix_spawnattr_destroy +func posix_spawnattr_destroy(sa *spawnAttr) int32 + +//go:linkname posix_spawnattr_setflags posix_spawnattr_setflags +func posix_spawnattr_setflags(sa *spawnAttr, flags int16) int32 + +//go:linkname posix_spawnattr_setsigmask posix_spawnattr_setsigmask +func posix_spawnattr_setsigmask(sa *spawnAttr, mask *sigset) int32 + +//go:linkname posix_spawnattr_setpgroup posix_spawnattr_setpgroup +func posix_spawnattr_setpgroup(sa *spawnAttr, pgroup int32) int32 + +// unsupportedSysFieldError names the SysProcAttr field that this +// implementation cannot honour. It unwraps to ErrNotImplementedSys. +type unsupportedSysFieldError struct { + field string +} + +func (e *unsupportedSysFieldError) Error() string { + return "os: SysProcAttr." + e.field + ": " + ErrNotImplementedSys.Error() +} + +func (e *unsupportedSysFieldError) Unwrap() error { + return ErrNotImplementedSys +} + +func errUnsupportedSysField(field string) error { + return &unsupportedSysFieldError{field: field} +} + +// checkSysProcAttrCommon rejects every field that both Linux and Darwin +// declare and that posix_spawn cannot express. Setpgid and Pgid are absent, +// because forkExec honours them. +func checkSysProcAttrCommon(sys *syscall.SysProcAttr) error { + switch { + case sys.Chroot != "": + return errUnsupportedSysField("Chroot") + case sys.Credential != nil: + return errUnsupportedSysField("Credential") + case sys.Ptrace: + return errUnsupportedSysField("Ptrace") + case sys.Setsid: + return errUnsupportedSysField("Setsid") + case sys.Setctty: + return errUnsupportedSysField("Setctty") + case sys.Noctty: + return errUnsupportedSysField("Noctty") + case sys.Ctty != 0: + return errUnsupportedSysField("Ctty") + case sys.Foreground: + return errUnsupportedSysField("Foreground") + } + return nil +} diff --git a/src/os/exec_posix_spawn_darwin.go b/src/os/exec_posix_spawn_darwin.go new file mode 100644 index 0000000000..bbc6ac76b8 --- /dev/null +++ b/src/os/exec_posix_spawn_darwin.go @@ -0,0 +1,22 @@ +//go:build darwin + +package os + +import "syscall" + +// Darwin declares both POSIX objects as opaque pointers, so the +// object a caller allocates is one pointer wide and libc allocates the rest. +// The type is uintptr because libc stores memory there that is not Go memory. +type spawnFileActions uintptr + +type spawnAttr uintptr + +// The sigset_t of Darwin is a 32-bit mask. The zero value is the empty set. +type sigset uint32 + +// checkSysProcAttr reports whether the SysProcAttr asks for something that +// posix_spawn cannot do. Darwin declares only the common fields plus Setpgid +// and Pgid. +func checkSysProcAttr(sys *syscall.SysProcAttr) error { + return checkSysProcAttrCommon(sys) +} diff --git a/src/os/exec_posix_spawn_linux.go b/src/os/exec_posix_spawn_linux.go new file mode 100644 index 0000000000..0d2e1c5e8c --- /dev/null +++ b/src/os/exec_posix_spawn_linux.go @@ -0,0 +1,66 @@ +//go:build linux && !baremetal && !tinygo.wasm && !nintendoswitch + +package os + +import "syscall" + +// Storage for the two by-value POSIX objects posix_spawn takes. musl declares +// them in lib/musl/include/spawn.h as +// +// typedef struct { +// int __pad0[2]; +// void *__actions; +// int __pad[16]; +// } posix_spawn_file_actions_t; +// +// typedef struct { +// int __flags; +// pid_t __pgrp; +// sigset_t __def, __mask; +// int __prio, __pol; +// void *__fn; +// char __pad[64-sizeof(void *)]; +// } posix_spawnattr_t; +// +// with a sigset_t of 128 bytes. The file-actions object is then 80 bytes on +// LP64 and 76 bytes on a 32-bit target, and the attribute object is 336 bytes. +// The arrays below are larger than that and uint64 for the alignment. Only +// libc looks inside them. +type spawnFileActions [16]uint64 + +type spawnAttr [48]uint64 + +// The sigset_t of musl, 128 bytes. The zero value is the empty set. +type sigset [16]uint64 + +// checkSysProcAttr reports whether the SysProcAttr asks for something that +// posix_spawn cannot do. Linux declares more fields than POSIX, and each of +// them needs Go code to run in the child between the clone and the exec. +func checkSysProcAttr(sys *syscall.SysProcAttr) error { + if err := checkSysProcAttrCommon(sys); err != nil { + return err + } + switch { + case sys.Pdeathsig != 0: + return errUnsupportedSysField("Pdeathsig") + case sys.Cloneflags != 0: + return errUnsupportedSysField("Cloneflags") + case sys.Unshareflags != 0: + return errUnsupportedSysField("Unshareflags") + case sys.UidMappings != nil: + return errUnsupportedSysField("UidMappings") + case sys.GidMappings != nil: + return errUnsupportedSysField("GidMappings") + case sys.GidMappingsEnableSetgroups: + return errUnsupportedSysField("GidMappingsEnableSetgroups") + case sys.AmbientCaps != nil: + return errUnsupportedSysField("AmbientCaps") + case sys.UseCgroupFD: + return errUnsupportedSysField("UseCgroupFD") + case sys.CgroupFD != 0: + return errUnsupportedSysField("CgroupFD") + case sys.PidFD != nil: + return errUnsupportedSysField("PidFD") + } + return nil +} diff --git a/src/os/exec_spawn_test.go b/src/os/exec_spawn_test.go new file mode 100644 index 0000000000..10e8efd993 --- /dev/null +++ b/src/os/exec_spawn_test.go @@ -0,0 +1,322 @@ +//go:build (linux || darwin) && !baremetal && !tinygo.wasm + +package os_test + +import ( + "errors" + . "os" + "strconv" + "strings" + "syscall" + "testing" +) + +// StartProcess spawns a new process and Wait reports its exit status. An +// empty ProcAttr.Files gives the child no standard descriptors, so the command +// here does not use any. +func TestForkExec(t *testing.T) { + proc, err := StartProcess("/bin/sh", []string{"sh", "-c", "exit 0"}, &ProcAttr{}) + if err != nil { + t.Fatalf("StartProcess failed: %v", err) + } + + if proc == nil { + t.Fatalf("proc is nil") + } + + if proc.Pid == 0 { + t.Fatalf("StartProcess failed: new process has pid 0") + } + + state, err := proc.Wait() + if err != nil { + t.Fatalf("Wait failed: %v", err) + } + if !state.Exited() { + t.Errorf("wanted the process to have exited, got %v", state) + } + if !state.Success() { + t.Errorf("wanted a successful exit, got %v", state) + } + if state.ExitCode() != 0 { + t.Errorf("wanted exit code 0, got %d", state.ExitCode()) + } + if _, ok := state.Sys().(syscall.WaitStatus); !ok { + t.Errorf("wanted Sys() to be a syscall.WaitStatus, got %T", state.Sys()) + } +} + +// A process that exits non-zero must report that status rather than an error. +func TestForkExecExitStatus(t *testing.T) { + proc, err := StartProcess("/bin/sh", []string{"sh", "-c", "exit 3"}, &ProcAttr{}) + if err != nil { + t.Fatalf("StartProcess failed: %v", err) + } + + state, err := proc.Wait() + if err != nil { + t.Fatalf("Wait failed: %v", err) + } + if state.Success() { + t.Errorf("wanted an unsuccessful exit, got %v", state) + } + if state.ExitCode() != 3 { + t.Errorf("wanted exit code 3, got %d", state.ExitCode()) + } + if state.String() != "exit status 3" { + t.Errorf("wanted %q, got %q", "exit status 3", state.String()) + } +} + +// Killing a process must be reported as a signalled, not an exited, status. +func TestForkExecKill(t *testing.T) { + proc, err := StartProcess("/bin/sh", []string{"sh", "-c", "sleep 30"}, &ProcAttr{}) + if err != nil { + t.Fatalf("StartProcess failed: %v", err) + } + + if err := proc.Kill(); err != nil { + t.Fatalf("Kill failed: %v", err) + } + + state, err := proc.Wait() + if err != nil { + t.Fatalf("Wait failed: %v", err) + } + if state.Exited() { + t.Errorf("wanted the process to have been signalled, got %v", state) + } + + // After the reap, a second signal must report that the process is done and + // must not reach an unrelated process with the same pid. + if err := proc.Kill(); !errors.Is(err, ErrProcessDone) { + t.Errorf("wanted ErrProcessDone, got %v", err) + } +} + +func TestForkExecErrNotExist(t *testing.T) { + proc, err := StartProcess("invalid", []string{"invalid"}, &ProcAttr{}) + if !errors.Is(err, ErrNotExist) { + t.Fatalf("wanted ErrNotExist, got %s\n", err) + } + + if proc != nil { + t.Fatalf("wanted nil, got %v\n", proc) + } +} + +// Dir is honoured through a chdir file action. +func TestForkExecProcDir(t *testing.T) { + proc, err := StartProcess("/bin/sh", []string{"sh", "-c", "test \"$(pwd -P)\" = /"}, &ProcAttr{Dir: "/"}) + if err != nil { + t.Fatalf("StartProcess failed: %v", err) + } + + state, err := proc.Wait() + if err != nil { + t.Fatalf("Wait failed: %v", err) + } + if !state.Success() { + t.Errorf("the child did not start in /, got %v", state) + } +} + +// A SysProcAttr with only zero fields asks for nothing, so it is accepted. +func TestForkExecProcSysEmpty(t *testing.T) { + proc, err := StartProcess("/bin/sh", []string{"sh", "-c", "exit 0"}, &ProcAttr{Sys: &syscall.SysProcAttr{}}) + if err != nil { + t.Fatalf("StartProcess failed: %v", err) + } + state, err := proc.Wait() + if err != nil { + t.Fatalf("Wait failed: %v", err) + } + if !state.Success() { + t.Errorf("wanted a successful exit, got %v", state) + } +} + +// Every field that posix_spawn cannot express is refused by name, and the +// error unwraps to ErrNotImplementedSys. +func TestForkExecProcSysUnsupported(t *testing.T) { + for _, test := range []struct { + field string + sys *syscall.SysProcAttr + }{ + {"Chroot", &syscall.SysProcAttr{Chroot: "/"}}, + {"Ptrace", &syscall.SysProcAttr{Ptrace: true}}, + {"Setsid", &syscall.SysProcAttr{Setsid: true}}, + {"Setctty", &syscall.SysProcAttr{Setctty: true}}, + {"Noctty", &syscall.SysProcAttr{Noctty: true}}, + {"Ctty", &syscall.SysProcAttr{Ctty: 1}}, + {"Foreground", &syscall.SysProcAttr{Foreground: true}}, + } { + proc, err := StartProcess("/bin/echo", []string{"echo", "hello"}, &ProcAttr{Sys: test.sys}) + if !errors.Is(err, ErrNotImplementedSys) { + t.Errorf("%s: wanted an error wrapping ErrNotImplementedSys, got %v", test.field, err) + } + if err != nil && !strings.Contains(err.Error(), test.field) { + t.Errorf("%s: wanted the error to name the field, got %q", test.field, err.Error()) + } + if proc != nil { + t.Errorf("%s: wanted nil, got %v", test.field, proc) + } + } +} + +// startSleeper spawns a process that stays alive long enough for a check. +func startSleeper(t *testing.T, sys *syscall.SysProcAttr) *Process { + t.Helper() + proc, err := StartProcess("/bin/sleep", []string{"sleep", "30"}, &ProcAttr{Sys: sys}) + if err != nil { + t.Fatalf("StartProcess failed: %v", err) + } + return proc +} + +// Setpgid with a Pgid of zero puts the child in a new process group whose id +// is the pid of the child. +func TestForkExecSetpgid(t *testing.T) { + proc := startSleeper(t, &syscall.SysProcAttr{Setpgid: true}) + defer func() { + proc.Kill() + proc.Wait() + }() + + pgid, err := syscall.Getpgid(proc.Pid) + if err != nil { + t.Fatalf("Getpgid(%d) failed: %v", proc.Pid, err) + } + if pgid != proc.Pid { + t.Errorf("wanted the child to lead its own group %d, got group %d", proc.Pid, pgid) + } + if pgid == Getpid() { + t.Errorf("the child stayed in the parent's group %d", pgid) + } +} + +// A non-zero Pgid joins an existing group instead of creating one. +func TestForkExecSetpgidJoin(t *testing.T) { + leader := startSleeper(t, &syscall.SysProcAttr{Setpgid: true}) + defer func() { + leader.Kill() + leader.Wait() + }() + + joiner := startSleeper(t, &syscall.SysProcAttr{Setpgid: true, Pgid: leader.Pid}) + defer func() { + joiner.Kill() + joiner.Wait() + }() + + pgid, err := syscall.Getpgid(joiner.Pid) + if err != nil { + t.Fatalf("Getpgid(%d) failed: %v", joiner.Pid, err) + } + if pgid != leader.Pid { + t.Errorf("wanted the second child in group %d, got group %d", leader.Pid, pgid) + } +} + +// Without Setpgid the child stays in the group that it inherited. +func TestForkExecInheritsProcessGroup(t *testing.T) { + proc := startSleeper(t, nil) + defer func() { + proc.Kill() + proc.Wait() + }() + + pgid, err := syscall.Getpgid(proc.Pid) + if err != nil { + t.Fatalf("Getpgid(%d) failed: %v", proc.Pid, err) + } + parent, err := syscall.Getpgid(Getpid()) + if err != nil { + t.Fatalf("Getpgid(self) failed: %v", err) + } + if pgid != parent { + t.Errorf("wanted the child in the parent's group %d, got group %d", parent, pgid) + } +} + +// A descriptor that the parent did not give to the child must not survive the +// exec. A child that holds a copy of the write end of a pipe keeps that pipe +// from a report of EOF. +func TestForkExecDescriptorsDoNotLeak(t *testing.T) { + r, w, err := Pipe() + if err != nil { + t.Fatalf("Pipe failed: %v", err) + } + defer r.Close() + defer w.Close() + + // The child gets no descriptors, so a successful write to the write end + // shows that the descriptor leaked. + script := "echo leaked >&" + strconv.Itoa(int(w.Fd())) + proc, err := StartProcess("/bin/sh", []string{"sh", "-c", script}, &ProcAttr{ + Files: []*File{nil, nil, nil}, + }) + if err != nil { + t.Fatalf("StartProcess failed: %v", err) + } + state, err := proc.Wait() + if err != nil { + t.Fatalf("Wait failed: %v", err) + } + if state.Success() { + t.Errorf("the child inherited the parent's pipe write end (fd %d)", w.Fd()) + } +} + +// A standard descriptor that ProcAttr.Files does not name is closed in the +// child, which is what syscall.forkAndExecInChild does in the standard +// library. +func TestForkExecClosesUnnamedStdio(t *testing.T) { + // A dup of a closed descriptor is a redirection error in the special + // built-in exec, which makes a non-interactive shell exit. See POSIX + // 2.8.1, Consequences of Shell Errors. + proc, err := StartProcess("/bin/sh", []string{"sh", "-c", "exec 3>&1"}, &ProcAttr{}) + if err != nil { + t.Fatalf("StartProcess failed: %v", err) + } + state, err := proc.Wait() + if err != nil { + t.Fatalf("Wait failed: %v", err) + } + if state.Success() { + t.Errorf("the child still had a standard output, got %v", state) + } +} + +// Files are handed to the child as its descriptors 0, 1 and 2. +func TestForkExecProcFiles(t *testing.T) { + r, w, err := Pipe() + if err != nil { + t.Fatalf("Pipe failed: %v", err) + } + defer r.Close() + + proc, err := StartProcess("/bin/echo", []string{"echo", "piped"}, &ProcAttr{ + Files: []*File{nil, w, nil}, + }) + if err != nil { + w.Close() + t.Fatalf("StartProcess failed: %v", err) + } + // Close the copy of the write end in the parent, or the read below does + // not see the end of the file. + w.Close() + + buf := make([]byte, 32) + n, err := r.Read(buf) + if err != nil { + t.Fatalf("Read failed: %v", err) + } + if got := string(buf[:n]); got != "piped\n" { + t.Errorf("wanted %q, got %q", "piped\n", got) + } + + if _, err := proc.Wait(); err != nil { + t.Fatalf("Wait failed: %v", err) + } +} 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/os/file_darwin.go b/src/os/file_darwin.go index 8d96b7296e..aa0af92753 100644 --- a/src/os/file_darwin.go +++ b/src/os/file_darwin.go @@ -3,5 +3,14 @@ package os import "syscall" func pipe(p []int) error { - return syscall.Pipe(p) + // Darwin has no pipe2, so mark the descriptors close-on-exec afterwards. + // ForkLock keeps a spawn out of the window between the two steps. + syscall.ForkLock.RLock() + defer syscall.ForkLock.RUnlock() + if err := syscall.Pipe(p); err != nil { + return err + } + syscall.CloseOnExec(p[0]) + syscall.CloseOnExec(p[1]) + return nil } diff --git a/src/os/osexec.go b/src/os/osexec.go deleted file mode 100644 index 6b2562a685..0000000000 --- a/src/os/osexec.go +++ /dev/null @@ -1,58 +0,0 @@ -//go:build linux && !baremetal && !tinygo.wasm && !nintendoswitch - -package os - -import ( - "syscall" - "unsafe" -) - -func fork() (pid int32, err error) { - pid = libc_fork() - if pid != 0 { - if errno := *libc_errno(); errno != 0 { - err = syscall.Errno(*libc_errno()) - } - } - return -} - -// the golang standard library does not expose interfaces for execve and fork, so we define them here the same way via the libc wrapper -func execve(pathname string, argv []string, envv []string) error { - argv0 := cstring(pathname) - - // transform argv and envv into the format expected by execve - argv1 := make([]*byte, len(argv)+1) - for i, arg := range argv { - argv1[i] = &cstring(arg)[0] - } - argv1[len(argv)] = nil - - env1 := make([]*byte, len(envv)+1) - for i, env := range envv { - env1[i] = &cstring(env)[0] - } - env1[len(envv)] = nil - - ret, _, err := syscall.Syscall(syscall.SYS_EXECVE, uintptr(unsafe.Pointer(unsafe.SliceData(argv0))), uintptr(unsafe.Pointer(unsafe.SliceData(argv1))), uintptr(unsafe.Pointer(unsafe.SliceData(env1)))) - if int(ret) != 0 { - return err - } - - return nil -} - -func cstring(s string) []byte { - data := make([]byte, len(s)+1) - copy(data, s) - // final byte should be zero from the initial allocation - return data -} - -//export fork -func libc_fork() int32 - -// Internal musl function to get the C errno pointer. -// -//export __errno_location -func libc_errno() *int32 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.