Skip to content

Commit b768015

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 fallback to start-time read from /proc/pid/stat file otherwise. Fixes: #25104 Signed-off-by: Jan Kaluza <[email protected]>
1 parent 2a9b149 commit b768015

File tree

5 files changed

+645
-21
lines changed

5 files changed

+645
-21
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 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 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 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 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 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, 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, 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: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
//go:build !linux && !windows
2+
3+
// Package for handling processes and PIDs.
4+
package pidhandle
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"strings"
10+
11+
"golang.org/x/sys/unix"
12+
)
13+
14+
type PIDHandle struct {
15+
pid int
16+
pidfd int
17+
pidData string
18+
}
19+
20+
func NewPIDHandle(pid int) (PIDHandle, error) {
21+
h := PIDHandle{pid: pid, pidfd: -1, pidData: ""}
22+
23+
isAlive, err := h.IsAlive()
24+
if err != nil {
25+
return h, err
26+
}
27+
if !isAlive {
28+
h.pidData = "no-such-process"
29+
}
30+
return h, err
31+
}
32+
33+
func NewPIDHandleFromString(pid int, pidData string) (PIDHandle, error) {
34+
h := PIDHandle{pid: pid, pidfd: -1, pidData: pidData}
35+
return h, nil
36+
}
37+
38+
// Close releases the pidfd resource.
39+
func (h *PIDHandle) Close() error {
40+
if h.pidfd != 0 {
41+
err := unix.Close(h.pidfd)
42+
if err != nil {
43+
return fmt.Errorf("failed to close pidfd: %w", err)
44+
}
45+
h.pidfd = 0
46+
}
47+
return nil
48+
}
49+
50+
func (h *PIDHandle) Kill(signal unix.Signal) error {
51+
startTime, found := strings.CutPrefix(h.pidData, "start-time:")
52+
if found {
53+
// Read the stat file.
54+
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", h.pid))
55+
if err != nil {
56+
return unix.ESRCH
57+
}
58+
59+
// Split stat line into fields.
60+
fields := strings.Fields(string(data))
61+
if len(fields) < 22 {
62+
return fmt.Errorf("not enough fields in stat file")
63+
}
64+
65+
// Field 22 is start-time.
66+
if fields[21] != startTime {
67+
return unix.ESRCH
68+
}
69+
}
70+
if h.pidData == "no-such-process" {
71+
return unix.ESRCH
72+
}
73+
return unix.Kill(h.pid, signal)
74+
}
75+
76+
func (h *PIDHandle) IsAlive() (bool, error) {
77+
err := h.Kill(0)
78+
if err != nil {
79+
if err == unix.ESRCH {
80+
return false, nil
81+
}
82+
return false, err
83+
}
84+
return true, nil
85+
}
86+
87+
// Generates the PID data uniquely identifying the process
88+
// defined by the pid.
89+
func (h *PIDHandle) String() (string, error) {
90+
// fallback to process start-time in case PidfdOpen or NameToHandleAt
91+
// is not supported.
92+
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", h.pid))
93+
if err != nil {
94+
return "", fmt.Errorf("could not read stat: %w", err)
95+
}
96+
97+
// Split stat line into fields
98+
fields := strings.Fields(string(data))
99+
if len(fields) < 22 {
100+
return "", fmt.Errorf("not enough fields in stat file")
101+
}
102+
103+
// Field 22 is the start-time.
104+
return "start-time:" + fields[21], nil
105+
}

0 commit comments

Comments
 (0)