oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package operatorlaunch
4
5import (
6 "context"
7 "crypto/sha256"
8 "encoding/hex"
9 "errors"
10 "time"
11
12 "amadan.net/carlos/platform.git/launchctx"
13 "golang.org/x/sys/unix"
14)
15
16const startupTimeout = 5 * time.Second
17const inputLimit = 64 << 10
18
19// ReadOperator replaces consumed fd0, fd3 and fd4 with readonly /dev/null.
20// It never opens a database or establishes owner, fence or lease authority.
21func ReadOperator(parent context.Context, database string) (launch launchctx.LaunchContext, input []byte, err error) {
22 if parent == nil {
23 return launchctx.LaunchContext{}, nil, ErrRefused
24 }
25 null, e := unix.Open("/dev/null", unix.O_RDONLY|unix.O_CLOEXEC, 0)
26 if e != nil {
27 return launchctx.LaunchContext{}, nil, ErrRefused
28 }
29 source, dupErr := unix.FcntlInt(uintptr(null), unix.F_DUPFD_CLOEXEC, 5)
30 closeErr := unix.Close(null)
31 if dupErr != nil || closeErr != nil {
32 if dupErr == nil {
33 unix.Close(source)
34 }
35 return launchctx.LaunchContext{}, nil, ErrRefused
36 }
37 owned := map[int]bool{0: true, 3: true, 4: true}
38 consumeFD := func(fd int) error {
39 if e := unix.Dup3(source, fd, unix.O_CLOEXEC); e != nil {
40 return e
41 }
42 delete(owned, fd)
43 return nil
44 }
45 defer func() {
46 for fd := range owned {
47 if consumeFD(fd) != nil {
48 err = ErrRefused
49 }
50 }
51 if unix.Close(source) != nil {
52 err = ErrRefused
53 }
54 if err != nil {
55 clear(input)
56 input = nil
57 launch = launchctx.LaunchContext{}
58 err = ErrRefused
59 }
60 }()
61 ctx, cancel := context.WithTimeout(parent, startupTimeout)
62 defer cancel()
63 raw, e := readPipe(ctx, 3, launchctx.MaxLaunchBytes)
64 if e != nil {
65 return launchctx.LaunchContext{}, nil, ErrRefused
66 }
67 defer clear(raw)
68 if consumeFD(3) != nil {
69 return launchctx.LaunchContext{}, nil, ErrRefused
70 }
71 launch, e = launchctx.DecodeOperator(raw)
72 if e != nil || launch.DatabasePath != database {
73 return launchctx.LaunchContext{}, nil, ErrRefused
74 }
75 deadline, e := time.Parse(time.RFC3339Nano, launch.ExecutionDeadline)
76 if e != nil || !time.Now().Before(deadline) {
77 return launchctx.LaunchContext{}, nil, ErrRefused
78 }
79 ctx, cancelLaunch := context.WithDeadline(ctx, deadline)
80 defer cancelLaunch()
81 if checkExecutable(ctx, 4, launch.ArtifactDigest) != nil || consumeFD(4) != nil {
82 return launchctx.LaunchContext{}, nil, ErrRefused
83 }
84 input, e = readPipe(ctx, 0, inputLimit)
85 if e != nil || consumeFD(0) != nil || ctx.Err() != nil {
86 return launchctx.LaunchContext{}, input, ErrRefused
87 }
88 sum := sha256.Sum256(input)
89 if hex.EncodeToString(sum[:]) != launch.InputSHA256 {
90 return launchctx.LaunchContext{}, input, ErrRefused
91 }
92 return launch, input, nil
93}
94
95func readPipe(ctx context.Context, fd, limit int) ([]byte, error) {
96 var st unix.Stat_t
97 flags, e := unix.FcntlInt(uintptr(fd), unix.F_GETFL, 0)
98 if e != nil || flags&unix.O_ACCMODE != unix.O_RDONLY || unix.Fstat(fd, &st) != nil || st.Mode&unix.S_IFMT != unix.S_IFIFO || unix.SetNonblock(fd, true) != nil {
99 return nil, ErrRefused
100 }
101 out := make([]byte, 0, limit+1)
102 buf := make([]byte, 4096)
103 defer clear(buf)
104 for ctx.Err() == nil {
105 n, e := unix.Read(fd, buf[:min(len(buf), limit+1-len(out))])
106 if n > 0 {
107 out = append(out, buf[:n]...)
108 if len(out) > limit {
109 break
110 }
111 }
112 if e == nil && n == 0 {
113 return out, nil
114 }
115 if errors.Is(e, unix.EINTR) {
116 continue
117 }
118 if errors.Is(e, unix.EAGAIN) {
119 select {
120 case <-ctx.Done():
121 case <-time.After(time.Millisecond):
122 }
123 continue
124 }
125 if e != nil {
126 break
127 }
128 }
129 clear(out)
130 return nil, ErrRefused
131}
132
133func checkExecutable(ctx context.Context, artifact int, digest string) (err error) {
134 flags, e := unix.FcntlInt(uintptr(artifact), unix.F_GETFL, 0)
135 var pin unix.Stat_t
136 if e != nil || flags&unix.O_ACCMODE != unix.O_RDONLY || flags&unix.O_PATH != 0 || unix.Fstat(artifact, &pin) != nil || pin.Mode&unix.S_IFMT != unix.S_IFREG {
137 return ErrRefused
138 }
139 self, e := unix.Open("/proc/self/exe", unix.O_RDONLY|unix.O_CLOEXEC, 0)
140 if e != nil {
141 return ErrRefused
142 }
143 defer func() {
144 if unix.Close(self) != nil {
145 err = ErrRefused
146 }
147 }()
148 var st unix.Stat_t
149 if unix.Fstat(self, &st) != nil || st.Dev != pin.Dev || st.Ino != pin.Ino || st.Size <= 0 || st.Size != pin.Size {
150 return ErrRefused
151 }
152 hash := sha256.New()
153 buf := make([]byte, 64<<10)
154 for offset := int64(0); offset < st.Size; {
155 if ctx.Err() != nil {
156 return ErrRefused
157 }
158 n, e := unix.Pread(self, buf[:min(int64(len(buf)), st.Size-offset)], offset)
159 if e != nil || n <= 0 {
160 return ErrRefused
161 }
162 hash.Write(buf[:n])
163 offset += int64(n)
164 }
165 var after unix.Stat_t
166 if ctx.Err() != nil || unix.Fstat(self, &after) != nil || after.Size != st.Size || hex.EncodeToString(hash.Sum(nil)) != digest {
167 return ErrRefused
168 }
169 return nil
170}
171