|
| 1 | +/* |
| 2 | + Copyright The containerd Authors. |
| 3 | +
|
| 4 | + Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + you may not use this file except in compliance with the License. |
| 6 | + You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | + Unless required by applicable law or agreed to in writing, software |
| 11 | + distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + See the License for the specific language governing permissions and |
| 14 | + limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +package bindir |
| 18 | + |
| 19 | +import ( |
| 20 | + "bufio" |
| 21 | + "context" |
| 22 | + "encoding/json" |
| 23 | + "errors" |
| 24 | + "fmt" |
| 25 | + "io" |
| 26 | + "os" |
| 27 | + "os/exec" |
| 28 | + "path/filepath" |
| 29 | + "strings" |
| 30 | + "time" |
| 31 | + |
| 32 | + "github.com/containerd/containerd/log" |
| 33 | + "github.com/containerd/containerd/pkg/imageverifier" |
| 34 | + ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 35 | + "github.com/sirupsen/logrus" |
| 36 | +) |
| 37 | + |
| 38 | +const outputLimitBytes = 1 << 15 // 32 KiB |
| 39 | + |
| 40 | +type Config struct { |
| 41 | + BinDir string `toml:"bin_dir"` |
| 42 | + MaxVerifiers int `toml:"max_verifiers"` |
| 43 | + PerVerifierTimeout time.Duration `toml:"per_verifier_timeout"` |
| 44 | +} |
| 45 | + |
| 46 | +type ImageVerifier struct { |
| 47 | + config *Config |
| 48 | +} |
| 49 | + |
| 50 | +var _ imageverifier.ImageVerifier = (*ImageVerifier)(nil) |
| 51 | + |
| 52 | +func NewImageVerifier(c *Config) *ImageVerifier { |
| 53 | + return &ImageVerifier{ |
| 54 | + config: c, |
| 55 | + } |
| 56 | +} |
| 57 | + |
| 58 | +func (v *ImageVerifier) VerifyImage(ctx context.Context, name string, desc ocispec.Descriptor) (*imageverifier.Judgement, error) { |
| 59 | + // os.ReadDir sorts entries by name. |
| 60 | + entries, err := os.ReadDir(v.config.BinDir) |
| 61 | + if err != nil { |
| 62 | + if errors.Is(err, os.ErrNotExist) { |
| 63 | + return &imageverifier.Judgement{ |
| 64 | + OK: true, |
| 65 | + Reason: fmt.Sprintf("image verifier directory %v does not exist", v.config.BinDir), |
| 66 | + }, nil |
| 67 | + } |
| 68 | + |
| 69 | + return nil, fmt.Errorf("failed to list directory contents: %w", err) |
| 70 | + } |
| 71 | + |
| 72 | + if len(entries) == 0 { |
| 73 | + return &imageverifier.Judgement{ |
| 74 | + OK: true, |
| 75 | + Reason: fmt.Sprintf("no image verifier binaries found in %v", v.config.BinDir), |
| 76 | + }, nil |
| 77 | + } |
| 78 | + |
| 79 | + reason := &strings.Builder{} |
| 80 | + for i, entry := range entries { |
| 81 | + if (i+1) > v.config.MaxVerifiers && v.config.MaxVerifiers >= 0 { |
| 82 | + log.G(ctx).Warnf("image verifiers are being skipped since directory %v has %v entries, more than configured max of %v verifiers", v.config.BinDir, len(entries), v.config.MaxVerifiers) |
| 83 | + break |
| 84 | + } |
| 85 | + |
| 86 | + bin := entry.Name() |
| 87 | + start := time.Now() |
| 88 | + exitCode, vr, err := v.runVerifier(ctx, bin, name, desc) |
| 89 | + runtime := time.Since(start) |
| 90 | + if err != nil { |
| 91 | + return nil, fmt.Errorf("failed to call verifier %v (runtime %v): %w", bin, runtime, err) |
| 92 | + } |
| 93 | + |
| 94 | + if exitCode != 0 { |
| 95 | + return &imageverifier.Judgement{ |
| 96 | + OK: false, |
| 97 | + Reason: fmt.Sprintf("verifier %v rejected image (exit code %v): %v", bin, exitCode, vr), |
| 98 | + }, nil |
| 99 | + } |
| 100 | + |
| 101 | + if i > 0 { |
| 102 | + reason.WriteString(", ") |
| 103 | + } |
| 104 | + reason.WriteString(fmt.Sprintf("%v => %v", bin, vr)) |
| 105 | + } |
| 106 | + |
| 107 | + return &imageverifier.Judgement{ |
| 108 | + OK: true, |
| 109 | + Reason: reason.String(), |
| 110 | + }, nil |
| 111 | +} |
| 112 | + |
| 113 | +func (v *ImageVerifier) runVerifier(ctx context.Context, bin string, imageName string, desc ocispec.Descriptor) (exitCode int, reason string, err error) { |
| 114 | + ctx, cancel := context.WithTimeout(ctx, v.config.PerVerifierTimeout) |
| 115 | + defer cancel() |
| 116 | + |
| 117 | + binPath := filepath.Join(v.config.BinDir, bin) |
| 118 | + args := []string{ |
| 119 | + "-name", imageName, |
| 120 | + "-digest", desc.Digest.String(), |
| 121 | + "-stdin-media-type", ocispec.MediaTypeDescriptor, |
| 122 | + } |
| 123 | + |
| 124 | + cmd := exec.CommandContext(ctx, binPath, args...) |
| 125 | + |
| 126 | + // We construct our own pipes instead of using the default StdinPipe, |
| 127 | + // StoutPipe, and StderrPipe in order to set timeouts on reads and writes. |
| 128 | + stdinRead, stdinWrite, err := os.Pipe() |
| 129 | + if err != nil { |
| 130 | + return -1, "", err |
| 131 | + } |
| 132 | + cmd.Stdin = stdinRead |
| 133 | + defer stdinRead.Close() |
| 134 | + defer stdinWrite.Close() |
| 135 | + |
| 136 | + stdoutRead, stdoutWrite, err := os.Pipe() |
| 137 | + if err != nil { |
| 138 | + return -1, "", err |
| 139 | + } |
| 140 | + cmd.Stdout = stdoutWrite |
| 141 | + defer stdoutRead.Close() |
| 142 | + defer stdoutWrite.Close() |
| 143 | + |
| 144 | + stderrRead, stderrWrite, err := os.Pipe() |
| 145 | + if err != nil { |
| 146 | + return -1, "", err |
| 147 | + } |
| 148 | + cmd.Stderr = stderrWrite |
| 149 | + defer stderrRead.Close() |
| 150 | + defer stderrWrite.Close() |
| 151 | + |
| 152 | + // Close parent ends of pipes on timeout. Without this, I/O may hang in the |
| 153 | + // parent process. |
| 154 | + if d, ok := ctx.Deadline(); ok { |
| 155 | + stdinWrite.SetDeadline(d) |
| 156 | + stdoutRead.SetDeadline(d) |
| 157 | + stderrRead.SetDeadline(d) |
| 158 | + } |
| 159 | + |
| 160 | + // Finish configuring, and then fork & exec the child process. |
| 161 | + p, err := startProcess(ctx, cmd) |
| 162 | + if err != nil { |
| 163 | + return -1, "", err |
| 164 | + } |
| 165 | + defer p.cleanup(ctx) |
| 166 | + |
| 167 | + // Close the child ends of the pipes in the parent process. |
| 168 | + stdinRead.Close() |
| 169 | + stdoutWrite.Close() |
| 170 | + stderrWrite.Close() |
| 171 | + |
| 172 | + // Write the descriptor to stdin. |
| 173 | + go func() { |
| 174 | + // Descriptors are usually small enough to fit in a pipe buffer (which is |
| 175 | + // often 64 KiB on Linux) so this write usually won't block on the child |
| 176 | + // process reading stdin. However, synchronously writing to stdin may cause |
| 177 | + // the parent to block if the descriptor is larger than the pipe buffer and |
| 178 | + // the child process doesn't read stdin. Therefore, we write to stdin |
| 179 | + // asynchronously, limited by the stdinWrite deadline set above. |
| 180 | + err := json.NewEncoder(stdinWrite).Encode(desc) |
| 181 | + if err != nil { |
| 182 | + // This may error out with a "broken pipe" error if the descriptor is |
| 183 | + // larger than the pipe buffer and the child process does not read all |
| 184 | + // of stdin. |
| 185 | + log.G(ctx).WithError(err).Warn("failed to completely write descriptor to stdin") |
| 186 | + } |
| 187 | + stdinWrite.Close() |
| 188 | + }() |
| 189 | + |
| 190 | + // Pipe verifier stderr lines to debug logs. |
| 191 | + stderrLog := log.G(ctx).Logger.WithFields(logrus.Fields{ |
| 192 | + "image_verifier": bin, |
| 193 | + "stream": "stderr", |
| 194 | + }) |
| 195 | + stderrLogDone := make(chan struct{}) |
| 196 | + go func() { |
| 197 | + defer close(stderrLogDone) |
| 198 | + defer stderrRead.Close() |
| 199 | + lr := &io.LimitedReader{ |
| 200 | + R: stderrRead, |
| 201 | + N: outputLimitBytes, |
| 202 | + } |
| 203 | + |
| 204 | + s := bufio.NewScanner(lr) |
| 205 | + for s.Scan() { |
| 206 | + stderrLog.Debug(s.Text()) |
| 207 | + } |
| 208 | + if err := s.Err(); err != nil { |
| 209 | + stderrLog.WithError(err).Debug("error logging image verifier stderr") |
| 210 | + } |
| 211 | + |
| 212 | + if lr.N == 0 { |
| 213 | + // Peek ahead to see if stderr reader was truncated. |
| 214 | + b := make([]byte, 1) |
| 215 | + if n, _ := stderrRead.Read(b); n > 0 { |
| 216 | + stderrLog.Debug("(previous logs may be truncated)") |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + // Discard the truncated part of stderr. Doing this rather than closing the |
| 221 | + // reader avoids broken pipe errors. This is bounded by the stderrRead |
| 222 | + // deadline. |
| 223 | + if _, err := io.Copy(io.Discard, stderrRead); err != nil { |
| 224 | + log.G(ctx).WithError(err).Error("error flushing stderr") |
| 225 | + } |
| 226 | + }() |
| 227 | + |
| 228 | + stdout, err := io.ReadAll(io.LimitReader(stdoutRead, outputLimitBytes)) |
| 229 | + if err != nil { |
| 230 | + log.G(ctx).WithError(err).Error("error reading stdout") |
| 231 | + } else { |
| 232 | + m := strings.Builder{} |
| 233 | + m.WriteString(strings.TrimSpace(string(stdout))) |
| 234 | + // Peek ahead to see if stdout is truncated. |
| 235 | + b := make([]byte, 1) |
| 236 | + if n, _ := stdoutRead.Read(b); n > 0 { |
| 237 | + m.WriteString("(stdout truncated)") |
| 238 | + } |
| 239 | + reason = m.String() |
| 240 | + } |
| 241 | + |
| 242 | + // Discard the truncated part of stdout. Doing this rather than closing the |
| 243 | + // reader avoids broken pipe errors. This is bounded by the stdoutRead |
| 244 | + // deadline. |
| 245 | + if _, err := io.Copy(io.Discard, stdoutRead); err != nil { |
| 246 | + log.G(ctx).WithError(err).Error("error flushing stdout") |
| 247 | + } |
| 248 | + stdoutRead.Close() |
| 249 | + |
| 250 | + <-stderrLogDone |
| 251 | + if err := cmd.Wait(); err != nil { |
| 252 | + if ee := (&exec.ExitError{}); errors.As(err, &ee) && ee.ProcessState.Exited() { |
| 253 | + return ee.ProcessState.ExitCode(), reason, nil |
| 254 | + } |
| 255 | + return -1, "", fmt.Errorf("waiting on command to exit: %v", err) |
| 256 | + } |
| 257 | + |
| 258 | + return cmd.ProcessState.ExitCode(), reason, nil |
| 259 | +} |
0 commit comments