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 hostexec
4
5import (
6 "context"
7 "crypto/sha256"
8 "encoding/hex"
9 "encoding/json"
10 "errors"
11 "fmt"
12 "os"
13 "path/filepath"
14 "strconv"
15 "strings"
16 "syscall"
17
18 "github.com/carlosframework/platform/internal/hostfloor"
19 "github.com/carlosframework/platform/internal/hostguard"
20 "golang.org/x/sys/unix"
21)
22
23func controllerProcessStart(pid int) (string, error) {
24 if pid <= 0 {
25 return "", ErrState
26 }
27 raw, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
28 if err != nil {
29 return "", err
30 }
31 end := strings.LastIndexByte(string(raw), ')')
32 if end < 0 {
33 return "", ErrState
34 }
35 fields := strings.Fields(string(raw[end+1:]))
36 if len(fields) < 20 {
37 return "", ErrState
38 }
39 start := fields[19]
40 if _, err = strconv.ParseUint(start, 10, 64); err != nil {
41 return "", ErrState
42 }
43 return start, nil
44}
45func currentControllerProcess() (ControllerProcess, error) {
46 boot, err := BootID()
47 if err != nil {
48 return ControllerProcess{}, err
49 }
50 start, err := controllerProcessStart(os.Getpid())
51 if err != nil {
52 return ControllerProcess{}, err
53 }
54 return ControllerProcess{KernelBootID: boot, PID: os.Getpid(), Start: start}, nil
55}
56func proveControllerDead(p ControllerProcess, boot string, probe func(int) (string, error)) error {
57 if !validID(p.KernelBootID) || p.PID <= 0 || p.Start == "" || !validID(boot) {
58 return ErrState
59 }
60 if n, err := strconv.ParseUint(p.Start, 10, 64); err != nil || n == 0 || strconv.FormatUint(n, 10) != p.Start {
61 return ErrState
62 }
63 if p.KernelBootID != boot {
64 return nil
65 }
66 start, err := probe(p.PID)
67 if errors.Is(err, os.ErrNotExist) {
68 return nil
69 }
70 if err != nil {
71 return err
72 }
73 if start == "" || start == p.Start {
74 return ErrControllerBusy
75 }
76 return nil
77}
78
79// RecoverPending is root's offline update recovery. It never clears active
80// or lost state and never removes remote participation or claim authority.
81func RecoverPending(ctx context.Context, dataDir, guardDir string) error {
82 if os.Geteuid() != 0 {
83 return os.ErrPermission
84 }
85 return recoverPending(ctx, hostguard.Guard{DataDir: dataDir}, "/run/carlos/hostexec", filepath.Join(guardDir, "settled-controllers"), BootID, controllerProcessStart)
86}
87func recoverPending(ctx context.Context, g hostguard.Guard, lockDir, archive string, boot func() (string, error), probe func(int) (string, error)) error {
88 d, err := openLocal(g.DataDir)
89 if err != nil {
90 return err
91 }
92 defer d.close()
93 fd, err := unix.Dup(d.fd)
94 if err != nil {
95 return err
96 }
97 f := os.NewFile(uintptr(fd), "controllers")
98 entries, err := f.ReadDir(-1)
99 _ = f.Close()
100 if err != nil {
101 return err
102 }
103 if len(entries) == 0 {
104 return nil
105 }
106 kernelBoot, err := boot()
107 if err != nil {
108 return err
109 }
110 for _, e := range entries {
111 if err = ctx.Err(); err != nil {
112 return err
113 }
114 if !strings.HasSuffix(e.Name(), ".json") {
115 return ErrState
116 }
117 host := strings.TrimSuffix(e.Name(), ".json")
118 err = g.WithLocal(ctx, host, func() error {
119 unlock, err := tryExistingPendingLock(lockDir, host, ".lock")
120 if err != nil {
121 return err
122 }
123 defer unlock()
124 unlive, err := tryExistingPendingLock(lockDir, host, ".live")
125 if err != nil {
126 return err
127 }
128 defer unlive()
129 old, err := d.read(host)
130 if err != nil {
131 return err
132 }
133 if old.State != "pending" {
134 return ErrState
135 }
136 if err = proveControllerDead(old.Process, kernelBoot, probe); err != nil {
137 return err
138 }
139 // Keep the complete original tuple durably before removing its denial.
140 // A crash after this write replays the same content-addressed evidence.
141 raw, err := json.Marshal(old)
142 if err != nil {
143 return err
144 }
145 sum := sha256.Sum256(raw)
146 if err = os.Mkdir(archive, 0700); err != nil && !errors.Is(err, os.ErrExist) {
147 return err
148 }
149 info, err := os.Lstat(archive)
150 if err != nil {
151 return err
152 }
153 stat, ok := info.Sys().(*syscall.Stat_t)
154 if !ok || stat.Uid != uint32(os.Geteuid()) || !info.IsDir() || info.Mode().Perm() != 0700 {
155 return ErrState
156 }
157 parent, err := os.Open(filepath.Dir(archive))
158 if err != nil {
159 return err
160 }
161 syncErr := parent.Sync()
162 closeErr := parent.Close()
163 if err = errors.Join(syncErr, closeErr); err != nil {
164 return err
165 }
166 if err = hostfloor.Durable(filepath.Join(archive, hex.EncodeToString(sum[:])+".json"), raw, 0600); err != nil {
167 return err
168 }
169 current, err := d.read(host)
170 if err != nil {
171 return err
172 }
173 currentRaw, err := json.Marshal(current)
174 if err != nil {
175 return err
176 }
177 if string(currentRaw) != string(raw) {
178 return ErrState
179 }
180 return d.remove(host)
181 })
182 if err != nil {
183 return err
184 }
185 }
186 return nil
187}
188
189// The enclosing host guard prevents a cooperating Open from creating these
190// names until recovery finishes. Root must never create agent lock files.
191func tryExistingPendingLock(dir, host, suffix string) (func(), error) {
192 if _, err := Key(host, "check"); err != nil {
193 return nil, err
194 }
195 fd, err := openDir(dir)
196 if errors.Is(err, os.ErrNotExist) {
197 return func() {}, nil
198 }
199 if err != nil {
200 return nil, err
201 }
202 defer unix.Close(fd)
203 lock, err := unix.Openat(fd, host+suffix, unix.O_RDWR|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0)
204 if errors.Is(err, os.ErrNotExist) {
205 return func() {}, nil
206 }
207 if err != nil {
208 return nil, err
209 }
210 fail := func(e error) (func(), error) { unix.Close(lock); return nil, e }
211 var st unix.Stat_t
212 if err = unix.Fstat(lock, &st); err != nil {
213 return fail(err)
214 }
215 if st.Mode&unix.S_IFMT != unix.S_IFREG || st.Nlink != 1 {
216 return fail(ErrState)
217 }
218 if err = unix.Flock(lock, unix.LOCK_EX|unix.LOCK_NB); err != nil {
219 if errors.Is(err, unix.EWOULDBLOCK) || errors.Is(err, unix.EAGAIN) {
220 return fail(ErrControllerBusy)
221 }
222 return fail(err)
223 }
224 return func() { _ = unix.Flock(lock, unix.LOCK_UN); _ = unix.Close(lock) }, nil
225}
226