Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions internal/os/linux/pidfd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//go:build linux

// SPDX-FileCopyrightText: 2025 k0s authors
// SPDX-License-Identifier: Apache-2.0

package linux

import (
"cmp"
"errors"
"fmt"
"os"
"syscall"

"golang.org/x/sys/unix"
)

// Sends a signal to the process.
func SendSignal(pidfd syscall.Conn, signal os.Signal) error {
sig, ok := signal.(syscall.Signal)
if !ok {
return fmt.Errorf("%w: %s", errors.ErrUnsupported, signal)
}

conn, err := pidfd.SyscallConn()
if err != nil {
return err
}

outerErr := conn.Control(func(fd uintptr) {
err = pidfdSendSignal(int(fd), sig)
})

return cmp.Or(err, outerErr)
}

// Send a signal to a process specified by a file descriptor.
//
// The calling process must either be in the same PID namespace as the process
// referred to by pidfd, or be in an ancestor of that namespace.
//
// Since Linux 5.1.
// https://man7.org/linux/man-pages/man2/pidfd_send_signal.2.html
// https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?h=3eb39f47934f9d5a3027fe00d906a45fe3a15fad
func pidfdSendSignal(pidfd int, sig syscall.Signal) error {
// If the info argument is a NULL pointer, this is equivalent to specifying
// a pointer to a siginfo_t buffer whose fields match the values that are
// implicitly supplied when a signal is sent using kill(2):
//
// * si_signo is set to the signal number;
// * si_errno is set to 0;
// * si_code is set to SI_USER;
// * si_pid is set to the caller's PID; and
// * si_uid is set to the caller's real user ID.
info := (*unix.Siginfo)(nil)

// The flags argument is reserved for future use; currently, this
// argument must be specified as 0.
flags := 0

return os.NewSyscallError("pidfd_send_signal", unix.PidfdSendSignal(pidfd, sig, info, flags))
}
67 changes: 67 additions & 0 deletions internal/os/linux/procfs/pid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//go:build linux

// SPDX-FileCopyrightText: 2024 k0s authors
// SPDX-License-Identifier: Apache-2.0

package procfs

import (
"bytes"
"errors"
"fmt"
"io"
"io/fs"
)

// A process-specific subdirectory within the proc(5) file system, i.e., a
// /proc/<pid> directory. It exposes methods to parse the contents of the
// well-known files inside it.
type PIDDir struct{ fs.FS }

var _ fs.ReadFileFS = (*PIDDir)(nil)

// ReadFile implements [fs.ReadFileFS].
func (d *PIDDir) ReadFile(name string) (_ []byte, err error) {
// The io/fs ReadFile implementation uses stat to optimize the read buffer
// size by first determining the file size. This doesn't make sense, and is
// even counter-productive for procfs files because they are usually
// reported as having zero bytes, which is, of course, not what you get when
// reading them. Hence PIDDir implements its own ReadFile method that skips
// this step and allows the buffer to grow as needed.

f, err := d.Open(name)
if err != nil {
return nil, err
}
defer func() { err = errors.Join(err, f.Close()) }()
return io.ReadAll(f)
}

// Reads and parses /proc/<pid>/cmdline.
// https://man7.org/linux/man-pages/man5/proc_pid_cmdline.5.html
func (d *PIDDir) Cmdline() ([]string, error) {
return d.readNulTerminatedStrings("cmdline")
}

// Reads and parses /proc/<pid>/environ.
// https://man7.org/linux/man-pages/man5/proc_pid_environ.5.html
func (d *PIDDir) Environ() ([]string, error) {
return d.readNulTerminatedStrings("environ")
}

func (d *PIDDir) readNulTerminatedStrings(name string) (items []string, _ error) {
raw, err := d.ReadFile(name)
if err != nil {
return nil, err
}

for len(raw) > 0 {
current, rest, ok := bytes.Cut(raw, []byte{0})
if !ok {
return nil, fmt.Errorf("not properly terminated: %q", raw)
}
items = append(items, string(current))
raw = rest
}
return items, nil
}
104 changes: 104 additions & 0 deletions internal/os/linux/procfs/procfs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//go:build linux

// SPDX-FileCopyrightText: 2024 k0s authors
// SPDX-License-Identifier: Apache-2.0

package procfs

import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
"syscall"

"github.com/k0sproject/k0s/internal/os/linux"
osunix "github.com/k0sproject/k0s/internal/os/unix"

"golang.org/x/sys/unix"
)

var _ = linux.SendSignal // for godoc links

// A proc(5) filesystem.
//
// See https://www.kernel.org/doc/html/latest/filesystems/proc.html.
// See https://man7.org/linux/man-pages/man5/proc.5.html.
type ProcFS string

const (
DefaultMountPoint = "/proc"
Default ProcFS = DefaultMountPoint
)

func At(mountPoint string) ProcFS {
return ProcFS(mountPoint)
}

func (p ProcFS) String() string {
return string(p)
}

// Delegates to [Default].
// See [ProcFS.OpenPID].
func OpenPID(pid int) (*osunix.Dir, error) {
return Default.OpenPID(pid)
}

// Returns a [*osunix.Dir] that points to a process-specific subdirectory inside
// the proc(5) filesystem. It therefore refers to a process or thread, and may
// be used in some syscalls that accept pidfds, most notably [linux.SendSignal].
//
// Operations on open /proc/<pid> Dirs corresponding to dead processes never act
// on any new process that the kernel may, through chance, have also assigned
// the same process ID. Instead, operations on these Dirs usually fail with
// [syscall.ESRCH].
//
// The underlying file descriptor of the Dir obtained in this way is not
// pollable and can't be waited on with waitid(2).
//
// https://docs.kernel.org/filesystems/proc.html#process-specific-subdirectories
func (p ProcFS) OpenPID(pid int) (*osunix.Dir, error) {
path := filepath.Join(p.String(), strconv.Itoa(pid))
path, err := filepath.Abs(path)
if err != nil {
return nil, err
}

pidDir, err := osunix.OpenDir(path, 0)
if err != nil {
// If there was an error, check if the procfs is actually valid.
verifyErr := p.Verify()
if verifyErr != nil {
err = fmt.Errorf("%w (%v)", verifyErr, err) //nolint:errorlint // shadow open err
}
return nil, err
}

return pidDir, nil
}

func (p ProcFS) Verify() error {
path, err := filepath.Abs(p.String())
if err != nil {
return fmt.Errorf("proc(5) filesystem check failed: %w", err)
}

var st syscall.Statfs_t
if err := syscall.Statfs(path, &st); err != nil {
statErr := &fs.PathError{Op: "statfs", Path: path, Err: err}
if errors.Is(err, os.ErrNotExist) {
err = fmt.Errorf("%w: proc(5) filesystem unavailable", errors.ErrUnsupported)
} else {
err = errors.New("proc(5) filesystem check failed")
}
return fmt.Errorf("%w: %v", err, statErr) //nolint:errorlint // shadow stat err
}

if st.Type != unix.PROC_SUPER_MAGIC {
return fmt.Errorf("%w: not a proc(5) filesystem: %s: type is 0x%x", errors.ErrUnsupported, p, st.Type)
}
return nil
}
54 changes: 54 additions & 0 deletions internal/os/linux/procfs/stat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//go:build linux

// SPDX-FileCopyrightText: 2024 k0s authors
// SPDX-License-Identifier: Apache-2.0

package procfs

import (
"bytes"
"fmt"
)

type PIDState byte

// Known values of the process state values as used in the third field of /proc/<pid>/stat.
const (
PIDStateRunning PIDState = 'R'
PIDStateSleeping PIDState = 'S' // in an interruptible wait
PIDStateWaiting PIDState = 'D' // in uninterruptible disk sleep
PIDStateZombie PIDState = 'Z'
PIDStateStopped PIDState = 'T' // (on a signal) or (before Linux 2.6.33) trace stopped
PIDStateTracingStop PIDState = 't' // (Linux 2.6.33 onward)
PIDStatePaging PIDState = 'W' // (only before Linux 2.6.0)
PIDStateDead PIDState = 'X' // (from Linux 2.6.0 onward)
PIDStateDeadX PIDState = 'x' // (Linux 2.6.33 to 3.13 only)
PIDStateWakekill PIDState = 'K' // (Linux 2.6.33 to 3.13 only)
PIDStateWaking PIDState = 'W' // (Linux 2.6.33 to 3.13 only)
PIDStateParked PIDState = 'P' // (Linux 3.9 to 3.13 only)
PIDStateIdle PIDState = 'I' // (Linux 4.14 onward)
)

// Reads the state field from /proc/<pid>/stat.
// https://man7.org/linux/man-pages/man5/proc_pid_stat.5.html
func (d *PIDDir) State() (PIDState, error) {
raw, err := d.ReadFile("stat")
if err != nil {
return 0, err
}

// Skip over the pid and comm fields: The last parenthesis marks the end of
// the comm field, all other fields won't contain parentheses. The end of
// comm needs to be at the fourth byte the earliest.
if idx := bytes.LastIndexByte(raw, ')'); idx < 0 {
return 0, fmt.Errorf("no closing parenthesis: %q", raw)
} else {
raw = raw[idx+1:]
}

if len(raw) < 3 || raw[0] != ' ' || raw[2] != ' ' {
return 0, fmt.Errorf("failed to locate state field: %q", raw)
}

return PIDState(raw[1]), nil
}
51 changes: 51 additions & 0 deletions internal/os/linux/procfs/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//go:build linux

// SPDX-FileCopyrightText: 2024 k0s authors
// SPDX-License-Identifier: Apache-2.0

package procfs

import (
"bytes"
"errors"
"fmt"
"strconv"
)

type PIDStatus map[string]string

var ErrNoSuchStatusField = errors.New("no such status field")

// Reads and parses /proc/<pid>/status.
// https://man7.org/linux/man-pages/man5/proc_pid_status.5.html
func (d *PIDDir) Status() (PIDStatus, error) {
raw, err := d.ReadFile("status")
if err != nil {
return nil, err
}

status := make(PIDStatus, 64)
for len(raw) > 0 {
line, rest, ok := bytes.Cut(raw, []byte{'\n'})
if !ok {
return nil, fmt.Errorf("status file not properly terminated: %q", raw)
}
name, val, ok := bytes.Cut(line, []byte{':'})
if !ok {
return nil, fmt.Errorf("line without colon: %q", line)
}
status[string(name)] = string(bytes.TrimSpace(val))
raw = rest
}

return status, nil
}

// Thread group ID (i.e., Process ID).
func (s PIDStatus) ThreadGroupID() (int, error) {
if tgid, ok := s["Tgid"]; ok {
tgid, err := strconv.Atoi(tgid)
return tgid, err
}
return 0, ErrNoSuchStatusField
}
Loading