package safefile import ( "crypto/rand" "encoding/hex" "fmt" "io" "os" "path/filepath" "strings" "golang.org/x/sys/unix" ) func openDir(path string, create bool, mode os.FileMode) (int, error) { abs, err := filepath.Abs(path) if err != nil { return -1, err } fd, err := unix.Open("/", unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) if err != nil { return -1, err } for _, name := range strings.Split(strings.TrimPrefix(abs, "/"), "/") { if name == "" { continue } next, e := unix.Openat(fd, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if e == unix.ENOENT && create { e = unix.Mkdirat(fd, name, uint32(mode.Perm())) if e == nil || e == unix.EEXIST { next, e = unix.Openat(fd, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) } } unix.Close(fd) if e != nil { return -1, &os.PathError{Op: "open directory", Path: path, Err: e} } fd = next } return fd, nil } func MkdirAll(path string, mode os.FileMode) error { fd, err := openDir(path, true, mode) if err == nil { err = unix.Close(fd) } return err } func OpenFile(path string, flags int, mode os.FileMode) (*os.File, error) { dir, err := openDir(filepath.Dir(path), false, 0) if err != nil { return nil, err } defer unix.Close(dir) return OpenFileAt(dir, filepath.Base(path), flags, mode) } func OpenFileAt(dir int, path string, flags int, mode os.FileMode) (*os.File, error) { if path != filepath.Base(path) || path == "." || path == ".." { return nil, fmt.Errorf("invalid file name") } var err error fd := -1 if flags&os.O_CREATE != 0 { fd, err = unix.Openat(dir, filepath.Base(path), (flags&^os.O_TRUNC)|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_NONBLOCK|unix.O_CLOEXEC, uint32(mode.Perm())) if err != nil && (err != unix.EEXIST || flags&os.O_EXCL != 0) { return nil, &os.PathError{Op: "open", Path: path, Err: err} } } if fd < 0 { pinned, e := unix.Openat(dir, filepath.Base(path), unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if e != nil { return nil, &os.PathError{Op: "open", Path: path, Err: e} } defer unix.Close(pinned) var st unix.Stat_t if e = unix.Fstat(pinned, &st); e != nil { return nil, e } if st.Mode&unix.S_IFMT != unix.S_IFREG || st.Nlink != 1 { return nil, fmt.Errorf("%s: not a regular file with one link", path) } fd, err = unix.Open(fmt.Sprintf("/proc/self/fd/%d", pinned), (flags&^(os.O_CREATE|os.O_EXCL|os.O_TRUNC))|unix.O_NONBLOCK|unix.O_CLOEXEC, 0) if err != nil { return nil, &os.PathError{Op: "open", Path: path, Err: err} } } f := os.NewFile(uintptr(fd), path) var st unix.Stat_t if err = unix.Fstat(fd, &st); err == nil && (st.Mode&unix.S_IFMT != unix.S_IFREG || st.Nlink != 1) { err = fmt.Errorf("not a regular file with one link") } if err == nil && flags&os.O_TRUNC != 0 { err = f.Truncate(0) } if err != nil { f.Close() return nil, &os.PathError{Op: "open", Path: path, Err: err} } return f, nil } func Remove(path string) error { dir, err := openDir(filepath.Dir(path), false, 0) if err != nil { return err } defer unix.Close(dir) err = unix.Unlinkat(dir, filepath.Base(path), 0) if err != nil { return &os.PathError{Op: "remove", Path: path, Err: err} } return nil } func Write(path string, mode os.FileMode, write func(io.Writer) error) error { dir, err := openDir(filepath.Dir(path), false, 0) if err != nil { return err } defer unix.Close(dir) return writeAt(dir, filepath.Base(path), mode, write) } func WriteAt(dir int, name string, body []byte, mode os.FileMode) error { return writeAt(dir, name, mode, func(w io.Writer) error { _, err := w.Write(body); return err }) } func OpenDir(path string) (*os.File, error) { fd, err := openDir(path, false, 0) if err != nil { return nil, err } return os.NewFile(uintptr(fd), path), nil } func writeAt(dir int, path string, mode os.FileMode, write func(io.Writer) error) error { return writeAtSteps(dir, path, mode, write, func(_ string, op func() error) error { return op() }) } func writeAtSteps(dir int, path string, mode os.FileMode, write func(io.Writer) error, step func(string, func() error) error) error { if path != filepath.Base(path) || path == "." || path == ".." { return fmt.Errorf("invalid file name") } var nonce [16]byte if _, err := rand.Read(nonce[:]); err != nil { return err } name := ".carlos-write-" + hex.EncodeToString(nonce[:]) if err := unix.Mkdirat(dir, name, 0o700); err != nil { return err } defer unix.Unlinkat(dir, name, unix.AT_REMOVEDIR) stage, err := unix.Openat(dir, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) if err != nil { return err } defer unix.Close(stage) var st unix.Stat_t if err := unix.Fstat(stage, &st); err != nil { return err } if st.Uid != uint32(os.Geteuid()) || st.Mode&0o077 != 0 { return fmt.Errorf("unsafe staging directory") } fd, err := unix.Openat(stage, "data", unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0o600) if err != nil { return err } defer unix.Unlinkat(stage, "data", 0) f := os.NewFile(uintptr(fd), path) defer f.Close() if err := unix.Fstat(fd, &st); err != nil { return err } if st.Mode&unix.S_IFMT != unix.S_IFREG || st.Nlink != 1 { return fmt.Errorf("not a regular file with one link") } if err := step("write", func() error { return write(f) }); err != nil { return err } if err := step("chmod", func() error { return f.Chmod(mode) }); err != nil { return err } if err := step("file-sync", f.Sync); err != nil { return err } if err := step("close", f.Close); err != nil { return err } if err := step("rename", func() error { return unix.Renameat(stage, "data", dir, filepath.Base(path)) }); err != nil { return err } return step("parent-sync", func() error { return unix.Fsync(dir) }) }