Merge pull request #12732 from pauloappbr/fix/12683-go124-symlink-resolution

fix(oci): handle absolute symlinks in rootfs user lookup
This commit is contained in:
Samuel Karp
2026-01-14 00:28:45 +00:00
committed by GitHub
2 changed files with 107 additions and 4 deletions

View File

@@ -1152,7 +1152,7 @@ func UserFromPath(root string, filter func(user.User) bool) (user.User, error) {
// UserFromFS inspects the user object using /etc/passwd in the specified fs.FS.
// filter can be nil.
func UserFromFS(root fs.FS, filter func(user.User) bool) (user.User, error) {
f, err := root.Open("etc/passwd")
f, err := openUserFile(root, "etc/passwd")
if err != nil {
return user.User{}, err
}
@@ -1184,7 +1184,7 @@ func GIDFromPath(root string, filter func(user.Group) bool) (gid uint32, err err
// GIDFromFS inspects the GID using /etc/group in the specified fs.FS.
// filter can be nil.
func GIDFromFS(root fs.FS, filter func(user.Group) bool) (gid uint32, err error) {
f, err := root.Open("etc/group")
f, err := openUserFile(root, "etc/group")
if err != nil {
return 0, err
}
@@ -1816,3 +1816,43 @@ func WithWindowsNetworkNamespace(ns string) SpecOpts {
return nil
}
}
// readLinker defines the ReadLink method locally.
// We keep this shim to ensure compatibility with build environments where
// the standard library's fs.ReadLinkFS interface is not yet available or recognized.
type readLinker interface {
ReadLink(name string) (string, error)
}
// openUserFile attempts to open a file within the root fs.
// It handles cases where the file is an absolute symlink (e.g., NixOS /etc/passwd -> /nix/store/...),
// which triggers "path escapes from parent" errors in Go 1.24+ due to stricter os.DirFS validation.
func openUserFile(root fs.FS, name string) (fs.File, error) {
f, err := root.Open(name)
if err == nil {
return f, nil
}
// Check if the FS implements our local ReadLink interface.
// We use a local interface instead of fs.ReadLinkFS to avoid strict dependency
// issues in some build environments.
if lfs, ok := root.(readLinker); ok {
if target, lerr := lfs.ReadLink(name); lerr == nil {
// Use filepath.IsAbs to handle platform-agnostic absolute path checks
if filepath.IsAbs(target) {
// Re-anchor the absolute path to the root.
// e.g. /nix/store/... becomes nix/store/... (relative to root fs)
// We use filepath.Rel to safely strip the leading separator.
rel, rerr := filepath.Rel(string(filepath.Separator), target)
if rerr == nil {
// filepath.Rel might return OS-specific separators (backslashes on Windows).
// fs.Open strictly expects forward slashes, so we convert it.
return root.Open(filepath.ToSlash(rel))
}
}
}
}
// Return the original error if we couldn't resolve it
return nil, err
}

View File

@@ -18,14 +18,18 @@ package oci
import (
"context"
"io"
"io/fs"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/pkg/namespaces"
"github.com/containerd/containerd/v2/pkg/testutil"
"github.com/containerd/continuity/fs/fstest"
"github.com/opencontainers/runtime-spec/specs-go"
)
func TestGenerateSpec(t *testing.T) {
@@ -325,3 +329,62 @@ func TestWithPrivileged(t *testing.T) {
t.Error("Did not find mount for cgroupfs")
}
}
func TestOpenUserFile_AbsoluteSymlink(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("absolute symlink handling is only supported on non-Windows platforms")
}
expectedContent := []byte("root:x:0:0:root:/root:/bin/bash" + t.Name())
root := t.TempDir()
// Use 'continuity' library to create a directory structure simulating NixOS
if err := fstest.Apply(
fstest.CreateDir("/etc", 0o755),
fstest.CreateDir("/nix/store/abcd", 0o755),
fstest.CreateFile("/nix/store/abcd/passwd", expectedContent, 0o644),
// /etc/passwd -> /nix/store/abcd/passwd (absolute symlink)
fstest.Symlink("/nix/store/abcd/passwd", "/etc/passwd"),
).Apply(root); err != nil {
t.Fatal(err)
}
rootFS := os.DirFS(root)
// Ensure the FS implements the ReadLink interface.
// If the native os.DirFS doesn't implement it (depending on Go version),
// wrap it in our readLinkFS helper.
if _, ok := rootFS.(readLinker); !ok {
t.Logf("os.DirFS does not implement ReadLink; wrapping to use ReadLink")
rootFS = readLinkFS{root: root, fs: rootFS}
}
f, err := openUserFile(rootFS, "etc/passwd")
if err != nil {
t.Fatalf("openUserFile failed on absolute symlink: %v", err)
}
defer f.Close()
content, err := io.ReadAll(f)
if err != nil {
t.Fatal(err)
}
if string(content) != string(expectedContent) {
t.Errorf("expected content %q, got %q", string(expectedContent), string(content))
}
}
// Helpers for testing ReadLink support
type readLinkFS struct {
root string
fs fs.FS
}
func (r readLinkFS) Open(name string) (fs.File, error) {
return r.fs.Open(name)
}
func (r readLinkFS) ReadLink(name string) (string, error) {
// Force link reading using the actual path on disk
return os.Readlink(filepath.Join(r.root, filepath.FromSlash(name)))
}