Files
podman/pkg/pidhandle/pidhandle_linux.go
Jan Kaluza f825639ebc 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 <jkaluza@redhat.com>
2025-05-06 06:24:13 +02:00

202 lines
5.1 KiB
Go

//go:build linux
// Package for handling processes and PIDs.
package pidhandle
import (
"encoding/hex"
"fmt"
"os"
"strconv"
"strings"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
type pidfdHandle struct {
pidfd int
normalHandle pidHandle
}
// Store the "unix." methods in variables so we can mock them
// in the unit-tests and test out different return value.
var (
pidfdOpen = unix.PidfdOpen
newFileHandle = unix.NewFileHandle
openByHandleAt = unix.OpenByHandleAt
nameToHandleAt = unix.NameToHandleAt
pidfdSendSignal = unix.PidfdSendSignal
)
// The pidData prefix used when the pidfd and name_to_handle is supported
// when creating the PIDHandle to uniquely identify the process.
const nameToHandlePrefix = "name-to-handle:"
// Creates new PIDHandle for a given process pid.
//
// Note that there still can be a race condition if the process terminates
// *before* the PIDHandle is created. It is a caller's responsibility
// to ensure that this either cannot happen or accept this risk.
func NewPIDHandle(pid int) (PIDHandle, error) {
// Use the pidfd to obtain the file-descriptor pointing to the process.
pidData := ""
pidfd, err := pidfdOpen(pid, 0)
if err != nil {
switch err {
case unix.ENOSYS:
// Do not fail if PidFdOpen is not supported, we will
// fallback to process start-time later.
case unix.ESRCH:
// The process does not exist, so any future call of Kill
// or IsAlive should return unix.ESRCH, even if the pid is
// recycled in the future. Let's note it in the pidData.
pidData = noSuchProcessID
case unix.EINVAL:
// The PidfdOpen returns EINVAL if pid is invalid or if it refers
// to a thread and not to process. This is not a valid PID for
// PIDHandle and it most likely means the pid has been recycled
// (or there is a programming error). We therefore store
// noSuchProcessID into pidData to return unix.ESRCH in
// the future Kill or IsAlive calls.
pidData = noSuchProcessID
default:
return nil, fmt.Errorf("pidfdOpen failed: %w", err)
}
}
h := pidfdHandle{
pidfd: pidfd,
normalHandle: pidHandle{pid: pid, pidData: pidData},
}
pidData, err = h.String()
if err != nil {
return nil, err
}
h.normalHandle.pidData = pidData
return &h, nil
}
// Creates new PIDHandle for a given process pid using the pidData
// originally obtained from PIDHandle.String().
func NewPIDHandleFromString(pid int, pidData string) (PIDHandle, error) {
h := pidfdHandle{
pidfd: -1,
normalHandle: pidHandle{pid: pid, pidData: pidData},
}
// Open the pidfd encoded in pidData.
data, found := strings.CutPrefix(pidData, nameToHandlePrefix)
if found {
// Split the data.
parts := strings.SplitN(data, " ", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid format, expected 2 parts")
}
// Parse fhType.
fhTypeInt, err := strconv.Atoi(parts[0])
if err != nil {
return nil, err
}
fhType := int32(fhTypeInt)
// Decode hex string to bytes.
bytes, err := hex.DecodeString(parts[1])
if err != nil {
return nil, err
}
// Create FileHandle and open it.
fh := newFileHandle(fhType, bytes)
fd, err := pidfdOpen(os.Getpid(), 0)
if err != nil {
return nil, err
}
defer unix.Close(fd)
pidfd, err := openByHandleAt(fd, fh, 0)
if err != nil {
if err == unix.ESTALE {
h.normalHandle.pidData = noSuchProcessID
return &h, nil
}
return nil, fmt.Errorf("openByHandleAt failed: %w", err)
}
h.pidfd = pidfd
return &h, nil
}
return &h, nil
}
// Returns the PID associated with this PIDHandle.
func (h *pidfdHandle) PID() int {
return h.normalHandle.PID()
}
// Close releases the pidfd resource.
func (h *pidfdHandle) Close() error {
if h.pidfd != 0 {
err := unix.Close(h.pidfd)
if err != nil {
return fmt.Errorf("failed to close pidfd: %w", err)
}
h.pidfd = 0
}
return h.normalHandle.Close()
}
// Sends the signal to process.
func (h *pidfdHandle) Kill(signal unix.Signal) error {
if h.pidfd > -1 {
return pidfdSendSignal(h.pidfd, signal, nil, 0)
}
return h.normalHandle.Kill(signal)
}
// Returns true in case the process is still alive.
func (h *pidfdHandle) IsAlive() (bool, error) {
err := h.Kill(0)
if err != nil {
if err == unix.ESRCH {
return false, nil
}
return false, err
}
return true, nil
}
// Returns a serialized representation of the PIDHandle.
// This string can be passed to NewPIDHandleFromString to recreate
// a PIDHandle that reliably refers to the same process as the original.
func (h *pidfdHandle) String() (string, error) {
if len(h.normalHandle.pidData) != 0 {
return h.normalHandle.pidData, nil
}
// Serialize the pidfd to string if possible.
if h.pidfd > -1 {
fh, _, err := nameToHandleAt(h.pidfd, "", unix.AT_EMPTY_PATH)
if err != nil {
// Do not fail if NameToHandleAt is not supported, we will
// fallback to process start-time later.
if err == unix.ENOTSUP {
logrus.Debugf("NameToHandleAt(%d) failed: %v", h.pidfd, err)
} else {
return "", err
}
} else {
hexStr := hex.EncodeToString(fh.Bytes())
return nameToHandlePrefix + strconv.Itoa(int(fh.Type())) + " " + hexStr, nil
}
}
// Fallback to default String().
return h.normalHandle.String()
}