Skip to content

Commit 628c5e6

Browse files
committed
os: start processes with posix_spawn
The process layer was a stub. StartProcess refused every ProcAttr that carried Dir, Sys or Files, and os/exec always passes three Files, so no command could run. Wait, Kill and Signal returned ErrNotImplemented, and ProcessState was an empty struct whose methods all reported a failure. The code that did exist was a fork() and an execve() with no branch on the result of the fork, so the parent fell into the exec as well. Use posix_spawn(3) on hosted Linux and macOS, which are the two targets where the standard library os/exec and syscall packages compile against this override. Those 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, malloc among them, 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, and it reports a failed exec as its return value, so the usual status pipe is not necessary. The descriptors of the child come from a file-actions list. There is a dup2 for each entry of ProcAttr.Files, a close for a missing one, and an addchdir_np for Dir. A nil Env means the environment of the parent, as Go documents. The attribute block installs an empty signal mask, because a blocked mask survives an exec and the spawning thread can carry the signal of the collector blocked. Setpgid and Pgid are honoured through posix_spawnattr_setpgroup, which is the one SysProcAttr request that posix_spawn can express. Every other field is refused by name, and the error unwraps to ErrNotImplementedSys. Wait reaps with wait4 and retries on EINTR, which a thread in wait4 gets as a matter of course, because the collector interrupts it. ProcessState now carries the pid and the real syscall.WaitStatus, so exec.ExitError reports "exit status N", and ExitCode, Exited, Success and Sys work. A killed child is reported as signalled. Signal refuses a pid that Wait reaped and maps ESRCH to ErrProcessDone, which is what exec.CommandContext expects when its context fires as the command finishes. macOS has no pipe2, so os.Pipe there marks both descriptors close-on-exec afterwards, under ForkLock. Without the flag every pipe goes into every child, and a child that holds a copy of a write end keeps that pipe from a report of EOF. Linux asks for O_CLOEXEC in pipe2 and gets it atomically. The minimal macOS SDK in lib/macos-minimal-sdk does not declare <spawn.h>, so the generated libSystem stub has none of the posix_spawn symbols and a darwin program that starts a process does not link. The builder now assembles the missing names into a second stub object. posix_spawn_file_actions_addchdir_np came with macOS 10.15, so a binary from this toolchain needs at least that release. Targets without a process model keep the previous stubs. Only the build tag on exec_other.go changes, to let macOS through to the new implementation.
1 parent c3bdc58 commit 628c5e6

11 files changed

Lines changed: 932 additions & 284 deletions

builder/darwin-libsystem.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,37 @@
11
package builder
22

33
import (
4+
"os"
45
"path/filepath"
56
"strings"
67

78
"github.com/tinygo-org/tinygo/compileopts"
89
"github.com/tinygo-org/tinygo/goenv"
910
)
1011

12+
// Symbols that libSystem exports but the minimal macOS SDK in
13+
// lib/macos-minimal-sdk does not declare. Its generator reads a fixed list of
14+
// headers that does not contain <spawn.h>, so these names are absent from the
15+
// generated libSystem.s. The stubs below have the same form as the generated
16+
// ones, because the linker only needs to know that the names are in
17+
// libSystem.B.dylib.
18+
var darwinExtraLibSystemSymbols = []string{
19+
// The posix_spawn family, which src/os uses to start processes.
20+
// posix_spawn_file_actions_addchdir_np came with macOS 10.15, so a binary
21+
// from this toolchain needs at least that release.
22+
"posix_spawn",
23+
"posix_spawn_file_actions_addchdir_np",
24+
"posix_spawn_file_actions_addclose",
25+
"posix_spawn_file_actions_adddup2",
26+
"posix_spawn_file_actions_destroy",
27+
"posix_spawn_file_actions_init",
28+
"posix_spawnattr_destroy",
29+
"posix_spawnattr_init",
30+
"posix_spawnattr_setflags",
31+
"posix_spawnattr_setpgroup",
32+
"posix_spawnattr_setsigmask",
33+
}
34+
1135
// Create a job that builds a Darwin libSystem.dylib stub library. This library
1236
// contains all the symbols needed so that we can link against it, but it
1337
// doesn't contain any real symbol implementations.
@@ -36,7 +60,34 @@ func makeDarwinLibSystemJob(config *compileopts.Config, tmpdir string) *compileJ
3660
return err
3761
}
3862

39-
// Link object file to dynamic library.
63+
// Compile the extra stubs into a second object file, so that the
64+
// generated one stays as it is.
65+
extrapath := filepath.Join(tmpdir, "libSystem-extra.s")
66+
extraobjpath := filepath.Join(tmpdir, "libSystem-extra.o")
67+
var extra strings.Builder
68+
extra.WriteString("// Stubs for symbols exported by libSystem but not declared in lib/macos-minimal-sdk.\n")
69+
for _, symbol := range darwinExtraLibSystemSymbols {
70+
extra.WriteString("\n.global _" + symbol + "\n_" + symbol + ":\n")
71+
}
72+
if err := os.WriteFile(extrapath, []byte(extra.String()), 0o666); err != nil {
73+
return err
74+
}
75+
flags = []string{
76+
"-nostdlib",
77+
"--target=" + config.Triple(),
78+
"-c",
79+
"-o", extraobjpath,
80+
extrapath,
81+
}
82+
if config.Options.PrintCommands != nil {
83+
config.Options.PrintCommands("clang", flags...)
84+
}
85+
err = runCCompiler(flags...)
86+
if err != nil {
87+
return err
88+
}
89+
90+
// Link object files to dynamic library.
4091
platformVersion := strings.TrimPrefix(strings.Split(config.Triple(), "-")[2], "macosx")
4192
flags = []string{
4293
"-flavor", "darwin",
@@ -48,6 +99,7 @@ func makeDarwinLibSystemJob(config *compileopts.Config, tmpdir string) *compileJ
4899
"-install_name", "/usr/lib/libSystem.B.dylib",
49100
"-o", job.result,
50101
objpath,
102+
extraobjpath,
51103
}
52104
if config.Options.PrintCommands != nil {
53105
config.Options.PrintCommands("ld.lld", flags...)

src/os/exec.go

Lines changed: 7 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import (
55
"syscall"
66
)
77

8+
// Errors StartProcess returns for a ProcAttr that it cannot honour. On a
9+
// hosted OS only ErrNotImplementedSys is reachable. The other two stay because
10+
// they are part of the exported API of this package.
811
var (
912
ErrNotImplementedDir = errors.New("directory setting not implemented")
1013
ErrNotImplementedSys = errors.New("sys setting not implemented")
@@ -36,35 +39,12 @@ type ProcAttr struct {
3639
// ErrProcessDone indicates a Process has finished.
3740
var ErrProcessDone = errors.New("os: process already finished")
3841

39-
type ProcessState struct {
40-
}
41-
42-
func (p *ProcessState) String() string {
43-
return "" // TODO
44-
}
45-
func (p *ProcessState) Success() bool {
46-
return false // TODO
47-
}
48-
49-
// Sys returns system-dependent exit information about
50-
// the process. Convert it to the appropriate underlying
51-
// type, such as syscall.WaitStatus on Unix, to access its contents.
52-
func (p *ProcessState) Sys() interface{} {
53-
return nil // TODO
54-
}
55-
56-
func (p *ProcessState) Exited() bool {
57-
return false // TODO
58-
}
59-
60-
// ExitCode returns the exit code of the exited process, or -1
61-
// if the process hasn't exited or was terminated by a signal.
62-
func (p *ProcessState) ExitCode() int {
63-
return -1 // TODO
64-
}
65-
6642
type Process struct {
6743
Pid int
44+
45+
// done reports whether Wait reaped this process. A signal to a reaped pid
46+
// is unsafe, because the number can belong to an unrelated process.
47+
done int32
6848
}
6949

7050
// 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)
7353
return startProcess(name, argv, attr)
7454
}
7555

76-
func (p *Process) Wait() (*ProcessState, error) {
77-
if p.Pid == -1 {
78-
return nil, syscall.EINVAL
79-
}
80-
return nil, ErrNotImplemented
81-
}
82-
83-
func (p *Process) Kill() error {
84-
return ErrNotImplemented
85-
}
86-
87-
func (p *Process) Signal(sig Signal) error {
88-
return ErrNotImplemented
89-
}
90-
9156
func Ignore(sig ...Signal) {
9257
// leave all the signals unaltered
9358
return

src/os/exec_linux.go

Lines changed: 0 additions & 103 deletions
This file was deleted.

src/os/exec_linux_test.go

Lines changed: 0 additions & 78 deletions
This file was deleted.

src/os/exec_other.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build (!aix && !android && !freebsd && !linux && !netbsd && !openbsd && !plan9 && !solaris) || baremetal || tinygo.wasm || nintendoswitch
1+
//go:build (!aix && !android && !darwin && !freebsd && !linux && !netbsd && !openbsd && !plan9 && !solaris) || baremetal || tinygo.wasm || nintendoswitch
22

33
package os
44

@@ -18,6 +18,49 @@ func (p *Process) release() error {
1818
return nil
1919
}
2020

21+
// ProcessState is a placeholder on targets that have no process model.
22+
type ProcessState struct {
23+
}
24+
25+
func (p *ProcessState) String() string {
26+
return "" // TODO
27+
}
28+
func (p *ProcessState) Success() bool {
29+
return false // TODO
30+
}
31+
32+
// Sys returns system-dependent exit information about
33+
// the process. Convert it to the appropriate underlying
34+
// type, such as syscall.WaitStatus on Unix, to access its contents.
35+
func (p *ProcessState) Sys() interface{} {
36+
return nil // TODO
37+
}
38+
39+
func (p *ProcessState) Exited() bool {
40+
return false // TODO
41+
}
42+
43+
// ExitCode returns the exit code of the exited process, or -1
44+
// if the process hasn't exited or was terminated by a signal.
45+
func (p *ProcessState) ExitCode() int {
46+
return -1 // TODO
47+
}
48+
49+
func (p *Process) Wait() (*ProcessState, error) {
50+
if p.Pid == -1 {
51+
return nil, syscall.EINVAL
52+
}
53+
return nil, ErrNotImplemented
54+
}
55+
56+
func (p *Process) Kill() error {
57+
return ErrNotImplemented
58+
}
59+
60+
func (p *Process) Signal(sig Signal) error {
61+
return ErrNotImplemented
62+
}
63+
2164
func forkExec(_ string, _ []string, _ *ProcAttr) (pid int, err error) {
2265
return 0, ErrNotImplemented
2366
}

0 commit comments

Comments
 (0)