| 1 | //go:build linux |
| 2 | |
| 3 | package provisioner |
| 4 | |
| 5 | import ( |
| 6 | "errors" |
| 7 | "io" |
| 8 | "os" |
| 9 | "path/filepath" |
| 10 | "strings" |
| 11 | |
| 12 | "golang.org/x/sys/unix" |
| 13 | ) |
| 14 | |
| 15 | var ErrRefused = errors.New("provisioner: refused") |
| 16 | var ErrUnavailable = errors.New("provisioner: unavailable") |
| 17 | |
| 18 | // privateDirectory retains an opened directory so later operations cannot follow |
| 19 | // a replacement path or a symlink supplied in a file name. |
| 20 | type privateDirectory struct{ file *os.File } |
| 21 | |
| 22 | func openPrivateDirectory(path string) (*privateDirectory, error) { |
| 23 | if !filepath.IsAbs(path) || filepath.Clean(path) != path || path == "/" { |
| 24 | return nil, ErrRefused |
| 25 | } |
| 26 | fd, err := unix.Open("/", unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) |
| 27 | if err != nil { |
| 28 | return nil, ErrUnavailable |
| 29 | } |
| 30 | for _, part := range strings.Split(strings.TrimPrefix(path, "/"), "/") { |
| 31 | next, e := unix.Openat(fd, part, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) |
| 32 | unix.Close(fd) |
| 33 | if e != nil { |
| 34 | return nil, ErrRefused |
| 35 | } |
| 36 | fd = next |
| 37 | } |
| 38 | var stat unix.Stat_t |
| 39 | var fs unix.Statfs_t |
| 40 | if unix.Fstat(fd, &stat) != nil || unix.Fstatfs(fd, &fs) != nil { |
| 41 | unix.Close(fd) |
| 42 | return nil, ErrUnavailable |
| 43 | } |
| 44 | if stat.Uid != uint32(os.Getuid()) || stat.Mode&0777 != 0700 || (fs.Type != unix.EXT4_SUPER_MAGIC && fs.Type != unix.XFS_SUPER_MAGIC) { |
| 45 | unix.Close(fd) |
| 46 | return nil, ErrRefused |
| 47 | } |
| 48 | return &privateDirectory{os.NewFile(uintptr(fd), path)}, nil |
| 49 | } |
| 50 | func (d *privateDirectory) close() error { return d.file.Close() } |
| 51 | func leaf(name string) bool { |
| 52 | return name != "" && name != "." && name != ".." && len(name) <= 128 && !strings.ContainsAny(name, "/\\\x00") |
| 53 | } |
| 54 | func (d *privateDirectory) create(name string, body []byte) error { |
| 55 | if !leaf(name) || len(body) > 128<<10 { |
| 56 | return ErrRefused |
| 57 | } |
| 58 | fd, e := unix.Openat(int(d.file.Fd()), name, unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0600) |
| 59 | if e != nil { |
| 60 | return ErrRefused |
| 61 | } |
| 62 | f := os.NewFile(uintptr(fd), name) |
| 63 | // A failed exclusive write remains present: recovery must validate or refuse |
| 64 | // the partial record instead of replacing evidence of an interrupted write. |
| 65 | _, e = f.Write(body) |
| 66 | if e == nil { |
| 67 | e = f.Sync() |
| 68 | } |
| 69 | closeErr := f.Close() |
| 70 | if e != nil || closeErr != nil || d.file.Sync() != nil { |
| 71 | return ErrUnavailable |
| 72 | } |
| 73 | return nil |
| 74 | } |
| 75 | func (d *privateDirectory) read(name string, limit int) ([]byte, error) { |
| 76 | if !leaf(name) || limit <= 0 || limit > 128<<10 { |
| 77 | return nil, ErrRefused |
| 78 | } |
| 79 | fd, e := unix.Openat(int(d.file.Fd()), name, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK|unix.O_CLOEXEC, 0) |
| 80 | if e != nil { |
| 81 | return nil, ErrRefused |
| 82 | } |
| 83 | f := os.NewFile(uintptr(fd), name) |
| 84 | defer f.Close() |
| 85 | var s unix.Stat_t |
| 86 | if unix.Fstat(fd, &s) != nil { |
| 87 | return nil, ErrUnavailable |
| 88 | } |
| 89 | if s.Mode&unix.S_IFMT != unix.S_IFREG || s.Mode&0777 != 0600 || s.Uid != uint32(os.Getuid()) || s.Nlink != 1 || s.Size > int64(limit) { |
| 90 | return nil, ErrRefused |
| 91 | } |
| 92 | body, e := io.ReadAll(io.LimitReader(f, int64(limit)+1)) |
| 93 | if e != nil { |
| 94 | return nil, ErrUnavailable |
| 95 | } |
| 96 | if len(body) > limit { |
| 97 | return nil, ErrRefused |
| 98 | } |
| 99 | return body, nil |
| 100 | } |
| 101 | func (d *privateDirectory) lock() (*os.File, error) { |
| 102 | fd, e := unix.Openat(int(d.file.Fd()), "lock", unix.O_RDWR|unix.O_CREAT|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0600) |
| 103 | if e != nil { |
| 104 | return nil, ErrRefused |
| 105 | } |
| 106 | f := os.NewFile(uintptr(fd), "lock") |
| 107 | var s unix.Stat_t |
| 108 | if unix.Fstat(fd, &s) != nil || s.Mode&unix.S_IFMT != unix.S_IFREG || s.Mode&0777 != 0600 || s.Uid != uint32(os.Getuid()) || s.Nlink != 1 { |
| 109 | f.Close() |
| 110 | return nil, ErrRefused |
| 111 | } |
| 112 | if unix.Flock(fd, unix.LOCK_EX|unix.LOCK_NB) != nil { |
| 113 | f.Close() |
| 114 | return nil, ErrRefused |
| 115 | } |
| 116 | if f.Sync() != nil || d.file.Sync() != nil { |
| 117 | f.Close() |
| 118 | return nil, ErrUnavailable |
| 119 | } |
| 120 | return f, nil |
| 121 | } |
| 122 | |
| 123 | func checkRegistryFile(d *privateDirectory) error { |
| 124 | fd, e := unix.Openat(int(d.file.Fd()), "registry.db", unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK|unix.O_CLOEXEC, 0) |
| 125 | if e != nil { |
| 126 | return ErrRefused |
| 127 | } |
| 128 | defer unix.Close(fd) |
| 129 | var s unix.Stat_t |
| 130 | if unix.Fstat(fd, &s) != nil { |
| 131 | return ErrUnavailable |
| 132 | } |
| 133 | if s.Mode&unix.S_IFMT != unix.S_IFREG || s.Mode&0777 != 0600 || s.Uid != uint32(os.Getuid()) || s.Nlink != 1 || s.Size > int64(maxRecords)*(128<<10) { |
| 134 | return ErrRefused |
| 135 | } |
| 136 | return nil |
| 137 | } |
| 138 | |