| 1 | //go:build linux |
| 2 | |
| 3 | package hostenv |
| 4 | |
| 5 | import ( |
| 6 | "crypto/rand" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | |
| 12 | "golang.org/x/sys/unix" |
| 13 | ) |
| 14 | |
| 15 | // writeHostEnv writes body as <root>/etc/carlos/host.env, 0600, owned by |
| 16 | // the process's user (root on a box). Every directory on the way is held |
| 17 | // by descriptor, never looked up again by name: etc and etc/carlos are |
| 18 | // opened with O_NOFOLLOW|O_DIRECTORY, so a symlink at either is refused, |
| 19 | // and each must be owned by this user and writable by no one else |
| 20 | // (checkedDir), so no other user can swap the file or the directory under |
| 21 | // it. etc/carlos is created 0755 if it is missing, then opened and |
| 22 | // checked like the rest. The new file is written beside the old one in |
| 23 | // that directory, synced, renamed over it, and the directory synced, so a |
| 24 | // reader sees the old file or the new one, never part of either. |
| 25 | func writeHostEnv(root string, body []byte) error { |
| 26 | rootFD, err := unix.Open(root, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) |
| 27 | if err != nil { |
| 28 | return err |
| 29 | } |
| 30 | defer unix.Close(rootFD) |
| 31 | etc, err := checkedDir(rootFD, filepath.Join(root, "etc"), "etc", false) |
| 32 | if err != nil { |
| 33 | return err |
| 34 | } |
| 35 | defer unix.Close(etc) |
| 36 | dir, err := checkedDir(etc, filepath.Join(root, "etc", "carlos"), "carlos", true) |
| 37 | if err != nil { |
| 38 | return err |
| 39 | } |
| 40 | defer unix.Close(dir) |
| 41 | tmp := ".host.env." + rand.Text() |
| 42 | fd, err := unix.Openat(dir, tmp, unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0o600) |
| 43 | if err != nil { |
| 44 | return err |
| 45 | } |
| 46 | defer unix.Unlinkat(dir, tmp, 0) // gone once renamed |
| 47 | f := os.NewFile(uintptr(fd), tmp) |
| 48 | if err := f.Chmod(0o600); err != nil { |
| 49 | f.Close() |
| 50 | return err |
| 51 | } |
| 52 | if _, err := f.Write(body); err != nil { |
| 53 | f.Close() |
| 54 | return err |
| 55 | } |
| 56 | if err := f.Sync(); err != nil { |
| 57 | f.Close() |
| 58 | return err |
| 59 | } |
| 60 | if err := f.Close(); err != nil { |
| 61 | return err |
| 62 | } |
| 63 | if err := unix.Renameat(dir, tmp, dir, "host.env"); err != nil { |
| 64 | return err |
| 65 | } |
| 66 | return unix.Fsync(dir) |
| 67 | } |
| 68 | |
| 69 | // checkedDir opens name under the directory parent as a real directory, |
| 70 | // creating it 0755 first when create is set and it is missing, and refuses |
| 71 | // one that is a symlink, is not a directory, is not owned by this |
| 72 | // process's user (hostEnvOwner), or is writable by group or others. path |
| 73 | // is its name for the refusal. |
| 74 | func checkedDir(parent int, path, name string, create bool) (int, error) { |
| 75 | const how = unix.O_RDONLY | unix.O_DIRECTORY | unix.O_NOFOLLOW | unix.O_CLOEXEC |
| 76 | fd, err := unix.Openat(parent, name, how, 0) |
| 77 | if errors.Is(err, unix.ENOENT) && create { |
| 78 | if err := unix.Mkdirat(parent, name, 0o755); err != nil && !errors.Is(err, unix.EEXIST) { |
| 79 | return -1, err |
| 80 | } |
| 81 | fd, err = unix.Openat(parent, name, how, 0) |
| 82 | } |
| 83 | if errors.Is(err, unix.ELOOP) || errors.Is(err, unix.ENOTDIR) { |
| 84 | return -1, notRootsDir(path) |
| 85 | } |
| 86 | if err != nil { |
| 87 | return -1, err |
| 88 | } |
| 89 | var st unix.Stat_t |
| 90 | if err := unix.Fstat(fd, &st); err != nil { |
| 91 | unix.Close(fd) |
| 92 | return -1, err |
| 93 | } |
| 94 | if st.Mode&unix.S_IFMT != unix.S_IFDIR || int(st.Uid) != hostEnvOwner() || st.Mode&0o022 != 0 { |
| 95 | unix.Close(fd) |
| 96 | return -1, notRootsDir(path) |
| 97 | } |
| 98 | return fd, nil |
| 99 | } |
| 100 | |
| 101 | // hostEnvOwner is the user etc and etc/carlos must belong to: this |
| 102 | // process's, which runHostEnv has already required to be root. A var so a |
| 103 | // test can stand for a directory another user owns. |
| 104 | var hostEnvOwner = os.Geteuid |
| 105 | |
| 106 | func notRootsDir(path string) error { |
| 107 | return fmt.Errorf("%s is not a directory only root can write", path) |
| 108 | } |
| 109 | |