Skip to content

Commit 6df986d

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 6df986d

File tree

7 files changed

+641
-22
lines changed

7 files changed

+641
-22
lines changed

libpod/container_exec.go

Lines changed: 39 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.
@@ -297,6 +302,16 @@ func (c *Container) ExecStart(sessionID string) error {
297302
session.PID = pid
298303
session.State = define.ExecStateRunning
299304

305+
pidHandle, err := pidhandle.NewPIDHandle(pid)
306+
if err != nil {
307+
return fmt.Errorf("getting the PID handle for pid %d: %w", pid, err)
308+
}
309+
defer pidHandle.Close()
310+
session.PIDData, err = pidHandle.String()
311+
if err != nil {
312+
logrus.Debugf("generating PIDData (%d) failed: %v", pid, err)
313+
}
314+
300315
return c.save()
301316
}
302317

@@ -354,6 +369,15 @@ func (c *Container) execStartAndAttach(sessionID string, streams *define.AttachS
354369
// Update and save session to reflect PID/running
355370
session.PID = pid
356371
session.State = define.ExecStateRunning
372+
pidHandle, err := pidhandle.NewPIDHandle(pid)
373+
if err != nil {
374+
return fmt.Errorf("getting the PID handle for pid %d: %w", pid, err)
375+
}
376+
defer pidHandle.Close()
377+
session.PIDData, err = pidHandle.String()
378+
if err != nil {
379+
logrus.Debugf("generating PIDData (%d) failed: %v", pid, err)
380+
}
357381

358382
if err := c.save(); err != nil {
359383
lastErr = err
@@ -535,6 +559,15 @@ func (c *Container) ExecHTTPStartAndAttach(sessionID string, r *http.Request, w
535559

536560
session.PID = pid
537561
session.State = define.ExecStateRunning
562+
pidHandle, err := pidhandle.NewPIDHandle(pid)
563+
if err != nil {
564+
return fmt.Errorf("getting the PID handle for pid %d: %w", pid, err)
565+
}
566+
defer pidHandle.Close()
567+
session.PIDData, err = pidHandle.String()
568+
if err != nil {
569+
logrus.Debugf("generating PIDData (%d) failed: %v", pid, err)
570+
}
538571

539572
if err := c.save(); err != nil {
540573
lastErr = err
@@ -1057,17 +1090,17 @@ func (c *Container) readExecExitCode(sessionID string) (int, error) {
10571090
}
10581091

10591092
// getExecSessionPID gets the PID of an active exec session
1060-
func (c *Container) getExecSessionPID(sessionID string) (int, error) {
1093+
func (c *Container) getExecSessionPID(sessionID string) (int, string, error) {
10611094
session, ok := c.state.ExecSessions[sessionID]
10621095
if ok {
1063-
return session.PID, nil
1096+
return session.PID, session.PIDData, nil
10641097
}
10651098
oldSession, ok := c.state.LegacyExecSessions[sessionID]
10661099
if ok {
1067-
return oldSession.PID, nil
1100+
return oldSession.PID, "", nil
10681101
}
10691102

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

10731106
// getKnownExecSessions gets a list of all exec sessions we think are running,
@@ -1128,6 +1161,7 @@ func (c *Container) getActiveExecSessions() ([]string, error) {
11281161
}
11291162
session.ExitCode = exitCode
11301163
session.PID = 0
1164+
session.PIDData = ""
11311165
session.State = define.ExecStateStopped
11321166

11331167
c.newExecDiedEvent(session.ID(), exitCode)
@@ -1262,6 +1296,7 @@ func justWriteExecExitCode(c *Container, sessionID string, exitCode int, emitEve
12621296
session.State = define.ExecStateStopped
12631297
session.ExitCode = exitCode
12641298
session.PID = 0
1299+
session.PIDData = ""
12651300

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

0 commit comments

Comments
 (0)