Skip to content

Commit 58b2eae

Browse files
Merge pull request #25906 from jankaluza/25104-pidfs
Verify the ExecSession pid before killing it.
2 parents 1b55e39 + f825639 commit 58b2eae

File tree

7 files changed

+664
-22
lines changed

7 files changed

+664
-22
lines changed

libpod/container_exec.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/containers/common/pkg/util"
1717
"github.com/containers/podman/v5/libpod/define"
1818
"github.com/containers/podman/v5/libpod/events"
19+
"github.com/containers/podman/v5/pkg/pidhandle"
1920
"github.com/containers/storage/pkg/stringid"
2021
"github.com/sirupsen/logrus"
2122
"golang.org/x/sys/unix"
@@ -101,6 +102,10 @@ type ExecSession struct {
101102
// Config is the configuration of this exec session.
102103
// Cannot be empty.
103104
Config *ExecConfig `json:"config"`
105+
106+
// PIDData is a string uniquely identifying the PID. The value is platform
107+
// specific. It is generated by pidhandle package and used by isSessionAlive.
108+
PIDData string `json:"pidData,omitempty"`
104109
}
105110

106111
// ID returns the ID of an exec session.
@@ -251,6 +256,27 @@ func (c *Container) ExecCreate(config *ExecConfig) (string, error) {
251256
return session.Id, nil
252257
}
253258

259+
// Returns a serialized representation of the pid using the PIDHandle package.
260+
// This string can be passed to NewPIDHandleFromString to recreate a PIDHandle
261+
// that reliably refers to the same process as the original.
262+
//
263+
// NOTE that there is a still race in generating the PIDData on start but this
264+
// is as good as we can do currently. Long term we could change conmon to
265+
// send pidfd back directly.
266+
func getPidData(pid int) string {
267+
pidHandle, err := pidhandle.NewPIDHandle(pid)
268+
if err != nil {
269+
logrus.Debugf("getting the PID handle for pid %d: %v", pid, err)
270+
return ""
271+
}
272+
defer pidHandle.Close()
273+
pidData, err := pidHandle.String()
274+
if err != nil {
275+
logrus.Debugf("generating PIDData (%d) failed: %v", pid, err)
276+
}
277+
return pidData
278+
}
279+
254280
// ExecStart starts an exec session in the container, but does not attach to it.
255281
// Returns immediately upon starting the exec session, unlike other ExecStart
256282
// functions, which will only return when the exec session exits.
@@ -296,6 +322,7 @@ func (c *Container) ExecStart(sessionID string) error {
296322
// Update and save session to reflect PID/running
297323
session.PID = pid
298324
session.State = define.ExecStateRunning
325+
session.PIDData = getPidData(pid)
299326

300327
return c.save()
301328
}
@@ -354,6 +381,7 @@ func (c *Container) execStartAndAttach(sessionID string, streams *define.AttachS
354381
// Update and save session to reflect PID/running
355382
session.PID = pid
356383
session.State = define.ExecStateRunning
384+
session.PIDData = getPidData(pid)
357385

358386
if err := c.save(); err != nil {
359387
lastErr = err
@@ -535,6 +563,7 @@ func (c *Container) ExecHTTPStartAndAttach(sessionID string, r *http.Request, w
535563

536564
session.PID = pid
537565
session.State = define.ExecStateRunning
566+
session.PIDData = getPidData(pid)
538567

539568
if err := c.save(); err != nil {
540569
lastErr = err
@@ -1057,17 +1086,17 @@ func (c *Container) readExecExitCode(sessionID string) (int, error) {
10571086
}
10581087

10591088
// getExecSessionPID gets the PID of an active exec session
1060-
func (c *Container) getExecSessionPID(sessionID string) (int, error) {
1089+
func (c *Container) getExecSessionPID(sessionID string) (int, string, error) {
10611090
session, ok := c.state.ExecSessions[sessionID]
10621091
if ok {
1063-
return session.PID, nil
1092+
return session.PID, session.PIDData, nil
10641093
}
10651094
oldSession, ok := c.state.LegacyExecSessions[sessionID]
10661095
if ok {
1067-
return oldSession.PID, nil
1096+
return oldSession.PID, "", nil
10681097
}
10691098

1070-
return -1, fmt.Errorf("no exec session with ID %s found in container %s: %w", sessionID, c.ID(), define.ErrNoSuchExecSession)
1099+
return -1, "", fmt.Errorf("no exec session with ID %s found in container %s: %w", sessionID, c.ID(), define.ErrNoSuchExecSession)
10711100
}
10721101

10731102
// getKnownExecSessions gets a list of all exec sessions we think are running,
@@ -1128,6 +1157,7 @@ func (c *Container) getActiveExecSessions() ([]string, error) {
11281157
}
11291158
session.ExitCode = exitCode
11301159
session.PID = 0
1160+
session.PIDData = ""
11311161
session.State = define.ExecStateStopped
11321162

11331163
c.newExecDiedEvent(session.ID(), exitCode)
@@ -1262,6 +1292,7 @@ func justWriteExecExitCode(c *Container, sessionID string, exitCode int, emitEve
12621292
session.State = define.ExecStateStopped
12631293
session.ExitCode = exitCode
12641294
session.PID = 0
1295+
session.PIDData = ""
12651296

12661297
// Finally, save our changes.
12671298
return c.save()

libpod/oci_conmon_exec_common.go

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/containers/podman/v5/libpod/define"
2222
"github.com/containers/podman/v5/pkg/errorhandling"
2323
"github.com/containers/podman/v5/pkg/lookup"
24+
"github.com/containers/podman/v5/pkg/pidhandle"
2425
spec "github.com/opencontainers/runtime-spec/specs-go"
2526
"github.com/sirupsen/logrus"
2627
"golang.org/x/sys/unix"
@@ -214,26 +215,32 @@ func (r *ConmonOCIRuntime) ExecAttachResize(ctr *Container, sessionID string, ne
214215

215216
// ExecStopContainer stops a given exec session in a running container.
216217
func (r *ConmonOCIRuntime) ExecStopContainer(ctr *Container, sessionID string, timeout uint) error {
217-
pid, err := ctr.getExecSessionPID(sessionID)
218+
pid, pidData, err := ctr.getExecSessionPID(sessionID)
218219
if err != nil {
219220
return err
220221
}
221222

222223
logrus.Debugf("Going to stop container %s exec session %s", ctr.ID(), sessionID)
223224

225+
pidHandle, err := pidhandle.NewPIDHandleFromString(pid, pidData)
226+
if err != nil {
227+
return fmt.Errorf("getting the PID handle for pid %d from '%s': %w", pid, pidData, err)
228+
}
229+
defer pidHandle.Close()
230+
224231
// Is the session dead?
225-
// Ping the PID with signal 0 to see if it still exists.
226-
if err := unix.Kill(pid, 0); err != nil {
227-
if err == unix.ESRCH {
228-
return nil
229-
}
230-
return fmt.Errorf("pinging container %s exec session %s PID %d with signal 0: %w", ctr.ID(), sessionID, pid, err)
232+
sessionAlive, err := pidHandle.IsAlive()
233+
if err != nil {
234+
return fmt.Errorf("getting the process status for pid %d: %w", pid, err)
235+
}
236+
if !sessionAlive {
237+
return nil
231238
}
232239

233240
if timeout > 0 {
234241
// Use SIGTERM by default, then SIGSTOP after timeout.
235242
logrus.Debugf("Killing exec session %s (PID %d) of container %s with SIGTERM", sessionID, pid, ctr.ID())
236-
if err := unix.Kill(pid, unix.SIGTERM); err != nil {
243+
if err := pidHandle.Kill(unix.SIGTERM); err != nil {
237244
if err == unix.ESRCH {
238245
return nil
239246
}
@@ -251,7 +258,7 @@ func (r *ConmonOCIRuntime) ExecStopContainer(ctr *Container, sessionID string, t
251258

252259
// SIGTERM did not work. On to SIGKILL.
253260
logrus.Debugf("Killing exec session %s (PID %d) of container %s with SIGKILL", sessionID, pid, ctr.ID())
254-
if err := unix.Kill(pid, unix.SIGTERM); err != nil {
261+
if err := pidHandle.Kill(unix.SIGTERM); err != nil {
255262
if err == unix.ESRCH {
256263
return nil
257264
}
@@ -268,23 +275,26 @@ func (r *ConmonOCIRuntime) ExecStopContainer(ctr *Container, sessionID string, t
268275

269276
// ExecUpdateStatus checks if the given exec session is still running.
270277
func (r *ConmonOCIRuntime) ExecUpdateStatus(ctr *Container, sessionID string) (bool, error) {
271-
pid, err := ctr.getExecSessionPID(sessionID)
278+
pid, pidData, err := ctr.getExecSessionPID(sessionID)
272279
if err != nil {
273280
return false, err
274281
}
275282

276283
logrus.Debugf("Checking status of container %s exec session %s", ctr.ID(), sessionID)
277284

285+
pidHandle, err := pidhandle.NewPIDHandleFromString(pid, pidData)
286+
if err != nil {
287+
return false, fmt.Errorf("getting the PID handle for pid %d from '%s': %w", pid, pidData, err)
288+
}
289+
defer pidHandle.Close()
290+
278291
// Is the session dead?
279-
// Ping the PID with signal 0 to see if it still exists.
280-
if err := unix.Kill(pid, 0); err != nil {
281-
if err == unix.ESRCH {
282-
return false, nil
283-
}
284-
return false, fmt.Errorf("pinging container %s exec session %s PID %d with signal 0: %w", ctr.ID(), sessionID, pid, err)
292+
sessionAlive, err := pidHandle.IsAlive()
293+
if err != nil {
294+
return false, fmt.Errorf("getting the process status for pid %d: %w", pid, err)
285295
}
286296

287-
return true, nil
297+
return sessionAlive, nil
288298
}
289299

290300
// ExecAttachSocketPath is the path to a container's exec session attach socket.

pkg/pidhandle/pidhandle.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//go:build !windows
2+
3+
// Package for handling processes and PIDs.
4+
package pidhandle
5+
6+
import (
7+
"strconv"
8+
"strings"
9+
10+
"github.com/shirou/gopsutil/v4/process"
11+
"github.com/sirupsen/logrus"
12+
"golang.org/x/sys/unix"
13+
)
14+
15+
// PIDHandle defines an interface for working with operating system processes
16+
// in a reliable way. OS-specific implementations include additional logic
17+
// to try to ensure that operations (e.g., sending signals) are performed
18+
// on the exact same process that was originally referenced when the PIDHandle
19+
// was created via NewPIDHandle or NewPIDHandleFromString.
20+
//
21+
// This prevents accidental interaction with a different process in scenarios
22+
// where the original process has exited and its PID has been reused by
23+
// the system for an unrelated process.
24+
type PIDHandle interface {
25+
// Returns the PID associated with this PIDHandle.
26+
PID() int
27+
// Releases the PIDHandle resources.
28+
Close() error
29+
// Sends the signal to process.
30+
Kill(signal unix.Signal) error
31+
// Returns true in case the process is still alive.
32+
IsAlive() (bool, error)
33+
// Returns a serialized representation of the PIDHandle.
34+
// This string can be passed to NewPIDHandleFromString to recreate
35+
// a PIDHandle that reliably refers to the same process as the original.
36+
String() (string, error)
37+
}
38+
39+
// The pidData value used when no process with this PID exists when creating
40+
// the PIDHandle.
41+
const noSuchProcessID = "no-proc"
42+
43+
// The pidData prefix used when only the process start time (creation time)
44+
// is supported when creating the PIDHandle to uniquely identify the process.
45+
const startTimePrefix = "start-time:"
46+
47+
type pidHandle struct {
48+
pid int
49+
pidData string
50+
}
51+
52+
// Returns the PID.
53+
func (h *pidHandle) PID() int {
54+
return h.pid
55+
}
56+
57+
// Close releases the PIDHandle resource.
58+
func (h *pidHandle) Close() error {
59+
// No resources for the default PIDHandle implementation.
60+
return nil
61+
}
62+
63+
// Sends the signal to process.
64+
func (h *pidHandle) Kill(signal unix.Signal) error {
65+
if h.pidData == noSuchProcessID {
66+
// The process did not exist when we created the PIDHandle, so return
67+
// ESRCH error.
68+
return unix.ESRCH
69+
}
70+
71+
// Get the start-time of the process and check if it is the same as
72+
// the one we store in pidData. If it is not, we know that the PID
73+
// has been recycled and return ESRCH error.
74+
startTime, found := strings.CutPrefix(h.pidData, startTimePrefix)
75+
if found {
76+
p, err := process.NewProcess(int32(h.pid))
77+
if err != nil {
78+
if err == process.ErrorProcessNotRunning {
79+
return unix.ESRCH
80+
}
81+
return err
82+
}
83+
84+
ctime, err := p.CreateTime()
85+
if err != nil {
86+
return err
87+
}
88+
89+
if strconv.FormatInt(ctime, 10) != startTime {
90+
return unix.ESRCH
91+
}
92+
}
93+
94+
return unix.Kill(h.pid, signal)
95+
}
96+
97+
// Returns true in case the process is still alive.
98+
func (h *pidHandle) IsAlive() (bool, error) {
99+
err := h.Kill(0)
100+
if err != nil {
101+
if err == unix.ESRCH {
102+
return false, nil
103+
}
104+
return false, err
105+
}
106+
return true, nil
107+
}
108+
109+
// Returns a serialized representation of the PIDHandle.
110+
// This string can be passed to NewPIDHandleFromString to recreate
111+
// a PIDHandle that reliably refers to the same process as the original.
112+
func (h *pidHandle) String() (string, error) {
113+
if len(h.pidData) != 0 {
114+
return h.pidData, nil
115+
}
116+
117+
// Get the start-time of the process and return it as string.
118+
p, err := process.NewProcess(int32(h.pid))
119+
if err != nil {
120+
if err == process.ErrorProcessNotRunning {
121+
return noSuchProcessID, nil
122+
}
123+
return "", err
124+
}
125+
126+
ctime, err := p.CreateTime()
127+
if err != nil {
128+
// The process existed, but we cannot get its start-time. There is
129+
// either an issue with getting it, or the process terminated in the
130+
// mean-time. We have no way to find out what actually happened, so
131+
// in this case, we just fallback to an empty string. This will mean
132+
// that Kill or IsAlive might kill wrong process in rare situation
133+
// when CreateTime() failed for different reason than the process
134+
// terminated...
135+
logrus.Debugf("Getting CreateTime for process (%d) failed: %v", h.pid, err)
136+
return "", nil
137+
}
138+
139+
return startTimePrefix + strconv.FormatInt(ctime, 10), nil
140+
}

0 commit comments

Comments
 (0)