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 "crypto/sha256"
7 "encoding/hex"
8 "errors"
9 "fmt"
10 "io"
11 "os"
12 "path/filepath"
13
14 "golang.org/x/sys/unix"
15)
16
17// ownByService hands one directory, and nothing beneath it, to the service
18// user and its group, as bootstrap's ownership step does
19// (bootstrapOwner). It opens the directory as bootstrap opens one
20// (bootstrapOpenDir: no symlink anywhere on the path, not a symlink
21// itself) and changes the open descriptor, so a path swapped for a
22// symlink is refused, never followed.
23func ownByService(path string) error {
24 uid, gid, err := bootstrapOwner(serviceUser, serviceUser)
25 if err != nil {
26 return err
27 }
28 return ownDir(path, uid, gid)
29}
30
31// ownDir is ownByService for a given owner.
32func ownDir(path string, uid, gid int) error {
33 fd, err := bootstrapOpenDir(path)
34 if err != nil {
35 return err
36 }
37 defer unix.Close(fd)
38 return unix.Fchown(fd, uid, gid)
39}
40
41// openChannelPolicy reads each operator-staged file install-channel --policy
42// takes, the --policy file and the --identity file (readChannelInputs):
43// opened with O_NOFOLLOW (a symlink is refused, never followed) and
44// O_NONBLOCK (a FIFO never blocks the open), fstat'd to be a regular file of
45// at most limit bytes, and only then read, from the descriptor, up to limit.
46func openChannelPolicy(path string, limit int64) ([]byte, error) {
47 fd, err := unix.Open(path, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0)
48 if err != nil {
49 return nil, err
50 }
51 f := os.NewFile(uintptr(fd), path)
52 defer f.Close()
53 st, err := f.Stat()
54 if err != nil {
55 return nil, err
56 }
57 if !st.Mode().IsRegular() {
58 return nil, errors.New("not a regular file")
59 }
60 if st.Size() > limit {
61 return nil, fmt.Errorf("larger than %d bytes", limit)
62 }
63 raw, err := io.ReadAll(io.LimitReader(f, limit+1))
64 if err != nil {
65 return nil, err
66 }
67 if int64(len(raw)) > limit {
68 return nil, fmt.Errorf("larger than %d bytes", limit)
69 }
70 return raw, nil
71}
72
73// openat2 is unix.Openat2, a seam for the test that a kernel without it
74// refuses the removal. There is no path-based fallback.
75var openat2 = unix.Openat2
76
77// slotState opens one protected slot with O_NOFOLLOW, so a symlink is never
78// followed, and hashes what it holds from the descriptor. A slot that is
79// absent is slotAbsent; one holding want, slotHolds; a symlink, a
80// non-regular file or any other content, slotForeign. Any other failure to
81// open or read it is an error.
82func slotState(path, want string) (slotKind, error) {
83 fd, err := unix.Open(path, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0)
84 switch {
85 case errors.Is(err, unix.ENOENT):
86 return slotAbsent, nil
87 case errors.Is(err, unix.ELOOP):
88 return slotForeign, nil
89 case err != nil:
90 return slotAbsent, err
91 }
92 f := os.NewFile(uintptr(fd), path)
93 defer f.Close()
94 st, err := f.Stat()
95 if err != nil {
96 return slotAbsent, err
97 }
98 if !st.Mode().IsRegular() {
99 return slotForeign, nil
100 }
101 h := sha256.New()
102 if _, err := io.Copy(h, io.LimitReader(f, systemArtifactLimit+1)); err != nil {
103 return slotAbsent, err
104 }
105 if want != "" && hex.EncodeToString(h.Sum(nil)) == want {
106 return slotHolds, nil
107 }
108 return slotForeign, nil
109}
110
111// removeCacheEntry removes the agent's cache entry for version, the one
112// directory install-channel made for the fetch, and what is in it. It never
113// resolves a path name through a symlink: the router directory is opened
114// as bootstrapOpenDir opens one (no symlink anywhere on the path), the
115// version directory is opened beneath it with no symlink followed, and
116// everything goes by unlinkat on a held descriptor. The service user owns
117// the parents, so a path-based RemoveAll could be walked out of /data. An
118// entry that is not there is already removed.
119func removeCacheEntry(dataDir, version string) error {
120 parts := systemCacheParts(version)
121 router, err := bootstrapOpenDir(filepath.Join(append([]string{dataDir}, parts[:len(parts)-2]...)...))
122 if err != nil {
123 if errors.Is(err, unix.ENOENT) {
124 return nil
125 }
126 return err
127 }
128 defer unix.Close(router)
129 return removeBeneath(router, parts[len(parts)-2])
130}
131
132// removeBeneath removes name, a directory, and everything under it, by
133// descriptor. A symlink is unlinked, never entered.
134func removeBeneath(parent int, name string) error {
135 fd, err := openat2(parent, name, &unix.OpenHow{
136 Flags: unix.O_RDONLY | unix.O_DIRECTORY | unix.O_CLOEXEC | unix.O_NOFOLLOW,
137 Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS,
138 })
139 if errors.Is(err, unix.ENOENT) {
140 return nil
141 }
142 if err != nil {
143 return err
144 }
145 dir := os.NewFile(uintptr(fd), name)
146 names, err := dir.Readdirnames(-1)
147 if err != nil {
148 dir.Close()
149 return err
150 }
151 for _, n := range names {
152 err := unix.Unlinkat(fd, n, 0)
153 if errors.Is(err, unix.EISDIR) {
154 err = removeBeneath(fd, n)
155 }
156 if err != nil && !errors.Is(err, unix.ENOENT) {
157 dir.Close()
158 return err
159 }
160 }
161 dir.Close()
162 if err := unix.Unlinkat(parent, name, unix.AT_REMOVEDIR); err != nil && !errors.Is(err, unix.ENOENT) {
163 return err
164 }
165 return nil
166}
167