mirror of
https://github.com/containerd/containerd.git
synced 2026-08-06 16:11:22 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user