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
490716e
Unify hypervisor liveness checks on ProcessExists
yummybomb Aug 6, 2026
58f0a6f
Wait for non-child hypervisor exit before finishing kill
yummybomb Aug 6, 2026
008fc8f
Verify socket ownership before treating a hypervisor PID as live
yummybomb Aug 6, 2026
2f6aa21
Fail closed on hypervisor liveness checks
yummybomb Aug 6, 2026
4943360
Fail closed on duplicate socket paths
yummybomb Aug 6, 2026
13c6bbf
Resolve socket owner from listening entries only
yummybomb Aug 7, 2026
47d80c0
Verify socket ownership before force-killing a hypervisor PID
yummybomb Aug 7, 2026
3592911
Skip hypervisor kill when socket ownership is unconfirmed
yummybomb Aug 8, 2026
14dbfda
Fail delete when hypervisor ownership is unconfirmed
yummybomb Aug 9, 2026
de61e09
Verify hypervisor ownership before killing
yummybomb Aug 9, 2026
588cc03
Fail closed on unconfirmed socket match with no stored PID
yummybomb Aug 9, 2026
97c8f00
Treat unsignalable hypervisor processes as alive
yummybomb Aug 9, 2026
fac0044
Document fail-closed hypervisor errors
yummybomb Aug 9, 2026
163f2e9
Handle process exit races during socket scans
yummybomb Aug 10, 2026
fc6dcd4
Confirm hypervisor identity before kill
yummybomb Aug 10, 2026
5d8049e
Handle hypervisor identity edge cases
yummybomb Aug 10, 2026
8f1bfa6
Disambiguate inherited hypervisor sockets
yummybomb Aug 10, 2026
5b21883
Add non-Linux process owner resolver
yummybomb Aug 10, 2026
715caac
Scope hypervisor identity to host boot
yummybomb Aug 10, 2026
50cf8b1
Verify graceful shutdown process ownership
yummybomb Aug 10, 2026
077885a
Mint hypervisor identity tokens only for confirmed PIDs
yummybomb Aug 11, 2026
d0d2eeb
Treat a hypervisor identity from a previous boot as dead
yummybomb Aug 11, 2026
dc86c3a
Treat a socket with no owning process as proof the hypervisor is gone
yummybomb Aug 11, 2026
6d98f30
Confirm the expected owner's socket fd before scanning all of /proc
yummybomb Aug 12, 2026
f6fdb31
Backfill hypervisor process identity at startup
yummybomb Aug 12, 2026
6850893
Memoize the host boot ID
yummybomb Aug 12, 2026
67ea25c
Skip unreadable fds in the candidate socket ownership check
yummybomb Aug 12, 2026
06ce0ee
Record a bare PID when the fallback hypervisor PID is dead
yummybomb Aug 13, 2026
abf8e5c
Resolve hypervisor ownership before shutdown kill
yummybomb Aug 13, 2026
595bce8
Handle dead owners in shutdown and socket classification
yummybomb Aug 13, 2026
4a4b28c
Keep the fail-closed resolver off the hydration hot path
yummybomb Aug 14, 2026
013570e
Extract hypervisor process identity logic into process_identity.go
yummybomb Aug 14, 2026
d301b4a
Group hypervisor process identity fields into a struct
yummybomb Aug 14, 2026
8779c34
Collapse the three SIGKILL-and-wait paths into one helper
yummybomb Aug 14, 2026
1352c51
Log a summary line after hypervisor identity backfill
yummybomb Aug 14, 2026
8035ad4
Reduce hypervisor SIGKILL wait from 30s to 2s
yummybomb Aug 14, 2026
004a770
Defer stuck delete teardown to a background finalizer
yummybomb Aug 14, 2026
a1ed4aa
Revert "Defer stuck delete teardown to a background finalizer"
yummybomb Aug 14, 2026
d497f3b
Drop unused identity checks and redundant kill-wait constant
yummybomb Aug 14, 2026
f21dd81
Consolidate redundant identity tests
yummybomb Aug 14, 2026
a67a379
Merge forceKillHypervisorProcess into killHypervisor
yummybomb Aug 14, 2026
23ea38c
Abort standby when the hypervisor cannot be confirmed dead
yummybomb Aug 14, 2026
066d9dc
Remove the hypervisor socket only after confirmed exit
yummybomb Aug 14, 2026
cb2ff54
Remove the command-line fallback from socket owner resolution
yummybomb Aug 17, 2026
6dae1e3
Skip the force-kill fallback after a confirmed hypervisor shutdown
yummybomb Aug 17, 2026
e0cb6f9
Remove the startup hypervisor identity backfill
yummybomb Aug 17, 2026
ced2e27
Reap zombie child VMMs and scan /proc in the churn test
yummybomb Aug 17, 2026
286c61e
Retry cleanup deletes until the hypervisor teardown converges
yummybomb Aug 20, 2026
3cdf2ed
Validate process identity without socket path
yummybomb Aug 24, 2026
18566e0
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