Skip to content

Commit 5d44d2c

Browse files
pauloappbrmxpv
authored andcommitted
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 <[email protected]>
1 parent 5e112cd commit 5d44d2c

2 files changed

Lines changed: 78 additions & 2 deletions

File tree

pkg/oci/spec_opts.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,7 +1142,7 @@ func UserFromPath(root string, filter func(user.User) bool) (user.User, error) {
11421142
// UserFromFS inspects the user object using /etc/passwd in the specified fs.FS.
11431143
// filter can be nil.
11441144
func UserFromFS(root fs.FS, filter func(user.User) bool) (user.User, error) {
1145-
f, err := root.Open("etc/passwd")
1145+
f, err := openUserFile(root, "etc/passwd")
11461146
if err != nil {
11471147
return user.User{}, err
11481148
}
@@ -1174,7 +1174,7 @@ func GIDFromPath(root string, filter func(user.Group) bool) (gid uint32, err err
11741174
// GIDFromFS inspects the GID using /etc/group in the specified fs.FS.
11751175
// filter can be nil.
11761176
func GIDFromFS(root fs.FS, filter func(user.Group) bool) (gid uint32, err error) {
1177-
f, err := root.Open("etc/group")
1177+
f, err := openUserFile(root, "etc/group")
11781178
if err != nil {
11791179
return 0, err
11801180
}
@@ -1789,3 +1789,43 @@ func WithWindowsNetworkNamespace(ns string) SpecOpts {
17891789
return nil
17901790
}
17911791
}
1792+
1793+
// readLinker defines the ReadLink method locally.
1794+
// We keep this shim to ensure compatibility with build environments where
1795+
// the standard library's fs.ReadLinkFS interface is not yet available or recognized.
1796+
type readLinker interface {
1797+
ReadLink(name string) (string, error)
1798+
}
1799+
1800+
// openUserFile attempts to open a file within the root fs.
1801+
// It handles cases where the file is an absolute symlink (e.g., NixOS /etc/passwd -> /nix/store/...),
1802+
// which triggers "path escapes from parent" errors in Go 1.24+ due to stricter os.DirFS validation.
1803+
func openUserFile(root fs.FS, name string) (fs.File, error) {
1804+
f, err := root.Open(name)
1805+
if err == nil {
1806+
return f, nil
1807+
}
1808+
1809+
// Check if the FS implements our local ReadLink interface.
1810+
// We use a local interface instead of fs.ReadLinkFS to avoid strict dependency
1811+
// issues in some build environments.
1812+
if lfs, ok := root.(readLinker); ok {
1813+
if target, lerr := lfs.ReadLink(name); lerr == nil {
1814+
// Use filepath.IsAbs to handle platform-agnostic absolute path checks
1815+
if filepath.IsAbs(target) {
1816+
// Re-anchor the absolute path to the root.
1817+
// e.g. /nix/store/... becomes nix/store/... (relative to root fs)
1818+
// We use filepath.Rel to safely strip the leading separator.
1819+
rel, rerr := filepath.Rel(string(filepath.Separator), target)
1820+
if rerr == nil {
1821+
// filepath.Rel might return OS-specific separators (backslashes on Windows).
1822+
// fs.Open strictly expects forward slashes, so we convert it.
1823+
return root.Open(filepath.ToSlash(rel))
1824+
}
1825+
}
1826+
}
1827+
}
1828+
1829+
// Return the original error if we couldn't resolve it
1830+
return nil, err
1831+
}

pkg/oci/spec_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ package oci
1818

1919
import (
2020
"context"
21+
"io"
22+
"os"
23+
"path/filepath"
2124
"runtime"
2225
"testing"
2326

@@ -325,3 +328,36 @@ func TestWithPrivileged(t *testing.T) {
325328
t.Error("Did not find mount for cgroupfs")
326329
}
327330
}
331+
332+
func TestOpenUserFile_AbsoluteSymlink(t *testing.T) {
333+
tmpDir := t.TempDir()
334+
335+
targetName := "passwd"
336+
targetPath := filepath.Join(tmpDir, targetName)
337+
expectedContent := []byte("root:x:0:0:root:/root:/bin/bash")
338+
if err := os.WriteFile(targetPath, expectedContent, 0644); err != nil {
339+
t.Fatal(err)
340+
}
341+
342+
linkName := "abs_link"
343+
linkPath := filepath.Join(tmpDir, linkName)
344+
if err := os.Symlink(targetPath, linkPath); err != nil {
345+
t.Fatal(err)
346+
}
347+
348+
rootFS := os.DirFS(tmpDir)
349+
350+
f, err := openUserFile(rootFS, linkName)
351+
if err != nil {
352+
t.Fatalf("openUserFile failed on absolute symlink: %v", err)
353+
}
354+
defer f.Close()
355+
356+
content, err := io.ReadAll(f)
357+
if err != nil {
358+
t.Fatal(err)
359+
}
360+
if string(content) != string(expectedContent) {
361+
t.Errorf("expected content %q, got %q", string(expectedContent), string(content))
362+
}
363+
}

0 commit comments

Comments
 (0)