cri: validate CRIU availability and version early

Perform an early validation check on both container checkpoint and
restore paths to fail-fast if the CRIU binary is missing or is older
than the minimum required version (3.16.0).

To support runtime-configured environments, the validation respects the
custom PATH from the shim manager environment if configured, skipping
any relative paths to avoid incorrect daemon-relative resolution. If not
configured, it falls back to a standard system PATH lookup. The check
result is cached using sync.Once to prevent redundant process spawning.

Assisted-by: Antigravity
Signed-off-by: Samuel Karp <samuelkarp@google.com>
This commit is contained in:
Samuel Karp
2026-06-04 04:39:15 +00:00
committed by k8s-infra-cherrypick-robot
parent 5e5581f651
commit dacd4c7d00
5 changed files with 177 additions and 12 deletions

View File

@@ -194,6 +194,16 @@ func (m *ShimManager) ID() string {
return plugins.ShimPlugin.String() + ".manager"
}
// Env returns the environment configured for the shim manager.
func (m *ShimManager) Env() []string {
if m.env == nil {
return nil
}
cp := make([]string, len(m.env))
copy(cp, m.env)
return cp
}
// Start launches a new shim instance
func (m *ShimManager) Start(ctx context.Context, id string, bundle *Bundle, opts runtime.CreateOpts) (_ ShimInstance, retErr error) {
shouldInvokeShimBinary := false

View File

@@ -26,11 +26,13 @@ import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
crmetadata "github.com/checkpoint-restore/checkpointctl/lib"
criu "github.com/checkpoint-restore/go-criu/v7"
"github.com/checkpoint-restore/go-criu/v7/utils"
"github.com/containerd/containerd/api/types/runc/options"
"github.com/containerd/containerd/v2/client"
@@ -135,6 +137,55 @@ func assertCheckpointDirSafe(root string) error {
})
}
func (c *criService) checkCriu() error {
c.checkCriuOnce.Do(func() {
c.checkCriuErr = c.doCheckCriu()
})
return c.checkCriuErr
}
func (c *criService) doCheckCriu() error {
path := resolveCriuPath(c.shimPath)
if path == "" {
return errors.New("criu binary not found in shim path or system PATH")
}
client := criu.MakeCriu()
client.SetCriuPath(path)
version, err := client.GetCriuVersion()
if err != nil {
return fmt.Errorf("failed to retrieve criu version: %w", err)
}
if version < utils.PodCriuVersion {
return fmt.Errorf("checkpoint/restore requires at least CRIU %d, current version is %d", utils.PodCriuVersion, version)
}
return nil
}
func resolveCriuPath(customPath string) string {
if customPath != "" {
// This logic is Linux-specific. If CRIU is ever supported on other
// operating systems, path lookup will need to respect that OS's
// conventions.
for _, dir := range filepath.SplitList(customPath) {
if !filepath.IsAbs(dir) {
continue
}
criuPath := filepath.Join(dir, "criu")
if fi, err := os.Stat(criuPath); err == nil && fi.Mode().IsRegular() && fi.Mode()&0111 != 0 {
return criuPath
}
}
return ""
}
if criuPath, err := exec.LookPath("criu"); err == nil {
if absPath, err := filepath.Abs(criuPath); err == nil {
return absPath
}
return criuPath
}
return ""
}
// checkIfCheckpointOCIImage returns checks if the input refers to a checkpoint image.
// It returns the StorageImageID of the image the input resolves to, nil otherwise.
func (c *criService) checkIfCheckpointOCIImage(ctx context.Context, input string) (string, error) {
@@ -189,6 +240,10 @@ func (c *criService) CRImportCheckpoint(
sandbox *sandbox.Sandbox,
sandboxConfig *runtime.PodSandboxConfig,
) (ctrID string, retErr error) {
if err := c.checkCriu(); err != nil {
return "", fmt.Errorf("checkpoint restore is not enabled: %w", err)
}
var mountPoint string
start := time.Now()
// Ensure that the image to restore the checkpoint from has been provided.
@@ -539,18 +594,9 @@ func (c *criService) CRImportCheckpoint(
func (c *criService) CheckpointContainer(ctx context.Context, r *runtime.CheckpointContainerRequest) (*runtime.CheckpointContainerResponse, error) {
start := time.Now()
if err := utils.CheckForCriu(utils.PodCriuVersion); err != nil {
errorMessage := fmt.Sprintf(
"CRIU binary not found or too old (<%d). Failed to checkpoint container %q",
utils.PodCriuVersion,
r.GetContainerId(),
)
log.G(ctx).WithError(err).Error(errorMessage)
return nil, fmt.Errorf(
"%s: %w",
errorMessage,
err,
)
if err := c.checkCriu(); err != nil {
log.G(ctx).WithError(err).Errorf("Failed to checkpoint container %q", r.GetContainerId())
return nil, fmt.Errorf("failed to checkpoint container %q: %w", r.GetContainerId(), err)
}
criContainerStatus, err := c.ContainerStatus(ctx, &runtime.ContainerStatusRequest{

View File

@@ -235,3 +235,84 @@ func TestFilterAndMergeAnnotations(t *testing.T) {
})
}
}
func TestResolveCriuPath(t *testing.T) {
tempDir, err := os.MkdirTemp("", "criu-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create a dummy executable criu
execDir := filepath.Join(tempDir, "bin-exec")
if err := os.MkdirAll(execDir, 0755); err != nil {
t.Fatal(err)
}
execPath := filepath.Join(execDir, "criu")
if err := os.WriteFile(execPath, []byte("dummy"), 0755); err != nil {
t.Fatal(err)
}
// Create a dummy non-executable criu
nonExecDir := filepath.Join(tempDir, "bin-nonexec")
if err := os.MkdirAll(nonExecDir, 0755); err != nil {
t.Fatal(err)
}
nonExecPath := filepath.Join(nonExecDir, "criu")
if err := os.WriteFile(nonExecPath, []byte("dummy"), 0644); err != nil {
t.Fatal(err)
}
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
relDir, err := filepath.Rel(wd, execDir)
if err != nil {
t.Fatal(err)
}
// Mock the system PATH to point to our executable directory
t.Setenv("PATH", execDir)
tests := []struct {
name string
customPath string
expectedPath string
}{
{
name: "custom PATH with executable criu",
customPath: execDir,
expectedPath: execPath,
},
{
name: "custom PATH with non-executable criu",
customPath: nonExecDir,
expectedPath: "",
},
{
name: "multiple directories in PATH, executable in second",
customPath: nonExecDir + string(filepath.ListSeparator) + execDir,
expectedPath: execPath,
},
{
name: "custom PATH with relative directory is skipped",
customPath: relDir,
expectedPath: "",
},
{
name: "empty customPath falls back to system PATH",
customPath: "",
expectedPath: execPath, // Falls back to system PATH (mocked to execDir)
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path := resolveCriuPath(tt.customPath)
if path != tt.expectedPath {
t.Errorf("expected %s, got %s", tt.expectedPath, path)
}
})
}
}

View File

@@ -169,6 +169,11 @@ type criService struct {
runtimeFeatures *runtime.RuntimeFeatures
// statsCollector collects CPU stats in background for UsageNanoCores calculation
statsCollector *StatsCollector
// shimPath is the custom PATH environment variable value from the shim manager
shimPath string
checkCriuOnce sync.Once //nolint:nolintlint,unused // Ignore on non-Linux
checkCriuErr error //nolint:nolintlint,unused // Ignore on non-Linux
}
type CRIServiceOptions struct {
@@ -187,6 +192,9 @@ type CRIServiceOptions struct {
//
// TODO: Replace this gradually with directly configured instances
Client *containerd.Client
// ShimPath is the custom PATH environment variable value from the shim manager
ShimPath string
}
// NewCRIService returns a new instance of CRIService
@@ -214,6 +222,7 @@ func NewCRIService(options *CRIServiceOptions) (CRIService, runtime.RuntimeServi
sandboxService: newCriSandboxService(&config, options.SandboxControllers),
runtimeHandlers: make(map[string]*runtime.RuntimeHandler),
statsCollector: statsCollector,
shimPath: options.ShimPath,
}
// TODO: Make discard time configurable

View File

@@ -20,6 +20,7 @@ import (
"context"
"fmt"
"io"
"strings"
"github.com/containerd/log"
"github.com/containerd/plugin"
@@ -58,6 +59,7 @@ func init() {
plugins.SandboxStorePlugin,
plugins.TransferPlugin,
plugins.WarningPlugin,
plugins.ShimPlugin,
},
Config: &defaultConfig,
ConfigMigration: configMigration,
@@ -131,6 +133,22 @@ func initCRIService(ic *plugin.InitContext) (any, error) {
return nil, fmt.Errorf("failed to get streaming config: %w", err)
}
var shimPath string
shimPlugin, err := ic.GetSingle(plugins.ShimPlugin)
if err != nil {
return nil, fmt.Errorf("failed to get shim plugin: %w", err)
}
if hasEnv, ok := shimPlugin.(interface{ Env() []string }); ok {
env := hasEnv.Env()
for i := len(env) - 1; i >= 0; i-- {
// iterate backwards to grab the last PATH=
if path, ok := strings.CutPrefix(env[i], "PATH="); ok {
shimPath = path
break
}
}
}
options := &server.CRIServiceOptions{
RuntimeService: runtimeSvc,
ImageService: imageSvc,
@@ -138,6 +156,7 @@ func initCRIService(ic *plugin.InitContext) (any, error) {
NRI: getNRIAPI(ic),
Client: client,
SandboxControllers: sbControllers,
ShimPath: shimPath,
}
is := criImagePlugin.(imageService).GRPCService()