Skip to content

Commit beaf7f3

Browse files
committed
os: overhaul handling of PID vs pidfd within Process
There are several issues with pidfd handling today: * The zero value of a Process makes the handle field appear valid, so methods attempt to use it as a pidfd rather than falling back to the PID as they should (#67634). * If a process doesn't exist, FindProcess returns a Process with Pid == -2, which is not a compatible change (#67640). * pidfd close is racy as-is. A Release call or successful Wait will clear the handle field and close the pidfd. However, a concurrent call may have already loaded the handle field and could then proceed to use the closed FD (which could have been reopened as a different pidfd, targeting a different process) (#67641). This CL performs multiple structural changes to the internals of Process. First and foremost, each method is refactored to clearly select either pidfd or raw pid mode. Previously, raw pid mode was structured as a fallback when pidfd mode is unavailable. This works fine, but it does not make it clear that a given Process object either always uses pidfd or always uses raw pid. Since each mode needs to handle different race conditions, it helps to make it clear that we can't switch between modes within a single Process object. Second, pidfd close safety is handled by reference counting uses of the FD. The last user of the FD will close the FD. For example, this means that with concurrent Release and Signal, the Signal call may be the one to close the FD. This is the bulk of this CL, though I find the end result makes the overall implementation easier to reason about. Third, the PID path handles a similar race condtion between Wait and Kill: Wait frees the PID value in the kernel, which could be reallocated causing Kill to target the wrong process. This is handled with a done flag and a mutex. The done flag now shares the same state field used for the handle. Similarly, the Windows implementation reuses all of the handle reference counting that Linux uses. This means the implementations more consistent, and make Windows safe against the same handle reuse problems. (Though I am unsure if Windows ever reuses handles). Wait has a slight behavior change on Windows: previously Wait after Release or an earlier Wait would hang indefinitely (WaitForSingleObject on syscall.InvalidHandle waits indefinitely). Now it returns the same errors as Linux (EINVAL and ErrProcessDone, respectively). Similarly, Release on Windows no longer returns close errors, as it may not actually be the place where the close occurs. Fixes #67634. Fixes #67640. Fixes #67641. Updates #67642. Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest,gotip-windows-amd64-longtest Change-Id: I2ad998f7b67d32031e6f870e8533dbd55d3c3d10 Reviewed-on: https://go-review.googlesource.com/c/go/+/588675 Reviewed-by: Austin Clements <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent a130fb6 commit beaf7f3

12 files changed

+536
-116
lines changed

src/os/exec.go

+234-11
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,234 @@ import (
1717
// ErrProcessDone indicates a [Process] has finished.
1818
var ErrProcessDone = errors.New("os: process already finished")
1919

20+
type processMode uint8
21+
22+
const (
23+
// modePID means that Process operations such use the raw PID from the
24+
// Pid field. handle is not used.
25+
//
26+
// This may be due to the host not supporting handles, or because
27+
// Process was created as a literal, leaving handle unset.
28+
//
29+
// This must be the zero value so Process literals get modePID.
30+
modePID processMode = iota
31+
32+
// modeHandle means that Process operations use handle, which is
33+
// initialized with an OS process handle.
34+
//
35+
// Note that Release and Wait will deactivate and eventually close the
36+
// handle, so acquire may fail, indicating the reason.
37+
modeHandle
38+
)
39+
40+
type processStatus uint64
41+
42+
const (
43+
// PID/handle OK to use.
44+
statusOK processStatus = 0
45+
46+
// statusDone indicates that the PID/handle should not be used because
47+
// the process is done (has been successfully Wait'd on).
48+
statusDone processStatus = 1 << 62
49+
50+
// statusReleased indicates that the PID/handle should not be used
51+
// because the process is released.
52+
statusReleased processStatus = 1 << 63
53+
54+
processStatusMask = 0x3 << 62
55+
)
56+
2057
// Process stores the information about a process created by [StartProcess].
2158
type Process struct {
22-
Pid int
23-
handle atomic.Uintptr // Process handle for Windows, pidfd for Linux.
24-
isdone atomic.Bool // process has been successfully waited on
25-
sigMu sync.RWMutex // avoid race between wait and signal
59+
Pid int
60+
61+
mode processMode
62+
63+
// State contains the atomic process state.
64+
//
65+
// In modePID, this consists only of the processStatus fields, which
66+
// indicate if the process is done/released.
67+
//
68+
// In modeHandle, the lower bits also contain a reference count for the
69+
// handle field.
70+
//
71+
// The Process itself initially holds 1 persistent reference. Any
72+
// operation that uses the handle with a system call temporarily holds
73+
// an additional transient reference. This prevents the handle from
74+
// being closed prematurely, which could result in the OS allocating a
75+
// different handle with the same value, leading to Process' methods
76+
// operating on the wrong process.
77+
//
78+
// Release and Wait both drop the Process' persistent reference, but
79+
// other concurrent references may delay actually closing the handle
80+
// because they hold a transient reference.
81+
//
82+
// Regardless, we want new method calls to immediately treat the handle
83+
// as unavailable after Release or Wait to avoid extending this delay.
84+
// This is achieved by setting either processStatus flag when the
85+
// Process' persistent reference is dropped. The only difference in the
86+
// flags is the reason the handle is unavailable, which affects the
87+
// errors returned by concurrent calls.
88+
state atomic.Uint64
89+
90+
// Used only in modePID.
91+
sigMu sync.RWMutex // avoid race between wait and signal
92+
93+
// handle is the OS handle for process actions, used only in
94+
// modeHandle.
95+
//
96+
// handle must be accessed only via the handleTransientAcquire method
97+
// (or during closeHandle), not directly! handle is immutable.
98+
//
99+
// On Windows, it is a handle from OpenProcess.
100+
// On Linux, it is a pidfd.
101+
// It is unused on other GOOSes.
102+
handle uintptr
103+
}
104+
105+
func newPIDProcess(pid int) *Process {
106+
p := &Process{
107+
Pid: pid,
108+
mode: modePID,
109+
}
110+
runtime.SetFinalizer(p, (*Process).Release)
111+
return p
112+
}
113+
114+
func newHandleProcess(pid int, handle uintptr) *Process {
115+
p := &Process{
116+
Pid: pid,
117+
mode: modeHandle,
118+
handle: handle,
119+
}
120+
p.state.Store(1) // 1 persistent reference
121+
runtime.SetFinalizer(p, (*Process).Release)
122+
return p
26123
}
27124

28-
func newProcess(pid int, handle uintptr) *Process {
29-
p := &Process{Pid: pid}
30-
p.handle.Store(handle)
125+
func newDoneProcess(pid int) *Process {
126+
p := &Process{
127+
Pid: pid,
128+
mode: modeHandle,
129+
// N.B Since we set statusDone, handle will never actually be
130+
// used, so its value doesn't matter.
131+
}
132+
p.state.Store(uint64(statusDone)) // No persistent reference, as there is no handle.
31133
runtime.SetFinalizer(p, (*Process).Release)
32134
return p
33135
}
34136

35-
func (p *Process) setDone() {
36-
p.isdone.Store(true)
137+
func (p *Process) handleTransientAcquire() (uintptr, processStatus) {
138+
if p.mode != modeHandle {
139+
panic("handleTransientAcquire called in invalid mode")
140+
}
141+
142+
for {
143+
refs := p.state.Load()
144+
if refs&processStatusMask != 0 {
145+
return 0, processStatus(refs & processStatusMask)
146+
}
147+
new := refs + 1
148+
if !p.state.CompareAndSwap(refs, new) {
149+
continue
150+
}
151+
return p.handle, statusOK
152+
}
153+
}
154+
155+
func (p *Process) handleTransientRelease() {
156+
if p.mode != modeHandle {
157+
panic("handleTransientRelease called in invalid mode")
158+
}
159+
160+
for {
161+
state := p.state.Load()
162+
refs := state &^ processStatusMask
163+
status := processStatus(state & processStatusMask)
164+
if refs == 0 {
165+
// This should never happen because
166+
// handleTransientRelease is always paired with
167+
// handleTransientAcquire.
168+
panic("release of handle with refcount 0")
169+
}
170+
if refs == 1 && status == statusOK {
171+
// Process holds a persistent reference and always sets
172+
// a status when releasing that reference
173+
// (handlePersistentRelease). Thus something has gone
174+
// wrong if this is the last release but a status has
175+
// not always been set.
176+
panic("final release of handle without processStatus")
177+
}
178+
new := state - 1
179+
if !p.state.CompareAndSwap(state, new) {
180+
continue
181+
}
182+
if new&^processStatusMask == 0 {
183+
p.closeHandle()
184+
}
185+
return
186+
}
37187
}
38188

39-
func (p *Process) done() bool {
40-
return p.isdone.Load()
189+
// Drop the Process' persistent reference on the handle, deactivating future
190+
// Wait/Signal calls with the passed reason.
191+
//
192+
// Returns the status prior to this call. If this is not statusOK, then the
193+
// reference was not dropped or status changed.
194+
func (p *Process) handlePersistentRelease(reason processStatus) processStatus {
195+
if p.mode != modeHandle {
196+
panic("handlePersistentRelease called in invalid mode")
197+
}
198+
199+
for {
200+
refs := p.state.Load()
201+
status := processStatus(refs & processStatusMask)
202+
if status != statusOK {
203+
// Both Release and successful Wait will drop the
204+
// Process' persistent reference on the handle. We
205+
// can't allow concurrent calls to drop the reference
206+
// twice, so we use the status as a guard to ensure the
207+
// reference is dropped exactly once.
208+
return status
209+
}
210+
if refs == 0 {
211+
// This should never happen because dropping the
212+
// persistent reference always sets a status.
213+
panic("release of handle with refcount 0")
214+
}
215+
new := (refs - 1) | uint64(reason)
216+
if !p.state.CompareAndSwap(refs, new) {
217+
continue
218+
}
219+
if new&^processStatusMask == 0 {
220+
p.closeHandle()
221+
}
222+
return status
223+
}
224+
}
225+
226+
func (p *Process) pidStatus() processStatus {
227+
if p.mode != modePID {
228+
panic("pidStatus called in invalid mode")
229+
}
230+
231+
return processStatus(p.state.Load())
232+
}
233+
234+
func (p *Process) pidDeactivate(reason processStatus) {
235+
if p.mode != modePID {
236+
panic("pidDeactivate called in invalid mode")
237+
}
238+
239+
// Both Release and successful Wait will deactivate the PID. Only one
240+
// of those should win, so nothing left to do here if the compare
241+
// fails.
242+
//
243+
// N.B. This means that results can be inconsistent. e.g., with a
244+
// racing Release and Wait, Wait may successfully wait on the process,
245+
// returning the wait status, while future calls error with "process
246+
// released" rather than "process done".
247+
p.state.CompareAndSwap(0, uint64(reason))
41248
}
42249

43250
// ProcAttr holds the attributes that will be applied to a new process
@@ -116,6 +323,22 @@ func StartProcess(name string, argv []string, attr *ProcAttr) (*Process, error)
116323
// rendering it unusable in the future.
117324
// Release only needs to be called if [Process.Wait] is not.
118325
func (p *Process) Release() error {
326+
// Note to future authors: the Release API is cursed.
327+
//
328+
// On Unix and Plan 9, Release sets p.Pid = -1. This is the only part of the
329+
// Process API that is not thread-safe, but it can't be changed now.
330+
//
331+
// On Windows, Release does _not_ modify p.Pid.
332+
//
333+
// On Windows, Wait calls Release after successfully waiting to
334+
// proactively clean up resources.
335+
//
336+
// On Unix and Plan 9, Wait also proactively cleans up resources, but
337+
// can not call Release, as Wait does not set p.Pid = -1.
338+
//
339+
// On Unix and Plan 9, calling Release a second time has no effect.
340+
//
341+
// On Windows, calling Release a second time returns EINVAL.
119342
return p.release()
120343
}
121344

src/os/exec_linux.go

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package os
6+
7+
import (
8+
"syscall"
9+
)
10+
11+
func (p *Process) closeHandle() {
12+
syscall.Close(int(p.handle))
13+
}

src/os/exec_nohandle.go

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build !linux && !windows
6+
7+
package os
8+
9+
func (p *Process) closeHandle() {}

src/os/exec_plan9.go

+16-7
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err e
3232
sysattr.Files = append(sysattr.Files, f.Fd())
3333
}
3434

35-
pid, h, e := syscall.StartProcess(name, argv, sysattr)
35+
pid, _, e := syscall.StartProcess(name, argv, sysattr)
3636
if e != nil {
3737
return nil, &PathError{Op: "fork/exec", Path: name, Err: e}
3838
}
3939

40-
return newProcess(pid, h), nil
40+
return newPIDProcess(pid), nil
4141
}
4242

4343
func (p *Process) writeProcFile(file string, data string) error {
@@ -51,9 +51,13 @@ func (p *Process) writeProcFile(file string, data string) error {
5151
}
5252

5353
func (p *Process) signal(sig Signal) error {
54-
if p.done() {
54+
switch p.pidStatus() {
55+
case statusDone:
5556
return ErrProcessDone
57+
case statusReleased:
58+
return syscall.ENOENT
5659
}
60+
5761
if e := p.writeProcFile("note", sig.String()); e != nil {
5862
return NewSyscallError("signal", e)
5963
}
@@ -67,15 +71,17 @@ func (p *Process) kill() error {
6771
func (p *Process) wait() (ps *ProcessState, err error) {
6872
var waitmsg syscall.Waitmsg
6973

70-
if p.Pid == -1 {
74+
switch p.pidStatus() {
75+
case statusReleased:
7176
return nil, ErrInvalid
7277
}
78+
7379
err = syscall.WaitProcess(p.Pid, &waitmsg)
7480
if err != nil {
7581
return nil, NewSyscallError("wait", err)
7682
}
7783

78-
p.setDone()
84+
p.pidDeactivate(statusDone)
7985
ps = &ProcessState{
8086
pid: waitmsg.Pid,
8187
status: &waitmsg,
@@ -84,16 +90,19 @@ func (p *Process) wait() (ps *ProcessState, err error) {
8490
}
8591

8692
func (p *Process) release() error {
87-
// NOOP for Plan 9.
8893
p.Pid = -1
94+
95+
// Just mark the PID unusable.
96+
p.pidDeactivate(statusReleased)
97+
8998
// no need for a finalizer anymore
9099
runtime.SetFinalizer(p, nil)
91100
return nil
92101
}
93102

94103
func findProcess(pid int) (p *Process, err error) {
95104
// NOOP for Plan 9.
96-
return newProcess(pid, 0), nil
105+
return newPIDProcess(pid), nil
97106
}
98107

99108
// ProcessState stores information about a process, as reported by Wait.

src/os/exec_posix.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@ import (
1313
"syscall"
1414
)
1515

16-
// unsetHandle is a value for Process.handle used when the handle is not set.
17-
// Same as syscall.InvalidHandle for Windows.
18-
const unsetHandle = ^uintptr(0)
19-
2016
// The only signal values guaranteed to be present in the os package on all
2117
// systems are os.Interrupt (send the process an interrupt) and os.Kill (force
2218
// the process to exit). On Windows, sending os.Interrupt to a process with
@@ -66,10 +62,14 @@ func startProcess(name string, argv []string, attr *ProcAttr) (p *Process, err e
6662

6763
// For Windows, syscall.StartProcess above already returned a process handle.
6864
if runtime.GOOS != "windows" {
69-
h = getPidfd(sysattr.Sys)
65+
var ok bool
66+
h, ok = getPidfd(sysattr.Sys)
67+
if !ok {
68+
return newPIDProcess(pid), nil
69+
}
7070
}
7171

72-
return newProcess(pid, h), nil
72+
return newHandleProcess(pid, h), nil
7373
}
7474

7575
func (p *Process) kill() error {

0 commit comments

Comments
 (0)