From 0d8ad7e9ecda7e8409dcced9deb552ebc4db28c6 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Fri, 24 Apr 2026 02:13:25 +0530 Subject: [PATCH] config: detect recursive includes Signed-off-by: Asish Kumar --- pkg/build/types/image_configuration.go | 39 ++++++++++++++------- pkg/build/types/image_configuration_test.go | 32 +++++++++++++++++ 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/pkg/build/types/image_configuration.go b/pkg/build/types/image_configuration.go index 245452a1f..3dba5746d 100644 --- a/pkg/build/types/image_configuration.go +++ b/pkg/build/types/image_configuration.go @@ -20,6 +20,7 @@ import ( "hash" "maps" "os" + "path/filepath" "reflect" "regexp" "slices" @@ -57,7 +58,7 @@ func (ic *ImageConfiguration) ProbeVCSUrl(ctx context.Context, imageConfigPath s } // Parse a configuration blob into an ImageConfiguration struct. -func (ic *ImageConfiguration) parse(ctx context.Context, configData []byte, includePaths []string, configHasher hash.Hash) error { +func (ic *ImageConfiguration) parse(ctx context.Context, configData []byte, includePaths []string, configHasher hash.Hash, includeStack []string) error { log := clog.FromContext(ctx) configHasher.Write(configData) dec := yaml.NewDecoder(strings.NewReader(string(configData))) @@ -71,7 +72,7 @@ func (ic *ImageConfiguration) parse(ctx context.Context, configData []byte, incl included := &ImageConfiguration{} - if err := included.Load(ctx, ic.Include, includePaths, configHasher); err != nil { + if err := included.load(ctx, ic.Include, includePaths, configHasher, includeStack); err != nil { return fmt.Errorf("failed to read include file: %w", err) } @@ -184,26 +185,40 @@ func (i *ImageContents) MergeInto(target *ImageContents) error { return nil } -func (ic *ImageConfiguration) readLocal(imageconfigPath string, includePaths []string) ([]byte, error) { - resolvedPath, err := paths.ResolvePath(imageconfigPath, includePaths) - if err != nil { - return nil, err - } - return os.ReadFile(resolvedPath) -} - // Load - loads an image configuration given a configuration file path. // Populates configHasher with the configuration data loaded from the imageConfigPath and the other referenced files. // You can pass any dummy hasher (like fnv.New32()), if you don't care about the hash of the configuration. // // Deprecated: This will be removed in a future release. func (ic *ImageConfiguration) Load(ctx context.Context, imageConfigPath string, includePaths []string, configHasher hash.Hash) error { - data, err := ic.readLocal(imageConfigPath, includePaths) + return ic.load(ctx, imageConfigPath, includePaths, configHasher, nil) +} + +func (ic *ImageConfiguration) load(ctx context.Context, imageConfigPath string, includePaths []string, configHasher hash.Hash, includeStack []string) error { + resolvedPath, err := paths.ResolvePath(imageConfigPath, includePaths) + if err != nil { + return err + } + + canonicalPath, err := filepath.Abs(resolvedPath) + if err != nil { + return err + } + if evaluatedPath, err := filepath.EvalSymlinks(canonicalPath); err == nil { + canonicalPath = evaluatedPath + } + + if slices.Contains(includeStack, canonicalPath) { + cycle := append(slices.Clone(includeStack), canonicalPath) + return fmt.Errorf("recursive include detected: %s", strings.Join(cycle, " -> ")) + } + + data, err := os.ReadFile(resolvedPath) if err != nil { return err } - return ic.parse(ctx, data, includePaths, configHasher) + return ic.parse(ctx, data, includePaths, configHasher, append(includeStack, canonicalPath)) } // Do preflight checks and mutations on an image configuration. diff --git a/pkg/build/types/image_configuration_test.go b/pkg/build/types/image_configuration_test.go index 120b8c87b..3df87c6fc 100644 --- a/pkg/build/types/image_configuration_test.go +++ b/pkg/build/types/image_configuration_test.go @@ -3,6 +3,7 @@ package types_test import ( "context" "crypto/sha256" + "os" "path/filepath" "testing" @@ -67,6 +68,37 @@ func TestUserContents(t *testing.T) { ic.Summarize(ctx) } +func TestLoadDetectsRecursiveInclude(t *testing.T) { + ctx := context.Background() + tmp := t.TempDir() + + self := filepath.Join(tmp, "self.apko.yaml") + require.NoError(t, os.WriteFile(self, []byte("include: self.apko.yaml\n"), 0o644)) + + hasher := sha256.New() + ic := types.ImageConfiguration{} + err := ic.Load(ctx, self, []string{tmp}, hasher) + require.ErrorContains(t, err, "recursive include detected") + require.ErrorContains(t, err, self) +} + +func TestLoadDetectsRecursiveIncludeChain(t *testing.T) { + ctx := context.Background() + tmp := t.TempDir() + + first := filepath.Join(tmp, "first.apko.yaml") + second := filepath.Join(tmp, "second.apko.yaml") + require.NoError(t, os.WriteFile(first, []byte("include: second.apko.yaml\n"), 0o644)) + require.NoError(t, os.WriteFile(second, []byte("include: first.apko.yaml\n"), 0o644)) + + hasher := sha256.New() + ic := types.ImageConfiguration{} + err := ic.Load(ctx, first, []string{tmp}, hasher) + require.ErrorContains(t, err, "recursive include detected") + require.ErrorContains(t, err, first) + require.ErrorContains(t, err, second) +} + func TestMergeInto(t *testing.T) { tests := []struct { name string