Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b9e77f0
Unify hypervisor liveness checks on ProcessExists
yummybomb Aug 6, 2026
f67582d
Wait for non-child hypervisor exit before finishing kill
yummybomb Aug 6, 2026
e17b70a
Verify socket ownership before treating a hypervisor PID as live
yummybomb Aug 6, 2026
e509495
Fail closed on hypervisor liveness checks
yummybomb Aug 6, 2026
f8a794d
Fail closed on duplicate socket paths
yummybomb Aug 6, 2026
e419637
Resolve socket owner from listening entries only
yummybomb Aug 7, 2026
fde66a7
Verify socket ownership before force-killing a hypervisor PID
yummybomb Aug 7, 2026
ccd124d
Skip hypervisor kill when socket ownership is unconfirmed
yummybomb Aug 8, 2026
d787bb9
Fail delete when hypervisor ownership is unconfirmed
yummybomb Aug 9, 2026
4adc722
Verify hypervisor ownership before killing
yummybomb Aug 9, 2026
93ae1fa
Fail closed on unconfirmed socket match with no stored PID
yummybomb Aug 9, 2026
ce514c7
Treat unsignalable hypervisor processes as alive
yummybomb Aug 9, 2026
9352c7c
Document fail-closed hypervisor errors
yummybomb Aug 9, 2026
2e48285
Handle process exit races during socket scans
yummybomb Aug 10, 2026
3a7521f
Confirm hypervisor identity before kill
yummybomb Aug 10, 2026
a7740ec
Handle hypervisor identity edge cases
yummybomb Aug 10, 2026
700d398
Disambiguate inherited hypervisor sockets
yummybomb Aug 10, 2026
3ef9f9e
Add non-Linux process owner resolver
yummybomb Aug 10, 2026
9c686f4
Scope hypervisor identity to host boot
yummybomb Aug 10, 2026
74e91c5
Verify graceful shutdown process ownership
yummybomb Aug 10, 2026
ea0f872
Mint hypervisor identity tokens only for confirmed PIDs
yummybomb Aug 11, 2026
9cf5ada
Treat a hypervisor identity from a previous boot as dead
yummybomb Aug 11, 2026
59775a8
Treat a socket with no owning process as proof the hypervisor is gone
yummybomb Aug 11, 2026
8a13458
Confirm the expected owner's socket fd before scanning all of /proc
yummybomb Aug 12, 2026
1555255
Backfill hypervisor process identity at startup
yummybomb Aug 12, 2026
8d8fd5a
Memoize the host boot ID
yummybomb Aug 12, 2026
976cadd
Skip unreadable fds in the candidate socket ownership check
yummybomb Aug 12, 2026
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
5 changes: 5 additions & 0 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,11 @@ func run() error {
}); ok {
reconciler.StartTAPGCReconciler(ctx)
}
if backfiller, ok := app.InstanceManager.(interface {
BackfillHypervisorProcessIdentities(context.Context)
}); ok {
go backfiller.BackfillHypervisorProcessIdentities(ctx)
}

// Log OTel status
if cfg.Otel.Enabled {
Expand Down
5 changes: 5 additions & 0 deletions lib/hypervisor/socket_pid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package hypervisor

import "errors"

var ErrNoOwningProcess = errors.New("no owning process found")
143 changes: 122 additions & 21 deletions lib/hypervisor/socket_pid_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,88 @@ package hypervisor

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

var procDir = "/proc"

// soAcceptcon marks a listening socket in /proc/net/unix (__SO_ACCEPTCON).
const soAcceptcon = 0x10000

// ResolveProcessPID finds the process currently holding the listening Unix
// socket for the given hypervisor control path.
func ResolveProcessPID(socketPath string) (int, error) {
socketRef, err := socketRefForPath(socketPath)
if err == nil {
if pid, refErr := pidBySocketRef(socketRef); refErr == nil {
return pid, nil
// socket for the given hypervisor control path. confirmed reports whether the
// PID was found through socket ownership rather than its command line.
func ResolveProcessPID(socketPath string) (pid int, confirmed bool, err error) {
return resolveProcessPID(socketPath, 0)
}

// ResolveProcessPIDForOwner resolves a socket while preferring an expected
// owner when the socket descriptor is temporarily shared with a child process.
func ResolveProcessPIDForOwner(socketPath string, ownerPID int) (pid int, confirmed bool, err error) {
return resolveProcessPID(socketPath, ownerPID)
}

func resolveProcessPID(socketPath string, ownerPID int) (pid int, confirmed bool, err error) {
socketRef, socketErr := socketRefForPath(socketPath)
var refErr error
if socketErr == nil {
// Confirm the expected owner first so a live stored PID does not
// require scanning every process fd.
if ownerPID > 0 && processHoldsSocketRef(ownerPID, socketRef) {
return ownerPID, true, nil
}
pid, refErr = pidBySocketRef(socketRef, ownerPID)
if refErr == nil {
return pid, true, nil
}
}

if pid, cmdErr := pidByCmdline(socketPath); cmdErr == nil {
return pid, nil
return pid, false, nil
}
if refErr != nil {
return 0, false, refErr
}
if socketErr != nil {
return 0, false, socketErr
}
return 0, false, fmt.Errorf("resolve process pid for socket %s: %w", socketPath, ErrNoOwningProcess)
Comment thread
cursor[bot] marked this conversation as resolved.
}

return 0, fmt.Errorf("resolve process pid for socket %s: no owning process found", socketPath)
func processHoldsSocketRef(pid int, socketRef string) bool {
fdEntries, err := os.ReadDir(filepath.Join(procDir, strconv.Itoa(pid), "fd"))
if err != nil {
return false
}
for _, fdEntry := range fdEntries {
target, err := os.Readlink(filepath.Join(procDir, strconv.Itoa(pid), "fd", fdEntry.Name()))
if err != nil {
// Skip fds that cannot be read, like the full scan does: an fd
// vanishing mid-scan must not hide a listener held by a later fd.
continue
}
if strings.TrimSpace(target) == socketRef {
return true
}
}
return false
}
Comment thread
cursor[bot] marked this conversation as resolved.

func pidBySocketRef(socketRef string) (int, error) {
procEntries, err := os.ReadDir("/proc")
func pidBySocketRef(socketRef string, ownerPID int) (int, error) {
procEntries, err := os.ReadDir(procDir)
if err != nil {
return 0, fmt.Errorf("read /proc: %w", err)
}

var owners []int
var scanErr error
for _, entry := range procEntries {
if !entry.IsDir() {
continue
Expand All @@ -44,30 +96,56 @@ func pidBySocketRef(socketRef string) (int, error) {
continue
}

fdEntries, err := os.ReadDir(filepath.Join("/proc", entry.Name(), "fd"))
fdEntries, err := os.ReadDir(filepath.Join(procDir, entry.Name(), "fd"))
if err != nil {
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ESRCH) {
continue
}
scanErr = err
continue
}
for _, fdEntry := range fdEntries {
target, err := os.Readlink(filepath.Join("/proc", entry.Name(), "fd", fdEntry.Name()))
target, err := os.Readlink(filepath.Join(procDir, entry.Name(), "fd", fdEntry.Name()))
if err != nil {
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ESRCH) {
continue
}
scanErr = err
continue
}
if strings.TrimSpace(target) == socketRef {
return pid, nil
owners = append(owners, pid)
break
}
}
}

return 0, fmt.Errorf("resolve process pid for %s: no owning process found", socketRef)
if ownerPID > 0 {
for _, pid := range owners {
if pid == ownerPID {
return pid, nil
}
}
}
if len(owners) == 1 {
return owners[0], nil
}
if len(owners) > 1 {
return 0, fmt.Errorf("resolve process pid for %s: multiple owning processes found: %v", socketRef, owners)
}
if scanErr != nil {
return 0, fmt.Errorf("resolve process pid for %s: inspect process fds: %w", socketRef, scanErr)
}
return 0, fmt.Errorf("resolve process pid for %s: %w", socketRef, ErrNoOwningProcess)
}

func pidByCmdline(socketPath string) (int, error) {
procEntries, err := os.ReadDir("/proc")
procEntries, err := os.ReadDir(procDir)
if err != nil {
return 0, fmt.Errorf("read /proc: %w", err)
}

var scanErr error
for _, entry := range procEntries {
if !entry.IsDir() {
continue
Expand All @@ -78,8 +156,15 @@ func pidByCmdline(socketPath string) (int, error) {
continue
}

cmdline, err := os.ReadFile(filepath.Join("/proc", entry.Name(), "cmdline"))
if err != nil || len(cmdline) == 0 {
cmdline, err := os.ReadFile(filepath.Join(procDir, entry.Name(), "cmdline"))
if err != nil {
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ESRCH) {
continue
}
scanErr = err
continue
}
if len(cmdline) == 0 {
continue
}
for _, arg := range strings.Split(string(cmdline), "\x00") {
Expand All @@ -89,17 +174,21 @@ func pidByCmdline(socketPath string) (int, error) {
}
}

return 0, fmt.Errorf("resolve process pid for socket %s: no matching command line found", socketPath)
if scanErr != nil {
return 0, fmt.Errorf("resolve process pid for socket %s: inspect process command lines: %w", socketPath, scanErr)
}
return 0, fmt.Errorf("resolve process pid for socket %s: no matching command line found: %w", socketPath, ErrNoOwningProcess)
}

func socketRefForPath(socketPath string) (string, error) {
file, err := os.Open("/proc/net/unix")
file, err := os.Open(filepath.Join(procDir, "net", "unix"))
if err != nil {
return "", fmt.Errorf("open /proc/net/unix: %w", err)
}
defer file.Close()

scanner := bufio.NewScanner(file)
var socketRef string
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 7 {
Expand All @@ -112,14 +201,26 @@ func socketRefForPath(socketPath string) (string, error) {
if path != socketPath {
continue
}
// Accepted server-side sockets list the bound path too; only the
// listener identifies the owning process.
flags, parseErr := strconv.ParseUint(fields[3], 16, 32)
if parseErr != nil || flags&soAcceptcon == 0 {
continue
}
inode := fields[6]
if inode == "" {
break
}
return fmt.Sprintf("socket:[%s]", inode), nil
if socketRef != "" {
return "", fmt.Errorf("resolve process pid for socket %s: multiple socket inodes found", socketPath)
}
socketRef = fmt.Sprintf("socket:[%s]", inode)
Comment thread
cursor[bot] marked this conversation as resolved.
}
if err := scanner.Err(); err != nil {
return "", fmt.Errorf("scan /proc/net/unix: %w", err)
}
return "", fmt.Errorf("resolve process pid for socket %s: socket inode not found", socketPath)
if socketRef != "" {
return socketRef, nil
}
return "", fmt.Errorf("resolve process pid for socket %s: socket inode not found: %w", socketPath, ErrNoOwningProcess)
}
Loading
Loading