//go:build linux package park import ( "bytes" "context" "crypto/rand" "encoding/hex" "encoding/json" "errors" "io" "os" "reflect" "strings" "golang.org/x/sys/unix" ) func localSupported() bool { return true } func localDirAt(parent int, name string, create bool) (int, error) { if !localComponent(name) { return -1, ErrLocalInventory } if create { if err := unix.Mkdirat(parent, name, 0770); err != nil && !errors.Is(err, unix.EEXIST) { return -1, err } } fd, err := unix.Openat(parent, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if err != nil { return -1, err } var st unix.Stat_t if err := unix.Fstat(fd, &st); err != nil || st.Mode&unix.S_IFMT != unix.S_IFDIR { unix.Close(fd) if err != nil { return -1, err } return -1, ErrLocalInventory } if create { // A retry can encounter another worker's directory. Root also respects its owner. if st.Uid == uint32(os.Geteuid()) { if err := unix.Fchmod(fd, 02770); err != nil { unix.Close(fd) return -1, err } } else if st.Mode&02770 != 02770 { unix.Close(fd) return -1, ErrLocalInventory } if err := unix.Fsync(fd); err != nil { unix.Close(fd) return -1, err } if err := unix.Fsync(parent); err != nil { unix.Close(fd) return -1, err } } return fd, nil } func localWalk(root int, parts []string, create bool) (int, error) { fd, err := unix.Dup(root) if err != nil { return -1, err } unix.CloseOnExec(fd) for _, p := range parts { next, err := localDirAt(fd, p, create) unix.Close(fd) if err != nil { return -1, err } fd = next } return fd, nil } func localParent(root int, p string, create bool) (int, string, error) { if !localPath(p) { return -1, "", ErrLocalInventory } parts := strings.Split(p, "/") fd, err := localWalk(root, parts[:len(parts)-1], create) return fd, parts[len(parts)-1], err } func localStat(fd int, name string) (unix.Stat_t, error) { var st unix.Stat_t err := unix.Fstatat(fd, name, &st, unix.AT_SYMLINK_NOFOLLOW) if err == nil && st.Mode&unix.S_IFMT != unix.S_IFDIR && st.Mode&unix.S_IFMT != unix.S_IFREG { return st, ErrLocalInventory } return st, err } func localSame(st unix.Stat_t, item localItem) bool { return uint64(st.Dev) == item.Dev && st.Ino == item.Ino && st.Mode&unix.S_IFMT == item.Type } func localReadJournal(dir int) (*localJournal, error) { fd, err := unix.Openat(dir, ".journal.json", unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0) if err != nil { return nil, err } f := os.NewFile(uintptr(fd), "journal") defer f.Close() st, err := f.Stat() if err != nil { return nil, err } if !st.Mode().IsRegular() { return nil, ErrLocalJournal } b, err := io.ReadAll(io.LimitReader(f, (4<<20)+1)) if err != nil { return nil, err } if len(b) > 4<<20 { return nil, ErrLocalJournal } d := json.NewDecoder(bytes.NewReader(b)) d.DisallowUnknownFields() var j localJournal if err := d.Decode(&j); err != nil { return nil, err } if _, err := d.Token(); err != io.EOF { return nil, ErrLocalJournal } return &j, nil } func localWriteJournal(dir int, j *localJournal) error { b, err := json.Marshal(j) if err != nil { return err } var nonce [16]byte if _, err := rand.Read(nonce[:]); err != nil { return err } name := ".journal-" + hex.EncodeToString(nonce[:]) fd, err := unix.Openat(dir, name, unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0660) if err != nil { return err } f := os.NewFile(uintptr(fd), "journal-temp") defer f.Close() defer unix.Unlinkat(dir, name, 0) if err := unix.Fchmod(fd, 0660); err != nil { return err } if _, err := f.Write(b); err != nil { return err } if err := f.Sync(); err != nil { return err } if err := unix.Renameat(dir, name, dir, ".journal.json"); err != nil { return err } return unix.Fsync(dir) } func localJournalMatches(a, b localJournal) bool { if len(a.Items) != len(b.Items) { return false } for i := range a.Items { if a.Items[i].Path != b.Items[i].Path || a.Items[i].Stamp != b.Items[i].Stamp { return false } if a.Items[i].Missing { if a.Items[i].Dev != 0 || a.Items[i].Ino != 0 || a.Items[i].Type != 0 { return false } } else if a.Items[i].Ino == 0 || (a.Items[i].Type != unix.S_IFDIR && a.Items[i].Type != unix.S_IFREG) { return false } } a.Items, b.Items = nil, nil return reflect.DeepEqual(a, b) } func (l Local) parkLocal(ctx context.Context, want *localJournal) error { root, err := unix.Open(l.DataDir, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if err != nil { return err } defer unix.Close(root) parked, err := localDirAt(root, ".parked", false) if err != nil { return err } defer unix.Close(parked) event, err := localWalk(parked, []string{want.Incarnation, want.Host, want.Retirement}, true) if err != nil { return err } defer unix.Close(event) dest, err := localDirAt(event, "artifacts", true) if err != nil { return err } defer unix.Close(dest) prior, err := localReadJournal(event) if errors.Is(err, unix.ENOENT) { for i := range want.Items { it := &want.Items[i] src, name, e := localParent(root, it.Path, false) if errors.Is(e, unix.ENOENT) { it.Missing = true continue } if e != nil { return e } st, e := localStat(src, name) unix.Close(src) if errors.Is(e, unix.ENOENT) { it.Missing = true continue } if e != nil { return e } it.Dev, it.Ino, it.Type = uint64(st.Dev), st.Ino, st.Mode&unix.S_IFMT dp, dn, e := localParent(dest, it.Path, true) if e != nil { return e } _, e = localStat(dp, dn) unix.Close(dp) if !errors.Is(e, unix.ENOENT) { if e != nil { return e } return ErrLocalJournal } } if err := localWriteJournal(event, want); err != nil { return err } } else if err != nil { return err } else { if !localJournalMatches(*prior, *want) { return ErrLocalJournal } *want = *prior } for i := range want.Items { if err := ctx.Err(); err != nil { return err } it := &want.Items[i] if it.Missing { // A still-active installation can recreate an absent path between attempts. sp, sn, err := localParent(root, it.Path, false) if err == nil { _, err = localStat(sp, sn) unix.Close(sp) } if !errors.Is(err, unix.ENOENT) { if err != nil { return err } return ErrLocalJournal } it.Done = true continue } dp, dn, err := localParent(dest, it.Path, true) if err != nil { return err } sp, sn, err := localParent(root, it.Path, false) if err != nil { unix.Close(dp) return err } err = l.moveLocalItem(sp, sn, dp, dn, it) if err == nil { err = unix.Fsync(sp) } if err == nil { err = unix.Fsync(dp) } unix.Close(sp) unix.Close(dp) if err != nil { return err } it.Done = true if err := localWriteJournal(event, want); err != nil { return err } } if err := localWriteJournal(event, want); err != nil { return err } if err := unix.Fsync(dest); err != nil { return err } return unix.Fsync(event) } func (l Local) moveLocalItem(sp int, sn string, dp int, dn string, it *localItem) error { dst, de := localStat(dp, dn) if de == nil { if !localSame(dst, *it) { return ErrLocalJournal } _, se := localStat(sp, sn) if se == nil { return ErrLocalJournal } if se != nil && !errors.Is(se, unix.ENOENT) { return se } return nil } if !errors.Is(de, unix.ENOENT) { return de } if it.Done { return ErrLocalJournal } src, err := localStat(sp, sn) if err != nil { return err } if !localSame(src, *it) { return ErrLocalJournal } if err := unix.Renameat2(sp, sn, dp, dn, unix.RENAME_NOREPLACE); err != nil { return err } if l.afterRename != nil { return l.afterRename(it.Path) } return nil }