Skip to content

Commit 60b35dd

Browse files
author
Jan Kaluza
committed
Verify the ExecSession pid before killing it.
When container is being removed, podman iterates through its exec sessions and checks whether exec session pid is still alive. The problem is that the pid can be reused for other processes, so that it may not belong to exec session. In this scenario podman may kill another process This commit prevents it by doing following changes: - Adds the PIDData string to ExecSession struct. This string is used to store additional context for a PID to later verify that the PID killed by the podman is really the one started by it. - Adds new package called pidhandle which implements the methods generating the PIDData, and killing the PID with the PIDData ensuring the right PID is killed by verifying the metadata. The new code uses pidfd_open and name_to_handle_at when available. It fallbacks to process start-time get using the gopsutil package. Fixes: #25104 Signed-off-by: Jan Kaluza <[email protected]>
1 parent 2a9b149 commit 60b35dd

File tree

7 files changed

+636
-22
lines changed

7 files changed

+636
-22
lines changed

libpod/container_exec.go

Lines changed: 28 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,20 @@ func (c *Container) ExecCreate(config *ExecConfig) (string, error) {
251256
return session.Id, nil
252257
}
253258

259+
func getPidData(pid int) string {
260+
pidHandle, err := pidhandle.NewPIDHandle(pid)
261+
if err != nil {
262+
logrus.Debugf("getting the PID handle for pid %d: %v", pid, err)
263+
return ""
264+
}
265+
defer pidHandle.Close()
266+
pidData, err := pidHandle.String()
267+
if err != nil {
268+
logrus.Debugf("generating PIDData (%d) failed: %v", pid, err)
269+
}
270+
return pidData
271+
}
272+
254273
// ExecStart starts an exec session in the container, but does not attach to it.
255274
// Returns immediately upon starting the exec session, unlike other ExecStart
256275
// functions, which will only return when the exec session exits.
@@ -296,6 +315,7 @@ func (c *Container) ExecStart(sessionID string) error {
296315
// Update and save session to reflect PID/running
297316
session.PID = pid
298317
session.State = define.ExecStateRunning
318+
session.PIDData = getPidData(pid)
299319

300320
return c.save()
301321
}
@@ -354,6 +374,7 @@ func (c *Container) execStartAndAttach(sessionID string, streams *define.AttachS
354374
// Update and save session to reflect PID/running
355375
session.PID = pid
356376
session.State = define.ExecStateRunning
377+
session.PIDData = getPidData(pid)
357378

358379
if err := c.save(); err != nil {
359380
lastErr = err
@@ -535,6 +556,7 @@ func (c *Container) ExecHTTPStartAndAttach(sessionID string, r *http.Request, w
535556

536557
session.PID = pid
537558
session.State = define.ExecStateRunning
559+
session.PIDData = getPidData(pid)
538560

539561
if err := c.save(); err != nil {
540562
lastErr = err
@@ -1057,17 +1079,17 @@ func (c *Container) readExecExitCode(sessionID string) (int, error) {
10571079
}
10581080

10591081
// getExecSessionPID gets the PID of an active exec session
1060-
func (c *Container) getExecSessionPID(sessionID string) (int, error) {
1082+
func (c *Container) getExecSessionPID(sessionID string) (int, string, error) {
10611083
session, ok := c.state.ExecSessions[sessionID]
10621084
if ok {
1063-
return session.PID, nil
1085+
return session.PID, session.PIDData, nil
10641086
}
10651087
oldSession, ok := c.state.LegacyExecSessions[sessionID]
10661088
if ok {
1067-
return oldSession.PID, nil
1089+
return oldSession.PID, "", nil
10681090
}
10691091

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

10731095
// getKnownExecSessions gets a list of all exec sessions we think are running,
@@ -1128,6 +1150,7 @@ func (c *Container) getActiveExecSessions() ([]string, error) {
11281150
}
11291151
session.ExitCode = exitCode
11301152
session.PID = 0
1153+
session.PIDData = ""
11311154
session.State = define.ExecStateStopped
11321155

11331156
c.newExecDiedEvent(session.ID(), exitCode)
@@ -1262,6 +1285,7 @@ func justWriteExecExitCode(c *Container, sessionID string, exitCode int, emitEve
12621285
session.State = define.ExecStateStopped
12631286
session.ExitCode = exitCode
12641287
session.PID = 0
1288+
session.PIDData = ""
12651289

12661290
// Finally, save our changes.
12671291
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: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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+
const noSuchProcessID = "no-proc"
40+
41+
type pidHandle struct {
42+
pid int
43+
pidData string
44+
}
45+
46+
// Returns the PID.
47+
func (h *pidHandle) PID() int {
48+
return h.pid
49+
}
50+
51+
// Close releases the PIDHandle resource.
52+
func (h *pidHandle) Close() error {
53+
// No resources for the default PIDHandle implementation.
54+
return nil
55+
}
56+
57+
// Sends the signal to process.
58+
func (h *pidHandle) Kill(signal unix.Signal) error {
59+
if h.pidData == noSuchProcessID {
60+
// The process did not exist when we created the PIDHandle, so return
61+
// ESRCH error.
62+
return unix.ESRCH
63+
}
64+
65+
// Get the start-time of the process and check if it is the same as
66+
// the one we store in pidData. If it is not, we know that the PID
67+
// has been recycled and return ESRCH error.
68+
startTime, found := strings.CutPrefix(h.pidData, "start-time:")
69+
if found {
70+
p, err := process.NewProcess(int32(h.pid))
71+
if err != nil {
72+
if err == process.ErrorProcessNotRunning {
73+
return unix.ESRCH
74+
}
75+
return err
76+
}
77+
78+
ctime, err := p.CreateTime()
79+
if err != nil {
80+
return err
81+
}
82+
83+
if strconv.FormatInt(ctime, 10) != startTime {
84+
return unix.ESRCH
85+
}
86+
}
87+
88+
return unix.Kill(h.pid, signal)
89+
}
90+
91+
// Returns true in case the process is still alive.
92+
func (h *pidHandle) IsAlive() (bool, error) {
93+
err := h.Kill(0)
94+
if err != nil {
95+
if err == unix.ESRCH {
96+
return false, nil
97+
}
98+
return false, err
99+
}
100+
return true, nil
101+
}
102+
103+
// Returns a serialized representation of the PIDHandle.
104+
// This string can be passed to NewPIDHandleFromString to recreate
105+
// a PIDHandle that reliably refers to the same process as the original.
106+
func (h *pidHandle) String() (string, error) {
107+
if len(h.pidData) != 0 {
108+
return h.pidData, nil
109+
}
110+
111+
// Get the start-time of the process and return it as string.
112+
p, err := process.NewProcess(int32(h.pid))
113+
if err != nil {
114+
if err == process.ErrorProcessNotRunning {
115+
return noSuchProcessID, nil
116+
}
117+
return "", err
118+
}
119+
120+
ctime, err := p.CreateTime()
121+
if err != nil {
122+
// The process existed, but we cannot get its start-time. There is
123+
// either an issue with getting it, or the process terminated in the
124+
// mean-time. We have no way to find out what actually happened, so
125+
// in this case, we just fallback to an empty string. This will mean
126+
// that Kill or IsAlive might kill wrong process in rare situation
127+
// when CreateTime() failed for different reason than the process
128+
// terminated...
129+
logrus.Debugf("Getting CreateTime for process (%d) failed: %v", h.pid, err)
130+
return "", nil
131+
}
132+
133+
return "start-time:" + strconv.FormatInt(ctime, 10), nil
134+
}

0 commit comments

Comments
 (0)