//go:build linux package host import ( "fmt" "os" "os/exec" "os/user" "runtime" "strconv" "syscall" "golang.org/x/sys/unix" ) var registryExecutable, registryExecutableError = os.Open("/proc/self/exe") func registryChildExec(path string, uid, gid int) (*exec.Cmd, error) { if registryExecutableError != nil { return nil, registryExecutableError } cmd := exec.Command("/proc/self/fd/3", registryChildCommand, path, strconv.Itoa(uid), strconv.Itoa(gid)) cmd.ExtraFiles = []*os.File{registryExecutable} return cmd, nil } func registryChildCloseExec() error { // This marks descriptors in the parent too; Go opens its own descriptors // close-on-exec, and ExtraFiles explicitly preserves the pinned executable. return unix.CloseRange(3, ^uint(0), unix.CLOSE_RANGE_CLOEXEC) } func registryChildStart(cmd *exec.Cmd) error { started := make(chan error, 1) go func() { runtime.LockOSThread() // Never unlock: exiting this goroutine retires this OS thread. The // parent keeps its privileges; every thread in the exec'd child inherits // no-new-privileges, including threads created by cgo or the Go runtime. if err := unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); err != nil { started <- fmt.Errorf("registry: no new privileges: %w", err) return } started <- cmd.Start() }() return <-started } func registryChildHarden() error { // Exec as root keeps runtime startup inaccessible to the service user. // Resolve the fixed service identity without inspecting child arguments. // syscall's credential setters update every thread, including with cgo. if err := syscall.Setgroups(nil); err != nil { return err } service, err := user.Lookup(serviceUser) if err != nil { return err } uid, err := strconv.Atoi(service.Uid) if err != nil { return err } gid, err := strconv.Atoi(service.Gid) if err != nil { return err } if uid <= 0 || gid < 0 { return fmt.Errorf("registry: invalid service credentials") } if err := syscall.Setresgid(gid, gid, gid); err != nil { return err } if err := syscall.Setresuid(uid, uid, uid); err != nil { return err } // Changing credentials resets dumpability to suid_dumpable. Explicitly // require zero too, before reading any registry arguments or pipe input. if err := unix.Prctl(unix.PR_SET_DUMPABLE, 0, 0, 0, 0); err != nil { return fmt.Errorf("registry: disable dumpability: %w", err) } dumpable, err := unix.PrctlRetInt(unix.PR_GET_DUMPABLE, 0, 0, 0, 0) if err != nil { return err } ruid, euid, suid := unix.Getresuid() rgid, egid, sgid := unix.Getresgid() groups, err := os.Getgroups() if err != nil { return err } if dumpable != 0 || ruid != uid || euid != uid || suid != uid || rgid != gid || egid != gid || sgid != gid || len(groups) != 0 { return fmt.Errorf("registry: child credentials were not dropped") } return nil } func closeRegistryExecutable() { // fd 3 is the parent's pinned executable, passed only for exec. unix.Close(3) if registryExecutable != nil { registryExecutable.Close() } }