Skip to content

Commit 50e2c41

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 50e2c41

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)