//go:build linux package console import ( "fmt" "os" "strconv" "strings" "sync" "syscall" ) var hostReportProcReadFile = os.ReadFile var hostReportProcessIdentity = sync.OnceValue(readHostReportProcessIdentity) func hostReportProcessStart(pid int) (string, error) { raw, err := hostReportProcReadFile(fmt.Sprintf("/proc/%d/stat", pid)) if err != nil { return "", err } end := strings.LastIndexByte(string(raw), ')') if end < 0 { return "", os.ErrInvalid } fields := strings.Fields(string(raw[end+1:])) if len(fields) < 20 { return "", os.ErrInvalid } start, err := strconv.ParseUint(fields[19], 10, 64) if err != nil || start == 0 { return "", os.ErrInvalid } return fields[19], nil } var hostReportPIDNamespace = func() (string, error) { info, err := os.Stat("/proc/self/ns/pid") if err != nil { return "", err } st, ok := info.Sys().(*syscall.Stat_t) if !ok || st.Ino == 0 { return "", os.ErrInvalid } return strconv.FormatUint(st.Ino, 10), nil } func readHostReportProcessIdentity() string { namespace, namespaceErr := hostReportPIDNamespace() start, err := hostReportProcessStart(os.Getpid()) if err != nil || namespaceErr != nil { return strconv.Itoa(os.Getpid()) } return fmt.Sprintf("%s:%d:%s", namespace, os.Getpid(), start) } func hostReportProcessID(boot string) string { return boot + ":" + hostReportProcessIdentity() } // Linux reserves PID_NS_INIT_INO (formerly PROC_PID_INIT_INO) for its initial // PID namespace. A container's own procfs also makes /proc/1 match self, so // that match alone cannot prove main's legacy boot:pid is local. // See include/uapi/linux/nsfs.h in the kernel source. const hostReportInitialPIDNamespace = "4026531836" // 0xEFFFFFFC var hostReportRootPIDNamespace = func() (string, error) { info, err := os.Stat("/proc/1/ns/pid") if err != nil { return "", err } st, ok := info.Sys().(*syscall.Stat_t) if !ok || st.Ino == 0 { return "", os.ErrInvalid } return strconv.FormatUint(st.Ino, 10), nil } func hostReportProcessGone(previous, next string) bool { old, current := strings.Split(previous, ":"), strings.Split(next, ":") switch { case len(old) == 4 && len(current) == 4 && old[0] == current[0] && old[1] == current[1]: namespace, err := hostReportPIDNamespace() if err != nil || namespace != old[1] || !hostReportBootMatches(old[0]) { return false } if start, err := strconv.ParseUint(old[3], 10, 64); err != nil || start == 0 { return false } return hostReportPIDAbsent(old[2]) case len(old) == 2 && len(current) >= 2 && old[0] == current[0]: // Main's boot:pid. It carries no namespace, but main only ran in the // host namespace, so accept it only from a process in that namespace // on the same boot, and only with the same absence proof. namespace, err := hostReportPIDNamespace() if err != nil { return false } root, err := hostReportRootPIDNamespace() if err != nil || root != hostReportInitialPIDNamespace || root != namespace || !hostReportBootMatches(old[0]) { return false } return hostReportPIDAbsent(old[1]) } return false } func hostReportBootMatches(boot string) bool { raw, err := hostReportProcReadFile("/proc/sys/kernel/random/boot_id") return err == nil && strings.TrimSpace(string(raw)) == boot } func hostReportPIDAbsent(text string) bool { pid, err := strconv.Atoi(text) if err != nil || pid <= 0 { return false } _, err = hostReportProcessStart(pid) if !os.IsNotExist(err) { return false } // A filtered proc mount may hide a live PID. Signal 0 sends no signal; // only ESRCH in the matching PID namespace proves that PID is absent. return syscall.Kill(pid, 0) == syscall.ESRCH }