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 unix
2
3package hostguard
4
5import (
6 "crypto/rand"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "io"
12 "os"
13 "path/filepath"
14
15 "golang.org/x/sys/unix"
16)
17
18type recordDir struct {
19 snapshot *record
20 root *RootAuthority
21 fd int
22 fsync func(int) error
23}
24
25func openDirectory(path string) (int, error) {
26 fd, err := unix.Open(path, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
27 if errors.Is(err, unix.ENOENT) {
28 return -1, fmt.Errorf("%w: %s", ErrNotProvisioned, path)
29 }
30 if err != nil {
31 return -1, err
32 }
33 var st unix.Stat_t
34 if err := unix.Fstat(fd, &st); err != nil {
35 unix.Close(fd)
36 return -1, err
37 }
38 if st.Mode&unix.S_IFMT != unix.S_IFDIR {
39 unix.Close(fd)
40 return -1, fmt.Errorf("hostguard: not a directory: %s", path)
41 }
42 return fd, nil
43}
44
45func openRecordDir(dataDir string) (*recordDir, error) {
46 fd, err := openDirectory(filepath.Join(dataDir, "hostguard"))
47 if err != nil {
48 return nil, err
49 }
50 return &recordDir{fd: fd, fsync: unix.Fsync}, nil
51}
52
53func (d *recordDir) close() { _ = unix.Close(d.fd) }
54
55func (d *recordDir) openRecord(host string) (*os.File, error) {
56 if err := validateFileHost(host); err != nil {
57 return nil, err
58 }
59 // NONBLOCK prevents a planted FIFO from hanging before Fstat can refuse it.
60 fd, err := unix.Openat(d.fd, host+".json", unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0)
61 if errors.Is(err, unix.ENOENT) {
62 return nil, ErrNoRecord
63 }
64 if err != nil {
65 return nil, err
66 }
67 f := os.NewFile(uintptr(fd), host+".json")
68 var st unix.Stat_t
69 if err := unix.Fstat(fd, &st); err != nil {
70 f.Close()
71 return nil, err
72 }
73 if st.Mode&unix.S_IFMT != unix.S_IFREG {
74 f.Close()
75 return nil, ErrUnsafeFile
76 }
77 return f, nil
78}
79
80func (d *recordDir) read(host string) (record, error) {
81 if d.snapshot != nil {
82 if d.snapshot.Host != host {
83 return record{}, ErrInvalidHost
84 }
85 return *d.snapshot, nil
86 }
87 if d.root != nil {
88 return d.root.read(host)
89 }
90 f, err := d.openRecord(host)
91 if err != nil {
92 return record{}, err
93 }
94 defer f.Close()
95 b, err := io.ReadAll(f)
96 if err != nil {
97 return record{}, err
98 }
99 return decodeRecord(b, host)
100}
101
102func (d *recordDir) sync(host string) error {
103 if d.snapshot != nil {
104 return ErrFenced
105 }
106 if d.root != nil {
107 r, err := d.root.read(host)
108 if err != nil {
109 return err
110 }
111 return d.root.write(r)
112 }
113 f, err := d.openRecord(host)
114 if err != nil {
115 return err
116 }
117 defer f.Close()
118 if err := d.fsync(int(f.Fd())); err != nil {
119 return err
120 }
121 return d.fsync(d.fd)
122}
123
124func (d *recordDir) write(r record) error {
125 if d.snapshot != nil {
126 return ErrFenced
127 }
128 if d.root != nil {
129 return d.root.write(r)
130 }
131 var nonce [16]byte
132 if _, err := rand.Read(nonce[:]); err != nil {
133 return err
134 }
135 return d.writeUsing(r, ".tmp-"+hex.EncodeToString(nonce[:]), unix.Renameat)
136}
137
138func (d *recordDir) writeUsing(r record, tmp string, rename func(int, string, int, string) error) error {
139 if err := validateFileHost(r.Host); err != nil {
140 return err
141 }
142 if err := r.validate(r.Host); err != nil {
143 return err
144 }
145 if tmp == "" || tmp == "." || tmp == ".." || filepath.Base(tmp) != tmp || tmp == r.Host+".json" {
146 return ErrUnsafeFile
147 }
148 // Refuse an existing symlink or unreadable record. Renameat itself never
149 // follows the destination, including a symlink planted after this check.
150 if _, err := d.read(r.Host); err != nil && !errors.Is(err, ErrNoRecord) {
151 return err
152 }
153 b, err := json.Marshal(r)
154 if err != nil {
155 return err
156 }
157 fd, err := unix.Openat(d.fd, tmp, unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0660)
158 if err != nil {
159 return err
160 }
161 // Only remove a name this call successfully created.
162 defer unix.Unlinkat(d.fd, tmp, 0)
163 f := os.NewFile(uintptr(fd), tmp)
164 defer f.Close()
165 if err := unix.Fchmod(fd, 0660); err != nil {
166 return err
167 }
168 if _, err := f.Write(append(b, '\n')); err != nil {
169 return err
170 }
171 if err := d.fsync(fd); err != nil {
172 return err
173 }
174 if err := f.Close(); err != nil {
175 return err
176 }
177 if err := rename(d.fd, tmp, d.fd, r.Host+".json"); err != nil {
178 return err
179 }
180 return d.fsync(d.fd)
181}
182