From e8b20b5a24245924b6cb4875656f6e0d005a6799 Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Mon, 16 Mar 2026 21:58:33 +0000 Subject: [PATCH 01/17] mz560: pass down ignoreList --- pkg/executor/build.go | 2 +- pkg/snapshot/snapshot.go | 10 +++++----- pkg/snapshot/snapshot_test.go | 2 +- pkg/util/fs_util.go | 11 ++++------- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/pkg/executor/build.go b/pkg/executor/build.go index fdb50071a..a604e4e7f 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -82,7 +82,7 @@ func makeSnapshotter(opts *config.KanikoOptions) (*snapshot.Snapshotter, error) return nil, err } l := snapshot.NewLayeredMap(hasher) - return snapshot.NewSnapshotter(l, config.RootDir), nil + return snapshot.NewSnapshotter(l, config.RootDir, util.IgnoreList()), nil } // newStageBuilder returns a new type stageBuilder which contains all the information required to build the stage diff --git a/pkg/snapshot/snapshot.go b/pkg/snapshot/snapshot.go index d73d0fc74..022fffd52 100644 --- a/pkg/snapshot/snapshot.go +++ b/pkg/snapshot/snapshot.go @@ -44,8 +44,8 @@ type Snapshotter struct { } // NewSnapshotter creates a new snapshotter rooted at d -func NewSnapshotter(l *LayeredMap, d string) *Snapshotter { - return &Snapshotter{l: l, directory: d, ignorelist: util.IgnoreList()} +func NewSnapshotter(l *LayeredMap, d string, wl []util.IgnoreListEntry) *Snapshotter { + return &Snapshotter{l: l, directory: d, ignorelist: wl} } // Init initializes a new snapshotter @@ -95,7 +95,7 @@ func (s *Snapshotter) TakeSnapshot(files []string, shdCheckDelete bool) (string, // Get whiteout paths var filesToWhiteout []string if shdCheckDelete { - _, deletedFiles, err := util.WalkFS(s.directory, s.l.GetCurrentPaths(), func(s string) (bool, error) { + _, deletedFiles, err := util.WalkFS(s.directory, s.l.GetCurrentPaths(), s.ignorelist, func(s string) (bool, error) { return true, nil }) if err != nil { @@ -188,7 +188,7 @@ func (s *Snapshotter) scanFullFilesystem() ([]string, []string, error) { logrus.Debugf("Current image filesystem: %v", s.l.currentImage) - changedPaths, deletedPaths, err := util.WalkFS(s.directory, s.l.GetCurrentPaths(), s.l.CheckFileChange) + changedPaths, deletedPaths, err := util.WalkFS(s.directory, s.l.GetCurrentPaths(), s.ignorelist, s.l.CheckFileChange) if err != nil { return nil, nil, err } @@ -200,7 +200,7 @@ func (s *Snapshotter) scanFullFilesystem() ([]string, []string, error) { return nil, nil, err } for _, path := range resolvedFiles { - if util.CheckIgnoreList(path) { + if util.CheckCleanedPathAgainstProvidedIgnoreList(filepath.Clean(path), s.ignorelist) { logrus.Debugf("Not adding %s to layer, as it's ignored", path) continue } diff --git a/pkg/snapshot/snapshot_test.go b/pkg/snapshot/snapshot_test.go index 49eccfd0b..becbd1c3f 100644 --- a/pkg/snapshot/snapshot_test.go +++ b/pkg/snapshot/snapshot_test.go @@ -619,7 +619,7 @@ func setUpTest(t *testing.T) (string, *Snapshotter, func(), error) { // Take the initial snapshot l := NewLayeredMap(util.Hasher()) - snapshotter := NewSnapshotter(l, testDir) + snapshotter := NewSnapshotter(l, testDir, util.IgnoreList()) if err := snapshotter.Init(); err != nil { return "", nil, nil, fmt.Errorf("initializing snapshotter: %w", err) } diff --git a/pkg/util/fs_util.go b/pkg/util/fs_util.go index 47f3be86e..c518195ce 100644 --- a/pkg/util/fs_util.go +++ b/pkg/util/fs_util.go @@ -430,10 +430,6 @@ func IsInProvidedIgnoreList(path string, wl []IgnoreListEntry) bool { return false } -func IsInIgnoreList(path string) bool { - return IsInProvidedIgnoreList(path, ignorelist) -} - func CheckCleanedPathAgainstProvidedIgnoreList(path string, wl []IgnoreListEntry) bool { for _, wl := range ignorelist { if hasCleanedFilepathPrefix(path, wl.Path, wl.PrefixMatchOnly) { @@ -1262,6 +1258,7 @@ type walkFSResult struct { func WalkFS( dir string, existingPaths map[string]struct{}, + wl []IgnoreListEntry, changeFunc func(string) (bool, error), ) ([]string, map[string]struct{}, error) { timeOutStr := os.Getenv(snapshotTimeout) @@ -1278,7 +1275,7 @@ func WalkFS( ch := make(chan walkFSResult, 1) go func() { - filesAdded, existingPaths, err := gowalkDir(dir, existingPaths, changeFunc) + filesAdded, existingPaths, err := gowalkDir(dir, existingPaths, wl, changeFunc) ch <- walkFSResult{filesAdded, existingPaths, err} }() @@ -1294,7 +1291,7 @@ func WalkFS( } } -func gowalkDir(dir string, existingPaths map[string]struct{}, changeFunc func(string) (bool, error)) ([]string, map[string]struct{}, error) { +func gowalkDir(dir string, existingPaths map[string]struct{}, wl []IgnoreListEntry, changeFunc func(string) (bool, error)) ([]string, map[string]struct{}, error) { foundPaths := make([]string, 0) deletedFiles := existingPaths // Make a reference. @@ -1304,7 +1301,7 @@ func gowalkDir(dir string, existingPaths map[string]struct{}, changeFunc func(st return err } - if IsInIgnoreList(path) { + if IsInProvidedIgnoreList(path, wl) { if IsDestDir(path) && info.IsDir() { logrus.Tracef("Skipping paths under '%s', as it is an ignored directory", path) return filepath.SkipDir From da33182b2e9d978dc194a1e99c4490c2cc2aff88 Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Mon, 16 Mar 2026 22:44:51 +0000 Subject: [PATCH 02/17] mz560: replace exact-match ignore checks with subtree-aware checks IsInProvidedIgnoreList only matched a path exactly, silently ignoring PrefixMatchOnly=true entries and failing to catch files nested under an ignored directory. The intent of the function was to manage the list of ignores, not to check whether a path should be ignored. Replace all call sites with CheckCleanedPathAgainstProvidedIgnoreList, which correctly covers both exact and descendant paths and honours PrefixMatchOnly semantics. Remove the now-unused IsInProvidedIgnoreList. --- pkg/filesystem/resolve.go | 2 +- pkg/filesystem/resolve_test.go | 4 ++-- pkg/util/fs_util.go | 13 +------------ 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/pkg/filesystem/resolve.go b/pkg/filesystem/resolve.go index b0cb9bcea..654223962 100644 --- a/pkg/filesystem/resolve.go +++ b/pkg/filesystem/resolve.go @@ -42,7 +42,7 @@ func ResolvePaths(paths []string, wl []util.IgnoreListEntry) (pathsToAdd []strin for _, f := range paths { // If the given path is part of the ignorelist ignore it - if util.IsInProvidedIgnoreList(f, wl) { + if util.CheckCleanedPathAgainstProvidedIgnoreList(filepath.Clean(f), wl) { logrus.Debugf("Path %s is in list to ignore, ignoring it", f) continue } diff --git a/pkg/filesystem/resolve_test.go b/pkg/filesystem/resolve_test.go index d54f69640..5a8698a73 100644 --- a/pkg/filesystem/resolve_test.go +++ b/pkg/filesystem/resolve_test.go @@ -118,7 +118,7 @@ func Test_ResolvePaths(t *testing.T) { link := filepath.Join(dir, "link", f) inputFiles = append(inputFiles, link) - if util.IsInProvidedIgnoreList(link, wl) { + if util.CheckCleanedPathAgainstProvidedIgnoreList(filepath.Clean(link), wl) { t.Logf("skipping %s", link) continue } @@ -127,7 +127,7 @@ func Test_ResolvePaths(t *testing.T) { target := filepath.Join(dir, "target", f) - if util.IsInProvidedIgnoreList(target, wl) { + if util.CheckCleanedPathAgainstProvidedIgnoreList(filepath.Clean(target), wl) { t.Logf("skipping %s", target) continue } diff --git a/pkg/util/fs_util.go b/pkg/util/fs_util.go index c518195ce..0af18658e 100644 --- a/pkg/util/fs_util.go +++ b/pkg/util/fs_util.go @@ -419,17 +419,6 @@ func ExtractFile(dest string, hdr *tar.Header, cleanedName string, tr io.Reader) return nil } -func IsInProvidedIgnoreList(path string, wl []IgnoreListEntry) bool { - path = filepath.Clean(path) - for _, entry := range wl { - if !entry.PrefixMatchOnly && path == entry.Path { - return true - } - } - - return false -} - func CheckCleanedPathAgainstProvidedIgnoreList(path string, wl []IgnoreListEntry) bool { for _, wl := range ignorelist { if hasCleanedFilepathPrefix(path, wl.Path, wl.PrefixMatchOnly) { @@ -1301,7 +1290,7 @@ func gowalkDir(dir string, existingPaths map[string]struct{}, wl []IgnoreListEnt return err } - if IsInProvidedIgnoreList(path, wl) { + if CheckCleanedPathAgainstProvidedIgnoreList(filepath.Clean(path), wl) { if IsDestDir(path) && info.IsDir() { logrus.Tracef("Skipping paths under '%s', as it is an ignored directory", path) return filepath.SkipDir From 50c3a2aafd069a2bb0b94c1bf59a3f87024afe9b Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Mon, 16 Mar 2026 22:46:48 +0000 Subject: [PATCH 03/17] mz560: fix CheckCleanedPathAgainstProvidedIgnoreList to use provided ignore list The function was shadowing the wl parameter by ranging over the global ignorelist instead, making all callers effectively ignore the list they passed in. --- pkg/util/fs_util.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/util/fs_util.go b/pkg/util/fs_util.go index 0af18658e..0354a9bff 100644 --- a/pkg/util/fs_util.go +++ b/pkg/util/fs_util.go @@ -420,8 +420,8 @@ func ExtractFile(dest string, hdr *tar.Header, cleanedName string, tr io.Reader) } func CheckCleanedPathAgainstProvidedIgnoreList(path string, wl []IgnoreListEntry) bool { - for _, wl := range ignorelist { - if hasCleanedFilepathPrefix(path, wl.Path, wl.PrefixMatchOnly) { + for _, w := range wl { + if hasCleanedFilepathPrefix(path, w.Path, w.PrefixMatchOnly) { return true } } From 7f74fe5d754b80d0e959ad0db010058bc5dc2387 Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Mon, 16 Mar 2026 23:29:51 +0000 Subject: [PATCH 04/17] mz560: add integration tests --- .../Dockerfile_test_ignore_path_subtree | 4 ++ .../Dockerfile_test_prefix_match_only | 7 +++ integration/images.go | 1 + integration/integration_test.go | 61 +++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 integration/dockerfiles/Dockerfile_test_ignore_path_subtree create mode 100644 integration/dockerfiles/Dockerfile_test_prefix_match_only diff --git a/integration/dockerfiles/Dockerfile_test_ignore_path_subtree b/integration/dockerfiles/Dockerfile_test_ignore_path_subtree new file mode 100644 index 000000000..9315a099d --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_ignore_path_subtree @@ -0,0 +1,4 @@ +FROM scratch +# mz560: in kaniko v1.27.0, when built with --ignore-path=/dest, +# the file copied into /dest/subdir/ would slip through and end up in the layer. +COPY context/foo /dest/subdir/file.txt diff --git a/integration/dockerfiles/Dockerfile_test_prefix_match_only b/integration/dockerfiles/Dockerfile_test_prefix_match_only new file mode 100644 index 000000000..f24884696 --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_prefix_match_only @@ -0,0 +1,7 @@ +FROM alpine +# mz560: Simulates apt-key writing temporary key material under /tmp/apt-key-gpghome. +# The directory itself should appear in the image layer, but files inside it +# should be excluded because /tmp/apt-key-gpghome is on the ignore list with +# PrefixMatchOnly=true. +RUN mkdir /tmp/apt-key-gpghome && \ + echo "key material" > /tmp/apt-key-gpghome/pubring.gpg diff --git a/integration/images.go b/integration/images.go index 83345b679..800a59ee1 100644 --- a/integration/images.go +++ b/integration/images.go @@ -122,6 +122,7 @@ var additionalKanikoFlagsMap = map[string][]string{ "Dockerfile_test_maintainer": {"--single-snapshot"}, "Dockerfile_test_target": {"--target=second"}, "Dockerfile_test_snapshotter_ignorelist": {"--use-new-run=true", "-v=trace"}, + "Dockerfile_test_ignore_path_subtree": {"--ignore-path=/dest"}, "Dockerfile_test_cache": {"--cache-copy-layers=true"}, "Dockerfile_test_cache_oci": {"--cache-copy-layers=true"}, "Dockerfile_test_cache_install": {"--cache-copy-layers=true"}, diff --git a/integration/integration_test.go b/integration/integration_test.go index ce021eb1c..f8d7ae79d 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -635,6 +635,67 @@ func TestLayers(t *testing.T) { } } +// mz560: TestPrefixMatchOnlyIgnoreList verifies that an ignore list entry with +// PrefixMatchOnly=true excludes files inside the directory from the snapshot +// but still captures the directory node itself. +// The canonical example is /tmp/apt-key-gpghome: apt-key writes temporary GPG +// key files there during a build; they must not end up in the image layer. +func TestPrefixMatchOnlyIgnoreList(t *testing.T) { + dockerfile := "Dockerfile_test_prefix_match_only" + buildImage(t, dockerfile, imageBuilder) + kanikoImage := GetKanikoImage(config.imageRepo, dockerfile) + + kanikoFiles, err := getLastLayerFiles(kanikoImage) + if err != nil { + t.Fatal(err) + } + + const ( + gpghomeDir = "tmp/apt-key-gpghome" + gpghomeFile = "tmp/apt-key-gpghome/pubring.gpg" + ) + + var hasDir, hasFile bool + for _, f := range kanikoFiles { + switch strings.TrimSuffix(f, "/") { + case gpghomeDir: + hasDir = true + case gpghomeFile: + hasFile = true + } + } + + if !hasDir { + t.Errorf("expected %s directory to be present in layer (PrefixMatchOnly=true should not exclude the directory itself), got %v", gpghomeDir, kanikoFiles) + } + if hasFile { + t.Errorf("expected %s to be excluded from layer (child of PrefixMatchOnly=true ignore entry), got %v", gpghomeFile, kanikoFiles) + } +} + +// mz560: TestIgnorePathSubtree verifies that --ignore-path excludes not just the named +// path but also any files nested beneath it. The observable difference: with an +// exact-match-only check (IsInProvidedIgnoreList), a COPY into /dest/subdir/ +// would pass the ResolvePaths input guard and appear in the layer even when +// --ignore-path=/dest is set. CheckCleanedPathAgainstProvidedIgnoreList (prefix +// match) catches the nested path and keeps it out of the layer. +func TestIgnorePathSubtree(t *testing.T) { + dockerfile := "Dockerfile_test_ignore_path_subtree" + buildImage(t, dockerfile, imageBuilder) + kanikoImage := GetKanikoImage(config.imageRepo, dockerfile) + + kanikoFiles, err := getLastLayerFiles(kanikoImage) + if err != nil { + t.Fatal(err) + } + + for _, f := range kanikoFiles { + if strings.HasPrefix(strings.TrimSuffix(f, "/"), "dest/") { + t.Errorf("file %s should be excluded from layer because it is under --ignore-path=/dest, got layer contents: %v", f, kanikoFiles) + } + } +} + func TestReplaceFolderWithFileOrLink(t *testing.T) { dockerfiles := []string{"TestReplaceFolderWithFile", "TestReplaceFolderWithLink"} for _, dockerfile := range dockerfiles { From 05110601dc1a0d9d1f6948495fe4a61caddb28ff Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Mon, 16 Mar 2026 23:39:12 +0000 Subject: [PATCH 05/17] tinker --- pkg/snapshot/snapshot.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/snapshot/snapshot.go b/pkg/snapshot/snapshot.go index 022fffd52..97698840e 100644 --- a/pkg/snapshot/snapshot.go +++ b/pkg/snapshot/snapshot.go @@ -201,6 +201,7 @@ func (s *Snapshotter) scanFullFilesystem() ([]string, []string, error) { } for _, path := range resolvedFiles { if util.CheckCleanedPathAgainstProvidedIgnoreList(filepath.Clean(path), s.ignorelist) { + logrus.Panic("Unreachable Code: ignored files should already be filtered out above") logrus.Debugf("Not adding %s to layer, as it's ignored", path) continue } From 7367ad793316a45cc90af02a07d45b26a3c5f7b4 Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Tue, 17 Mar 2026 21:50:34 +0000 Subject: [PATCH 06/17] mz560: drop redundant check --- pkg/snapshot/snapshot.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/pkg/snapshot/snapshot.go b/pkg/snapshot/snapshot.go index 97698840e..3d9f095ae 100644 --- a/pkg/snapshot/snapshot.go +++ b/pkg/snapshot/snapshot.go @@ -194,20 +194,10 @@ func (s *Snapshotter) scanFullFilesystem() ([]string, []string, error) { } timer := timing.Start("Resolving Paths") - filesToAdd := []string{} - resolvedFiles, err := filesystem.ResolvePaths(changedPaths, s.ignorelist) + filesToAdd, err := filesystem.ResolvePaths(changedPaths, s.ignorelist) if err != nil { return nil, nil, err } - for _, path := range resolvedFiles { - if util.CheckCleanedPathAgainstProvidedIgnoreList(filepath.Clean(path), s.ignorelist) { - logrus.Panic("Unreachable Code: ignored files should already be filtered out above") - logrus.Debugf("Not adding %s to layer, as it's ignored", path) - continue - } - filesToAdd = append(filesToAdd, path) - } - logrus.Debugf("Adding to layer: %v", filesToAdd) logrus.Debugf("Deleting in layer: %v", deletedPaths) From f54ec5fbbc9c8d5b971775bbd0fc0efc870d8000 Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Fri, 13 Mar 2026 22:44:22 +0000 Subject: [PATCH 07/17] mz560: integration test for hijacking --- integration/config.go | 1 + .../dockerfiles/Dockerfile_test_issue_mz560 | 19 +++++++++++++++++++ integration/integration_test.go | 7 +++++++ 3 files changed, 27 insertions(+) create mode 100644 integration/dockerfiles/Dockerfile_test_issue_mz560 diff --git a/integration/config.go b/integration/config.go index ee15b8bb3..849499ea8 100644 --- a/integration/config.go +++ b/integration/config.go @@ -28,6 +28,7 @@ type integrationTestConfig struct { onbuildBaseImage string onbuildCopyImage string hardlinkBaseImage string + hijackBaseImage string serviceAccount string dockerMajorVersion int gcsClient *storage.Client diff --git a/integration/dockerfiles/Dockerfile_test_issue_mz560 b/integration/dockerfiles/Dockerfile_test_issue_mz560 new file mode 100644 index 000000000..85a6fcf6b --- /dev/null +++ b/integration/dockerfiles/Dockerfile_test_issue_mz560 @@ -0,0 +1,19 @@ +# mz560: In kaniko v1.27.0 there is no consistency check for /kaniko binaries. +# A bug was discovered where tarballs can be constructed to unpack arbitrary files into +# /kaniko directory. As kaniko is no longer a single-binary application, +# every RUN runs via tini, this means that that arbitrary payload will be executed. +# But actually it is way simpler than that, as shown here the same can be achieved with ONBUILD. +# This sounds bad, but is actually by design. It is not less secure than having an `ONBUILD` per-se. +# Docker build is RCE as a service, that is the reason why kaniko does run without privileges. +ARG IMAGE_REPO +FROM busybox AS base +# We simulate command injection by hijacking the tini binary +ONBUILD COPY --chmod=755 < Date: Fri, 13 Mar 2026 23:35:35 +0000 Subject: [PATCH 08/17] mz560: block hijacking via COPY --- pkg/commands/copy.go | 4 ++++ pkg/util/fs_util.go | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/pkg/commands/copy.go b/pkg/commands/copy.go index e3e392951..3648d2924 100644 --- a/pkg/commands/copy.go +++ b/pkg/commands/copy.go @@ -150,6 +150,10 @@ func (c *CopyCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bu if err != nil { return fmt.Errorf("find destination path: %w", err) } + if util.CheckIgnoreList(destPath) { + logrus.Debugf("Skipping copy for ignored path: %s", destPath) + return nil + } srcFile := strings.NewReader(src.Data) err = util.CreateFile(destPath, srcFile, chmod, uint32(uid), uint32(gid)) diff --git a/pkg/util/fs_util.go b/pkg/util/fs_util.go index 0354a9bff..30a2890b8 100644 --- a/pkg/util/fs_util.go +++ b/pkg/util/fs_util.go @@ -775,6 +775,10 @@ func CopySymlink(src, dest string, context FileContext) (bool, error) { logrus.Debugf("%s found in .dockerignore, ignoring", src) return true, nil } + if CheckIgnoreList(dest) { + logrus.Debugf("Skipping copy for ignored path: %s", dest) + return true, nil + } if FilepathExists(dest) { if err := os.RemoveAll(dest); err != nil { return false, err @@ -796,6 +800,10 @@ func CopyFile(src, dest string, context FileContext, uid, gid int64, chmod fs.Fi logrus.Debugf("%s found in .dockerignore, ignoring", src) return true, nil } + if CheckIgnoreList(dest) { + logrus.Debugf("Skipping copy for ignored path: %s", dest) + return true, nil + } if src == dest { // This is a no-op. Move on, but don't list it as ignored. // We have to make sure we do this so we don't overwrite our own file. From 00b2d4b54271021db84a39c3175800ec53d08230 Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Sun, 15 Mar 2026 10:53:52 +0000 Subject: [PATCH 09/17] mz560: integration test for hijacking --- .../dockerfiles/Dockerfile_test_issue_mz560 | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/integration/dockerfiles/Dockerfile_test_issue_mz560 b/integration/dockerfiles/Dockerfile_test_issue_mz560 index 85a6fcf6b..de3ac66cb 100644 --- a/integration/dockerfiles/Dockerfile_test_issue_mz560 +++ b/integration/dockerfiles/Dockerfile_test_issue_mz560 @@ -8,12 +8,33 @@ ARG IMAGE_REPO FROM busybox AS base # We simulate command injection by hijacking the tini binary + +# First we hijack it by adding a hijacked binary to this base image. +# This base image gets built with docker, so it is part of the image. +# When kaniko unrolls the filesystem it might already override the binary with our hijacked version. +COPY --chmod=755 < /kaniko/tini \ + && echo "echo HIJACKED" >> /kaniko/tini \ + && echo "exit 1" >> /kaniko/tini \ + && chmod +x /kaniko/tini + FROM ${IMAGE_REPO}hijack:latest # /kaniko/tini gets implicitly executed here RUN ls -la /kaniko || true From 7c9357cd160514c2ddd0ccff838d9b4c2149c187 Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Sun, 15 Mar 2026 11:33:11 +0000 Subject: [PATCH 10/17] mz560: assert that kanikodir wasn't touched --- pkg/executor/build.go | 20 ++++++++++++++++++++ pkg/filesystem/resolve.go | 15 +++++++-------- pkg/filesystem/resolve_test.go | 25 +++++++++++++------------ pkg/snapshot/snapshot.go | 34 +++++++++++++++++----------------- pkg/snapshot/snapshot_test.go | 10 +++++----- pkg/util/fs_util.go | 6 +++--- pkg/util/fs_util_test.go | 2 +- 7 files changed, 66 insertions(+), 46 deletions(-) diff --git a/pkg/executor/build.go b/pkg/executor/build.go index a604e4e7f..2865a87db 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -338,6 +338,8 @@ func (s *stageBuilder) build(compositeKey CompositeCache, opts *config.KanikoOpt timing.DefaultRun.Stop(t) initSnapshotTaken = true } + kanikoDirSnapshotter := snapshot.NewSnapshotter(snapshot.NewLayeredMap(util.Hasher()), config.KanikoDir, nil) + kanikoDirSnapshotter.Init() cacheGroup := errgroup.Group{} for index, command := range s.cmds { @@ -381,9 +383,27 @@ func (s *stageBuilder) build(compositeKey CompositeCache, opts *config.KanikoOpt initSnapshotTaken = true } + if !command.ProvidesFilesToSnapshot() { + _, _, err = kanikoDirSnapshotter.ScanFullFilesystem() + if err != nil { + return err + } + } + if err := command.ExecuteCommand(&s.cf.Config, s.args); err != nil { return fmt.Errorf("failed to execute command: %w", err) } + + if !command.ProvidesFilesToSnapshot() { + add, del, err := kanikoDirSnapshotter.ScanFullFilesystem() + if err != nil { + return err + } + if len(add) > 0 || len(del) > 0 { + logrus.Fatalf("We noticed a diff in the KanikoDir (%s), this could be indicative of a hijacking attempt", config.KanikoDir) + } + } + files = command.FilesToSnapshot() timing.DefaultRun.Stop(t) diff --git a/pkg/filesystem/resolve.go b/pkg/filesystem/resolve.go index 654223962..956b61239 100644 --- a/pkg/filesystem/resolve.go +++ b/pkg/filesystem/resolve.go @@ -22,7 +22,6 @@ import ( "os" "path/filepath" - "github.com/osscontainertools/kaniko/pkg/config" "github.com/osscontainertools/kaniko/pkg/util" "github.com/sirupsen/logrus" ) @@ -35,7 +34,7 @@ import ( // * If path is a symlink, resolve it's target. If the target is not ignored add it to the // output set. // * Add all ancestors of each path to the output set. -func ResolvePaths(paths []string, wl []util.IgnoreListEntry) (pathsToAdd []string, err error) { +func ResolvePaths(root string, paths []string, wl []util.IgnoreListEntry) (pathsToAdd []string, err error) { logrus.Tracef("Resolving paths %s", paths) fileSet := make(map[string]bool) @@ -47,7 +46,7 @@ func ResolvePaths(paths []string, wl []util.IgnoreListEntry) (pathsToAdd []strin continue } - link, e := resolveSymlinkAncestor(f) + link, e := resolveSymlinkAncestor(root, f) if e != nil { continue } @@ -93,20 +92,20 @@ func ResolvePaths(paths []string, wl []util.IgnoreListEntry) (pathsToAdd []strin } // Also add parent directories to keep the permission of them correctly. - pathsToAdd = filesWithParentDirs(pathsToAdd) + pathsToAdd = filesWithParentDirs(root, pathsToAdd) return } // filesWithParentDirs returns every ancestor path for each provided file path. // I.E. /foo/bar/baz/boom.txt => [/, /foo, /foo/bar, /foo/bar/baz, /foo/bar/baz/boom.txt] -func filesWithParentDirs(files []string) []string { +func filesWithParentDirs(root string, files []string) []string { filesSet := map[string]bool{} for _, file := range files { file = filepath.Clean(file) filesSet[file] = true - for _, dir := range util.ParentDirectories(file) { + for _, dir := range util.ParentDirectories(root, file) { dir = filepath.Clean(dir) filesSet[dir] = true } @@ -125,7 +124,7 @@ func filesWithParentDirs(files []string) []string { // E.G /baz/boom/bar.txt links to /usr/bin/bar.txt but /baz/boom/bar.txt itself is not a link. // Instead /bar/boom is actually a link to /usr/bin. In this case resolveSymlinkAncestor would // return /bar/boom. -func resolveSymlinkAncestor(path string) (string, error) { +func resolveSymlinkAncestor(root string, path string) (string, error) { if !filepath.IsAbs(path) { return "", errors.New("dest path must be abs") } @@ -134,7 +133,7 @@ func resolveSymlinkAncestor(path string) (string, error) { newPath := filepath.Clean(path) loop: - for newPath != config.RootDir { + for newPath != root { fi, err := os.Lstat(newPath) if err != nil { return "", fmt.Errorf("resolvePaths: failed to lstat: %w", err) diff --git a/pkg/filesystem/resolve_test.go b/pkg/filesystem/resolve_test.go index 5a8698a73..8f80a9c8c 100644 --- a/pkg/filesystem/resolve_test.go +++ b/pkg/filesystem/resolve_test.go @@ -24,6 +24,7 @@ import ( "sort" "testing" + "github.com/osscontainertools/kaniko/pkg/config" "github.com/osscontainertools/kaniko/pkg/util" ) @@ -94,9 +95,9 @@ func Test_ResolvePaths(t *testing.T) { expectedFiles = append(expectedFiles, target) } - expectedFiles = filesWithParentDirs(expectedFiles) + expectedFiles = filesWithParentDirs(config.RootDir, expectedFiles) - files, err := ResolvePaths(inputFiles, wl) + files, err := ResolvePaths(config.RootDir, inputFiles, wl) validateResults(t, files, expectedFiles, err) }) @@ -158,9 +159,9 @@ func Test_ResolvePaths(t *testing.T) { targetFile := filepath.Join(target, "meow.txt") expectedFiles = append(expectedFiles, targetFile) - expectedFiles = filesWithParentDirs(expectedFiles) + expectedFiles = filesWithParentDirs(config.RootDir, expectedFiles) - files, err := ResolvePaths(inputFiles, wl) + files, err := ResolvePaths(config.RootDir, inputFiles, wl) validateResults(t, files, expectedFiles, err) }) @@ -173,7 +174,7 @@ func Test_ResolvePaths(t *testing.T) { wl := []util.IgnoreListEntry{} - files, err := ResolvePaths(inputFiles, wl) + files, err := ResolvePaths(config.RootDir, inputFiles, wl) validateResults(t, files, expectedFiles, err) }) @@ -216,7 +217,7 @@ func Test_resolveSymlinkAncestor(t *testing.T) { expected := linkPath - actual, err := resolveSymlinkAncestor(linkPath) + actual, err := resolveSymlinkAncestor(config.RootDir, linkPath) if err != nil { t.Errorf("expected err to be nil but was %s", err) } @@ -237,7 +238,7 @@ func Test_resolveSymlinkAncestor(t *testing.T) { expected := linkDir - actual, err := resolveSymlinkAncestor(fmt.Sprintf("%s/", linkDir)) + actual, err := resolveSymlinkAncestor(config.RootDir, fmt.Sprintf("%s/", linkDir)) if err != nil { t.Errorf("expected err to be nil but was %s", err) } @@ -269,7 +270,7 @@ func Test_resolveSymlinkAncestor(t *testing.T) { expected := linkPath - actual, err := resolveSymlinkAncestor(linkPath) + actual, err := resolveSymlinkAncestor(config.RootDir, linkPath) if err != nil { t.Errorf("expected err to be nil but was %s", err) } @@ -285,7 +286,7 @@ func Test_resolveSymlinkAncestor(t *testing.T) { expected := targetPath - actual, err := resolveSymlinkAncestor(targetPath) + actual, err := resolveSymlinkAncestor(config.RootDir, targetPath) if err != nil { t.Errorf("expected err to be nil but was %s", err) } @@ -317,7 +318,7 @@ func Test_resolveSymlinkAncestor(t *testing.T) { expected := linkDir - actual, err := resolveSymlinkAncestor(linkPath) + actual, err := resolveSymlinkAncestor(config.RootDir, linkPath) if err != nil { t.Errorf("expected err to be nil but was %s", err) } @@ -351,7 +352,7 @@ func Test_resolveSymlinkAncestor(t *testing.T) { linkPath := filepath.Join(linkDir, filepath.Base(targetPath)) - _, err := resolveSymlinkAncestor(linkPath) + _, err := resolveSymlinkAncestor(config.RootDir, linkPath) if err == nil { t.Error("expected err to not be nil") } @@ -379,7 +380,7 @@ func Test_resolveSymlinkAncestor(t *testing.T) { expected := linkDir - actual, err := resolveSymlinkAncestor(linkPath) + actual, err := resolveSymlinkAncestor(config.RootDir, linkPath) if err != nil { t.Errorf("expected err to be nil but was %s", err) } diff --git a/pkg/snapshot/snapshot.go b/pkg/snapshot/snapshot.go index 3d9f095ae..7fa2634cc 100644 --- a/pkg/snapshot/snapshot.go +++ b/pkg/snapshot/snapshot.go @@ -51,7 +51,7 @@ func NewSnapshotter(l *LayeredMap, d string, wl []util.IgnoreListEntry) *Snapsho // Init initializes a new snapshotter func (s *Snapshotter) Init() error { logrus.Info("Initializing snapshotter ...") - _, _, err := s.scanFullFilesystem() + _, _, err := s.ScanFullFilesystem() return err } @@ -75,7 +75,7 @@ func (s *Snapshotter) TakeSnapshot(files []string, shdCheckDelete bool) (string, s.l.Snapshot() - filesToAdd, err := filesystem.ResolvePaths(files, s.ignorelist) + filesToAdd, err := filesystem.ResolvePaths(s.directory, files, s.ignorelist) if err != nil { return "", err } @@ -116,7 +116,7 @@ func (s *Snapshotter) TakeSnapshot(files []string, shdCheckDelete bool) (string, t := util.NewTar(f) defer t.Close() - if err := writeToTar(t, filesToAdd, filesToWhiteout); err != nil { + if err := writeToTar(t, s.directory, filesToAdd, filesToWhiteout); err != nil { return "", err } return f.Name(), nil @@ -138,12 +138,12 @@ func (s *Snapshotter) TakeSnapshotFS() (string, error) { t := util.NewTar(f) defer t.Close() - filesToAdd, filesToWhiteOut, err := s.scanFullFilesystem() + filesToAdd, filesToWhiteOut, err := s.ScanFullFilesystem() if err != nil { return "", err } - if err := writeToTar(t, filesToAdd, filesToWhiteOut); err != nil { + if err := writeToTar(t, s.directory, filesToAdd, filesToWhiteOut); err != nil { return "", err } return f.Name(), nil @@ -156,7 +156,7 @@ func (s *Snapshotter) getSnashotPathPrefix() string { return snapshotPathPrefix } -func (s *Snapshotter) scanFullFilesystem() ([]string, []string, error) { +func (s *Snapshotter) ScanFullFilesystem() ([]string, []string, error) { logrus.Info("Taking snapshot of full filesystem...") // Some of the operations that follow (e.g. hashing) depend on the file system being synced, @@ -194,7 +194,7 @@ func (s *Snapshotter) scanFullFilesystem() ([]string, []string, error) { } timer := timing.Start("Resolving Paths") - filesToAdd, err := filesystem.ResolvePaths(changedPaths, s.ignorelist) + filesToAdd, err := filesystem.ResolvePaths(s.directory, changedPaths, s.ignorelist) if err != nil { return nil, nil, err } @@ -236,16 +236,16 @@ func removeObsoleteWhiteouts(deletedFiles map[string]struct{}) (filesToWhiteout return filesToWhiteout } -func writeToTar(t util.Tar, files, whiteouts []string) error { +func writeToTar(t util.Tar, root string, files, whiteouts []string) error { timer := timing.Start("Writing tar file") defer timing.DefaultRun.Stop(timer) // Now create the tar. addedPaths := make(map[string]bool) - addedPaths[config.RootDir] = true + addedPaths[root] = true for _, path := range whiteouts { - skipWhiteout, err := parentPathIncludesNonDirectory(path) + skipWhiteout, err := parentPathIncludesNonDirectory(root, path) if err != nil { return err } @@ -253,7 +253,7 @@ func writeToTar(t util.Tar, files, whiteouts []string) error { continue } - if err := addParentDirectories(t, addedPaths, path); err != nil { + if err := addParentDirectories(t, root, addedPaths, path); err != nil { return err } if err := t.Whiteout(path); err != nil { @@ -262,7 +262,7 @@ func writeToTar(t util.Tar, files, whiteouts []string) error { } for _, path := range files { - if err := addParentDirectories(t, addedPaths, path); err != nil { + if err := addParentDirectories(t, root, addedPaths, path); err != nil { return err } if _, pathAdded := addedPaths[path]; pathAdded { @@ -277,8 +277,8 @@ func writeToTar(t util.Tar, files, whiteouts []string) error { } // Returns true if a parent of the given path has been replaced with anything other than a directory -func parentPathIncludesNonDirectory(path string) (bool, error) { - for _, parentPath := range util.ParentDirectories(path) { +func parentPathIncludesNonDirectory(root, path string) (bool, error) { + for _, parentPath := range util.ParentDirectories(root, path) { lstat, err := os.Lstat(parentPath) if err != nil { return false, err @@ -291,9 +291,9 @@ func parentPathIncludesNonDirectory(path string) (bool, error) { return false, nil } -func addParentDirectories(t util.Tar, addedPaths map[string]bool, path string) error { - for _, parentPath := range util.ParentDirectories(path) { - if parentPath == config.RootDir { +func addParentDirectories(t util.Tar, root string, addedPaths map[string]bool, path string) error { + for _, parentPath := range util.ParentDirectories(root, path) { + if parentPath == root { continue } if _, pathAdded := addedPaths[parentPath]; pathAdded { diff --git a/pkg/snapshot/snapshot_test.go b/pkg/snapshot/snapshot_test.go index becbd1c3f..c2f285b7e 100644 --- a/pkg/snapshot/snapshot_test.go +++ b/pkg/snapshot/snapshot_test.go @@ -66,7 +66,7 @@ func TestSnapshotFSFileChange(t *testing.T) { batPath: "baz", } for _, path := range util.ParentDirectoriesWithoutLeadingSlash(batPath) { - if path == config.RootDir { + if !strings.HasPrefix(path, testDirWithoutLeadingSlash+"/") { continue } snapshotFiles[path+"/"] = "" @@ -155,7 +155,7 @@ func TestSnapshotFSChangePermissions(t *testing.T) { batPathWithoutLeadingSlash: "baz2", } for _, path := range util.ParentDirectoriesWithoutLeadingSlash(batPathWithoutLeadingSlash) { - if path == config.RootDir { + if !strings.HasPrefix(path, testDirWithoutLeadingSlash+"/") { continue } snapshotFiles[path+"/"] = "" @@ -224,7 +224,7 @@ func TestSnapshotFSReplaceDirWithLink(t *testing.T) { filepath.Join(testDirWithoutLeadingSlash, "foo"), } for _, path := range util.ParentDirectoriesWithoutLeadingSlash(filepath.Join(testDir, "foo")) { - if path == config.RootDir { + if !strings.HasPrefix(path, testDirWithoutLeadingSlash+"/") { continue } expectedFiles = append(expectedFiles, strings.TrimRight(path, "/")+"/") @@ -262,7 +262,7 @@ func TestSnapshotFiles(t *testing.T) { filepath.Join(testDirWithoutLeadingSlash, "foo"), } for _, path := range util.ParentDirectoriesWithoutLeadingSlash(filepath.Join(testDir, "foo")) { - if path == config.RootDir { + if !strings.HasPrefix(path, testDirWithoutLeadingSlash+"/") { continue } expectedFiles = append(expectedFiles, strings.TrimRight(path, "/")+"/") @@ -460,7 +460,7 @@ func TestSnapshotIncludesParentDirBeforeWhiteoutFile(t *testing.T) { filepath.Join(testDirWithoutLeadingSlash, "kaniko/new-file"), filepath.Join(testDirWithoutLeadingSlash, ".wh.bar"), } - for parentDir := filepath.Dir(expectedFiles[0]); parentDir != "."; parentDir = filepath.Dir(parentDir) { + for parentDir := filepath.Dir(expectedFiles[0]); parentDir != "." && parentDir != testDirWithoutLeadingSlash; parentDir = filepath.Dir(parentDir) { expectedFiles = append(expectedFiles, parentDir+"/") } diff --git a/pkg/util/fs_util.go b/pkg/util/fs_util.go index 30a2890b8..3d0878ab1 100644 --- a/pkg/util/fs_util.go +++ b/pkg/util/fs_util.go @@ -513,16 +513,16 @@ func RelativeFiles(fp string, root string) ([]string, error) { // ParentDirectories returns a list of paths to all parent directories // Ex. /some/temp/dir -> [/, /some, /some/temp, /some/temp/dir] -func ParentDirectories(path string) []string { +func ParentDirectories(root string, path string) []string { dir := filepath.Clean(path) var paths []string - for !(dir == filepath.Clean(config.RootDir) || dir == "" || dir == ".") { + for !(dir == filepath.Clean(root) || dir == "" || dir == ".") { dir, _ = filepath.Split(dir) dir = filepath.Clean(dir) paths = append([]string{dir}, paths...) } if len(paths) == 0 { - paths = []string{config.RootDir} + paths = []string{root} } return paths } diff --git a/pkg/util/fs_util_test.go b/pkg/util/fs_util_test.go index 72799a466..62548dd37 100644 --- a/pkg/util/fs_util_test.go +++ b/pkg/util/fs_util_test.go @@ -221,7 +221,7 @@ func Test_ParentDirectories(t *testing.T) { original := config.RootDir defer func() { config.RootDir = original }() config.RootDir = tt.rootDir - actual := ParentDirectories(tt.path) + actual := ParentDirectories(config.RootDir, tt.path) testutil.CheckErrorAndDeepEqual(t, false, nil, tt.expected, actual) }) From 17385c2c7481e19f3dcfba66e368d9807df0ea6b Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Tue, 17 Mar 2026 21:57:03 +0000 Subject: [PATCH 11/17] tinker --- pkg/filesystem/resolve.go | 14 +++++++------- pkg/filesystem/resolve_test.go | 25 ++++++++++++------------- pkg/snapshot/snapshot.go | 30 +++++++++++++++--------------- pkg/snapshot/snapshot_test.go | 10 +++++----- pkg/util/fs_util.go | 6 +++--- pkg/util/fs_util_test.go | 2 +- 6 files changed, 43 insertions(+), 44 deletions(-) diff --git a/pkg/filesystem/resolve.go b/pkg/filesystem/resolve.go index 956b61239..be6b753ed 100644 --- a/pkg/filesystem/resolve.go +++ b/pkg/filesystem/resolve.go @@ -34,7 +34,7 @@ import ( // * If path is a symlink, resolve it's target. If the target is not ignored add it to the // output set. // * Add all ancestors of each path to the output set. -func ResolvePaths(root string, paths []string, wl []util.IgnoreListEntry) (pathsToAdd []string, err error) { +func ResolvePaths(paths []string, wl []util.IgnoreListEntry) (pathsToAdd []string, err error) { logrus.Tracef("Resolving paths %s", paths) fileSet := make(map[string]bool) @@ -46,7 +46,7 @@ func ResolvePaths(root string, paths []string, wl []util.IgnoreListEntry) (paths continue } - link, e := resolveSymlinkAncestor(root, f) + link, e := resolveSymlinkAncestor(f) if e != nil { continue } @@ -92,20 +92,20 @@ func ResolvePaths(root string, paths []string, wl []util.IgnoreListEntry) (paths } // Also add parent directories to keep the permission of them correctly. - pathsToAdd = filesWithParentDirs(root, pathsToAdd) + pathsToAdd = filesWithParentDirs(pathsToAdd) return } // filesWithParentDirs returns every ancestor path for each provided file path. // I.E. /foo/bar/baz/boom.txt => [/, /foo, /foo/bar, /foo/bar/baz, /foo/bar/baz/boom.txt] -func filesWithParentDirs(root string, files []string) []string { +func filesWithParentDirs(files []string) []string { filesSet := map[string]bool{} for _, file := range files { file = filepath.Clean(file) filesSet[file] = true - for _, dir := range util.ParentDirectories(root, file) { + for _, dir := range util.ParentDirectories(file) { dir = filepath.Clean(dir) filesSet[dir] = true } @@ -124,7 +124,7 @@ func filesWithParentDirs(root string, files []string) []string { // E.G /baz/boom/bar.txt links to /usr/bin/bar.txt but /baz/boom/bar.txt itself is not a link. // Instead /bar/boom is actually a link to /usr/bin. In this case resolveSymlinkAncestor would // return /bar/boom. -func resolveSymlinkAncestor(root string, path string) (string, error) { +func resolveSymlinkAncestor(path string) (string, error) { if !filepath.IsAbs(path) { return "", errors.New("dest path must be abs") } @@ -133,7 +133,7 @@ func resolveSymlinkAncestor(root string, path string) (string, error) { newPath := filepath.Clean(path) loop: - for newPath != root { + for newPath != config.RootDir { fi, err := os.Lstat(newPath) if err != nil { return "", fmt.Errorf("resolvePaths: failed to lstat: %w", err) diff --git a/pkg/filesystem/resolve_test.go b/pkg/filesystem/resolve_test.go index 8f80a9c8c..5a8698a73 100644 --- a/pkg/filesystem/resolve_test.go +++ b/pkg/filesystem/resolve_test.go @@ -24,7 +24,6 @@ import ( "sort" "testing" - "github.com/osscontainertools/kaniko/pkg/config" "github.com/osscontainertools/kaniko/pkg/util" ) @@ -95,9 +94,9 @@ func Test_ResolvePaths(t *testing.T) { expectedFiles = append(expectedFiles, target) } - expectedFiles = filesWithParentDirs(config.RootDir, expectedFiles) + expectedFiles = filesWithParentDirs(expectedFiles) - files, err := ResolvePaths(config.RootDir, inputFiles, wl) + files, err := ResolvePaths(inputFiles, wl) validateResults(t, files, expectedFiles, err) }) @@ -159,9 +158,9 @@ func Test_ResolvePaths(t *testing.T) { targetFile := filepath.Join(target, "meow.txt") expectedFiles = append(expectedFiles, targetFile) - expectedFiles = filesWithParentDirs(config.RootDir, expectedFiles) + expectedFiles = filesWithParentDirs(expectedFiles) - files, err := ResolvePaths(config.RootDir, inputFiles, wl) + files, err := ResolvePaths(inputFiles, wl) validateResults(t, files, expectedFiles, err) }) @@ -174,7 +173,7 @@ func Test_ResolvePaths(t *testing.T) { wl := []util.IgnoreListEntry{} - files, err := ResolvePaths(config.RootDir, inputFiles, wl) + files, err := ResolvePaths(inputFiles, wl) validateResults(t, files, expectedFiles, err) }) @@ -217,7 +216,7 @@ func Test_resolveSymlinkAncestor(t *testing.T) { expected := linkPath - actual, err := resolveSymlinkAncestor(config.RootDir, linkPath) + actual, err := resolveSymlinkAncestor(linkPath) if err != nil { t.Errorf("expected err to be nil but was %s", err) } @@ -238,7 +237,7 @@ func Test_resolveSymlinkAncestor(t *testing.T) { expected := linkDir - actual, err := resolveSymlinkAncestor(config.RootDir, fmt.Sprintf("%s/", linkDir)) + actual, err := resolveSymlinkAncestor(fmt.Sprintf("%s/", linkDir)) if err != nil { t.Errorf("expected err to be nil but was %s", err) } @@ -270,7 +269,7 @@ func Test_resolveSymlinkAncestor(t *testing.T) { expected := linkPath - actual, err := resolveSymlinkAncestor(config.RootDir, linkPath) + actual, err := resolveSymlinkAncestor(linkPath) if err != nil { t.Errorf("expected err to be nil but was %s", err) } @@ -286,7 +285,7 @@ func Test_resolveSymlinkAncestor(t *testing.T) { expected := targetPath - actual, err := resolveSymlinkAncestor(config.RootDir, targetPath) + actual, err := resolveSymlinkAncestor(targetPath) if err != nil { t.Errorf("expected err to be nil but was %s", err) } @@ -318,7 +317,7 @@ func Test_resolveSymlinkAncestor(t *testing.T) { expected := linkDir - actual, err := resolveSymlinkAncestor(config.RootDir, linkPath) + actual, err := resolveSymlinkAncestor(linkPath) if err != nil { t.Errorf("expected err to be nil but was %s", err) } @@ -352,7 +351,7 @@ func Test_resolveSymlinkAncestor(t *testing.T) { linkPath := filepath.Join(linkDir, filepath.Base(targetPath)) - _, err := resolveSymlinkAncestor(config.RootDir, linkPath) + _, err := resolveSymlinkAncestor(linkPath) if err == nil { t.Error("expected err to not be nil") } @@ -380,7 +379,7 @@ func Test_resolveSymlinkAncestor(t *testing.T) { expected := linkDir - actual, err := resolveSymlinkAncestor(config.RootDir, linkPath) + actual, err := resolveSymlinkAncestor(linkPath) if err != nil { t.Errorf("expected err to be nil but was %s", err) } diff --git a/pkg/snapshot/snapshot.go b/pkg/snapshot/snapshot.go index 7fa2634cc..c97e5e6b4 100644 --- a/pkg/snapshot/snapshot.go +++ b/pkg/snapshot/snapshot.go @@ -75,7 +75,7 @@ func (s *Snapshotter) TakeSnapshot(files []string, shdCheckDelete bool) (string, s.l.Snapshot() - filesToAdd, err := filesystem.ResolvePaths(s.directory, files, s.ignorelist) + filesToAdd, err := filesystem.ResolvePaths(files, s.ignorelist) if err != nil { return "", err } @@ -116,7 +116,7 @@ func (s *Snapshotter) TakeSnapshot(files []string, shdCheckDelete bool) (string, t := util.NewTar(f) defer t.Close() - if err := writeToTar(t, s.directory, filesToAdd, filesToWhiteout); err != nil { + if err := writeToTar(t, filesToAdd, filesToWhiteout); err != nil { return "", err } return f.Name(), nil @@ -138,12 +138,12 @@ func (s *Snapshotter) TakeSnapshotFS() (string, error) { t := util.NewTar(f) defer t.Close() - filesToAdd, filesToWhiteOut, err := s.ScanFullFilesystem() + filesToAdd, filesToWhiteOut, err := s.scanFullFilesystem() if err != nil { return "", err } - if err := writeToTar(t, s.directory, filesToAdd, filesToWhiteOut); err != nil { + if err := writeToTar(t, filesToAdd, filesToWhiteOut); err != nil { return "", err } return f.Name(), nil @@ -194,7 +194,7 @@ func (s *Snapshotter) ScanFullFilesystem() ([]string, []string, error) { } timer := timing.Start("Resolving Paths") - filesToAdd, err := filesystem.ResolvePaths(s.directory, changedPaths, s.ignorelist) + filesToAdd, err := filesystem.ResolvePaths(changedPaths, s.ignorelist) if err != nil { return nil, nil, err } @@ -236,16 +236,16 @@ func removeObsoleteWhiteouts(deletedFiles map[string]struct{}) (filesToWhiteout return filesToWhiteout } -func writeToTar(t util.Tar, root string, files, whiteouts []string) error { +func writeToTar(t util.Tar, files, whiteouts []string) error { timer := timing.Start("Writing tar file") defer timing.DefaultRun.Stop(timer) // Now create the tar. addedPaths := make(map[string]bool) - addedPaths[root] = true + addedPaths[config.RootDir] = true for _, path := range whiteouts { - skipWhiteout, err := parentPathIncludesNonDirectory(root, path) + skipWhiteout, err := parentPathIncludesNonDirectory(path) if err != nil { return err } @@ -253,7 +253,7 @@ func writeToTar(t util.Tar, root string, files, whiteouts []string) error { continue } - if err := addParentDirectories(t, root, addedPaths, path); err != nil { + if err := addParentDirectories(t, addedPaths, path); err != nil { return err } if err := t.Whiteout(path); err != nil { @@ -262,7 +262,7 @@ func writeToTar(t util.Tar, root string, files, whiteouts []string) error { } for _, path := range files { - if err := addParentDirectories(t, root, addedPaths, path); err != nil { + if err := addParentDirectories(t, addedPaths, path); err != nil { return err } if _, pathAdded := addedPaths[path]; pathAdded { @@ -277,8 +277,8 @@ func writeToTar(t util.Tar, root string, files, whiteouts []string) error { } // Returns true if a parent of the given path has been replaced with anything other than a directory -func parentPathIncludesNonDirectory(root, path string) (bool, error) { - for _, parentPath := range util.ParentDirectories(root, path) { +func parentPathIncludesNonDirectory(path string) (bool, error) { + for _, parentPath := range util.ParentDirectories(path) { lstat, err := os.Lstat(parentPath) if err != nil { return false, err @@ -291,9 +291,9 @@ func parentPathIncludesNonDirectory(root, path string) (bool, error) { return false, nil } -func addParentDirectories(t util.Tar, root string, addedPaths map[string]bool, path string) error { - for _, parentPath := range util.ParentDirectories(root, path) { - if parentPath == root { +func addParentDirectories(t util.Tar, addedPaths map[string]bool, path string) error { + for _, parentPath := range util.ParentDirectories(path) { + if parentPath == config.RootDir { continue } if _, pathAdded := addedPaths[parentPath]; pathAdded { diff --git a/pkg/snapshot/snapshot_test.go b/pkg/snapshot/snapshot_test.go index c2f285b7e..becbd1c3f 100644 --- a/pkg/snapshot/snapshot_test.go +++ b/pkg/snapshot/snapshot_test.go @@ -66,7 +66,7 @@ func TestSnapshotFSFileChange(t *testing.T) { batPath: "baz", } for _, path := range util.ParentDirectoriesWithoutLeadingSlash(batPath) { - if !strings.HasPrefix(path, testDirWithoutLeadingSlash+"/") { + if path == config.RootDir { continue } snapshotFiles[path+"/"] = "" @@ -155,7 +155,7 @@ func TestSnapshotFSChangePermissions(t *testing.T) { batPathWithoutLeadingSlash: "baz2", } for _, path := range util.ParentDirectoriesWithoutLeadingSlash(batPathWithoutLeadingSlash) { - if !strings.HasPrefix(path, testDirWithoutLeadingSlash+"/") { + if path == config.RootDir { continue } snapshotFiles[path+"/"] = "" @@ -224,7 +224,7 @@ func TestSnapshotFSReplaceDirWithLink(t *testing.T) { filepath.Join(testDirWithoutLeadingSlash, "foo"), } for _, path := range util.ParentDirectoriesWithoutLeadingSlash(filepath.Join(testDir, "foo")) { - if !strings.HasPrefix(path, testDirWithoutLeadingSlash+"/") { + if path == config.RootDir { continue } expectedFiles = append(expectedFiles, strings.TrimRight(path, "/")+"/") @@ -262,7 +262,7 @@ func TestSnapshotFiles(t *testing.T) { filepath.Join(testDirWithoutLeadingSlash, "foo"), } for _, path := range util.ParentDirectoriesWithoutLeadingSlash(filepath.Join(testDir, "foo")) { - if !strings.HasPrefix(path, testDirWithoutLeadingSlash+"/") { + if path == config.RootDir { continue } expectedFiles = append(expectedFiles, strings.TrimRight(path, "/")+"/") @@ -460,7 +460,7 @@ func TestSnapshotIncludesParentDirBeforeWhiteoutFile(t *testing.T) { filepath.Join(testDirWithoutLeadingSlash, "kaniko/new-file"), filepath.Join(testDirWithoutLeadingSlash, ".wh.bar"), } - for parentDir := filepath.Dir(expectedFiles[0]); parentDir != "." && parentDir != testDirWithoutLeadingSlash; parentDir = filepath.Dir(parentDir) { + for parentDir := filepath.Dir(expectedFiles[0]); parentDir != "."; parentDir = filepath.Dir(parentDir) { expectedFiles = append(expectedFiles, parentDir+"/") } diff --git a/pkg/util/fs_util.go b/pkg/util/fs_util.go index 3d0878ab1..30a2890b8 100644 --- a/pkg/util/fs_util.go +++ b/pkg/util/fs_util.go @@ -513,16 +513,16 @@ func RelativeFiles(fp string, root string) ([]string, error) { // ParentDirectories returns a list of paths to all parent directories // Ex. /some/temp/dir -> [/, /some, /some/temp, /some/temp/dir] -func ParentDirectories(root string, path string) []string { +func ParentDirectories(path string) []string { dir := filepath.Clean(path) var paths []string - for !(dir == filepath.Clean(root) || dir == "" || dir == ".") { + for !(dir == filepath.Clean(config.RootDir) || dir == "" || dir == ".") { dir, _ = filepath.Split(dir) dir = filepath.Clean(dir) paths = append([]string{dir}, paths...) } if len(paths) == 0 { - paths = []string{root} + paths = []string{config.RootDir} } return paths } diff --git a/pkg/util/fs_util_test.go b/pkg/util/fs_util_test.go index 62548dd37..72799a466 100644 --- a/pkg/util/fs_util_test.go +++ b/pkg/util/fs_util_test.go @@ -221,7 +221,7 @@ func Test_ParentDirectories(t *testing.T) { original := config.RootDir defer func() { config.RootDir = original }() config.RootDir = tt.rootDir - actual := ParentDirectories(config.RootDir, tt.path) + actual := ParentDirectories(tt.path) testutil.CheckErrorAndDeepEqual(t, false, nil, tt.expected, actual) }) From 76211078f62147aea034c3a156549b3283bb005e Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Tue, 17 Mar 2026 21:59:50 +0000 Subject: [PATCH 12/17] fixup integration tests --- integration/dockerfiles/Dockerfile_test_issue_mz560 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/dockerfiles/Dockerfile_test_issue_mz560 b/integration/dockerfiles/Dockerfile_test_issue_mz560 index de3ac66cb..2393be4aa 100644 --- a/integration/dockerfiles/Dockerfile_test_issue_mz560 +++ b/integration/dockerfiles/Dockerfile_test_issue_mz560 @@ -29,7 +29,7 @@ tini # Thirdly, we can use a `ONBUILD RUN` statement directly # here kaniko has no chance to block us as RUN statements # are per design an all powerful blackbox. -ONBUILD RUN mv /kaniko/tini /dev/null \ +ONBUILD RUN mv /kaniko/tini /dev/null || true \ && echo "#!/bin/sh" > /kaniko/tini \ && echo "echo HIJACKED" >> /kaniko/tini \ && echo "exit 1" >> /kaniko/tini \ @@ -37,4 +37,4 @@ ONBUILD RUN mv /kaniko/tini /dev/null \ FROM ${IMAGE_REPO}hijack:latest # /kaniko/tini gets implicitly executed here -RUN ls -la /kaniko || true +RUN ls -la /kaniko && cat /kaniko/tini || true From 3f8da70a7c731b2f2c299fe9a8b3842369b20637 Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Tue, 17 Mar 2026 22:01:03 +0000 Subject: [PATCH 13/17] fixup tinker --- pkg/filesystem/resolve.go | 1 + pkg/snapshot/snapshot.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/filesystem/resolve.go b/pkg/filesystem/resolve.go index be6b753ed..654223962 100644 --- a/pkg/filesystem/resolve.go +++ b/pkg/filesystem/resolve.go @@ -22,6 +22,7 @@ import ( "os" "path/filepath" + "github.com/osscontainertools/kaniko/pkg/config" "github.com/osscontainertools/kaniko/pkg/util" "github.com/sirupsen/logrus" ) diff --git a/pkg/snapshot/snapshot.go b/pkg/snapshot/snapshot.go index c97e5e6b4..baa43f3a6 100644 --- a/pkg/snapshot/snapshot.go +++ b/pkg/snapshot/snapshot.go @@ -138,7 +138,7 @@ func (s *Snapshotter) TakeSnapshotFS() (string, error) { t := util.NewTar(f) defer t.Close() - filesToAdd, filesToWhiteOut, err := s.scanFullFilesystem() + filesToAdd, filesToWhiteOut, err := s.ScanFullFilesystem() if err != nil { return "", err } From f907887cb30db18a3fd30a4e0ce9306a4304b37b Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Tue, 17 Mar 2026 22:08:02 +0000 Subject: [PATCH 14/17] reduce verbosity --- pkg/snapshot/snapshot.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/snapshot/snapshot.go b/pkg/snapshot/snapshot.go index baa43f3a6..f22d4b99a 100644 --- a/pkg/snapshot/snapshot.go +++ b/pkg/snapshot/snapshot.go @@ -50,7 +50,7 @@ func NewSnapshotter(l *LayeredMap, d string, wl []util.IgnoreListEntry) *Snapsho // Init initializes a new snapshotter func (s *Snapshotter) Init() error { - logrus.Info("Initializing snapshotter ...") + logrus.Debug("Initializing snapshotter ...") _, _, err := s.ScanFullFilesystem() return err } @@ -157,7 +157,7 @@ func (s *Snapshotter) getSnashotPathPrefix() string { } func (s *Snapshotter) ScanFullFilesystem() ([]string, []string, error) { - logrus.Info("Taking snapshot of full filesystem...") + logrus.Debugf("Taking snapshot of %s filesystem...", s.directory) // Some of the operations that follow (e.g. hashing) depend on the file system being synced, // for example the hashing function that determines if files are equal uses the mtime of the files, From a35d88fa94da80598bdc539cbbfb7a5d8da18c37 Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Tue, 17 Mar 2026 22:28:35 +0000 Subject: [PATCH 15/17] mz560: expect error --- .../dockerfiles/Dockerfile_test_issue_mz560 | 9 +++----- integration/images.go | 21 ++++++++++++++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/integration/dockerfiles/Dockerfile_test_issue_mz560 b/integration/dockerfiles/Dockerfile_test_issue_mz560 index 2393be4aa..56fffe3bb 100644 --- a/integration/dockerfiles/Dockerfile_test_issue_mz560 +++ b/integration/dockerfiles/Dockerfile_test_issue_mz560 @@ -14,16 +14,14 @@ FROM busybox AS base # When kaniko unrolls the filesystem it might already override the binary with our hijacked version. COPY --chmod=755 < /kaniko/tini \ - && echo "echo HIJACKED" >> /kaniko/tini \ - && echo "exit 1" >> /kaniko/tini \ + && echo "echo 'WARN HIJACKED'" >> /kaniko/tini \ && chmod +x /kaniko/tini FROM ${IMAGE_REPO}hijack:latest diff --git a/integration/images.go b/integration/images.go index 800a59ee1..f30280cde 100644 --- a/integration/images.go +++ b/integration/images.go @@ -19,6 +19,7 @@ package integration import ( "bytes" "context" + "errors" "fmt" "os" "os/exec" @@ -140,6 +141,10 @@ var additionalKanikoFlagsMap = map[string][]string{ "Dockerfile_test_issue_mz529": {"--cleanup"}, } +var expectErr = map[string]int{ + "Dockerfile_test_issue_mz560": 1, +} + // Arguments to diffoci when comparing dockerfiles var diffArgsMap = map[string][]string{ // /root/.config 0x1c0 0x1ed @@ -445,11 +450,21 @@ func (d *DockerFileBuilder) BuildImageWithContext(t *testing.T, config *integrat kanikoImage := GetKanikoImage(imageRepo, dockerfile) timer = timing.Start(dockerfile + "_kaniko") - if _, err := buildKanikoImage(t.Logf, dockerfilesPath, dockerfile, buildArgs, additionalKanikoFlags, kanikoImage, - contextDir, gcsBucket, gcsClient, serviceAccount, true); err != nil { + defer timing.DefaultRun.Stop(timer) + _, err := buildKanikoImage(t.Logf, dockerfilesPath, dockerfile, buildArgs, additionalKanikoFlags, kanikoImage, + contextDir, gcsBucket, gcsClient, serviceAccount, true) + if expectErr, ok := expectErr[dockerfile]; ok { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) && exitErr.ExitCode() == expectErr { + return nil + } + if err == nil { + return fmt.Errorf("expected exit code %d but command succeeded", expectErr) + } + } + if err != nil { return err } - timing.DefaultRun.Stop(timer) d.filesBuilt[dockerfile] = struct{}{} From 0ec70d336f9f40b5eb5e00c7a7dc71c82a9c4fb4 Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Tue, 17 Mar 2026 23:20:31 +0000 Subject: [PATCH 16/17] fixup build protection --- pkg/executor/build.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/executor/build.go b/pkg/executor/build.go index 2865a87db..099febc27 100644 --- a/pkg/executor/build.go +++ b/pkg/executor/build.go @@ -338,7 +338,12 @@ func (s *stageBuilder) build(compositeKey CompositeCache, opts *config.KanikoOpt timing.DefaultRun.Stop(t) initSnapshotTaken = true } - kanikoDirSnapshotter := snapshot.NewSnapshotter(snapshot.NewLayeredMap(util.Hasher()), config.KanikoDir, nil) + kanikoDirSnapshotter := snapshot.NewSnapshotter(snapshot.NewLayeredMap(util.Hasher()), config.KanikoDir, []util.IgnoreListEntry{ + { + Path: config.KanikoCacheDir, + PrefixMatchOnly: true, + }, + }) kanikoDirSnapshotter.Init() cacheGroup := errgroup.Group{} From b70a691c9c40d4a0a913070bee986b5a6725c517 Mon Sep 17 00:00:00 2001 From: Martin Zihlmann Date: Tue, 17 Mar 2026 23:21:16 +0000 Subject: [PATCH 17/17] assert mount paths --- .../dockerfiles/Dockerfile_test_issue_mz560 | 9 +++++++++ pkg/commands/run.go | 14 ++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/integration/dockerfiles/Dockerfile_test_issue_mz560 b/integration/dockerfiles/Dockerfile_test_issue_mz560 index 56fffe3bb..267634085 100644 --- a/integration/dockerfiles/Dockerfile_test_issue_mz560 +++ b/integration/dockerfiles/Dockerfile_test_issue_mz560 @@ -24,6 +24,15 @@ ONBUILD COPY --chmod=755 < /tmp/kaniko/tini \ + && echo "echo 'WARN HIJACKED'" >> /tmp/kaniko/tini \ + && chmod +x /tmp/kaniko/tini + +ONBUILD RUN --mount=type=cache,id=hijack,target=/kaniko \ + ls -la /kaniko && cat /kaniko/tini || true + # Thirdly, we can use a `ONBUILD RUN` statement directly # here kaniko has no chance to block us as RUN statements # are per design an all powerful blackbox. diff --git a/pkg/commands/run.go b/pkg/commands/run.go index b00951706..0c8d92854 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -77,6 +77,7 @@ func runCommandWithFlags(config *v1.Config, buildArgs *dockerfile.BuildArgs, cmd switch { // https://docs.docker.com/reference/dockerfile/#run---mounttypecache case m.Type == instructions.MountTypeCache: + assertProtectedKanikoDir(m.Target) cacheId := m.CacheID if cacheId == "" { cacheId = filepath.Clean(m.Target) @@ -177,6 +178,7 @@ func runCommandWithFlags(config *v1.Config, buildArgs *dockerfile.BuildArgs, cmd if target == "" { target = fmt.Sprintf("/run/secrets/%s", secretId) } + assertProtectedKanikoDir(target) parent := filepath.Dir(target) created, err := ensureDir(parent) if err != nil { @@ -514,6 +516,18 @@ func ensureDir(target string) (string, error) { return firstCreated, nil } +func assertProtectedKanikoDir(target string) { + wl := []util.IgnoreListEntry{ + { + Path: kConfig.KanikoDir, + PrefixMatchOnly: false, + }, + } + if util.CheckCleanedPathAgainstProvidedIgnoreList(target, wl) { + logrus.Fatalf("mount option targetting protected KanikoDir (%s), this could be indicative of a hijacking attempt", kConfig.KanikoDir) + } +} + func assignIfNil(dst *error, fn func() error) { if err := fn(); err != nil && *dst == nil { *dst = err