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 host
4
5import (
6 "errors"
7 "fmt"
8 "os"
9 "os/user"
10 "path/filepath"
11 "strconv"
12 "syscall"
13 "time"
14
15 "github.com/carlosframework/platform/internal/safefile"
16 "golang.org/x/sys/unix"
17)
18
19// movesDir is <DataDir>/.moves, opened with O_NOFOLLOW below a data dir
20// root trusts, so nothing root reads or writes through it can be a
21// symlink the sandboxed agent planted. Every operation on the floor files
22// goes through one: the lock and the writes are all relative to its fd.
23type movesDir struct {
24 fd int
25 path string
26 lock *os.File
27 lockName string
28}
29
30// floorLockFile serialises every writer of the floor files (root's apply,
31// the agent's refresh) with every reader whose answer must not change under
32// it (root's judgement of carlos.prev and its activation guard).
33const floorLockFile = "floor.lock"
34
35const floorLockPoll = 100 * time.Millisecond
36
37// openMovesDir opens <root>/.moves. With create it makes the directory
38// when missing; without, a missing directory is os.ErrNotExist.
39//
40// Crash ordering (fix round 2): with create, the parent (root) is fsynced
41// EVERY time, after .moves is known to exist, whoever made it. A writer
42// never relies on having created the directory itself: another writer may
43// have made it and not yet synced the parent, or failed to. Only then is
44// any file written inside, each renamed into place and followed by an fsync
45// of .moves. So a floor write that returned nil survives a power loss,
46// directory entry and all. A failed fsync is the open's error, which the
47// writers report as a floor that could not be written (errFloorWrite, which
48// defers an apply) and the activation guard as a lock it could not take.
49// The cost is one fsync of the data dir per write.
50func openMovesDir(root string, create bool) (*movesDir, error) {
51 name := movesDirName
52 rootDir, err := safefile.OpenDir(root)
53 if err != nil {
54 return nil, fmt.Errorf("open %s: %w", root, err)
55 }
56 defer rootDir.Close()
57 rfd := int(rootDir.Fd())
58 flags := syscall.O_RDONLY | syscall.O_DIRECTORY | syscall.O_NOFOLLOW | syscall.O_CLOEXEC
59 fd, err := syscall.Openat(rfd, name, flags, 0)
60 if errors.Is(err, syscall.ENOENT) && create {
61 if err := syscall.Mkdirat(rfd, name, 0o755); err != nil && !errors.Is(err, syscall.EEXIST) {
62 return nil, fmt.Errorf("mkdir %s: %w", filepath.Join(root, name), err)
63 }
64 if fd, err = syscall.Openat(rfd, name, flags, 0); err == nil {
65 fchownToService(fd)
66 }
67 }
68 if err != nil {
69 return nil, fmt.Errorf("open %s: %w", filepath.Join(root, name), err)
70 }
71 if create {
72 if err := syncParent(rfd); err != nil {
73 syscall.Close(fd)
74 return nil, fmt.Errorf("fsync %s: %w", root, err)
75 }
76 }
77 return &movesDir{fd: fd, path: filepath.Join(root, name)}, nil
78}
79
80// syncParent fsyncs the data dir. A variable so a test can count the calls.
81var syncParent = func(fd int) error { return syscall.Fsync(fd) }
82
83// Close releases the lock (closing its fd) and the directory.
84func (d *movesDir) Close() {
85 if d.lock != nil {
86 d.lock.Close()
87 d.lock = nil
88 }
89 syscall.Close(d.fd)
90}
91
92// Lock takes floor.lock, waiting up to floorLockWait.
93func (d *movesDir) Lock() error { return d.lockNamed(floorLockFile, floorLockWait, errFloorLockBusy) }
94
95// lockNamed takes an exclusive flock on the file name in the directory,
96// waiting up to wait and then failing with busy. Opened read-only (a
97// read-only fd can be flocked), so the agent can lock a file root created
98// and root a file the agent created; O_NOFOLLOW and O_NONBLOCK so a symlink
99// or a FIFO planted in its place cannot redirect or park root. The fd is
100// kept until Close (or unlockRemoving), which releases the lock. One lock
101// per movesDir.
102//
103// A holder may remove the file it holds before releasing it
104// (unlockRemoving, the held-stage lock). A waiter that opened that file
105// would then lock a file no longer at the name, beside whoever has locked
106// the new one there. So once it has the flock it checks that the name still
107// names the file it locked (fstat of the fd against an fstatat of the name)
108// and, when the name is gone or names another file, closes it and starts
109// again on whatever is there now. The same removal can land inside the
110// open itself (the name vanishes, or the file opened has no link left);
111// that is retried too, while a symlink, a FIFO or a second link at the name
112// is still refused. Each retry pauses (lockNamedRetryPause, capped at what
113// is left of the wait), and none starts once the wait has passed: the call
114// fails with busy then, an open race included, and holds nothing.
115//
116// The deadline bounds the wait; correctness does not depend on it. A flock
117// taken just after the deadline (the check and the syscall are not one
118// step) is still a valid, exclusive lock, and is kept.
119func (d *movesDir) lockNamed(name string, wait time.Duration, busy error) error {
120 deadline := time.Now().Add(wait)
121 timedOut := func() error { return fmt.Errorf("lock %s: %w", filepath.Join(d.path, name), busy) }
122 for {
123 if time.Now().After(deadline) {
124 return timedOut()
125 }
126 f, err := lockNamedOpenAt(d.fd, name, os.O_RDONLY|os.O_CREATE, 0o644)
127 if err != nil {
128 if !d.removedDuringOpen(name, err) {
129 return fmt.Errorf("open %s: %w", filepath.Join(d.path, name), err)
130 }
131 // A holder's removal landed inside the open: start again.
132 lockNamedPause(deadline)
133 continue
134 }
135 lockNamedOpened(name)
136 if err := flockWithin(f, deadline); err != nil {
137 f.Close()
138 if errors.Is(err, syscall.EWOULDBLOCK) {
139 return timedOut()
140 }
141 return fmt.Errorf("lock %s: %w", filepath.Join(d.path, name), err)
142 }
143 same, err := d.names(name, f)
144 if err != nil {
145 f.Close()
146 return fmt.Errorf("lock %s: %w", filepath.Join(d.path, name), err)
147 }
148 if same {
149 d.lock, d.lockName = f, name
150 return nil
151 }
152 // The holder before us removed this file as it released it. Start
153 // again, within the wait: the publication waits inside the
154 // registry's freeze, so a name that keeps changing ends in busy.
155 f.Close()
156 lockNamedPause(deadline)
157 }
158}
159
160// lockNamedPause sleeps lockNamedRetryPause, or what is left of the wait
161// when that is less.
162func lockNamedPause(deadline time.Time) {
163 if left := time.Until(deadline); left > 0 {
164 time.Sleep(min(lockNamedRetryPause, left))
165 }
166}
167
168// flockWithin takes an exclusive flock on f, polling until deadline, and
169// once the deadline has passed stops trying and returns EWOULDBLOCK. A
170// flock that lands just after the deadline is kept: it is a valid,
171// exclusive lock, and the deadline only bounds the wait.
172func flockWithin(f *os.File, deadline time.Time) error {
173 for {
174 if time.Now().After(deadline) {
175 return syscall.EWOULDBLOCK
176 }
177 err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
178 if err == nil || !errors.Is(err, syscall.EWOULDBLOCK) {
179 return err
180 }
181 time.Sleep(floorLockPoll)
182 }
183}
184
185// writeHook, when set, is called with each file name WriteDurable is about
186// to write: a test seam for the order the floor files are written in.
187var writeHook func(name string)
188
189// names reports whether name in the directory is still the file f has
190// open: a regular file at the name with f's device and inode. A name that
191// is gone is false, not an error.
192func (d *movesDir) names(name string, f *os.File) (bool, error) {
193 var held, now unix.Stat_t
194 if err := unix.Fstat(int(f.Fd()), &held); err != nil {
195 return false, err
196 }
197 err := unix.Fstatat(d.fd, name, &now, unix.AT_SYMLINK_NOFOLLOW)
198 if errors.Is(err, unix.ENOENT) {
199 return false, nil
200 }
201 if err != nil {
202 return false, err
203 }
204 return now.Dev == held.Dev && now.Ino == held.Ino, nil
205}
206
207// removedDuringOpen reports whether err, from opening name, is the race
208// with a holder removing it: not an errno other than ENOENT (a real refusal,
209// such as EACCES or ELOOP), and the name is now gone or a regular file with
210// one link (a fresh lock file). Anything else at the name keeps the open's
211// refusal, and so does a .moves that has itself been removed (no create
212// can succeed in it, so waiting out the lock's wait would only delay the
213// error).
214func (d *movesDir) removedDuringOpen(name string, err error) bool {
215 var errno syscall.Errno
216 if errors.As(err, &errno) && errno != syscall.ENOENT {
217 return false
218 }
219 var st unix.Stat_t
220 if derr := unix.Fstat(d.fd, &st); derr != nil || st.Nlink == 0 {
221 return false
222 }
223 serr := unix.Fstatat(d.fd, name, &st, unix.AT_SYMLINK_NOFOLLOW)
224 if errors.Is(serr, unix.ENOENT) {
225 return true
226 }
227 return serr == nil && st.Mode&unix.S_IFMT == unix.S_IFREG && st.Nlink == 1
228}
229
230// unlockRemoving removes the lock file while still holding its flock, then
231// releases the lock (closing its fd). The name is removed only while it
232// still names the file held, so another holder's file is never taken. A
233// waiter on the removed file finds the name gone after its flock and starts
234// again (lockNamed), so two holders never exist at once. The lock is
235// released whatever the removal does; an error is the removal's.
236func (d *movesDir) unlockRemoving() error {
237 if d.lock == nil {
238 return nil
239 }
240 f, name := d.lock, d.lockName
241 d.lock, d.lockName = nil, ""
242 defer f.Close()
243 same, err := d.names(name, f)
244 if err != nil || !same {
245 return err
246 }
247 if err := unix.Unlinkat(d.fd, name, 0); err != nil && !errors.Is(err, unix.ENOENT) {
248 return fmt.Errorf("remove %s: %w", filepath.Join(d.path, name), err)
249 }
250 return nil
251}
252
253// lockNamedRetryPause is the pause between lockNamed's retries after a
254// holder's removal, capped at what is left of the wait.
255const lockNamedRetryPause = 10 * time.Millisecond
256
257// lockNamedOpenAt opens the lock file for lockNamed. A test seam: a test
258// can make every open meet the race with a holder's removal.
259var lockNamedOpenAt = safefile.OpenFileAt
260
261// lockNamedOpened runs in lockNamed after each open of the lock file and
262// before its flock. A test seam: the window in which the file a waiter has
263// open can be removed by the holder before it and replaced by a new one.
264var lockNamedOpened = func(name string) {}
265
266// WriteDurable writes data to .moves/name the way writeDurable does (a
267// temp file, fsync, rename, fsync of the directory), all relative to the
268// directory's fd. The temp name is random, so two writers never share one.
269func (d *movesDir) WriteDurable(name string, data []byte, mode os.FileMode) error {
270 if writeHook != nil {
271 writeHook(name)
272 }
273 return safefile.WriteAt(d.fd, name, data, mode)
274}
275
276// fchownToService hands a directory root just created to the service user,
277// best-effort: a process that is not root, or a box with no such user (a
278// test), keeps the owner it has.
279func fchownToService(fd int) {
280 if os.Geteuid() != 0 {
281 return
282 }
283 u, err := user.Lookup(serviceUser)
284 if err != nil {
285 return
286 }
287 uid, uerr := strconv.Atoi(u.Uid)
288 gid, gerr := strconv.Atoi(u.Gid)
289 if uerr != nil || gerr != nil {
290 return
291 }
292 syscall.Fchown(fd, uid, gid)
293}
294
295// floorLockCannotExist reports whether err, from opening or locking the
296// floor lock, means the lock cannot exist on this box at all: .moves or
297// floor.lock cannot be created (a read-only, full or unwritable data dir).
298// Then no writer can arm a floor either, which is what makes the F15
299// fallback safe. Contention (errFloorLockBusy) is never this: a writer is
300// active.
301func floorLockCannotExist(err error) bool {
302 if errors.Is(err, errFloorLockBusy) {
303 return false
304 }
305 for _, e := range []syscall.Errno{syscall.EROFS, syscall.ENOSPC, syscall.EACCES, syscall.EPERM, syscall.EDQUOT} {
306 if errors.Is(err, e) {
307 return true
308 }
309 }
310 return false
311}
312