| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "fmt" |
| 7 | "os" |
| 8 | "os/exec" |
| 9 | "os/user" |
| 10 | "runtime" |
| 11 | "strconv" |
| 12 | "syscall" |
| 13 | |
| 14 | "golang.org/x/sys/unix" |
| 15 | ) |
| 16 | |
| 17 | var registryExecutable, registryExecutableError = os.Open("/proc/self/exe") |
| 18 | |
| 19 | func registryChildExec(path string, uid, gid int) (*exec.Cmd, error) { |
| 20 | if registryExecutableError != nil { |
| 21 | return nil, registryExecutableError |
| 22 | } |
| 23 | cmd := exec.Command("/proc/self/fd/3", registryChildCommand, path, strconv.Itoa(uid), strconv.Itoa(gid)) |
| 24 | cmd.ExtraFiles = []*os.File{registryExecutable} |
| 25 | return cmd, nil |
| 26 | } |
| 27 | |
| 28 | func registryChildCloseExec() error { |
| 29 | // This marks descriptors in the parent too; Go opens its own descriptors |
| 30 | // close-on-exec, and ExtraFiles explicitly preserves the pinned executable. |
| 31 | return unix.CloseRange(3, ^uint(0), unix.CLOSE_RANGE_CLOEXEC) |
| 32 | } |
| 33 | |
| 34 | func registryChildStart(cmd *exec.Cmd) error { |
| 35 | started := make(chan error, 1) |
| 36 | go func() { |
| 37 | runtime.LockOSThread() |
| 38 | // Never unlock: exiting this goroutine retires this OS thread. The |
| 39 | // parent keeps its privileges; every thread in the exec'd child inherits |
| 40 | // no-new-privileges, including threads created by cgo or the Go runtime. |
| 41 | if err := unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); err != nil { |
| 42 | started <- fmt.Errorf("registry: no new privileges: %w", err) |
| 43 | return |
| 44 | } |
| 45 | started <- cmd.Start() |
| 46 | }() |
| 47 | return <-started |
| 48 | } |
| 49 | |
| 50 | func registryChildHarden() error { |
| 51 | // Exec as root keeps runtime startup inaccessible to the service user. |
| 52 | // Resolve the fixed service identity without inspecting child arguments. |
| 53 | // syscall's credential setters update every thread, including with cgo. |
| 54 | if err := syscall.Setgroups(nil); err != nil { |
| 55 | return err |
| 56 | } |
| 57 | service, err := user.Lookup(serviceUser) |
| 58 | if err != nil { |
| 59 | return err |
| 60 | } |
| 61 | uid, err := strconv.Atoi(service.Uid) |
| 62 | if err != nil { |
| 63 | return err |
| 64 | } |
| 65 | gid, err := strconv.Atoi(service.Gid) |
| 66 | if err != nil { |
| 67 | return err |
| 68 | } |
| 69 | if uid <= 0 || gid < 0 { |
| 70 | return fmt.Errorf("registry: invalid service credentials") |
| 71 | } |
| 72 | if err := syscall.Setresgid(gid, gid, gid); err != nil { |
| 73 | return err |
| 74 | } |
| 75 | if err := syscall.Setresuid(uid, uid, uid); err != nil { |
| 76 | return err |
| 77 | } |
| 78 | // Changing credentials resets dumpability to suid_dumpable. Explicitly |
| 79 | // require zero too, before reading any registry arguments or pipe input. |
| 80 | if err := unix.Prctl(unix.PR_SET_DUMPABLE, 0, 0, 0, 0); err != nil { |
| 81 | return fmt.Errorf("registry: disable dumpability: %w", err) |
| 82 | } |
| 83 | dumpable, err := unix.PrctlRetInt(unix.PR_GET_DUMPABLE, 0, 0, 0, 0) |
| 84 | if err != nil { |
| 85 | return err |
| 86 | } |
| 87 | ruid, euid, suid := unix.Getresuid() |
| 88 | rgid, egid, sgid := unix.Getresgid() |
| 89 | groups, err := os.Getgroups() |
| 90 | if err != nil { |
| 91 | return err |
| 92 | } |
| 93 | if dumpable != 0 || ruid != uid || euid != uid || suid != uid || rgid != gid || egid != gid || sgid != gid || len(groups) != 0 { |
| 94 | return fmt.Errorf("registry: child credentials were not dropped") |
| 95 | } |
| 96 | return nil |
| 97 | } |
| 98 | |
| 99 | func closeRegistryExecutable() { |
| 100 | // fd 3 is the parent's pinned executable, passed only for exec. |
| 101 | unix.Close(3) |
| 102 | if registryExecutable != nil { |
| 103 | registryExecutable.Close() |
| 104 | } |
| 105 | } |
| 106 | |