1// SPDX-License-Identifier: MIT
2
3package sandbox
4
5import (
6 "context"
7 "debug/elf"
8 "errors"
9 "fmt"
10 "os"
11 "os/exec"
12 "strings"
13
14 "github.com/bnema/zerowrap"
15)
16
17// Factory builds the driver of one mode. It is injected so that this package
18// does not import the drivers (which import it).
19type Factory func(ctx context.Context, cfg Config) (Driver, error)
20
21// Factories are the available drivers.
22type Factories struct {
23 VM Factory
24 Container Factory
25}
26
27// Prober checks the host. The zero value probes the real host; tests replace
28// fields.
29type Prober struct {
30 // OpenKVM opens the KVM device read-write and closes it.
31 OpenKVM func(path string) error
32 // LookPath finds a binary in PATH.
33 LookPath func(name string) (string, error)
34 // Stat reports whether a path exists.
35 Stat func(path string) error
36}
37
38func (p Prober) withDefaults() Prober {
39 if p.OpenKVM == nil {
40 p.OpenKVM = func(path string) error {
41 f, err := os.OpenFile(path, os.O_RDWR, 0)
42 if err != nil {
43 return err
44 }
45 return f.Close()
46 }
47 }
48 if p.LookPath == nil {
49 p.LookPath = exec.LookPath
50 }
51 if p.Stat == nil {
52 p.Stat = func(path string) error { _, err := os.Stat(path); return err }
53 }
54 return p
55}
56
57// VMUnavailable lists why the vm mode cannot run on this host: an empty result
58// means every prerequisite (readable-writable KVM, Firecracker, jailer unless
59// disabled, guest kernel, base image) is there.
60func VMUnavailable(cfg Config, p Prober) []string {
61 p = p.withDefaults()
62 var why []string
63 if err := p.OpenKVM(cfg.KVMPath); err != nil {
64 why = append(why, fmt.Sprintf("%s not usable read-write: %v", cfg.KVMPath, err))
65 }
66 if cfg.Firecracker != "" {
67 if err := p.Stat(cfg.Firecracker); err != nil {
68 why = append(why, fmt.Sprintf("firecracker %s: %v", cfg.Firecracker, err))
69 }
70 } else if _, err := p.LookPath("firecracker"); err != nil {
71 why = append(why, "firecracker not found in PATH")
72 }
73 if !cfg.JailerOff {
74 if cfg.Jailer != "" {
75 if err := p.Stat(cfg.Jailer); err != nil {
76 why = append(why, fmt.Sprintf("jailer %s: %v", cfg.Jailer, err))
77 }
78 } else if _, err := p.LookPath("jailer"); err != nil {
79 why = append(why, "jailer not found in PATH")
80 }
81 }
82 if err := p.Stat(cfg.KernelPath()); err != nil {
83 why = append(why, fmt.Sprintf("guest kernel %s: %v", cfg.KernelPath(), err))
84 }
85 if err := p.Stat(cfg.BaseImagePath()); err != nil {
86 why = append(why, fmt.Sprintf("base image %s: %v", cfg.BaseImagePath(), err))
87 }
88 return why
89}
90
91// Select picks the isolation mode and builds its driver. For auto it chooses vm
92// when VMUnavailable is empty, container otherwise. For vm it fails when a
93// prerequisite is missing; for container it never probes the VM side. The
94// chosen mode and the reasons are logged with the logger of ctx. A driver
95// factory error is returned as is (auto does not mask a broken vm setup by
96// silently running jobs with weaker isolation).
97func Select(ctx context.Context, cfg Config, f Factories, p Prober) (Driver, error) {
98 log := zerowrap.FromCtx(ctx)
99 mode := cfg.Isolation
100 var reasons []string
101 switch mode {
102 case VM, Auto:
103 reasons = VMUnavailable(cfg, p)
104 if mode == VM && len(reasons) > 0 {
105 return nil, fmt.Errorf("isolation vm is not available: %s", strings.Join(reasons, "; "))
106 }
107 if len(reasons) == 0 {
108 mode = VM
109 } else {
110 mode = Container
111 }
112 case Container:
113 default:
114 return nil, fmt.Errorf("unknown isolation %q", mode)
115 }
116
117 build := f.VM
118 if mode == Container {
119 build = f.Container
120 }
121 if build == nil {
122 return nil, fmt.Errorf("isolation %s: no driver compiled in", mode)
123 }
124 ev := log.Info().Str("configured", string(cfg.Isolation)).Str("mode", string(mode))
125 switch {
126 case cfg.Isolation == Auto && mode == VM:
127 ev = ev.Str("reason", "KVM, firecracker, kernel and base image are available")
128 case cfg.Isolation == Auto:
129 ev = ev.Strs("reasons", reasons)
130 default:
131 ev = ev.Str("reason", "set by "+EnvIsolation)
132 }
133 ev.Msg("isolation mode chosen")
134 d, err := build(ctx, cfg)
135 if err != nil {
136 return nil, fmt.Errorf("isolation %s: %w", mode, err)
137 }
138 return d, nil
139}
140
141// CheckStatic fails unless the executable at path is a statically linked ELF
142// binary: it runs as PID 1 on a root disk that holds nothing else, and in an
143// image of another distribution.
144func CheckStatic(path string) error {
145 f, err := elf.Open(path)
146 if err != nil {
147 return fmt.Errorf("%s is not an ELF executable (%w); build the runner with CGO_ENABLED=0", path, err)
148 }
149 defer func() { _ = f.Close() }()
150 for _, p := range f.Progs {
151 if p.Type == elf.PT_INTERP {
152 return errors.New(path + " is dynamically linked; build the runner with CGO_ENABLED=0")
153 }
154 }
155 return nil
156}