| 1 | //go:build linux |
| 2 | |
| 3 | package hostfloor |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "fmt" |
| 8 | "golang.org/x/sys/unix" |
| 9 | "os" |
| 10 | "os/exec" |
| 11 | "path/filepath" |
| 12 | "strconv" |
| 13 | "strings" |
| 14 | "syscall" |
| 15 | ) |
| 16 | |
| 17 | func OpenProtected(path string) (*os.File, error) { |
| 18 | if !filepath.IsAbs(path) || filepath.Clean(path) != path { |
| 19 | return nil, ErrFloor |
| 20 | } |
| 21 | // Every parent is root-owned and cannot be changed by a platform worker. |
| 22 | for p := filepath.Dir(path); ; p = filepath.Dir(p) { |
| 23 | s, err := os.Lstat(p) |
| 24 | if err != nil { |
| 25 | return nil, err |
| 26 | } |
| 27 | stat, ok := s.Sys().(*syscall.Stat_t) |
| 28 | if !ok || stat.Uid != 0 || !s.IsDir() || s.Mode().Perm()&0022 != 0 { |
| 29 | return nil, ErrFloor |
| 30 | } |
| 31 | if p == "/" { |
| 32 | break |
| 33 | } |
| 34 | } |
| 35 | fd, err := unix.Open(path, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0) |
| 36 | if err != nil { |
| 37 | return nil, err |
| 38 | } |
| 39 | f := os.NewFile(uintptr(fd), path) |
| 40 | s, err := f.Stat() |
| 41 | if err != nil { |
| 42 | f.Close() |
| 43 | return nil, err |
| 44 | } |
| 45 | stat, ok := s.Sys().(*syscall.Stat_t) |
| 46 | if !ok || stat.Uid != 0 || !s.Mode().IsRegular() || s.Mode().Perm()&0022 != 0 { |
| 47 | f.Close() |
| 48 | return nil, ErrFloor |
| 49 | } |
| 50 | return f, nil |
| 51 | } |
| 52 | func (d Disk) Exec(path, platform string, args, env []string, roles ...string) error { |
| 53 | f, err := OpenProtected(path) |
| 54 | if err != nil { |
| 55 | return err |
| 56 | } |
| 57 | defer f.Close() |
| 58 | if _, err = d.VerifyFile(f, platform, roles...); err != nil { |
| 59 | return err |
| 60 | } |
| 61 | return syscall.Exec("/proc/self/fd/"+strconv.Itoa(int(f.Fd())), append([]string{path}, args...), env) |
| 62 | } |
| 63 | func (d Disk) Run(ctx context.Context, path, platform string, args ...string) (string, error) { |
| 64 | f, err := OpenProtected(path) |
| 65 | if err != nil { |
| 66 | return "", err |
| 67 | } |
| 68 | defer f.Close() |
| 69 | if _, err = d.VerifyFile(f, platform); err != nil { |
| 70 | return "", err |
| 71 | } |
| 72 | return runImage(ctx, f, args...) |
| 73 | } |
| 74 | |
| 75 | // runImage runs the verified image open as f and returns its stdout only. |
| 76 | // A caller parses that output (a version line, a JSON plan), so anything |
| 77 | // the image writes to stderr, such as progress or an alert, must not be |
| 78 | // mixed into it; stderr is kept for the error when the run fails. |
| 79 | func runImage(ctx context.Context, f *os.File, args ...string) (string, error) { |
| 80 | cmd := exec.CommandContext(ctx, "/proc/self/fd/3", args...) |
| 81 | cmd.ExtraFiles = []*os.File{f} |
| 82 | var stderr strings.Builder |
| 83 | cmd.Stderr = &stderr |
| 84 | out, err := cmd.Output() |
| 85 | if err != nil { |
| 86 | if msg := strings.TrimSpace(stderr.String()); msg != "" { |
| 87 | err = fmt.Errorf("%w: %s", err, msg) |
| 88 | } |
| 89 | } |
| 90 | return string(out), err |
| 91 | } |
| 92 | func ExecutingHash() (string, error) { |
| 93 | f, err := os.Open("/proc/self/exe") |
| 94 | if err != nil { |
| 95 | return "", err |
| 96 | } |
| 97 | defer f.Close() |
| 98 | image, err := os.Open(fmt.Sprintf("/proc/self/fd/%d", f.Fd())) |
| 99 | if err != nil { |
| 100 | return "", err |
| 101 | } |
| 102 | defer image.Close() |
| 103 | return DigestReader(image) |
| 104 | } |
| 105 | |