Skip to content

Commit 2ad99b0

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 Container.generatePIDData(pid) which generates the PIDData string using the unix.NameToHandleAt if available. If not available, it fallbacks to start-time read from /proc/pid/stat file. - Adds Container.isSessionAlive(pid, pidData) to use this additional PIDData context to verify whether the pid is created by podman or not. Fixes: #25104 Signed-off-by: Jan Kaluza <[email protected]>
1 parent 2a9b149 commit 2ad99b0

File tree

4 files changed

+223
-19
lines changed

4 files changed

+223
-19
lines changed

libpod/container_exec.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ type ExecSession struct {
101101
// Config is the configuration of this exec session.
102102
// Cannot be empty.
103103
Config *ExecConfig `json:"config"`
104+
105+
// PIDData is a string uniquely identifying the PID. The value is platform
106+
// specific. It is generated by generatePIDData and used by isSessionAlive.
107+
PIDData string `json:"pidData,omitempty"`
104108
}
105109

106110
// ID returns the ID of an exec session.
@@ -295,6 +299,10 @@ func (c *Container) ExecStart(sessionID string) error {
295299

296300
// Update and save session to reflect PID/running
297301
session.PID = pid
302+
session.PIDData, err = c.generatePIDData(pid)
303+
if err != nil {
304+
logrus.Debugf("generatePidData(%d) failed: %v", pid, err)
305+
}
298306
session.State = define.ExecStateRunning
299307

300308
return c.save()
@@ -353,6 +361,10 @@ func (c *Container) execStartAndAttach(sessionID string, streams *define.AttachS
353361

354362
// Update and save session to reflect PID/running
355363
session.PID = pid
364+
session.PIDData, err = c.generatePIDData(pid)
365+
if err != nil {
366+
logrus.Debugf("generatePidData(%d) failed: %v", pid, err)
367+
}
356368
session.State = define.ExecStateRunning
357369

358370
if err := c.save(); err != nil {
@@ -534,6 +546,10 @@ func (c *Container) ExecHTTPStartAndAttach(sessionID string, r *http.Request, w
534546
var lastErr error
535547

536548
session.PID = pid
549+
session.PIDData, err = c.generatePIDData(pid)
550+
if err != nil {
551+
logrus.Debugf("generatePidData(%d) failed: %v", pid, err)
552+
}
537553
session.State = define.ExecStateRunning
538554

539555
if err := c.save(); err != nil {
@@ -1057,17 +1073,17 @@ func (c *Container) readExecExitCode(sessionID string) (int, error) {
10571073
}
10581074

10591075
// getExecSessionPID gets the PID of an active exec session
1060-
func (c *Container) getExecSessionPID(sessionID string) (int, error) {
1076+
func (c *Container) getExecSessionPID(sessionID string) (int, string, error) {
10611077
session, ok := c.state.ExecSessions[sessionID]
10621078
if ok {
1063-
return session.PID, nil
1079+
return session.PID, session.PIDData, nil
10641080
}
10651081
oldSession, ok := c.state.LegacyExecSessions[sessionID]
10661082
if ok {
1067-
return oldSession.PID, nil
1083+
return oldSession.PID, "", nil
10681084
}
10691085

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

10731089
// getKnownExecSessions gets a list of all exec sessions we think are running,
@@ -1128,6 +1144,7 @@ func (c *Container) getActiveExecSessions() ([]string, error) {
11281144
}
11291145
session.ExitCode = exitCode
11301146
session.PID = 0
1147+
session.PIDData = ""
11311148
session.State = define.ExecStateStopped
11321149

11331150
c.newExecDiedEvent(session.ID(), exitCode)
@@ -1262,6 +1279,7 @@ func justWriteExecExitCode(c *Container, sessionID string, exitCode int, emitEve
12621279
session.State = define.ExecStateStopped
12631280
session.ExitCode = exitCode
12641281
session.PID = 0
1282+
session.PIDData = ""
12651283

12661284
// Finally, save our changes.
12671285
return c.save()

libpod/container_internal_freebsd.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,3 +435,61 @@ func containerPathIsFile(unsafeRoot string, containerPath string) (bool, error)
435435
}
436436
return false, err
437437
}
438+
439+
// Generates the PID data uniquely identifying the process
440+
// defined by the pid.
441+
func (c *Container) generatePIDData(pid int) (string, error) {
442+
// fallback to process start-time in case PidfdOpen or NameToHandleAt
443+
// is not supported.
444+
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
445+
if err != nil {
446+
return "", fmt.Errorf("could not read stat: %w", err)
447+
}
448+
449+
// Split stat line into fields
450+
fields := strings.Fields(string(data))
451+
if len(fields) < 22 {
452+
return "", fmt.Errorf("not enough fields in stat file")
453+
}
454+
455+
// Field 22 is the start-time.
456+
return "start-time:" + fields[21], nil
457+
}
458+
459+
// Verifies that the PID is still alive and matches the exactly
460+
// same process as described by the pidData.
461+
func (c *Container) isSessionAlive(pid int, pidData string) (bool, error) {
462+
prefix := "start-time:"
463+
if strings.HasPrefix(pidData, prefix) {
464+
startTime := strings.TrimPrefix(pidData, prefix)
465+
466+
// Read the stat file.
467+
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
468+
if err != nil {
469+
return false, nil
470+
}
471+
472+
// Split stat line into fields.
473+
fields := strings.Fields(string(data))
474+
if len(fields) < 22 {
475+
return false, fmt.Errorf("not enough fields in stat file")
476+
}
477+
478+
// Field 22 is start-time.
479+
if fields[21] == startTime {
480+
return true, nil
481+
}
482+
return false, nil
483+
}
484+
485+
// Fallback to unix.Kill to verify if the PID is still alive.
486+
// Ping the PID with signal 0 to see if it still exists.
487+
if err := unix.Kill(pid, 0); err != nil {
488+
if err == unix.ESRCH {
489+
return false, nil
490+
}
491+
return false, fmt.Errorf("pinging PID %d with signal 0: %w", pid, err)
492+
}
493+
494+
return true, nil
495+
}

libpod/container_internal_linux.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
package libpod
44

55
import (
6+
"encoding/hex"
67
"errors"
78
"fmt"
89
"io/fs"
910
"os"
1011
"path"
1112
"path/filepath"
13+
"strconv"
1214
"strings"
1315
"sync"
1416
"syscall"
@@ -846,3 +848,132 @@ func containerPathIsFile(unsafeRoot string, containerPath string) (bool, error)
846848
}
847849
return false, err
848850
}
851+
852+
// Generates the PID data uniquely identifying the process
853+
// defined by the pid.
854+
func (c *Container) generatePIDData(pid int) (string, error) {
855+
// Try to use the PidFdOpen followed by theNameToHandleAt and
856+
// encode it into string.
857+
fd, err := unix.PidfdOpen(pid, 0)
858+
if err != nil {
859+
// Do not fail if PidFdOpen is not supported, we will
860+
// fallback to process start-time later.
861+
if err != unix.ENOSYS {
862+
logrus.Debugf("PidfdOpen(%d) failed: %v", pid, err)
863+
} else {
864+
return "", err
865+
}
866+
}
867+
868+
if fd > -1 {
869+
defer unix.Close(fd)
870+
fh, _, err := unix.NameToHandleAt(fd, "", unix.AT_EMPTY_PATH)
871+
if err != nil {
872+
// Do not fail if NameToHandleAt is not supported, we will
873+
// fallback to process start-time later.
874+
if err == unix.ENOTSUP {
875+
logrus.Debugf("NameToHandleAt(%d) failed: %v", fd, err)
876+
} else {
877+
return "", err
878+
}
879+
} else {
880+
hexStr := hex.EncodeToString(fh.Bytes())
881+
return "name-to-handle:" + strconv.Itoa(int(fh.Type())) + " " + hexStr, nil
882+
}
883+
}
884+
885+
// fallback to process start-time in case PidfdOpen or NameToHandleAt
886+
// is not supported.
887+
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
888+
if err != nil {
889+
return "", fmt.Errorf("could not read stat: %w", err)
890+
}
891+
892+
// Split stat line into fields
893+
fields := strings.Fields(string(data))
894+
if len(fields) < 22 {
895+
return "", fmt.Errorf("not enough fields in stat file")
896+
}
897+
898+
// Field 22 is the start-time.
899+
return "start-time:" + fields[21], nil
900+
}
901+
902+
// Verifies that the PID is still alive and matches the exactly
903+
// same process as described by the pidData.
904+
func (c *Container) isSessionAlive(pid int, pidData string) (bool, error) {
905+
prefix := "name-to-handle:"
906+
if strings.HasPrefix(pidData, prefix) {
907+
// Strip the prefix
908+
data := strings.TrimPrefix(pidData, prefix)
909+
parts := strings.SplitN(data, " ", 2)
910+
if len(parts) != 2 {
911+
return false, fmt.Errorf("invalid format, expected 2 parts")
912+
}
913+
914+
// Parse fhType
915+
fhTypeInt, err := strconv.Atoi(parts[0])
916+
if err != nil {
917+
return false, err
918+
}
919+
fhType := int32(fhTypeInt)
920+
921+
// Decode hex string to bytes
922+
bytes, err := hex.DecodeString(parts[1])
923+
if err != nil {
924+
return false, err
925+
}
926+
927+
// Create FileHandle and open it.
928+
fh := unix.NewFileHandle(fhType, bytes)
929+
pidfd, err := unix.PidfdOpen(os.Getpid(), 0)
930+
if err != nil {
931+
return false, err
932+
}
933+
defer unix.Close(pidfd)
934+
fd, err := unix.OpenByHandleAt(pidfd, fh, 0)
935+
if err != nil {
936+
// ESTALE means the process exited already.
937+
if err == unix.ESTALE {
938+
return false, nil
939+
}
940+
return false, err
941+
}
942+
defer unix.Close(fd)
943+
return true, nil
944+
}
945+
946+
prefix = "start-time:"
947+
if strings.HasPrefix(pidData, prefix) {
948+
startTime := strings.TrimPrefix(pidData, prefix)
949+
950+
// Read the stat file.
951+
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
952+
if err != nil {
953+
return false, nil
954+
}
955+
956+
// Split stat line into fields.
957+
fields := strings.Fields(string(data))
958+
if len(fields) < 22 {
959+
return false, fmt.Errorf("not enough fields in stat file")
960+
}
961+
962+
// Field 22 is start-time.
963+
if fields[21] == startTime {
964+
return true, nil
965+
}
966+
return false, nil
967+
}
968+
969+
// Fallback to unix.Kill to verify if the PID is still alive.
970+
// Ping the PID with signal 0 to see if it still exists.
971+
if err := unix.Kill(pid, 0); err != nil {
972+
if err == unix.ESRCH {
973+
return false, nil
974+
}
975+
return false, fmt.Errorf("pinging PID %d with signal 0: %w", pid, err)
976+
}
977+
978+
return true, nil
979+
}

libpod/oci_conmon_exec_common.go

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -214,20 +214,20 @@ func (r *ConmonOCIRuntime) ExecAttachResize(ctr *Container, sessionID string, ne
214214

215215
// ExecStopContainer stops a given exec session in a running container.
216216
func (r *ConmonOCIRuntime) ExecStopContainer(ctr *Container, sessionID string, timeout uint) error {
217-
pid, err := ctr.getExecSessionPID(sessionID)
217+
pid, pidData, err := ctr.getExecSessionPID(sessionID)
218218
if err != nil {
219219
return err
220220
}
221221

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

224224
// 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)
225+
sessionAlive, err := ctr.isSessionAlive(pid, pidData)
226+
if err != nil {
227+
return err
228+
}
229+
if !sessionAlive {
230+
return nil
231231
}
232232

233233
if timeout > 0 {
@@ -268,23 +268,20 @@ func (r *ConmonOCIRuntime) ExecStopContainer(ctr *Container, sessionID string, t
268268

269269
// ExecUpdateStatus checks if the given exec session is still running.
270270
func (r *ConmonOCIRuntime) ExecUpdateStatus(ctr *Container, sessionID string) (bool, error) {
271-
pid, err := ctr.getExecSessionPID(sessionID)
271+
pid, pidData, err := ctr.getExecSessionPID(sessionID)
272272
if err != nil {
273273
return false, err
274274
}
275275

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

278278
// 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)
279+
sessionAlive, err := ctr.isSessionAlive(pid, pidData)
280+
if err != nil {
281+
return false, err
285282
}
286283

287-
return true, nil
284+
return sessionAlive, nil
288285
}
289286

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

0 commit comments

Comments
 (0)