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 "context"
7 "errors"
8 "fmt"
9 "io/fs"
10 "os"
11 "path/filepath"
12 "time"
13
14 "github.com/carlosframework/platform/internal/release"
15 "github.com/carlosframework/platform/internal/rootstate"
16 "golang.org/x/sys/unix"
17)
18
19// census checks the whole staged namespace, including unreserved builds. This
20// verifier accepts running tenants only with root-retained launch containment.
21// Other service tasks and unexplained namespaces still refuse completeness.
22func (k generationKernel) census(ctx context.Context, tree string) (string, string, error) {
23 before, err := k.boot()
24 if err != nil {
25 return "", "", err
26 }
27 f, err := os.Open(tree)
28 if err != nil {
29 return "", "", err
30 }
31 id, err := rootstate.Identity(f)
32 f.Close()
33 if err != nil {
34 return "", "", err
35 }
36 r := release.GenerationReservation{Binding: release.GenerationBinding{Tree: tree, Path: tree, TreeIdentity: id, Executable: id}}
37 digest, err := k.absent(ctx, r)
38 if err != nil {
39 return "", "", err
40 }
41 after, err := k.boot()
42 if err != nil || after != before {
43 return "", "", os.ErrPermission
44 }
45 return before, digest, nil
46}
47func checkedGenerationTree(ctx context.Context, tree string) error {
48 state, err := rootstate.Open(tree, false, true)
49 if err != nil {
50 return err
51 }
52 defer state.Close()
53 count := 0
54 return filepath.WalkDir(tree, func(path string, entry fs.DirEntry, walkErr error) error {
55 if walkErr != nil {
56 return walkErr
57 }
58 if err := ctx.Err(); err != nil {
59 return err
60 }
61 count++
62 if count > 8192 {
63 return os.ErrPermission
64 }
65 var st unix.Stat_t
66 if err := unix.Lstat(path, &st); err != nil {
67 return err
68 }
69 if st.Uid != 0 || st.Mode&0022 != 0 || (st.Mode&unix.S_IFMT == unix.S_IFREG && st.Nlink != 1) || (st.Mode&unix.S_IFMT != unix.S_IFREG && st.Mode&unix.S_IFMT != unix.S_IFDIR) {
70 return os.ErrPermission
71 }
72 return nil
73 })
74}
75
76// productionGenerationKernelCensus is root's kernel generation census. Its
77// every refusal is a generationCensusRefusal, so a no-journal recovery can
78// tell it from the other ways a settlement fails.
79func productionGenerationKernelCensus(ctx context.Context) (boot, digest string, err error) {
80 defer func() {
81 if err != nil {
82 boot, digest, err = "", "", &generationCensusRefusal{err: err}
83 }
84 }()
85 if os.Geteuid() != 0 {
86 return "", "", os.ErrPermission
87 }
88 if err := verifyStopKernelMounts("/sys/fs/cgroup", "/proc"); err != nil {
89 return "", "", err
90 }
91 hardlinks, err := generationRead("/proc/sys/fs/protected_hardlinks")
92 if err != nil || string(hardlinks) != "1\n" {
93 return "", "", os.ErrPermission
94 }
95 tree := filepath.Join(release.OptDir(), "versions")
96 if err := checkedGenerationTree(ctx, tree); err != nil {
97 return "", "", err
98 }
99 records, _ := ctx.Value(generationCensusReservationsKey{}).([]release.GenerationReservation)
100 return generationCensusRetry(ctx, func(ctx context.Context) (string, string, error) {
101 service, recheck := generationServiceReader(ctx)
102 return (generationKernel{proc: "/proc", cgroups: "/sys/fs/cgroup", contained: records, service: service, recheck: recheck, parent: generationCensusParent(ctx)}).census(ctx, tree)
103 })
104}
105
106// generationCensusWindow bounds a whole census, its passes and the waits
107// between them: a task that exited or was replaced mid-pass, or one the
108// service lookup refused (a unit still activating, the guard's prelude, a
109// login's or sshd's short-lived child), is waited out inside it. A task
110// still refused, or a pass still running, when it ends refuses the census.
111const generationCensusWindow = 20 * time.Second
112
113// errGenerationCensusLate is a census that did not finish inside its
114// window: the last pass ran past it, successful or not, or no pass could
115// start before it ended.
116var errGenerationCensusLate = fmt.Errorf("generation census ran past its window: %w", os.ErrPermission)
117
118// generationTaskRefusal is a census refusal of one task, named by its /proc
119// path. Its text is the path and the cause, as before it had a type.
120type generationTaskRefusal struct {
121 task string
122 err error
123}
124
125func (r *generationTaskRefusal) Error() string { return r.task + ": " + r.err.Error() }
126
127func (r *generationTaskRefusal) Unwrap() error { return r.err }
128
129// errGenerationTaskReplaced is a task whose start time moved during its
130// inspection: the PID now names another task. It is still a refusal
131// (os.ErrPermission), but one a fresh pass can clear.
132var errGenerationTaskReplaced = fmt.Errorf("task replaced during the census: %w", os.ErrPermission)
133
134// transient is true for the task-level refusals a later pass can clear:
135// the task exited or was replaced, or the service lookup refused it. A
136// task that references a retired generation, or a namespace or mount
137// refusal, is not: those refuse at once.
138func (r *generationTaskRefusal) transient() bool {
139 var service *generationServiceRefusal
140 return errors.Is(r.err, os.ErrNotExist) || errors.Is(r.err, unix.ESRCH) || errors.Is(r.err, errGenerationTaskReplaced) || errors.As(r.err, &service)
141}
142
143func generationCensusRetry(ctx context.Context, inspect func(context.Context) (string, string, error)) (string, string, error) {
144 return generationCensusRetryFor(ctx, inspect, generationCensusWindow, time.Now, generationSleep)
145}
146
147func generationSleep(ctx context.Context, d time.Duration) error {
148 t := time.NewTimer(d)
149 defer t.Stop()
150 select {
151 case <-ctx.Done():
152 return ctx.Err()
153 case <-t.C:
154 return nil
155 }
156}
157
158// generationCensusRetryFor runs whole census passes under one deadline,
159// window from its start. Each pass gets a context that ends at the
160// deadline, which its blocking reads (systemctl show, the image hash) obey,
161// and a pass that ends past the deadline refuses even if it succeeded, so
162// nothing it saw is accepted late. A transient task refusal is retried with
163// backoff (250 ms, doubling to 4 s) while the wait ends before the
164// deadline; then the last refusal is returned as it is. Any other
165// missing-file or vanished-process error is retried at once, up to three
166// passes, as before. Everything else returns at once. now is the clock the
167// deadline is read on.
168func generationCensusRetryFor(ctx context.Context, inspect func(context.Context) (string, string, error), window time.Duration, now func() time.Time, sleep func(context.Context, time.Duration) error) (string, string, error) {
169 deadline := now().Add(window)
170 bounded, cancel := context.WithTimeout(ctx, window)
171 defer cancel()
172 late := func(last error) error {
173 if last == nil {
174 return errGenerationCensusLate
175 }
176 return fmt.Errorf("%w (last pass: %v)", errGenerationCensusLate, last)
177 }
178 delay, quick := 250*time.Millisecond, 0
179 var last error
180 for {
181 if err := ctx.Err(); err != nil {
182 return "", "", err
183 }
184 if !now().Before(deadline) || bounded.Err() != nil {
185 return "", "", late(last)
186 }
187 boot, digest, err := inspect(bounded)
188 if ctx.Err() == nil && (!now().Before(deadline) || bounded.Err() != nil) {
189 return "", "", late(err)
190 }
191 if err == nil {
192 return boot, digest, nil
193 }
194 last = err
195 var task *generationTaskRefusal
196 switch {
197 case errors.As(err, &task) && task.transient():
198 if !now().Add(delay).Before(deadline) || sleep(bounded, delay) != nil {
199 return boot, digest, err
200 }
201 delay = min(2*delay, 4*time.Second)
202 case quick < 2 && (errors.Is(err, os.ErrNotExist) || errors.Is(err, unix.ESRCH)):
203 quick++
204 default:
205 return boot, digest, err
206 }
207 }
208}
209