Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
34e9270
Unify hypervisor liveness checks on ProcessExists
yummybomb Aug 6, 2026
1e5c0a1
Wait for non-child hypervisor exit before finishing kill
yummybomb Aug 6, 2026
0b1a387
Verify socket ownership before treating a hypervisor PID as live
yummybomb Aug 6, 2026
bc1ba16
Fail closed on hypervisor liveness checks
yummybomb Aug 6, 2026
d03b190
Fail closed on duplicate socket paths
yummybomb Aug 6, 2026
fd372fa
Resolve socket owner from listening entries only
yummybomb Aug 7, 2026
5fcd915
Verify socket ownership before force-killing a hypervisor PID
yummybomb Aug 7, 2026
e7c5782
Skip hypervisor kill when socket ownership is unconfirmed
yummybomb Aug 8, 2026
f7b141b
Fail delete when hypervisor ownership is unconfirmed
yummybomb Aug 9, 2026
5d7e89f
Verify hypervisor ownership before killing
yummybomb Aug 9, 2026
3689efb
Fail closed on unconfirmed socket match with no stored PID
yummybomb Aug 9, 2026
929baf2
Treat unsignalable hypervisor processes as alive
yummybomb Aug 9, 2026
aedcdd1
Document fail-closed hypervisor errors
yummybomb Aug 9, 2026
5bbd742
Handle process exit races during socket scans
yummybomb Aug 10, 2026
18e779c
Confirm hypervisor identity before kill
yummybomb Aug 10, 2026
8af4066
Handle hypervisor identity edge cases
yummybomb Aug 10, 2026
1e2f0cf
Disambiguate inherited hypervisor sockets
yummybomb Aug 10, 2026
a146a3b
Add non-Linux process owner resolver
yummybomb Aug 10, 2026
8cd027b
Scope hypervisor identity to host boot
yummybomb Aug 10, 2026
95506fd
Verify graceful shutdown process ownership
yummybomb Aug 10, 2026
89a6e38
Mint hypervisor identity tokens only for confirmed PIDs
yummybomb Aug 11, 2026
ad6d0f7
Treat a hypervisor identity from a previous boot as dead
yummybomb Aug 11, 2026
a8c2729
Treat a socket with no owning process as proof the hypervisor is gone
yummybomb Aug 11, 2026
68c3044
Confirm the expected owner's socket fd before scanning all of /proc
yummybomb Aug 12, 2026
66b1200
Backfill hypervisor process identity at startup
yummybomb Aug 12, 2026
78d6c1e
Memoize the host boot ID
yummybomb Aug 12, 2026
498155f
Skip unreadable fds in the candidate socket ownership check
yummybomb Aug 12, 2026
5e805e4
Record a bare PID when the fallback hypervisor PID is dead
yummybomb Aug 13, 2026
36cf13a
Resolve hypervisor ownership before shutdown kill
yummybomb Aug 13, 2026
3894897
Handle dead owners in shutdown and socket classification
yummybomb Aug 13, 2026
67e0bf3
Keep the fail-closed resolver off the hydration hot path
yummybomb Aug 14, 2026
c9b384a
Extract hypervisor process identity logic into process_identity.go
yummybomb Aug 14, 2026
bb2a847
Group hypervisor process identity fields into a struct
yummybomb Aug 14, 2026
bda8a2c
Collapse the three SIGKILL-and-wait paths into one helper
yummybomb Aug 14, 2026
ae21765
Log a summary line after hypervisor identity backfill
yummybomb Aug 14, 2026
50e9491
Reduce hypervisor SIGKILL wait from 30s to 2s
yummybomb Aug 14, 2026
b5fed73
Defer stuck delete teardown to a background finalizer
yummybomb Aug 14, 2026
c703ab3
Revert "Defer stuck delete teardown to a background finalizer"
yummybomb Aug 14, 2026
3e9b706
Drop unused identity checks and redundant kill-wait constant
yummybomb Aug 14, 2026
c9230a1
Consolidate redundant identity tests
yummybomb Aug 14, 2026
e9f69cd
Merge forceKillHypervisorProcess into killHypervisor
yummybomb Aug 14, 2026
a1c8eda
Abort standby when the hypervisor cannot be confirmed dead
yummybomb Aug 14, 2026
40f806c
Remove the hypervisor socket only after confirmed exit
yummybomb Aug 14, 2026
5c154df
Remove the command-line fallback from socket owner resolution
yummybomb Aug 17, 2026
338f27b
Skip the force-kill fallback after a confirmed hypervisor shutdown
yummybomb Aug 17, 2026
df725d2
Remove the startup hypervisor identity backfill
yummybomb Aug 17, 2026
f5d1a9e
Reap zombie child VMMs and scan /proc in the churn test
yummybomb Aug 17, 2026
1d69bbd
Retry cleanup deletes until the hypervisor teardown converges
yummybomb Aug 20, 2026
4a92e23
Validate process identity without socket path
yummybomb Aug 24, 2026
eb9678f
Capture hypervisor PID before guest shutdown
yummybomb Aug 24, 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 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")
142 changes: 95 additions & 47 deletions lib/hypervisor/socket_pid_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,70 +4,77 @@ package hypervisor

import (
"bufio"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"slices"
"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) {
// socket for the given hypervisor control path, via the socket inode in
// /proc/net/unix and each process's fd table. The fd scan requires the
// caller to hold CAP_SYS_PTRACE (or run as root) so no live owner is missed;
// an ErrNoOwningProcess result is proof the listener is gone.
func ResolveProcessPID(socketPath string) (pid int, 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, err error) {
return resolveProcessPID(socketPath, ownerPID)
}

func resolveProcessPID(socketPath string, ownerPID int) (pid int, err error) {
socketRef, err := socketRefForPath(socketPath)
if err == nil {
if pid, refErr := pidBySocketRef(socketRef); refErr == nil {
return pid, nil
}
if err != nil {
return 0, err
}

if pid, cmdErr := pidByCmdline(socketPath); cmdErr == nil {
return pid, 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, nil
}

return 0, fmt.Errorf("resolve process pid for socket %s: no owning process found", socketPath)
return pidBySocketRef(socketRef, ownerPID)
}

func pidBySocketRef(socketRef string) (int, error) {
procEntries, err := os.ReadDir("/proc")
func processHoldsSocketRef(pid int, socketRef string) bool {
fdEntries, err := os.ReadDir(filepath.Join(procDir, strconv.Itoa(pid), "fd"))
if err != nil {
return 0, fmt.Errorf("read /proc: %w", err)
return false
}

for _, entry := range procEntries {
if !entry.IsDir() {
continue
}

pid, err := strconv.Atoi(entry.Name())
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
}

fdEntries, err := os.ReadDir(filepath.Join("/proc", entry.Name(), "fd"))
if err != nil {
continue
}
for _, fdEntry := range fdEntries {
target, err := os.Readlink(filepath.Join("/proc", entry.Name(), "fd", fdEntry.Name()))
if err != nil {
continue
}
if strings.TrimSpace(target) == socketRef {
return pid, nil
}
if strings.TrimSpace(target) == socketRef {
return true
}
}

return 0, fmt.Errorf("resolve process pid for %s: no owning process found", socketRef)
return false
}
Comment thread
cursor[bot] marked this conversation as resolved.

func pidByCmdline(socketPath 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 @@ -78,28 +85,57 @@ func pidByCmdline(socketPath string) (int, error) {
continue
}

cmdline, err := os.ReadFile(filepath.Join("/proc", entry.Name(), "cmdline"))
if err != nil || len(cmdline) == 0 {
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 _, arg := range strings.Split(string(cmdline), "\x00") {
if arg == socketPath {
return pid, nil
for _, fdEntry := range fdEntries {
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 {
owners = append(owners, pid)
break
}
}
}

return 0, fmt.Errorf("resolve process pid for socket %s: no matching command line found", socketPath)
// The scan observed ownerPID holding the listener fd — the same evidence
// the fast path uses — so a child transiently sharing the inherited fd
// must not turn a proven owner into an error.
if ownerPID > 0 && slices.Contains(owners, ownerPID) {
return ownerPID, 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 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 +148,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