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 activator
4
5import (
6 "context"
7 "encoding/json"
8 "errors"
9 "net"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "slices"
14 "strconv"
15 "strings"
16 "time"
17
18 "github.com/carlosframework/platform/internal/cgroup"
19 "github.com/carlosframework/platform/internal/hostbox"
20 "github.com/carlosframework/platform/internal/hostclaim"
21 "github.com/carlosframework/platform/internal/hostwrite"
22 "github.com/carlosframework/platform/internal/instancectl"
23 "github.com/carlosframework/platform/internal/instancefs"
24 "golang.org/x/sys/unix"
25)
26
27// startIsolated is Start through the reservation-bearing root protocol
28// (claim Plan 11, R10). The agent writes the role's launch file and opens
29// its log socket, exactly as main's nested start does, then asks root to
30// run the reservation. It never builds a process of its own: the command it
31// returns names the unit's main process root reported, and the settle it
32// returns is root's stop of that same reservation.
33func (l RoleLaunch) startIsolated(ctx context.Context, role string, attempt hostclaim.WakeRetryAttempt, build func() *exec.Cmd, before func() error) (*exec.Cmd, *cgroup.Prepared, func(context.Context) error, RoleProducer, error) {
34 fail := func(err error) (*exec.Cmd, *cgroup.Prepared, func(context.Context) error, RoleProducer, error) {
35 return nil, nil, nil, RoleProducer{}, err
36 }
37 if role != "main" {
38 return fail(hostbox.ErrRoleLaunchShape)
39 }
40 if l.Work == nil || l.Executable == nil || build == nil || before == nil || l.MemoryMax <= 0 ||
41 !cleanAbs(l.LaunchDir) || !cleanAbs(l.RunDir) || !cleanAbs(l.LogSocket) ||
42 attempt.ID == "" || attempt.InvocationID == "" || attempt.LaunchInputs == "" || attempt.Session != l.Target.Session || attempt.Box != l.Target.Box ||
43 attempt.Enrollment != l.Target.Enrollment || attempt.BootID != l.Target.BootID || attempt.ControllerID != l.Target.ControllerID {
44 return fail(hostwrite.ErrUnavailable)
45 }
46 if l.InitializeUnlocked != nil {
47 if err := l.InitializeUnlocked(ctx); err != nil {
48 return fail(err)
49 }
50 }
51 host := l.Target.Host
52 var cmd *exec.Cmd
53 var executable *os.File
54 defer func() {
55 if executable != nil {
56 executable.Close()
57 }
58 }()
59 reservation := hostbox.LaunchReservation{ID: attempt.ID + ":" + role, Role: role, InvocationID: attempt.InvocationID, Inputs: attempt.LaunchInputs, Target: l.Target, Group: hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(host)}}
60 if l.Unit != nil {
61 if !instancectl.ValidBoundClaimUnit(l.Unit.Unit) {
62 return fail(hostbox.ErrRoleLaunchShape)
63 }
64 reservation.Group.RelativePath = instancectl.BoundClaimUnitControlGroup(host, l.Unit.Unit)
65 }
66 err := l.Work.With(ctx, l.Identity, func() error {
67 cmd = build()
68 if cmd == nil {
69 return hostwrite.ErrUnavailable
70 }
71 var argv []string
72 var socket string
73 var err error
74 if l.Unit != nil {
75 if l.Spec == nil || len(cmd.Args) == 0 || !slices.Equal(cmd.Args[1:], l.Unit.Args) || !slices.Equal(cmd.Env, l.Unit.Env) || !slices.Equal(l.Spec.Args, l.Unit.Args) || !slices.Equal(l.Spec.Env, l.Unit.Env) {
76 return hostbox.ErrRoleLaunchShape
77 }
78 argv = append([]string{filepath.Join(instancectl.LiveDir, host)}, l.Unit.Args...)
79 } else {
80 argv, socket, err = isolatedArgv(cmd, host, l.LaunchDir, l.RunDir)
81 if err != nil {
82 return err
83 }
84 }
85 if l.Initialize != nil {
86 if err = l.Initialize(ctx); err != nil {
87 return err
88 }
89 }
90 if executable, err = l.Executable(ctx, cmd); err != nil {
91 return err
92 }
93 if executable == nil {
94 return hostwrite.ErrUnavailable
95 }
96 var stat unix.Stat_t
97 if err = unix.Fstat(int(executable.Fd()), &stat); err != nil {
98 return err
99 }
100 if stat.Mode&unix.S_IFMT != unix.S_IFREG || stat.Mode&0111 == 0 || !cleanAbs(executable.Name()) {
101 return hostwrite.ErrUnavailable
102 }
103 reservation.Executable = hostbox.LaunchFile{Device: uint64(stat.Dev), Inode: stat.Ino}
104 // The nested start's own preparation (isolated-exec design §6.1): the
105 // run directory, no stale socket, and the launch file the trampoline
106 // execs. The file grants nothing the tenant could not write itself.
107 if err = os.MkdirAll(l.RunDir, 0o750|os.ModeSetgid); err != nil {
108 return err
109 }
110 if socket != "" {
111 if err = os.Remove(socket); err != nil && !errors.Is(err, os.ErrNotExist) {
112 return err
113 }
114 }
115 raw, err := json.MarshalIndent(instancectl.Launch{Schema: instancectl.LaunchSchema, Host: host, Argv: argv, Env: append([]string(nil), cmd.Env...)}, "", " ")
116 if err != nil {
117 return err
118 }
119 return instancefs.WriteAtomic(l.LaunchDir, instancectl.LaunchFileName(instancectl.RoleRun), append(raw, '\n'), 0o600)
120 })
121 if err != nil {
122 return fail(err)
123 }
124 request := hostbox.RoleLaunchRequest{Spec: l.Spec, V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchStart, Reservation: &reservation, Executable: executable.Name(), MemoryMax: l.MemoryMax}
125 stopRequest := hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchStop, Reservation: &reservation}
126 if err = request.Validate(); err != nil {
127 return fail(err)
128 }
129 logs, err := listenLogs(l.LogSocket, nil)
130 if err != nil {
131 return fail(err)
132 }
133 var producer RoleProducer
134 if l.RegisterProducer != nil {
135 if producer, err = l.RegisterProducer(ctx, role); err != nil {
136 logs.Close()
137 return fail(err)
138 }
139 }
140 finish := func(err error) {
141 if producer.Finish != nil {
142 producer.Finish(err)
143 }
144 }
145 // This hook runs after strict preparation and immediately before Start.
146 if err = before(); err != nil {
147 logs.Close()
148 finish(nil)
149 return fail(err)
150 }
151 settle := func(ctx context.Context) error {
152 if err := l.Root.Stop(ctx, stopRequest); err != nil {
153 return err
154 }
155 finish(nil)
156 return logs.Close()
157 }
158 grant, err := l.Root.Launch(ctx, request)
159 if err == nil {
160 err = grant.Check(reservation)
161 }
162 var process *os.Process
163 if err == nil {
164 process, err = pinRootProcess(grant.PID, grant.ProcessStart)
165 }
166 if err != nil {
167 // A refused, unanswered or unusable launch is stopped by its own
168 // reservation, which root then refuses ever to start. Only a stop
169 // that also failed leaves the caller a settle to retry.
170 cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
171 stopErr := settle(cleanup)
172 cancel()
173 if stopErr != nil {
174 return nil, nil, settle, RoleProducer{}, errors.Join(err, stopErr)
175 }
176 return fail(err)
177 }
178 cmd.Process = process
179 return cmd, nil, settle, producer, nil
180}
181
182func cleanAbs(p string) bool { return filepath.IsAbs(p) && filepath.Clean(p) == p }
183
184// isolatedArgv is the trampoline's argv for cmd: the unit's view of the
185// host's live build, then exactly --socket in the run directory and --db in
186// the instance directory, each once. Any other argument is a shape the
187// isolated unit does not carry.
188func isolatedArgv(cmd *exec.Cmd, host, launchDir, runDir string) ([]string, string, error) {
189 if len(cmd.Args) == 0 {
190 return nil, "", hostbox.ErrRoleLaunchShape
191 }
192 argv, socket, _, err := IsolatedArgv(host, launchDir, runDir, cmd.Args[1:], cmd.Env)
193 return argv, socket, err
194}
195
196// PeerInRole verifies that conn's peer runs inside the unit root started
197// for cmd: the journaled unit reservation names cmd's process, its cgroup
198// still has the journaled identity, and the peer is a member of exactly that
199// group. The tenant answering the socket is the trampoline's child, so its
200// pid is never cmd's.
201func (l RoleLaunch) PeerInRole(ctx context.Context, conn net.Conn, cmd *exec.Cmd) error {
202 unixConn, ok := conn.(*net.UnixConn)
203 if l.Root == nil || l.Journal == nil || !ok || cmd == nil || cmd.Process == nil || !filepath.IsAbs(l.CgroupRoot) {
204 return hostwrite.ErrUnavailable
205 }
206 raw, err := unixConn.SyscallConn()
207 if err != nil {
208 return err
209 }
210 var peer *unix.Ucred
211 var sockErr error
212 if err = raw.Control(func(fd uintptr) { peer, sockErr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) }); err != nil {
213 return err
214 }
215 if sockErr != nil {
216 return sockErr
217 }
218 if peer == nil || peer.Pid <= 0 {
219 return hostwrite.ErrUnavailable
220 }
221 inventory, err := l.Journal.Read(ctx, l.Target)
222 if err != nil {
223 return err
224 }
225 for _, r := range inventory.Reservations {
226 if r.State != hostbox.UnitStarted || r.PID != cmd.Process.Pid {
227 continue
228 }
229 start, err := roleProcessStart(r.PID)
230 if err != nil {
231 return err
232 }
233 if start != r.ProcessStart {
234 return hostbox.ErrChanged
235 }
236 var stat unix.Stat_t
237 if err = unix.Stat(filepath.Join(l.CgroupRoot, r.Group.RelativePath), &stat); err != nil {
238 return err
239 }
240 if uint64(stat.Dev) != r.Group.Device || stat.Ino != r.Group.Inode {
241 return hostbox.ErrChanged
242 }
243 membership, err := os.ReadFile("/proc/" + strconv.Itoa(int(peer.Pid)) + "/cgroup")
244 if err != nil {
245 return err
246 }
247 for _, line := range strings.Split(string(membership), "\n") {
248 if line == "0::/"+r.Group.RelativePath {
249 return nil
250 }
251 }
252 return hostbox.ErrChanged
253 }
254 return hostbox.ErrChanged
255}
256