From 7b69e94cd3587918e9ebd13148392ba51dcda91a Mon Sep 17 00:00:00 2001 From: Sueun Cho Date: Tue, 18 Aug 2026 12:59:55 +0900 Subject: [PATCH] context: follow symlink roots during Walk Context.Walk currently replaces a symlink root with the raw os.Readlink result before calling filepath.Walk. That only works when the symlink target is absolute. For a relative symlink, filepath.Walk starts from the process working directory; for a symlink-to-symlink root, it walks the intermediate link instead of the referenced directory. Use the context root path with a trailing separator when the root itself is a symlink, so filepath.Walk enters the target directory while the reported paths remain relative to the context root. Add a regression test for absolute symlink, relative symlink, and symlink-to-symlink roots. Signed-off-by: Sueun Cho --- context.go | 5 +-- context_unix_test.go | 101 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 context_unix_test.go diff --git a/context.go b/context.go index 8339c44..48d013c 100644 --- a/context.go +++ b/context.go @@ -587,10 +587,7 @@ func (c *context) Walk(fn filepath.WalkFunc) error { root := c.root fi, err := c.driver.Lstat(c.root) if err == nil && fi.Mode()&os.ModeSymlink != 0 { - root, err = c.driver.Readlink(c.root) - if err != nil { - return err - } + root += string(c.pathDriver.Separator()) } return c.pathDriver.Walk(root, func(p string, fi os.FileInfo, _ error) error { contained, err := c.containWithRoot(p, root) diff --git a/context_unix_test.go b/context_unix_test.go new file mode 100644 index 0000000..28024e9 --- /dev/null +++ b/context_unix_test.go @@ -0,0 +1,101 @@ +//go:build !windows + +/* + 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 continuity + +import ( + "fmt" + "os" + "path/filepath" + "reflect" + "testing" +) + +func TestContextWalkSymlinkRoot(t *testing.T) { + root := t.TempDir() + + target := filepath.Join(root, "target") + if err := os.MkdirAll(filepath.Join(target, "subdir"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(target, "subdir", "file"), []byte("content"), 0o644); err != nil { + t.Fatal(err) + } + + links := filepath.Join(root, "links") + if err := os.Mkdir(links, 0o755); err != nil { + t.Fatal(err) + } + link := filepath.Join(links, "root") + if err := os.Symlink("../target", link); err != nil { + t.Skipf("symlink not supported: %v", err) + } + absoluteLink := filepath.Join(links, "absolute-root") + if err := os.Symlink(target, absoluteLink); err != nil { + t.Skipf("absolute symlink not supported: %v", err) + } + chain := filepath.Join(links, "chain") + if err := os.Symlink("root", chain); err != nil { + t.Skipf("symlink-to-symlink not supported: %v", err) + } + + for _, tc := range []struct { + name string + root string + }{ + { + name: "relative_symlink", + root: link, + }, + { + name: "absolute_symlink", + root: absoluteLink, + }, + { + name: "symlink_to_symlink", + root: chain, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx, err := NewContext(tc.root) + if err != nil { + t.Fatal(err) + } + + var got []string + err = ctx.Walk(func(p string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + if fi == nil { + return fmt.Errorf("missing file info for %q", p) + } + got = append(got, filepath.ToSlash(p)) + return nil + }) + if err != nil { + t.Fatal(err) + } + + want := []string{"/", "/subdir", "/subdir/file"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("paths mismatch\nwant: %v\n got: %v", want, got) + } + }) + } +}