carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package hostenv
4
5import (
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.
25func 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.
74func 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.
104var hostEnvOwner = os.Geteuid
105
106func notRootsDir(path string) error {
107 return fmt.Errorf("%s is not a directory only root can write", path)
108}
109