Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f490302
Unify hypervisor liveness checks on ProcessExists
yummybomb Aug 6, 2026
a3220e7
Wait for non-child hypervisor exit before finishing kill
yummybomb Aug 6, 2026
5b8a779
Verify socket ownership before treating a hypervisor PID as live
yummybomb Aug 6, 2026
bfe5261
Fail closed on hypervisor liveness checks
yummybomb Aug 6, 2026
55f19ea
Fail closed on duplicate socket paths
yummybomb Aug 6, 2026
f756f91
Resolve socket owner from listening entries only
yummybomb Aug 7, 2026
8c9d618
Verify socket ownership before force-killing a hypervisor PID
yummybomb Aug 7, 2026
d52c2f2
Skip hypervisor kill when socket ownership is unconfirmed
yummybomb Aug 8, 2026
8eee663
Fail delete when hypervisor ownership is unconfirmed
yummybomb Aug 9, 2026
f3459f0
Verify hypervisor ownership before killing
yummybomb Aug 9, 2026
0554e32
Fail closed on unconfirmed socket match with no stored PID
yummybomb Aug 9, 2026
9b88d3d
Treat unsignalable hypervisor processes as alive
yummybomb Aug 9, 2026
9f9354d
Document fail-closed hypervisor errors
yummybomb Aug 9, 2026
434e9af
Handle process exit races during socket scans
yummybomb Aug 10, 2026
9179165
Confirm hypervisor identity before kill
yummybomb Aug 10, 2026
dc4f7fa
Handle hypervisor identity edge cases
yummybomb Aug 10, 2026
d98b253
Disambiguate inherited hypervisor sockets
yummybomb Aug 10, 2026
51fd6be
Add non-Linux process owner resolver
yummybomb Aug 10, 2026
8681699
Scope hypervisor identity to host boot
yummybomb Aug 10, 2026
07d61a2
Verify graceful shutdown process ownership
yummybomb Aug 10, 2026
1b2ed44
Mint hypervisor identity tokens only for confirmed PIDs
yummybomb Aug 11, 2026
49cdd50
Treat a hypervisor identity from a previous boot as dead
yummybomb Aug 11, 2026
c6443db
Treat a socket with no owning process as proof the hypervisor is gone
yummybomb Aug 11, 2026
64c2b7f
Confirm the expected owner's socket fd before scanning all of /proc
yummybomb Aug 12, 2026
312affc
Backfill hypervisor process identity at startup
yummybomb Aug 12, 2026
ecf49be
Memoize the host boot ID
yummybomb Aug 12, 2026
bf291eb
Skip unreadable fds in the candidate socket ownership check
yummybomb Aug 12, 2026
1376779
Record a bare PID when the fallback hypervisor PID is dead
yummybomb Aug 13, 2026
1efc4be
Resolve hypervisor ownership before shutdown kill
yummybomb Aug 13, 2026
cf8c7d4
Handle dead owners in shutdown and socket classification
yummybomb Aug 13, 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