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 hostbox
4
5import (
6 "context"
7 "errors"
8 "fmt"
9 "io"
10 "os"
11 "path/filepath"
12 "strings"
13
14 "github.com/carlosframework/platform/internal/rootstate"
15 "golang.org/x/sys/unix"
16)
17
18var ErrTypedParkUnsupported = errors.New("hostbox: typed park unsupported")
19
20// TypedParker resolves inventory root IDs only from trusted configuration.
21// ParkedRoot is the provisioned filesystem root containing .parked.
22type TypedParker struct {
23 CapturePersistent bool
24 Roots map[string]string
25 ParkedRoots map[string]string
26 ParkedRoot, Incarnation, Host, Retirement string
27 syncParents func(int, int) error
28}
29
30func (p TypedParker) validate() error {
31 if (p.ParkedRoot == "" && len(p.ParkedRoots) == 0) || !component(p.Incarnation) || !component(p.Host) || !component(p.Retirement) || len(p.Roots) == 0 {
32 return ErrTypedInventory
33 }
34 return nil
35}
36
37// CaptureArtifact records the no-follow inode identity used at retirement.
38func (p TypedParker) CaptureArtifact(rootID, name, owner string, kind ArtifactKind) (Artifact, error) {
39 if err := p.validate(); err != nil {
40 return Artifact{}, err
41 }
42 rootPath, ok := p.Roots[rootID]
43 if !ok || !managedName(name) || owner == "" {
44 return Artifact{}, ErrTypedInventory
45 }
46 fd, err := openParent(rootPath, name, false)
47 if err != nil {
48 return Artifact{}, err
49 }
50 defer unix.Close(fd.dir)
51 var st unix.Stat_t
52 if err := unix.Fstatat(fd.dir, fd.base, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil {
53 return Artifact{}, err
54 }
55 identity, err := inventoryIdentityAt(fd.dir, fd.base, true)
56 if err != nil {
57 return Artifact{}, err
58 }
59 if identity.Device != uint64(st.Dev) || identity.Inode != st.Ino {
60 return Artifact{}, ErrChanged
61 }
62 return Artifact{Identity: identity, Kind: kind, Root: rootID, Name: name, Owner: owner, Dev: uint64(st.Dev), Ino: st.Ino, Type: st.Mode & unix.S_IFMT}, nil
63}
64
65func (p TypedParker) Park(ctx context.Context, id string, a Artifact) error {
66 if err := ctx.Err(); err != nil {
67 return err
68 }
69 if err := p.validate(); err != nil {
70 return err
71 }
72 if p.CapturePersistent && a.Identity.Boot == "" {
73 return ErrTypedInventory
74 }
75 if id == "" || !managedName(a.Name) || a.Dev == 0 || a.Ino == 0 || a.Type == 0 {
76 return ErrTypedInventory
77 }
78 srcRoot, ok := p.Roots[a.Root]
79 if !ok {
80 return ErrTypedInventory
81 }
82 src, err := openParent(srcRoot, a.Name, false)
83 if err != nil {
84 return err
85 }
86 defer unix.Close(src.dir)
87 var st unix.Stat_t
88 if err := unix.Fstatat(src.dir, src.base, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil {
89 if errors.Is(err, unix.ENOENT) {
90 return p.verifyDestination(a, src.dir)
91 }
92 return err
93 }
94 if !sameArtifactAt(a, src.dir, src.base, &st) || !artifactRootChecked(a, srcRoot) {
95 return ErrChanged
96 }
97 dstRel := filepath.Join(".parked", p.Incarnation, p.Host, p.Retirement, a.Root, a.Name)
98 parkedRoot, err := p.ParkedArtifactRoot(a)
99 if err != nil {
100 return err
101 }
102 dst, err := openParent(parkedRoot, dstRel, true)
103 if err != nil {
104 return err
105 }
106 defer unix.Close(dst.dir)
107 var srcFS, dstFS unix.Stat_t
108 if err := unix.Fstat(src.dir, &srcFS); err != nil {
109 return err
110 }
111 if err := unix.Fstat(dst.dir, &dstFS); err != nil {
112 return err
113 }
114 if srcFS.Dev != dstFS.Dev {
115 return unix.EXDEV
116 }
117 if err := unix.Renameat2(src.dir, src.base, dst.dir, dst.base, unix.RENAME_NOREPLACE); err != nil {
118 if errors.Is(err, unix.EEXIST) {
119 return ErrChanged
120 }
121 return err
122 }
123 return p.sync(src.dir, dst.dir)
124}
125
126func (p TypedParker) verifyDestination(a Artifact, srcParent int) error {
127 rel := filepath.Join(".parked", p.Incarnation, p.Host, p.Retirement, a.Root, a.Name)
128 parkedRoot, err := p.ParkedArtifactRoot(a)
129 if err != nil {
130 return err
131 }
132 fd, err := openParent(parkedRoot, rel, false)
133 if err != nil {
134 return err
135 }
136 defer unix.Close(fd.dir)
137 var st unix.Stat_t
138 if err := unix.Fstatat(fd.dir, fd.base, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil {
139 return err
140 }
141 if !sameArtifactAt(a, fd.dir, fd.base, &st) || !artifactRootChecked(a, parkedRoot) {
142 return ErrChanged
143 }
144 return p.sync(srcParent, fd.dir)
145}
146
147func (p TypedParker) sync(a, b int) error {
148 if p.syncParents != nil {
149 return p.syncParents(a, b)
150 }
151 return syncFDs(a, b)
152}
153
154type parentFD struct {
155 dir int
156 base string
157}
158
159func openParent(rootPath, name string, create bool) (parentFD, error) {
160 root, err := unix.Openat2(unix.AT_FDCWD, rootPath, &unix.OpenHow{Flags: unix.O_RDONLY | unix.O_DIRECTORY | unix.O_CLOEXEC, Resolve: unix.RESOLVE_NO_SYMLINKS})
161 if err != nil {
162 return parentFD{}, err
163 }
164 parts := strings.Split(filepath.Clean(name), string(filepath.Separator))
165 cur := root
166 for _, part := range parts[:len(parts)-1] {
167 next, er := unix.Openat(cur, part, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
168 if er != nil && create && errors.Is(er, unix.ENOENT) {
169 if er = unix.Mkdirat(cur, part, 0o700); er == nil || errors.Is(er, unix.EEXIST) {
170 if er == nil {
171 er = unix.Fsync(cur)
172 }
173 if er != nil {
174 if cur != root {
175 unix.Close(cur)
176 }
177 unix.Close(root)
178 return parentFD{}, er
179 }
180 next, er = unix.Openat(cur, part, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
181 }
182 }
183 if cur != root {
184 unix.Close(cur)
185 }
186 if er != nil {
187 unix.Close(root)
188 return parentFD{}, er
189 }
190 cur = next
191 }
192 if cur != root {
193 unix.Close(root)
194 }
195 return parentFD{cur, parts[len(parts)-1]}, nil
196}
197func artifactRootChecked(a Artifact, root string) bool {
198 boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
199 return err == nil && (a.Identity.Boot == strings.TrimSpace(string(boot)) || checkedInventoryAncestry(root) == nil)
200}
201
202func sameArtifactAt(a Artifact, parent int, name string, st *unix.Stat_t) bool {
203 if a.Identity == (rootstate.FileIdentity{}) {
204 return false
205 }
206 current, err := inventoryIdentityAt(parent, name, true)
207 return err == nil && current.Device == uint64(st.Dev) && current.Inode == st.Ino && a.Identity.Device == a.Dev && a.Identity.Inode == a.Ino && a.Type == current.Mode&unix.S_IFMT && sameInventoryIdentity(a.Identity, current)
208}
209
210func syncFDs(a, b int) error {
211 if err := unix.Fsync(a); err != nil {
212 return fmt.Errorf("sync source: %w", err)
213 }
214 if err := unix.Fsync(b); err != nil {
215 return fmt.Errorf("sync destination: %w", err)
216 }
217 return nil
218}
219
220// VerifyEmptyDirectory reads the captured directory itself, with no symlink
221// traversal. The caller still parks by the same recorded inode under cleanup.
222func (p TypedParker) VerifyEmptyDirectory(a Artifact) error {
223 if err := p.validate(); err != nil {
224 return err
225 }
226 if p.CapturePersistent && a.Identity.Boot == "" {
227 return ErrTypedInventory
228 }
229 root, ok := p.Roots[a.Root]
230 if !ok || !managedName(a.Name) || a.Type != unix.S_IFDIR {
231 return ErrTypedInventory
232 }
233 parent, err := openParent(root, a.Name, false)
234 if err != nil {
235 return err
236 }
237 defer unix.Close(parent.dir)
238 fd, err := unix.Openat(parent.dir, parent.base, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
239 if err != nil {
240 return err
241 }
242 f := os.NewFile(uintptr(fd), a.Name)
243 defer f.Close()
244 var st unix.Stat_t
245 if err := unix.Fstat(fd, &st); err != nil {
246 return err
247 }
248 if !sameArtifactAt(a, parent.dir, parent.base, &st) || !artifactRootChecked(a, root) {
249 return ErrChanged
250 }
251 if names, err := f.Readdirnames(1); len(names) != 0 || !errors.Is(err, io.EOF) {
252 return ErrChanged
253 }
254 return nil
255}
256