//go:build linux package operatorlaunch import ( "context" "crypto/sha256" "encoding/hex" "errors" "time" "amadan.net/carlos/platform.git/launchctx" "golang.org/x/sys/unix" ) const startupTimeout = 5 * time.Second const inputLimit = 64 << 10 // ReadOperator replaces consumed fd0, fd3 and fd4 with readonly /dev/null. // It never opens a database or establishes owner, fence or lease authority. func ReadOperator(parent context.Context, database string) (launch launchctx.LaunchContext, input []byte, err error) { if parent == nil { return launchctx.LaunchContext{}, nil, ErrRefused } null, e := unix.Open("/dev/null", unix.O_RDONLY|unix.O_CLOEXEC, 0) if e != nil { return launchctx.LaunchContext{}, nil, ErrRefused } source, dupErr := unix.FcntlInt(uintptr(null), unix.F_DUPFD_CLOEXEC, 5) closeErr := unix.Close(null) if dupErr != nil || closeErr != nil { if dupErr == nil { unix.Close(source) } return launchctx.LaunchContext{}, nil, ErrRefused } owned := map[int]bool{0: true, 3: true, 4: true} consumeFD := func(fd int) error { if e := unix.Dup3(source, fd, unix.O_CLOEXEC); e != nil { return e } delete(owned, fd) return nil } defer func() { for fd := range owned { if consumeFD(fd) != nil { err = ErrRefused } } if unix.Close(source) != nil { err = ErrRefused } if err != nil { clear(input) input = nil launch = launchctx.LaunchContext{} err = ErrRefused } }() ctx, cancel := context.WithTimeout(parent, startupTimeout) defer cancel() raw, e := readPipe(ctx, 3, launchctx.MaxLaunchBytes) if e != nil { return launchctx.LaunchContext{}, nil, ErrRefused } defer clear(raw) if consumeFD(3) != nil { return launchctx.LaunchContext{}, nil, ErrRefused } launch, e = launchctx.DecodeOperator(raw) if e != nil || launch.DatabasePath != database { return launchctx.LaunchContext{}, nil, ErrRefused } deadline, e := time.Parse(time.RFC3339Nano, launch.ExecutionDeadline) if e != nil || !time.Now().Before(deadline) { return launchctx.LaunchContext{}, nil, ErrRefused } ctx, cancelLaunch := context.WithDeadline(ctx, deadline) defer cancelLaunch() if checkExecutable(ctx, 4, launch.ArtifactDigest) != nil || consumeFD(4) != nil { return launchctx.LaunchContext{}, nil, ErrRefused } input, e = readPipe(ctx, 0, inputLimit) if e != nil || consumeFD(0) != nil || ctx.Err() != nil { return launchctx.LaunchContext{}, input, ErrRefused } sum := sha256.Sum256(input) if hex.EncodeToString(sum[:]) != launch.InputSHA256 { return launchctx.LaunchContext{}, input, ErrRefused } return launch, input, nil } func readPipe(ctx context.Context, fd, limit int) ([]byte, error) { var st unix.Stat_t flags, e := unix.FcntlInt(uintptr(fd), unix.F_GETFL, 0) 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 { return nil, ErrRefused } out := make([]byte, 0, limit+1) buf := make([]byte, 4096) defer clear(buf) for ctx.Err() == nil { n, e := unix.Read(fd, buf[:min(len(buf), limit+1-len(out))]) if n > 0 { out = append(out, buf[:n]...) if len(out) > limit { break } } if e == nil && n == 0 { return out, nil } if errors.Is(e, unix.EINTR) { continue } if errors.Is(e, unix.EAGAIN) { select { case <-ctx.Done(): case <-time.After(time.Millisecond): } continue } if e != nil { break } } clear(out) return nil, ErrRefused } func checkExecutable(ctx context.Context, artifact int, digest string) (err error) { flags, e := unix.FcntlInt(uintptr(artifact), unix.F_GETFL, 0) var pin unix.Stat_t 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 { return ErrRefused } self, e := unix.Open("/proc/self/exe", unix.O_RDONLY|unix.O_CLOEXEC, 0) if e != nil { return ErrRefused } defer func() { if unix.Close(self) != nil { err = ErrRefused } }() var st unix.Stat_t if unix.Fstat(self, &st) != nil || st.Dev != pin.Dev || st.Ino != pin.Ino || st.Size <= 0 || st.Size != pin.Size { return ErrRefused } hash := sha256.New() buf := make([]byte, 64<<10) for offset := int64(0); offset < st.Size; { if ctx.Err() != nil { return ErrRefused } n, e := unix.Pread(self, buf[:min(int64(len(buf)), st.Size-offset)], offset) if e != nil || n <= 0 { return ErrRefused } hash.Write(buf[:n]) offset += int64(n) } var after unix.Stat_t if ctx.Err() != nil || unix.Fstat(self, &after) != nil || after.Size != st.Size || hex.EncodeToString(hash.Sum(nil)) != digest { return ErrRefused } return nil }