From 820323abd55c26ca58e87f73e70c06f8edcc034d Mon Sep 17 00:00:00 2001 From: Snider Date: Wed, 15 Apr 2026 12:42:15 +0100 Subject: [PATCH] Harden SDK docker runtime cache --- pkg/sdk/generators/docker_runtime.go | 34 ++++++++++++++++++++++- pkg/sdk/generators/docker_runtime_test.go | 29 +++++++++++++++---- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/pkg/sdk/generators/docker_runtime.go b/pkg/sdk/generators/docker_runtime.go index ff5021f..8514af3 100644 --- a/pkg/sdk/generators/docker_runtime.go +++ b/pkg/sdk/generators/docker_runtime.go @@ -2,6 +2,10 @@ package generators import ( "context" + "crypto/sha256" + "encoding/hex" + "errors" + stdio "io" "strconv" "sync" "time" @@ -20,6 +24,8 @@ var ( var availabilityProbeTimeout = 2 * time.Second +const dockerRuntimeFingerprintBytes = 4 * 1024 + func dockerRuntimeAvailable() bool { ctx, cancel := availabilityProbeContext() defer cancel() @@ -50,6 +56,9 @@ func dockerRuntimeAvailableWithContext(ctx context.Context) bool { if err != nil && ctx.Err() != nil { return false } + if ctx.Err() != nil { + return false + } available := err == nil storeDockerRuntimeAvailability(dockerCommand, commandState, available) @@ -84,9 +93,22 @@ func dockerRuntimeCommandState(command string) (string, error) { return "", err } + file, err := ax.Open(command) + if err != nil { + return "", err + } + defer func() { _ = file.Close() }() + + hasher := sha256.New() + if _, err := stdio.CopyN(hasher, file, dockerRuntimeFingerprintBytes); err != nil && !errors.Is(err, stdio.EOF) { + return "", err + } + return command + "|" + strconv.FormatInt(info.Size(), 10) + "|" + - strconv.FormatInt(info.ModTime().UnixNano(), 10), nil + strconv.FormatInt(info.ModTime().UnixNano(), 10) + "|" + + info.Mode().String() + "|" + + hex.EncodeToString(hasher.Sum(nil)), nil } func cachedDockerRuntimeAvailability(command, state string) (bool, bool) { @@ -111,3 +133,13 @@ func storeDockerRuntimeAvailability(command, state string, available bool) { dockerRuntimeOK = available dockerRuntimeChecked = true } + +func resetDockerRuntimeAvailabilityCache() { + dockerRuntimeMu.Lock() + defer dockerRuntimeMu.Unlock() + + dockerRuntimeChecked = false + dockerRuntimeOK = false + dockerRuntimeCommand = "" + dockerRuntimeState = "" +} diff --git a/pkg/sdk/generators/docker_runtime_test.go b/pkg/sdk/generators/docker_runtime_test.go index 90bc1b9..d1a4919 100644 --- a/pkg/sdk/generators/docker_runtime_test.go +++ b/pkg/sdk/generators/docker_runtime_test.go @@ -2,8 +2,8 @@ package generators import ( "context" + "os" "strings" - "sync" "testing" "time" @@ -13,11 +13,7 @@ import ( ) func resetDockerRuntimeState() { - dockerRuntimeMu = sync.Mutex{} - dockerRuntimeChecked = false - dockerRuntimeOK = false - dockerRuntimeCommand = "" - dockerRuntimeState = "" + resetDockerRuntimeAvailabilityCache() } func setAvailabilityProbeTimeout(t *testing.T, timeout time.Duration) { @@ -197,3 +193,24 @@ func TestSDK_DockerRuntimeAvailabilityInvalidatesCachedSuccessWhenCommandMutates assert.False(t, dockerRuntimeAvailable()) } + +func TestSDK_DockerRuntimeAvailabilityInvalidatesCachedSuccessWhenCommandKeepsSizeAndMTime_Good(t *testing.T) { + resetDockerRuntimeState() + t.Cleanup(resetDockerRuntimeState) + + dockerDir := t.TempDir() + successScript := "#!/bin/sh\nif [ \"$1\" = \"--help\" ]; then\n exit 0\nfi\nexit 0\n" + failureScript := "#!/bin/sh\nif [ \"$1\" = \"--help\" ]; then\n exit 1\nfi\nexit 0\n" + dockerPath := writeFakeDockerRuntime(t, dockerDir, successScript) + t.Setenv("PATH", dockerDir) + + assert.True(t, dockerRuntimeAvailable()) + + info, err := os.Stat(dockerPath) + require.NoError(t, err) + + require.NoError(t, ax.WriteFile(dockerPath, []byte(failureScript), 0o755)) + require.NoError(t, os.Chtimes(dockerPath, info.ModTime(), info.ModTime())) + + assert.False(t, dockerRuntimeAvailable()) +}