| /* |
| Copyright The containerd Authors. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package bindir |
| |
| import ( |
| "bufio" |
| "context" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "io" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| "time" |
| |
| "github.com/containerd/containerd/v2/internal/tomlext" |
| "github.com/containerd/containerd/v2/pkg/imageverifier" |
| "github.com/containerd/log" |
| ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| ) |
| |
| const outputLimitBytes = 1 << 15 // 32 KiB |
| |
| type Config struct { |
| BinDir string `toml:"bin_dir"` |
| MaxVerifiers int `toml:"max_verifiers"` |
| PerVerifierTimeout tomlext.Duration `toml:"per_verifier_timeout"` |
| } |
| |
| type ImageVerifier struct { |
| config *Config |
| } |
| |
| var _ imageverifier.ImageVerifier = (*ImageVerifier)(nil) |
| |
| func NewImageVerifier(c *Config) *ImageVerifier { |
| return &ImageVerifier{ |
| config: c, |
| } |
| } |
| |
| func (v *ImageVerifier) VerifyImage(ctx context.Context, name string, desc ocispec.Descriptor) (*imageverifier.Judgement, error) { |
| // os.ReadDir sorts entries by name. |
| entries, err := os.ReadDir(v.config.BinDir) |
| if err != nil { |
| if errors.Is(err, os.ErrNotExist) { |
| return &imageverifier.Judgement{ |
| OK: true, |
| Reason: fmt.Sprintf("image verifier directory %v does not exist", v.config.BinDir), |
| }, nil |
| } |
| |
| return nil, fmt.Errorf("failed to list directory contents: %w", err) |
| } |
| |
| if len(entries) == 0 { |
| return &imageverifier.Judgement{ |
| OK: true, |
| Reason: fmt.Sprintf("no image verifier binaries found in %v", v.config.BinDir), |
| }, nil |
| } |
| |
| reason := &strings.Builder{} |
| for i, entry := range entries { |
| if (i+1) > v.config.MaxVerifiers && v.config.MaxVerifiers >= 0 { |
| 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) |
| break |
| } |
| |
| bin := entry.Name() |
| start := time.Now() |
| exitCode, vr, err := v.runVerifier(ctx, bin, name, desc) |
| runtime := time.Since(start) |
| if err != nil { |
| return nil, fmt.Errorf("failed to call verifier %v (runtime %v): %w", bin, runtime, err) |
| } |
| |
| if exitCode != 0 { |
| return &imageverifier.Judgement{ |
| OK: false, |
| Reason: fmt.Sprintf("verifier %v rejected image (exit code %v): %v", bin, exitCode, vr), |
| }, nil |
| } |
| |
| if i > 0 { |
| reason.WriteString(", ") |
| } |
| reason.WriteString(bin + " => " + vr) |
| } |
| |
| return &imageverifier.Judgement{ |
| OK: true, |
| Reason: reason.String(), |
| }, nil |
| } |
| |
| func (v *ImageVerifier) runVerifier(ctx context.Context, bin string, imageName string, desc ocispec.Descriptor) (exitCode int, reason string, err error) { |
| ctx, cancel := context.WithTimeout(ctx, tomlext.ToStdTime(v.config.PerVerifierTimeout)) |
| defer cancel() |
| |
| binPath := filepath.Join(v.config.BinDir, bin) |
| args := []string{ |
| "-name", imageName, |
| "-digest", desc.Digest.String(), |
| "-stdin-media-type", ocispec.MediaTypeDescriptor, |
| } |
| |
| cmd := exec.CommandContext(ctx, binPath, args...) |
| |
| // We construct our own pipes instead of using the default StdinPipe, |
| // StoutPipe, and StderrPipe in order to set timeouts on reads and writes. |
| stdinRead, stdinWrite, err := os.Pipe() |
| if err != nil { |
| return -1, "", err |
| } |
| cmd.Stdin = stdinRead |
| defer stdinRead.Close() |
| defer stdinWrite.Close() |
| |
| stdoutRead, stdoutWrite, err := os.Pipe() |
| if err != nil { |
| return -1, "", err |
| } |
| cmd.Stdout = stdoutWrite |
| defer stdoutRead.Close() |
| defer stdoutWrite.Close() |
| |
| stderrRead, stderrWrite, err := os.Pipe() |
| if err != nil { |
| return -1, "", err |
| } |
| cmd.Stderr = stderrWrite |
| defer stderrRead.Close() |
| defer stderrWrite.Close() |
| |
| // Close parent ends of pipes on timeout. Without this, I/O may hang in the |
| // parent process. |
| if d, ok := ctx.Deadline(); ok { |
| stdinWrite.SetDeadline(d) |
| stdoutRead.SetDeadline(d) |
| stderrRead.SetDeadline(d) |
| } |
| |
| // Finish configuring, and then fork & exec the child process. |
| p, err := startProcess(ctx, cmd) |
| if err != nil { |
| return -1, "", err |
| } |
| defer p.cleanup(ctx) |
| |
| // Close the child ends of the pipes in the parent process. |
| stdinRead.Close() |
| stdoutWrite.Close() |
| stderrWrite.Close() |
| |
| // Write the descriptor to stdin. |
| go func() { |
| // Descriptors are usually small enough to fit in a pipe buffer (which is |
| // often 64 KiB on Linux) so this write usually won't block on the child |
| // process reading stdin. However, synchronously writing to stdin may cause |
| // the parent to block if the descriptor is larger than the pipe buffer and |
| // the child process doesn't read stdin. Therefore, we write to stdin |
| // asynchronously, limited by the stdinWrite deadline set above. |
| err := json.NewEncoder(stdinWrite).Encode(desc) |
| if err != nil { |
| // This may error out with a "broken pipe" error if the descriptor is |
| // larger than the pipe buffer and the child process does not read all |
| // of stdin. |
| log.G(ctx).WithError(err).Warn("failed to completely write descriptor to stdin") |
| } |
| stdinWrite.Close() |
| }() |
| |
| // Pipe verifier stderr lines to debug logs. |
| stderrLog := log.G(ctx).Logger.WithFields(log.Fields{ |
| "image_verifier": bin, |
| "stream": "stderr", |
| }) |
| stderrLogDone := make(chan struct{}) |
| go func() { |
| defer close(stderrLogDone) |
| defer stderrRead.Close() |
| lr := &io.LimitedReader{ |
| R: stderrRead, |
| N: outputLimitBytes, |
| } |
| |
| s := bufio.NewScanner(lr) |
| for s.Scan() { |
| stderrLog.Debug(s.Text()) |
| } |
| if err := s.Err(); err != nil { |
| stderrLog.WithError(err).Debug("error logging image verifier stderr") |
| } |
| |
| if lr.N == 0 { |
| // Peek ahead to see if stderr reader was truncated. |
| b := make([]byte, 1) |
| if n, _ := stderrRead.Read(b); n > 0 { |
| stderrLog.Debug("(previous logs may be truncated)") |
| } |
| } |
| |
| // Discard the truncated part of stderr. Doing this rather than closing the |
| // reader avoids broken pipe errors. This is bounded by the stderrRead |
| // deadline. |
| if _, err := io.Copy(io.Discard, stderrRead); err != nil { |
| log.G(ctx).WithError(err).Error("error flushing stderr") |
| } |
| }() |
| |
| stdout, err := io.ReadAll(io.LimitReader(stdoutRead, outputLimitBytes)) |
| if err != nil { |
| log.G(ctx).WithError(err).Error("error reading stdout") |
| } else { |
| m := strings.Builder{} |
| m.WriteString(strings.TrimSpace(string(stdout))) |
| // Peek ahead to see if stdout is truncated. |
| b := make([]byte, 1) |
| if n, _ := stdoutRead.Read(b); n > 0 { |
| m.WriteString("(stdout truncated)") |
| } |
| reason = m.String() |
| } |
| |
| // Discard the truncated part of stdout. Doing this rather than closing the |
| // reader avoids broken pipe errors. This is bounded by the stdoutRead |
| // deadline. |
| if _, err := io.Copy(io.Discard, stdoutRead); err != nil { |
| log.G(ctx).WithError(err).Error("error flushing stdout") |
| } |
| stdoutRead.Close() |
| |
| <-stderrLogDone |
| if err := cmd.Wait(); err != nil { |
| if ee := (&exec.ExitError{}); errors.As(err, &ee) && ee.ProcessState.Exited() { |
| return ee.ProcessState.ExitCode(), reason, nil |
| } |
| return -1, "", fmt.Errorf("waiting on command to exit: %v", err) |
| } |
| |
| return cmd.ProcessState.ExitCode(), reason, nil |
| } |