//go:build unix package hostfiles import ( "errors" "io" "os" "sort" "strconv" "strings" "golang.org/x/sys/unix" ) // restoreDirFlags is how Restore opens a directory it is about to sync: // read-only, a directory only, never through a link at the last // component, and never waiting on a FIFO. const restoreDirFlags = unix.O_RDONLY | unix.O_DIRECTORY | unix.O_NONBLOCK | unix.O_NOFOLLOW // dirHandle is a directory held open by descriptor. Everything beneath it // is reached one path component at a time with openat and O_NOFOLLOW, so // no symlink is ever followed, inside the tree or out of it, and a // directory swapped for something else after it was looked at is refused // rather than entered. Every open is non-blocking, so a FIFO swapped in // for a file or a directory cannot hang the publisher. type dirHandle struct{ f *os.File } // openTree opens the root of a files directory. The path to the root is // the platform's own and may contain symlinks; nothing beneath it may. func openTree(path string) (*dirHandle, error) { fd, err := unix.Open(path, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NONBLOCK, 0) if err != nil { return nil, &os.PathError{Op: "open", Path: path, Err: err} } return &dirHandle{os.NewFile(uintptr(fd), path)}, nil } // handleOf wraps a directory the caller opened, and keeps, as the root of // a walk: SyncDirAt never closes it. Anything but a directory is refused. func handleOf(f *os.File) (*dirHandle, error) { if f == nil { return nil, errors.New("hostfiles: sync of a nil directory") } info, err := f.Stat() if err != nil { return nil, err } if !info.IsDir() { return nil, &os.PathError{Op: "sync", Path: f.Name(), Err: unix.ENOTDIR} } return &dirHandle{f}, nil } func (d *dirHandle) Close() error { return d.f.Close() } func (d *dirHandle) Name() string { return d.f.Name() } // openDir opens the directory name inside d. A name that is not a real // directory (a symlink, a file, a FIFO) fails with ENOTDIR or ELOOP // without blocking; a missing one with ENOENT. func (d *dirHandle) openDir(name string) (*dirHandle, error) { fd, err := unix.Openat(int(d.f.Fd()), name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0) if err != nil { return nil, &os.PathError{Op: "openat", Path: name, Err: err} } return &dirHandle{os.NewFile(uintptr(fd), name)}, nil } // names lists d's entries, sorted. func (d *dirHandle) names() ([]string, error) { if _, err := d.f.Seek(0, io.SeekStart); err != nil { return nil, err } names, err := d.f.Readdirnames(-1) if err != nil { return nil, err } sort.Strings(names) return names, nil } // look inspects name inside d without following a symlink. func (d *dirHandle) look(name string) (unix.Stat_t, error) { var st unix.Stat_t err := unix.Fstatat(int(d.f.Fd()), name, &st, unix.AT_SYMLINK_NOFOLLOW) return st, err } func isDir(st unix.Stat_t) bool { return st.Mode&unix.S_IFMT == unix.S_IFDIR } func isRegular(st unix.Stat_t) bool { return st.Mode&unix.S_IFMT == unix.S_IFREG } func sameFile(a, b unix.Stat_t) bool { return a.Dev == b.Dev && a.Ino == b.Ino } func sameImage(a, b unix.Stat_t) bool { return sameFile(a, b) && a.Size == b.Size && a.Mtim == b.Mtim } // notADirectory reports the errors openDir gives for a name that exists // but is not a real directory. func notADirectory(err error) bool { return errors.Is(err, unix.ENOTDIR) || errors.Is(err, unix.ELOOP) } // descend opens the directory rel ("" or "a/b") beneath d, one component // at a time. gone is true when some component is missing or is not a real // directory: nothing of ours lives at a path under it. // // A component that is "", "." or ".." is refused with an error, never // reported as gone: publishOne relies on descend never leaving the tree. // descend("") returns d itself, so the caller must not close it. func (d *dirHandle) descend(rel string) (sub *dirHandle, gone bool, err error) { if rel == "" { return d, false, nil } parts := strings.Split(rel, "/") for _, part := range parts { if part == "" || part == "." || part == ".." { return nil, false, errors.New("hostfiles: path component " + strconv.Quote(part) + " in " + strconv.Quote(rel) + " is not a plain name") } } cur, owned := d, false for _, part := range parts { betweenLookAndOpen() next, err := cur.openDir(part) if owned { cur.Close() } if err != nil { if errors.Is(err, unix.ENOENT) || notADirectory(err) { return nil, true, nil } return nil, false, err } cur, owned = next, true } return cur, false, nil } // readStable reads the file name inside d for publishing. // // It reads only what it opened without following a link and found to be // the same regular file it had just inspected. A file over MaxFileBytes is // refused on its size alone, before any read. What it reports: // // - fileGone: nothing by that name. // - fileNotRegular: something by that name that is not a regular file. // - fileUnreadable: a regular file that could not be opened or read. It // still exists, so the caller must keep what the manifest has for it. // - fileRetry: the regular file was replaced by another regular file // between the look and the open. Nothing was read; ask again. // - fileTorn: read, but its size, mtime or identity changed meanwhile. // The bytes are a possibly inconsistent image. func readStable(d *dirHandle, name string) ([]byte, uint32, fileState, error) { before, err := d.look(name) if errors.Is(err, unix.ENOENT) { return nil, 0, fileGone, nil } if err != nil { return nil, 0, fileUnreadable, nil } if !isRegular(before) { return nil, 0, fileNotRegular, nil } if before.Size > MaxFileBytes { return nil, 0, fileTooLarge, nil } betweenLookAndOpen() fd, err := unix.Openat(int(d.f.Fd()), name, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0) switch { case errors.Is(err, unix.ENOENT): return nil, 0, fileGone, nil case errors.Is(err, unix.ELOOP): return nil, 0, fileNotRegular, nil // it became a symlink case err != nil: return nil, 0, fileUnreadable, nil } f := os.NewFile(uintptr(fd), name) defer f.Close() var opened unix.Stat_t if err := unix.Fstat(fd, &opened); err != nil { return nil, 0, fileUnreadable, nil } if !isRegular(opened) { return nil, 0, fileNotRegular, nil // swapped for a FIFO, a device, a directory } if !sameFile(before, opened) { return nil, 0, fileRetry, nil } if opened.Size > MaxFileBytes { return nil, 0, fileTooLarge, nil } body, err := readAll(io.LimitReader(f, MaxFileBytes+1)) if err != nil { return nil, 0, fileUnreadable, nil } if int64(len(body)) > MaxFileBytes { return nil, 0, fileTooLarge, nil // it grew past the cap while being read } state := fileOK if after, err := d.look(name); err != nil || !sameImage(before, after) { state = fileTorn } return body, uint32(before.Mode & 0o777), state, nil }