//go:build linux package host import ( "context" "net" "net/url" "os" "os/exec" "path/filepath" "strconv" "strings" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/registry" "golang.org/x/sys/unix" ) // claimProbeDepth bounds the parent walk from a peer to the spawned process. const claimProbeDepth = 64 // claimProbeSocketReady is the claim's probe readiness check (claim Plan 9 // D3; moves ยง7.7 as ruled in H12): GET the declared readiness path over the // route's socket and accept 2xx only, and only from a peer whose pid is the // spawned process or a descendant of it and is a member of exactly the main // role's cgroup. A preforking server's worker passes; any process outside the // spawned tree or the role's group never receives the request. func claimProbeSocketReady(ctx context.Context, cmd *exec.Cmd, rt registry.Route, in ClaimLaunchInputs, roleCgroup string) error { target, ok := claimReadyURL(in.ReadyPath) if cmd == nil || cmd.Process == nil || cmd.Process.Pid <= 1 || rt.Socket == "" || !ok || !filepath.IsAbs(roleCgroup) { return hostwrite.ErrUnavailable } rel, err := filepath.Rel("/sys/fs/cgroup", filepath.Clean(roleCgroup)) if err != nil || !filepath.IsLocal(rel) || rel == "." { return hostwrite.ErrUnavailable } root, member := cmd.Process.Pid, "0::/"+filepath.ToSlash(rel) return claimPollReady(ctx, rt.Socket, target, func(conn net.Conn) error { return claimProbePeer(conn, root, member) }, func(status int) bool { return status >= 200 && status < 300 }) } // claimReadyURL is the request target for a declared absolute path on the // socket's fixed host. A path that could name another host is refused. func claimReadyURL(path string) (string, bool) { if !strings.HasPrefix(path, "/") || strings.HasPrefix(path, "//") || strings.ContainsAny(path, "#\\ \t\r\n\x00") { return "", false } u, err := url.Parse("http://carlos" + path) if err != nil || u.Host != "carlos" || u.User != nil || u.Opaque != "" { return "", false } return u.String(), true } // claimProbePeer accepts the connection's peer only when its parent chain // reaches root and its cgroup v2 membership is exactly member. func claimProbePeer(conn net.Conn, root int, member string) error { unixConn, ok := conn.(*net.UnixConn) if !ok || root <= 1 { return hostwrite.ErrUnavailable } raw, err := unixConn.SyscallConn() if err != nil { return err } var peer *unix.Ucred var socketErr error if err = raw.Control(func(fd uintptr) { peer, socketErr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) }); err != nil { return err } if socketErr != nil { return socketErr } if peer == nil || peer.Pid <= 1 { return hostwrite.ErrUnavailable } pid := int(peer.Pid) for at, steps := pid, 0; at != root; steps++ { if at <= 1 || steps >= claimProbeDepth { return hostbox.ErrChanged } if at, err = claimParentPID(at); err != nil { return err } } groups, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/cgroup") if err != nil { return err } for _, line := range strings.Split(string(groups), "\n") { if line == member { return nil } } return hostbox.ErrChanged } func claimParentPID(pid int) (int, error) { raw, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat") if err != nil { return 0, err } i := strings.LastIndexByte(string(raw), ')') if i < 0 { return 0, hostwrite.ErrUnavailable } fields := strings.Fields(string(raw)[i+1:]) if len(fields) < 2 { return 0, hostwrite.ErrUnavailable } return strconv.Atoi(fields[1]) }