fix(oci): handle absolute symlinks in rootfs user lookup

Go 1.24 introduced stricter checks for os.DirFS (via os.Root), which causes failures when /etc/passwd or /etc/group are absolute symlinks pointing outside the mount root (common in NixOS).

This patch introduces a helper that detects absolute symlinks and resolves them relative to the rootfs before opening, preventing the 'path escapes from parent' error.

Fixes #12683

Signed-off-by: Paulo Oliveira <paulo.hco47@gmail.com>
This commit is contained in:
Paulo Oliveira
2025-12-27 14:47:09 -03:00
parent 5566f35c18
commit 85b5418ef5
2 changed files with 78 additions and 2 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,6 +18,9 @@ package oci
import (
"context"
"io"
"os"
"path/filepath"
"runtime"
"testing"
@@ -325,3 +328,36 @@ func TestWithPrivileged(t *testing.T) {
t.Error("Did not find mount for cgroupfs")
}
}
func TestOpenUserFile_AbsoluteSymlink(t *testing.T) {
tmpDir := t.TempDir()
targetName := "passwd"
targetPath := filepath.Join(tmpDir, targetName)
expectedContent := []byte("root:x:0:0:root:/root:/bin/bash")
if err := os.WriteFile(targetPath, expectedContent, 0644); err != nil {
t.Fatal(err)
}
linkName := "abs_link"
linkPath := filepath.Join(tmpDir, linkName)
if err := os.Symlink(targetPath, linkPath); err != nil {
t.Fatal(err)
}
rootFS := os.DirFS(tmpDir)
f, err := openUserFile(rootFS, linkName)
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))
}
}