Bump to Buildah v1.45.2 and storage v1.64.2

Signed-off-by: Matt Heon <matthew.heon@pm.me>
This commit is contained in:
Matt Heon 2026-10-12 13:01:31 -04:00
parent 2215c3b8a7
commit 3c4bc1617c
48 changed files with 1626 additions and 886 deletions

4
go.mod
View file

@ -64,10 +64,10 @@ require (
github.com/vbauerster/mpb/v8 v8.14.0
github.com/vishvananda/netlink v1.3.1
go.etcd.io/bbolt v1.5.0
go.podman.io/buildah v1.45.1
go.podman.io/buildah v1.45.2
go.podman.io/common v0.69.2
go.podman.io/image/v5 v5.41.2
go.podman.io/storage v1.64.1
go.podman.io/storage v1.64.2
golang.org/x/crypto v0.57.0
golang.org/x/net v0.59.0
golang.org/x/sync v0.23.0

8
go.sum
View file

@ -433,14 +433,14 @@ go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRk
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.podman.io/buildah v1.45.1 h1:QYJrpxBaTM9b5IraRwG53kcWxfrBAlSqBJAQIgrIuZY=
go.podman.io/buildah v1.45.1/go.mod h1:RwoZrDdgSYBQHHqBVPgnOIa1Ee7cLeTEgL8uhMGb/pA=
go.podman.io/buildah v1.45.2 h1:nMyRiNCoIK4Ign2z1ho0HPoq7vMzP+xNYp4i4RFC37E=
go.podman.io/buildah v1.45.2/go.mod h1:cXUOsSy78r05MpwDfvdTwZPG/Ksobx87wvrFFVpekNg=
go.podman.io/common v0.69.2 h1:5ZV7xkfFTOwgf21xaqNZSqjy9G9JVG7p2bF5Tj6IVjE=
go.podman.io/common v0.69.2/go.mod h1:cQ1+nD9QcwnUGiuJHMX61SoYK5MU90NDbl0lLWmDF2s=
go.podman.io/image/v5 v5.41.2 h1:YnVaFrGLPWSBmipKfNvlOsGcBtk5bUxlPxQ+A6CS/uo=
go.podman.io/image/v5 v5.41.2/go.mod h1:T/bzxEFpdpAzyHabPmcq5K+my3wBsht22sFkFjJSKT4=
go.podman.io/storage v1.64.1 h1:DqbG8scW/bD3xHiSRzV7O5fzn46qVC+SwmUN6C6Fr5w=
go.podman.io/storage v1.64.1/go.mod h1:ft0DzCRMZs1yn0rszmLYXEEwgd7IIc66JRwSRPUDwwc=
go.podman.io/storage v1.64.2 h1:sIBU2vsdGo2gghTF17oxw4G4gojEZkPmt0LKiZWIwqc=
go.podman.io/storage v1.64.2/go.mod h1:ft0DzCRMZs1yn0rszmLYXEEwgd7IIc66JRwSRPUDwwc=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=

View file

@ -131,6 +131,7 @@ jobs:
update_release: false
dist_git_branches: &fedora_targets
- fedora-rawhide
- fedora-45
# Sync to CentOS Stream
- job: propose_downstream

View file

@ -2,6 +2,13 @@
# Changelog
## v1.45.2 (2026-10-12)
copier: reject relative symlink escapes, only resolve absolute ones
copier: put: resolve symlinks before os.Root path check
ci: pin go version for lint to 1.26
vendor: update storage to v1.64.2
## v1.45.1 (2026-09-15)
Bump go.podman.io/storage, go.podman.io/image/v5, and go.podman.io/common

View file

@ -1,3 +1,9 @@
- Changelog for v1.45.2 (2026-10-12)
* copier: reject relative symlink escapes, only resolve absolute ones
* copier: put: resolve symlinks before os.Root path check
* ci: pin go version for lint to 1.26
* vendor: update storage to v1.64.2
- Changelog for v1.45.1 (2026-09-15)
* Bump go.podman.io/storage, go.podman.io/image/v5, and go.podman.io/common
* Add FIXMEs for the read path

View file

@ -681,6 +681,135 @@ func cleanerReldirectory(candidate string) string {
return cleaned
}
// pathHasAbsoluteSymlink resolves name component by component using readlink
// (typically osRoot.Readlink) and returns true if any symlink in the
// resolution has an absolute target. This distinguishes absolute symlinks
// (e.g. /var/run -> /run) that need chroot-style resolution from relative
// escapes (e.g. escape -> ..) that should be rejected.
//
// When a relative symlink is encountered, its target components are
// prepended to the remaining work list, mirroring the algorithm used by
// resolvePath and os.Root. Escape detection relies on readlink rejecting
// paths that leave the root (e.g. readlink("..") must fail); without that
// guarantee, this function does not confine.
func pathHasAbsoluteSymlink(name string, readlink func(string) (string, error)) bool {
workingRel := ""
components := strings.Split(filepath.Clean(name), string(os.PathSeparator))
linksFollowed := 0
for len(components) > 0 {
comp := components[0]
components = components[1:]
if comp == "" || comp == "." {
continue
}
nextRel := filepath.Join(workingRel, comp)
target, err := readlink(nextRel)
if err != nil {
// Not a symlink (or does not exist): advance.
// When readlink is osRoot.Readlink, this also covers
// the case where ".." escapes the root. Readlink
// rejects the path, we advance with a broken workingRel,
// and all subsequent readlink calls fail too.
workingRel = nextRel
continue
}
linksFollowed++
if linksFollowed > maxLoopsFollowed {
return false
}
if filepath.IsAbs(target) || (len(target) > 0 && looksLikeAbs(target)) {
return true
}
// Relative symlink: prepend target components to the work
// list and continue from the current position. Any ".."
// components are handled naturally by filepath.Join at the
// top of the loop, and readlink will reject paths that
// escape the root.
components = append(strings.Split(target, string(os.PathSeparator)), components...)
}
return false
}
// lstatPathComponents walks name component by component using osRoot.Lstat,
// returning the first error encountered (or nil). This is used as a
// read-only compatibility check against recent versions of go-archive/tar
// which reject paths that escape from the root.
func lstatPathComponents(osRoot *os.Root, name string) error {
var components []string
for name != "." && name != "/" {
components = append(components, name)
name = filepath.Dir(name)
}
for _, partial := range slices.Backward(components) {
if _, err := osRoot.Lstat(partial); err != nil {
return err
}
}
return nil
}
// checkArchivePathEscape is a read-only check verifying that a tar entry
// path does not escape from the target directory. It never modifies any
// paths used for actual file operations.
//
// osRoot must be opened on targetDirectory (the resolved form of directory
// within root). root is the container rootfs used by resolvePath as the
// chroot boundary for symlink resolution.
//
// osRoot rejects symlinks with absolute targets (e.g. /var/run -> /run),
// even when those symlinks are valid inside a container. When the raw path
// is rejected and the rejection is caused by an absolute symlink, this
// function resolves symlinks with chroot-like semantics and re-checks the
// resolved path through osRoot. Relative escapes (e.g. escape -> ..) are
// kept rejected, matching Docker behavior (moby/go-archive#93).
func checkArchivePathEscape(osRoot *os.Root, root, directory, targetDirectory, cleanerHdrName string) error {
// Fast path: os.Root walks the raw entry path component by component.
// Example: "var/run/foo" where var/run is a plain directory -- OK.
err := lstatPathComponents(osRoot, cleanerHdrName)
if err == nil || errors.Is(err, os.ErrNotExist) {
return nil
}
// osRoot rejected the path. Only fall back to chroot-style resolution
// when the path traverses an absolute symlink (e.g. /var/run -> /run).
// Relative escapes (e.g. escape -> ..) are kept rejected, matching
// Docker behavior (moby/go-archive#93).
if !pathHasAbsoluteSymlink(cleanerHdrName, osRoot.Readlink) {
return err
}
// An absolute symlink was found. Resolve symlinks treating root as a
// chroot boundary (absolute targets become root-relative).
// - /var/run -> /run resolves to root/run, relPath = "run/foo"
resolvedPath, resolveErr := resolvePath(root, filepath.Join(directory, cleanerHdrName), false)
if resolveErr != nil {
return errors.Join(err, resolveErr)
}
// Verify the resolved path did not land outside targetDirectory.
relPath, relErr := filepath.Rel(targetDirectory, resolvedPath)
if relErr != nil {
return errors.Join(err, relErr)
}
if !filepath.IsLocal(relPath) {
return err
}
// Re-check the resolved (symlink-free) path through os.Root.
// Example: "run/foo" -- no symlinks left, os.Root is happy.
// If os.Root still rejects it, it is a genuine escape.
resolvedErr := lstatPathComponents(osRoot, relPath)
if resolvedErr == nil || errors.Is(resolvedErr, os.ErrNotExist) {
return nil
}
return err
}
// convertToRelSubdirectory returns the path of directory, bound and relative to
// root, as a relative path, or an error if that path can't be computed or if
// the two directories are on different volumes
@ -2200,19 +2329,7 @@ func copierHandlerPut(bulkReader io.Reader, req request, idMappings *idtools.IDM
// of the root, to improve our compatibility with
// recent versions of go-archive
cleanerHdrName := cleanerReldirectory(filepath.FromSlash(hdr.Name))
if err := func(hdrName string) error {
var hdrNameByComponent []string
for hdrName != "." && hdrName != "/" {
hdrNameByComponent = append(hdrNameByComponent, hdrName)
hdrName = filepath.Dir(hdrName)
}
for _, partial := range slices.Backward(hdrNameByComponent) {
if _, err := osRoot.Lstat(partial); err != nil {
return err
}
}
return nil
}(cleanerHdrName); err != nil && !errors.Is(err, os.ErrNotExist) {
if err := checkArchivePathEscape(osRoot, req.Root, req.Directory, targetDirectory, cleanerHdrName); err != nil {
return err
}
// figure out who should own this new item

View file

@ -30,7 +30,7 @@ const (
// identify working containers.
Package = "buildah"
// Version for the Package. Also used by .packit.sh for Packit builds.
Version = "1.45.1"
Version = "1.45.2"
// DefaultRuntime if containers.conf fails.
DefaultRuntime = "runc"

View file

@ -1 +1 @@
1.64.1
1.64.2

View file

@ -0,0 +1,148 @@
package rootlookupcache
import (
"fmt"
"os"
"path"
)
// Cache attempts to speed up repeated path lookups in an os.Root.
//
// As of Go 1.26, operations on os.Root always open a handle for the parent directory of the target
// path before performing the actual operation. That is currently fairly costly, opening a handle
// for each path component individually (e.g. it does not use openat2 on Linux).
//
// So, try to amortize the cost: if the target path is only a basename, os.Root can just do the
// actual operation as a single system call.
// Even with openat2 this would avoid repeated openat2() lookups of the parent.
//
// This is valuable for code needing to do further I/O on files within a fs.WalkDir handler
// (sadly fs.WalkDir does not give us a parent handle, it needs one internally); or for similar
// code that needs to repeatedly make operations within a single directory.
type Cache struct {
root *os.Root
rootFile *os.File // For root; or nil
// This is a minimal implementation, ideal for fairly shallow directory hierarchies with several files.
// For a deeper hierarchy, operations on (root/dir/file1, root/dir/subdir1, root/dir/file2, root/dir/subdir2, ...)
// would, e.g. with fs.WalkDir, result in requests for (root/dir, root/dir/subdir1, root/dir, root/dir/subdir2, ...).
// That would be better handled by caching a few of the parent directories (but not caching siblings).
//
// Also, the cache key is a potentially long string. In situations where we receive arbitrary paths, that’s
// the best we can do; but callers that implement tree walks manually could also explicitly manage the lifetime
// / equality of cache values (e.g. while recursing to subdirectories, the caller knows when it returns back to a
// parent, and could perhaps maintain some kind of a direct reference to a cache entry).
cachedDirRoot *os.Root // Or nil
cachedPath string // Valid if cachedDirRoot != nil
cachedDirFile *os.File // For cachedDirRoot, which must be non-nil; or nil
}
// NewCache creates a cache for operations within root.
// The root must be valid until Close() is called.
func NewCache(root *os.Root) *Cache {
return &Cache{
root: root,
}
}
// Close releases any possibly cached handles.
func (c *Cache) Close() error {
if c.rootFile != nil {
_ = c.rootFile.Close()
}
if c.cachedDirRoot != nil {
_ = c.cachedDirRoot.Close()
}
if c.cachedDirFile != nil {
_ = c.cachedDirFile.Close()
}
return nil
}
// RootForDir returns an *os.Root for a specified directory (a fs.ValidPath) within the cache’s root.
// The returned value can be used only until the next call to RootForDir() or Close();
// the caller must not close it manually.
func (c *Cache) RootForDir(dir string) (*os.Root, error) {
if dir == "." {
return c.root, nil
}
if c.cachedDirRoot != nil && c.cachedPath == dir {
return c.cachedDirRoot, nil
}
dirRoot, err := c.root.OpenRoot(dir)
if err != nil {
return nil, err
}
if c.cachedDirRoot != nil {
_ = c.cachedDirRoot.Close()
}
if c.cachedDirFile != nil {
_ = c.cachedDirFile.Close()
c.cachedDirFile = nil
}
c.cachedDirRoot = dirRoot
c.cachedPath = dir
return dirRoot, nil
}
// PreparePath returns a parent-directory *os.Root and a basename for efficiently accessing fsPath (per fs.ValidPath),
// while restricting it to the cache’s root.
//
// The returned root can be used only until the next call to RootForDir() or Close();
// the caller must not close it manually.
func (c *Cache) PreparePath(fsPath string) (*os.Root, string, error) {
parentPath := path.Dir(fsPath)
parentRoot, err := c.RootForDir(parentPath)
if err != nil {
return nil, "", err
}
// A path per fs.ValidPath should not contain a ".."; reject it so that we can ensure no escape from parentPath and therefore from c.root.
basename := path.Base(fsPath)
if basename == ".." {
return nil, "", fmt.Errorf("internal error: trailing .. in a path that should conform to fs.ValidPath: %q", fsPath)
}
return parentRoot, basename, nil
}
// FileForRoot returns, or newly opens, an *os.File descriptor for reading root (which must have been the last value obtained by RootForDir or PreparePath).
// The returned value can be used only until the next call to RootForDir(), FileForRoot() or Close();
// the caller must not close it manually.
func (c *Cache) FileForRoot(root *os.Root) (*os.File, error) {
// Ideally, we should be able to use the file descriptor already existing in *os.Root, without the extra Open(".")/Close().
// We should not really _need_ the special case for c.root, we probably only need one cached *os.File
// for accessing all files in a directory, and c.root is not really special WRT expected use patterns.
//
// But we have the RootForDir(".") special case because it is cheap to implement there, and without a
// corresponding File cache for "." we’d need a more complex invalidation logic in RootForDir.
if root == c.root {
if c.rootFile == nil {
fd, err := c.root.Open(".")
if err != nil {
return nil, err
}
c.rootFile = fd
}
return c.rootFile, nil
}
if c.cachedDirRoot != nil && root == c.cachedDirRoot {
if c.cachedDirFile == nil {
fd, err := c.cachedDirRoot.Open(".")
if err != nil {
return nil, err
}
c.cachedDirFile = fd
}
return c.cachedDirFile, nil
}
// Otherwise we would need to open a new file descriptor, and then we would need to close it at some point,
// i.e. we would need to be tracking it in the cache.
// That could work under the assumption that the caller is following the cache rules, but under that assumption,
// we should never get here.
return nil, fmt.Errorf("internal error: FileForRoot called with an unexpected root")
}

View file

@ -1,6 +1,6 @@
//go:build !freebsd
package system
package stat
type platformStatT struct{}

View file

@ -1,9 +1,9 @@
package system
package stat
import "syscall"
// fromStatT converts a syscall.Stat_t type to a system.Stat_t type
func fromStatT(s *syscall.Stat_t) (*StatT, error) {
// FromStatT converts a syscall.Stat_t type to a StatT type
func FromStatT(s *syscall.Stat_t) *StatT {
return &StatT{
size: s.Size,
mode: uint32(s.Mode),
@ -11,5 +11,5 @@ func fromStatT(s *syscall.Stat_t) (*StatT, error) {
gid: s.Gid,
rdev: uint64(s.Rdev),
mtim: s.Mtimespec,
}, nil
}
}

View file

@ -1,4 +1,4 @@
package system
package stat
import "syscall"
@ -11,8 +11,8 @@ func (s StatT) Flags() uint32 {
return s.flags
}
// fromStatT converts a syscall.Stat_t type to a system.Stat_t type
func fromStatT(s *syscall.Stat_t) (*StatT, error) {
// FromStatT converts a syscall.Stat_t type to a StatT type
func FromStatT(s *syscall.Stat_t) *StatT {
st := &StatT{
size: s.Size,
mode: uint32(s.Mode),
@ -24,5 +24,5 @@ func fromStatT(s *syscall.Stat_t) (*StatT, error) {
}
st.flags = s.Flags
st.dev = s.Dev
return st, nil
return st
}

View file

@ -0,0 +1,16 @@
package stat
import "syscall"
// FromStatT converts a syscall.Stat_t type to a StatT type
func FromStatT(s *syscall.Stat_t) *StatT {
return &StatT{
size: s.Size,
mode: s.Mode,
uid: s.Uid,
gid: s.Gid,
rdev: uint64(s.Rdev), //nolint:unconvert
mtim: s.Mtim,
dev: uint64(s.Dev), //nolint:unconvert
}
}

View file

@ -1,9 +1,9 @@
package system
package stat
import "syscall"
// fromStatT converts a syscall.Stat_t type to a system.Stat_t type
func fromStatT(s *syscall.Stat_t) (*StatT, error) {
// FromStatT converts a syscall.Stat_t type to a StatT type
func FromStatT(s *syscall.Stat_t) *StatT {
return &StatT{
size: s.Size,
mode: uint32(s.Mode),
@ -11,5 +11,5 @@ func fromStatT(s *syscall.Stat_t) (*StatT, error) {
gid: s.Gid,
rdev: uint64(s.Rdev),
mtim: s.Mtimespec,
}, nil
}
}

View file

@ -1,9 +1,9 @@
package system
package stat
import "syscall"
// fromStatT converts a syscall.Stat_t type to a system.Stat_t type
func fromStatT(s *syscall.Stat_t) (*StatT, error) {
// FromStatT converts a syscall.Stat_t type to a StatT type
func FromStatT(s *syscall.Stat_t) *StatT {
return &StatT{
size: s.Size,
mode: uint32(s.Mode),
@ -11,5 +11,5 @@ func fromStatT(s *syscall.Stat_t) (*StatT, error) {
gid: s.Gid,
rdev: uint64(s.Rdev),
mtim: s.Mtim,
}, nil
}
}

View file

@ -1,9 +1,9 @@
package system
package stat
import "syscall"
// fromStatT converts a syscall.Stat_t type to a system.Stat_t type
func fromStatT(s *syscall.Stat_t) (*StatT, error) {
// FromStatT converts a syscall.Stat_t type to a StatT type
func FromStatT(s *syscall.Stat_t) *StatT {
return &StatT{
size: s.Size,
mode: uint32(s.Mode),
@ -11,5 +11,5 @@ func fromStatT(s *syscall.Stat_t) (*StatT, error) {
gid: s.Gid,
rdev: uint64(s.Rdev),
mtim: s.Mtim,
}, nil
}
}

73
vendor/go.podman.io/storage/internal/stat/stat_unix.go generated vendored Normal file
View file

@ -0,0 +1,73 @@
//go:build !windows
package stat
import (
"os"
"syscall"
"golang.org/x/sys/unix"
)
// StatT type contains status of a file. It contains metadata
// like permission, owner, group, size, etc about a file.
//
// Note that this is public as pkg/system.StatT.
type StatT struct {
mode uint32
uid uint32
gid uint32
rdev uint64
size int64
mtim syscall.Timespec
dev uint64
platformStatT
}
// Mode returns file's permission mode.
func (s StatT) Mode() uint32 {
return s.mode
}
// UID returns file's user id of owner.
func (s StatT) UID() uint32 {
return s.uid
}
// GID returns file's group id of owner.
func (s StatT) GID() uint32 {
return s.gid
}
// Rdev returns file's device ID (if it's special file).
func (s StatT) Rdev() uint64 {
return s.rdev
}
// Size returns file's size.
func (s StatT) Size() int64 {
return s.size
}
// Mtim returns file's last modification time.
func (s StatT) Mtim() syscall.Timespec {
return s.mtim
}
// Dev returns a unique identifier for owning filesystem
func (s StatT) Dev() uint64 {
return s.dev
}
func (s StatT) IsDir() bool {
return (s.mode & unix.S_IFDIR) != 0
}
func (s StatT) IsSymlink() bool {
return (s.mode & unix.S_IFMT) == unix.S_IFLNK
}
// FromFileInfo converts a os.FileInfo type to a StatT type
func FromFileInfo(fi os.FileInfo) *StatT {
return FromStatT(fi.Sys().(*syscall.Stat_t))
}

View file

@ -0,0 +1,68 @@
package stat
import (
"os"
"time"
)
// StatT type contains status of a file. It contains metadata
// like permission, size, etc about a file.
//
// Note that this is public as pkg/system.StatT.
type StatT struct {
mode os.FileMode
size int64
mtim time.Time
platformStatT
}
// Size returns file's size.
func (s StatT) Size() int64 {
return s.size
}
// Mode returns file's permission mode.
func (s StatT) Mode() os.FileMode {
return os.FileMode(s.mode)
}
// Mtim returns file's last modification time.
func (s StatT) Mtim() time.Time {
return time.Time(s.mtim)
}
// UID returns file's user id of owner.
//
// on windows this is always 0 because there is no concept of UID
func (s StatT) UID() uint32 {
return 0
}
// GID returns file's group id of owner.
//
// on windows this is always 0 because there is no concept of GID
func (s StatT) GID() uint32 {
return 0
}
// Dev returns a unique identifier for owning filesystem
func (s StatT) Dev() uint64 {
return 0
}
func (s StatT) IsDir() bool {
return s.Mode().IsDir()
}
func (s StatT) IsSymlink() bool {
return s.Mode()&os.ModeSymlink != 0
}
// FromFileInfo converts a os.FileInfo type to a StatT type
func FromFileInfo(fi os.FileInfo) *StatT {
return &StatT{
size: fi.Size(),
mode: fi.Mode(),
mtim: fi.ModTime(),
}
}

View file

@ -0,0 +1,8 @@
package xattrs
import (
"errors"
)
// ErrNotSupportedPlatform means the platform is not supported.
var ErrNotSupportedPlatform = errors.New("platform and architecture is not supported")

View file

@ -0,0 +1,115 @@
//go:build freebsd
package xattrs
import (
"os"
"unsafe"
"golang.org/x/sys/unix"
)
// extattrGet is the logic underlying ExtattrGetLink and extattrGetFd.
// Returns a []byte slice if the extattr is set and nil otherwise.
func extattrGet(syscallName string, pathInError func() string, getSyscall func(dest uintptr, nbytes int) (int, error)) ([]byte, error) {
size, errno := getSyscall(uintptr(unsafe.Pointer(nil)), 0)
if errno != nil {
if errno == unix.ENOATTR {
return nil, nil
}
return nil, &os.PathError{Op: syscallName, Path: pathInError(), Err: errno}
}
if size == 0 {
return []byte{}, nil
}
dest := make([]byte, size)
size, errno = getSyscall(uintptr(unsafe.Pointer(&dest[0])), size)
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError(), Err: errno}
}
return dest[:size], nil
}
// ExtattrGetLink retrieves the value of the extended attribute identified by attrname
// in the given namespace and associated with the given path in the file system.
// If the path is a symbolic link, the extended attribute is retrieved from the link itself.
// Returns a []byte slice if the extattr is set and nil otherwise.
func ExtattrGetLink(path string, attrnamespace int, attrname string) ([]byte, error) {
return extattrGet("extattr_get_link", func() string { return path }, func(dest uintptr, nbytes int) (int, error) {
return unix.ExtattrGetLink(path, attrnamespace, attrname, dest, nbytes)
})
}
// extattrGetFd retrieves the value of the extended attribute identified by attrname
// in the given namespace and associated with the given file descriptor.
// Returns a []byte slice if the extattr is set and nil otherwise.
func extattrGetFd(fd int, pathInError func() string, attrnamespace int, attrname string) ([]byte, error) {
return extattrGet("extattr_get_fd", pathInError, func(dest uintptr, nbytes int) (int, error) {
return unix.ExtattrGetFd(fd, attrnamespace, attrname, dest, nbytes)
})
}
// ExtattrSetLink sets the value of extended attribute identified by attrname
// in the given namespace and associated with the given path in the file system.
// If the path is a symbolic link, the extended attribute is set on the link itself.
func ExtattrSetLink(path string, attrnamespace int, attrname string, data []byte) error {
if len(data) == 0 {
data = []byte{} // ensure non-nil for empty data
}
if _, errno := unix.ExtattrSetLink(path, attrnamespace, attrname,
uintptr(unsafe.Pointer(&data[0])), len(data)); errno != nil {
return &os.PathError{Op: "extattr_set_link", Path: path, Err: errno}
}
return nil
}
// extattrList is the logic underlying ExtattrListLink and extattrListFd.
func extattrList(syscallName string, pathInError func() string, listSyscall func(dest uintptr, nbytes int) (int, error)) ([]string, error) {
size, errno := listSyscall(uintptr(unsafe.Pointer(nil)), 0)
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError(), Err: errno}
}
if size == 0 {
return []string{}, nil
}
dest := make([]byte, size)
size, errno = listSyscall(uintptr(unsafe.Pointer(&dest[0])), size)
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError(), Err: errno}
}
var attrs []string
for i := 0; i < size; {
// Each attribute is preceded by a single byte length
length := int(dest[i])
i++
if i+length > size {
break
}
attrs = append(attrs, string(dest[i:i+length]))
i += length
}
return attrs, nil
}
// ExtattrListLink lists extended attributes associated with the given path
// in the specified namespace. If the path is a symbolic link, the attributes
// are listed from the link itself.
func ExtattrListLink(path string, attrnamespace int) ([]string, error) {
return extattrList("extattr_list_link", func() string { return path }, func(dest uintptr, nbytes int) (int, error) {
return unix.ExtattrListLink(path, attrnamespace, dest, nbytes)
})
}
// extattrListFd lists extended attributes associated with fd
// in the specified namespace.
func extattrListFd(fd int, pathInError func() string, attrnamespace int) ([]string, error) {
return extattrList("extattr_list_fd", pathInError, func(dest uintptr, nbytes int) (int, error) {
return unix.ExtattrListFd(fd, attrnamespace, dest, nbytes)
})
}

View file

@ -0,0 +1,181 @@
package xattrs
import (
"bytes"
"os"
"path/filepath"
"go.podman.io/storage/internal/rootlookupcache"
"golang.org/x/sys/unix"
)
const (
// Not in x/sys/unix as of v0.40.0.
o_RESOLVE_BENEATH = 0x00001000
)
// handleForFSPathBeneath creates a Handle for rootFile/fsPath (using errorRoot/fsPath for error reporting).
func handleForFSPathBeneath(rootFile *os.File, fsPath string, errorRoot *os.Root) (*Handle, error) {
fd, err := syscallConnControl(rootFile, func(rootFD uintptr) (int, error) {
// macOS does not have O_PATH, so hope the user has enough permissions.
return unix.Openat(int(rootFD), filepath.FromSlash(fsPath), unix.O_RDONLY|unix.O_CLOEXEC|unix.O_SYMLINK|o_RESOLVE_BENEATH, 0)
})
if err != nil {
return nil, err
}
return &Handle{
fd: fd,
errorRoot: errorRoot,
errorPath: fsPath,
}, nil
}
// NewLHandle creates a Handle for fsBasename in parentRoot, which was the one last obtained from rootCache.
// If fsBasename is a symbolic link, it refers to the symbolic link, not to the target.
//
// The handle must be closed using .Close().
func NewLHandle(parentRoot *os.Root, fsBasename string, rootCache *rootlookupcache.Cache) (*Handle, error) {
parentFile, err := rootCache.FileForRoot(parentRoot)
if err != nil {
return nil, err
}
return handleForFSPathBeneath(parentFile, fsBasename, parentRoot)
}
// getxattr is the logic underlying Lgetxattr and Fgetxattr.
// Returns a []byte slice if the xattr is set and nil otherwise.
func getxattr(syscallName string, pathInError func() string, getSyscall func(dest []byte) (int, error)) ([]byte, error) {
// Start with a 128 length byte array
dest := make([]byte, 128)
sz, errno := getSyscall(dest)
for errno == unix.ERANGE {
// Buffer too small, use zero-sized buffer to get the actual size
sz, errno = getSyscall([]byte{})
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError(), Err: errno}
}
dest = make([]byte, sz)
sz, errno = getSyscall(dest)
}
switch {
case errno == unix.ENOATTR:
return nil, nil
case errno != nil:
return nil, &os.PathError{Op: syscallName, Path: pathInError(), Err: errno}
}
return dest[:sz], nil
}
// Lgetxattr retrieves the value of the extended attribute identified by attr
// and associated with the given path in the file system.
// Returns a []byte slice if the xattr is set and nil otherwise.
func Lgetxattr(path string, attr string) ([]byte, error) {
return getxattr("lgetxattr", func() string { return path }, func(dest []byte) (int, error) {
return unix.Lgetxattr(path, attr, dest)
})
}
// Getxattr retrieves the value of the extended attribute identified by attr.
// Returns a []byte slice if the xattr is set and nil otherwise.
func (h *Handle) Getxattr(attr string) ([]byte, error) {
return getxattr("Handle.Getxattr", h.pathInError, func(dest []byte) (int, error) {
return unix.Fgetxattr(h.fd, attr, dest)
})
}
// RootLgetxattr retrieves the value of the extended attribute identified by attr
// in fsPath (per fs.ValidPath) under root.
// Returns a []byte slice if the xattr is set and nil otherwise.
func RootLgetxattr(root *os.Root, fsPath string, attr string) ([]byte, error) {
// We can’t use root.Open(fsPath) because it follows trailing symlinks.
rootFD, err := root.Open(".")
if err != nil {
return nil, err
}
defer rootFD.Close()
handle, err := handleForFSPathBeneath(rootFD, fsPath, root)
if err != nil {
return nil, err
}
defer handle.Close()
return handle.Getxattr(attr)
}
// Lsetxattr sets the value of the extended attribute identified by attr
// and associated with the given path in the file system.
func Lsetxattr(path string, attr string, data []byte, flags int) error {
if err := unix.Lsetxattr(path, attr, data, flags); err != nil {
return &os.PathError{Op: "lsetxattr", Path: path, Err: err}
}
return nil
}
// listxattr is the logic underlying Llistxattr and RootLlistxattr.
func listxattr(syscallName string, pathInError func() string, listSyscall func(dest []byte) (int, error)) ([]string, error) {
dest := make([]byte, 128)
sz, errno := listSyscall(dest)
for errno == unix.ERANGE {
// Buffer too small, use zero-sized buffer to get the actual size
sz, errno = listSyscall([]byte{})
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError(), Err: errno}
}
dest = make([]byte, sz)
sz, errno = listSyscall(dest)
}
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError(), Err: errno}
}
var attrs []string
for token := range bytes.SplitSeq(dest[:sz], []byte{0}) {
if len(token) > 0 {
attrs = append(attrs, string(token))
}
}
return attrs, nil
}
// Llistxattr lists extended attributes associated with the given path
// in the file system.
func Llistxattr(path string) ([]string, error) {
return listxattr("llistxattr", func() string { return path }, func(dest []byte) (int, error) {
return unix.Llistxattr(path, dest)
})
}
// Listxattr lists extended attributes associated with the given handle.
func (h *Handle) Listxattr() ([]string, error) {
return listxattr("Handle.Listxattr", h.pathInError, func(dest []byte) (int, error) {
return unix.Flistxattr(h.fd, dest)
})
}
// RootLlistxattr lists extended attributes associated with
// fsPath (per fs.ValidPath) under root.
func RootLlistxattr(root *os.Root, fsPath string) ([]string, error) {
// We can’t use root.Open(fsPath) because it follows trailing symlinks.
rootFD, err := root.Open(".")
if err != nil {
return nil, err
}
defer rootFD.Close()
handle, err := handleForFSPathBeneath(rootFD, fsPath, root)
if err != nil {
return nil, err
}
defer handle.Close()
return handle.Listxattr()
}

View file

@ -0,0 +1,177 @@
package xattrs
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"go.podman.io/storage/internal/rootlookupcache"
"golang.org/x/sys/unix"
)
// O_PATH value on freebsd. We must define O_PATH ourselves
// until https://github.com/golang/go/issues/54355 is fixed.
const o_PATH = 0x00400000 //nolint:staticcheck // ST1003: should not use ALL_CAPS
// newLHandle creates a Handle for parentFile/fsBasename (using errorRoot/errorPath for error reporting).
// If fsBasename is a symbolic link, it refers to the symbolic link, not to the target.
func newLHandle(parentFile *os.File, fsBasename string, errorRoot *os.Root, errorPath string) (*Handle, error) {
// A path per fs.ValidPath should not contain a ".."; reject it so that we can ensure no escape from parentFile.
if fsBasename == ".." {
return nil, fmt.Errorf("trailing .. in newLhandle in %q", errorPath)
}
fd, err := syscallConnControl(parentFile, func(parentDir uintptr) (int, error) {
return unix.Openat(int(parentDir), filepath.FromSlash(fsBasename), o_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
})
if err != nil {
return nil, err
}
return &Handle{
fd: fd,
errorRoot: errorRoot,
errorPath: errorPath,
}, nil
}
// NewLHandle creates a Handle for fsBasename in parentRoot, which was the one last obtained from rootCache.
// If fsBasename is a symbolic link, it refers to the symbolic link, not to the target.
//
// The handle must be closed using .Close().
func NewLHandle(parentRoot *os.Root, fsBasename string, rootCache *rootlookupcache.Cache) (*Handle, error) {
parentFile, err := rootCache.FileForRoot(parentRoot)
if err != nil {
return nil, err
}
return newLHandle(parentFile, fsBasename, parentRoot, fsBasename)
}
var namespaceMap = map[string]int{
"user": unix.EXTATTR_NAMESPACE_USER,
"system": unix.EXTATTR_NAMESPACE_SYSTEM,
}
func xattrToExtattr(xattr string) (namespace int, extattr string, err error) {
namespaceName, extattr, found := strings.Cut(xattr, ".")
if !found {
return -1, "", unix.ENOTSUP
}
namespace, ok := namespaceMap[namespaceName]
if !ok {
return -1, "", unix.ENOTSUP
}
return namespace, extattr, nil
}
// Lgetxattr retrieves the value of the extended attribute identified by attr
// and associated with the given path in the file system.
// Returns a []byte slice if the xattr is set and nil otherwise.
func Lgetxattr(path string, attr string) ([]byte, error) {
namespace, extattr, err := xattrToExtattr(attr)
if err != nil {
return nil, err
}
return ExtattrGetLink(path, namespace, extattr)
}
// Getxattr retrieves the value of the extended attribute identified by attr.
// Returns a []byte slice if the xattr is set and nil otherwise.
func (h *Handle) Getxattr(attr string) ([]byte, error) {
namespace, extattr, err := xattrToExtattr(attr)
if err != nil {
return nil, err
}
return extattrGetFd(h.fd, h.pathInError, namespace, extattr)
}
// RootLgetxattr retrieves the value of the extended attribute identified by attr
// in fsPath (per fs.ValidPath) under root.
// Returns a []byte slice if the xattr is set and nil otherwise.
func RootLgetxattr(root *os.Root, fsPath string, attr string) ([]byte, error) {
// We can’t use root.Open(fsPath) because it follows trailing symlinks.
parentDir, err := root.Open(path.Dir(fsPath))
if err != nil {
return nil, err
}
defer parentDir.Close()
handle, err := newLHandle(parentDir, path.Base(fsPath), root, fsPath)
if err != nil {
return nil, err
}
defer handle.Close()
return handle.Getxattr(attr)
}
// Lsetxattr sets the value of the extended attribute identified by attr
// and associated with the given path in the file system.
func Lsetxattr(path string, attr string, value []byte, flags int) error {
if flags != 0 {
// FIXME: Flags are not supported on FreeBSD, but we can implement
// them mimicking the behavior of the Linux implementation.
// See lsetxattr(2) on Linux for more information.
return unix.ENOTSUP
}
namespace, extattr, err := xattrToExtattr(attr)
if err != nil {
return err
}
return ExtattrSetLink(path, namespace, extattr, value)
}
// listxattr is the logic underlying Llistxattr and RootLlistxattr.
func listxattr(listOperation func(namespace int) ([]string, error)) ([]string, error) {
attrs := []string{}
for namespaceName, namespace := range namespaceMap {
namespaceAttrs, err := listOperation(namespace)
if err != nil {
return nil, err
}
for _, attr := range namespaceAttrs {
attrs = append(attrs, namespaceName+"."+attr)
}
}
return attrs, nil
}
// Llistxattr lists extended attributes associated with the given path
// in the file system.
func Llistxattr(path string) ([]string, error) {
return listxattr(func(namespace int) ([]string, error) {
return ExtattrListLink(path, namespace)
})
}
// Listxattr lists extended attributes associated with the given handle.
func (h *Handle) Listxattr() ([]string, error) {
return listxattr(func(namespace int) ([]string, error) {
return extattrListFd(h.fd, h.pathInError, namespace)
})
}
// RootLlistxattr lists extended attributes associated with
// fsPath (per fs.ValidPath) under root.
func RootLlistxattr(root *os.Root, fsPath string) ([]string, error) {
// We can’t use root.Open(fsPath) because it follows trailing symlinks.
parentDir, err := root.Open(path.Dir(fsPath))
if err != nil {
return nil, err
}
defer parentDir.Close()
handle, err := newLHandle(parentDir, path.Base(fsPath), root, fsPath)
if err != nil {
return nil, err
}
defer handle.Close()
return handle.Listxattr()
}

View file

@ -0,0 +1,184 @@
package xattrs
import (
"bytes"
"fmt"
"os"
"path"
"path/filepath"
"go.podman.io/storage/internal/rootlookupcache"
"golang.org/x/sys/unix"
)
// newLHandle creates a Handle for parentFile/fsBasename (using errorRoot/errorPath for error reporting).
// If fsBasename is a symbolic link, it refers to the symbolic link, not to the target.
func newLHandle(parentFile *os.File, fsBasename string, errorRoot *os.Root, errorPath string) (*Handle, error) {
// A path per fs.ValidPath should not contain a ".."; reject it so that we can ensure no escape from parentFile.
if fsBasename == ".." {
return nil, fmt.Errorf("trailing .. in newLhandle in %q", errorPath)
}
fd, err := syscallConnControl(parentFile, func(parentDir uintptr) (int, error) {
return unix.Openat(int(parentDir), filepath.FromSlash(fsBasename), unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
})
if err != nil {
return nil, err
}
return &Handle{
fd: fd,
errorRoot: errorRoot,
errorPath: errorPath,
}, nil
}
// NewLHandle creates a Handle for fsBasename in parentRoot, which was the one last obtained from rootCache.
// If fsBasename is a symbolic link, it refers to the symbolic link, not to the target.
//
// The handle must be closed using .Close().
func NewLHandle(parentRoot *os.Root, fsBasename string, rootCache *rootlookupcache.Cache) (*Handle, error) {
parentFile, err := rootCache.FileForRoot(parentRoot)
if err != nil {
return nil, err
}
return newLHandle(parentFile, fsBasename, parentRoot, fsBasename)
}
// getxattr is the logic underlying Lgetxattr and RootLgetxattr.
// Returns a []byte slice if the xattr is set and nil otherwise.
func getxattr(syscallName string, pathInError func() string, getSyscall func(dest []byte) (int, error)) ([]byte, error) {
// Start with a 128 length byte array
dest := make([]byte, 128)
sz, errno := getSyscall(dest)
for errno == unix.ERANGE {
// Buffer too small, use zero-sized buffer to get the actual size
sz, errno = getSyscall([]byte{})
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError(), Err: errno}
}
dest = make([]byte, sz)
sz, errno = getSyscall(dest)
}
switch {
case errno == unix.ENODATA:
return nil, nil
case errno != nil:
return nil, &os.PathError{Op: syscallName, Path: pathInError(), Err: errno}
}
return dest[:sz], nil
}
// Lgetxattr retrieves the value of the extended attribute identified by attr
// and associated with the given path in the file system.
// Returns a []byte slice if the xattr is set and nil otherwise.
func Lgetxattr(path string, attr string) ([]byte, error) {
return getxattr("lgetxattr", func() string { return path }, func(dest []byte) (int, error) {
return unix.Lgetxattr(path, attr, dest)
})
}
// Getxattr retrieves the value of the extended attribute identified by attr.
// Returns a []byte slice if the xattr is set and nil otherwise.
func (h *Handle) Getxattr(attr string) ([]byte, error) {
// Ideally we would use getxattrat() here, but as of 2026-05 that might be too recent.
return getxattr("Handle.Getxattr", h.pathInError, func(dest []byte) (int, error) {
return unix.Getxattr(fmt.Sprintf("/proc/self/fd/%d", h.fd), attr, dest)
})
}
// RootLgetxattr retrieves the value of the extended attribute identified by attr
// in fsPath (per fs.ValidPath) under root.
// Returns a []byte slice if the xattr is set and nil otherwise.
func RootLgetxattr(root *os.Root, fsPath string, attr string) ([]byte, error) {
// We can use neither root.Open nor pathrs.OpenatInRoot to get the target file descriptor because they follow trailing symlinks.
parentDir, err := root.Open(path.Dir(fsPath))
if err != nil {
return nil, err
}
defer parentDir.Close()
handle, err := newLHandle(parentDir, path.Base(fsPath), root, fsPath)
if err != nil {
return nil, err
}
defer handle.Close()
return handle.Getxattr(attr)
}
// Lsetxattr sets the value of the extended attribute identified by attr
// and associated with the given path in the file system.
func Lsetxattr(path string, attr string, data []byte, flags int) error {
if err := unix.Lsetxattr(path, attr, data, flags); err != nil {
return &os.PathError{Op: "lsetxattr", Path: path, Err: err}
}
return nil
}
// listxattr is the logic underlying Llistxattr and RootLlistxattr.
func listxattr(syscallName string, pathInError func() string, listSyscall func(dest []byte) (int, error)) ([]string, error) {
dest := make([]byte, 128)
sz, errno := listSyscall(dest)
for errno == unix.ERANGE {
// Buffer too small, use zero-sized buffer to get the actual size
sz, errno = listSyscall([]byte{})
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError(), Err: errno}
}
dest = make([]byte, sz)
sz, errno = listSyscall(dest)
}
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError(), Err: errno}
}
var attrs []string
for token := range bytes.SplitSeq(dest[:sz], []byte{0}) {
if len(token) > 0 {
attrs = append(attrs, string(token))
}
}
return attrs, nil
}
// Llistxattr lists extended attributes associated with the given path
// in the file system.
func Llistxattr(path string) ([]string, error) {
return listxattr("llistxattr", func() string { return path }, func(dest []byte) (int, error) {
return unix.Llistxattr(path, dest)
})
}
// Listxattr lists extended attributes associated with the given handle.
func (h *Handle) Listxattr() ([]string, error) {
// Ideally we would use listxattrat() here, but as of 2026-05 that might be too recent.
return listxattr("Handle.Listxattr", h.pathInError, func(dest []byte) (int, error) {
return unix.Listxattr(fmt.Sprintf("/proc/self/fd/%d", h.fd), dest)
})
}
// RootLlistxattr lists extended attributes associated with
// fsPath (per fs.ValidPath) under root.
func RootLlistxattr(root *os.Root, fsPath string) ([]string, error) {
// We can use neither root.Open nor pathrs.OpenatInRoot to get the target file descriptor because they follow trailing symlinks.
parentDir, err := root.Open(path.Dir(fsPath))
if err != nil {
return nil, err
}
defer parentDir.Close()
handle, err := newLHandle(parentDir, path.Base(fsPath), root, fsPath)
if err != nil {
return nil, err
}
defer handle.Close()
return handle.Listxattr()
}

View file

@ -0,0 +1,48 @@
//go:build linux || darwin || freebsd
package xattrs
import (
"os"
"path/filepath"
"golang.org/x/sys/unix"
)
// XattrHandle allows efficient llistxattr / llistxattr operations on a single file.
type Handle struct {
fd int
// Only for error reporting
errorRoot *os.Root
errorPath string // Within errorRoot
}
func (h *Handle) Close() error {
return unix.Close(h.fd)
}
func (h *Handle) pathInError() string {
return filepath.Join(h.errorRoot.Name(), h.errorPath)
}
// syscallConnControl calls fn with the file descriptor of fd,
// simplifying the boilerplate of f.SyscallConn().Control().
func syscallConnControl[T any](fd *os.File, fn func(uintptr) (T, error)) (T, error) {
var zeroRes T
conn, err := fd.SyscallConn()
if err != nil {
return zeroRes, err
}
var res T
var resErr error
if err := conn.Control(func(fd uintptr) {
res, resErr = fn(fd)
}); err != nil {
return zeroRes, err
}
if resErr != nil {
return zeroRes, resErr
}
return res, nil
}

View file

@ -0,0 +1,78 @@
//go:build !linux && !darwin && !freebsd
package xattrs
import (
"os"
"syscall"
"go.podman.io/storage/internal/rootlookupcache"
)
const (
// Value is larger than the maximum size allowed
E2BIG syscall.Errno = syscall.Errno(0)
// Operation not supported
ENOTSUP syscall.Errno = syscall.Errno(0)
// Value is too small or too large for maximum size allowed
EOVERFLOW syscall.Errno = syscall.Errno(0)
)
// XattrHandle allows efficient llistxattr / llistxattr operations on a single file.
type Handle struct {
}
// NewLHandle creates a Handle for fsBasename in parentRoot, which was the one last obtained from rootCache.
// If fsBasename is a symbolic link, it refers to the symbolic link, not to the target.
//
// The handle must be closed using .Close().
func NewLHandle(parentRoot *os.Root, fsBasename string, rootCache *rootlookupcache.Cache) (*Handle, error) {
// This is not implemented, but we don’t fail, so that callers don’t need to add an extra ErrNotSupportedPlatform check;
// Those checks need to exist in the individual operations anyway.
return &Handle{}, nil
}
func (h *Handle) Close() error {
return nil
}
// Lgetxattr is not supported on platforms other than linux.
func Lgetxattr(path string, attr string) ([]byte, error) {
return nil, ErrNotSupportedPlatform
}
// Getxattr retrieves the value of the extended attribute identified by attr.
// Returns a []byte slice if the xattr is set and nil otherwise.
func (h *Handle) Getxattr(attr string) ([]byte, error) {
return nil, ErrNotSupportedPlatform
}
// RootLgetxattr retrieves the value of the extended attribute identified by attr
// in fsPath (per fs.ValidPath) under root.
// Returns a []byte slice if the xattr is set and nil otherwise.
func RootLgetxattr(root *os.Root, fsPath string, attr string) ([]byte, error) {
return nil, ErrNotSupportedPlatform
}
// Lsetxattr is not supported on platforms other than linux.
func Lsetxattr(path string, attr string, data []byte, flags int) error {
return ErrNotSupportedPlatform
}
// Llistxattr is not supported on platforms other than linux.
func Llistxattr(path string) ([]string, error) {
return nil, ErrNotSupportedPlatform
}
// Listxattr lists extended attributes associated with the given handle.
func (h *Handle) Listxattr() ([]string, error) {
return nil, ErrNotSupportedPlatform
}
// RootLlistxattr lists extended attributes associated with
// fsPath (per fs.ValidPath) under root.
func RootLlistxattr(root *os.Root, fsPath string) ([]string, error) {
return nil, ErrNotSupportedPlatform
}

View file

@ -24,6 +24,8 @@ import (
"github.com/sirupsen/logrus"
"github.com/ulikunitz/xz"
"go.podman.io/storage/internal/createpath"
"go.podman.io/storage/internal/rootlookupcache"
"go.podman.io/storage/internal/xattrs"
"go.podman.io/storage/pkg/fileutils"
"go.podman.io/storage/pkg/idtools"
"go.podman.io/storage/pkg/pools"
@ -470,21 +472,21 @@ func normalizeCapabilityRootID(idMappings *idtools.IDMappings, capData []byte) (
}
// readSecurityXattrToTarHeader reads security.capability, security,image
// xattrs from filesystem to a tar header
func (ta *tarWriter) readSecurityXattrToTarHeader(root *os.Root, fsPath string, hdr *tar.Header) error {
// xattrs from filesystem to a tar header, using xattrHandle (if not nil) or a non-os.Root access to parentRoot/fsBasename.
func (ta *tarWriter) readSecurityXattrToTarHeader(xattrHandle *xattrs.Handle, parentRoot *os.Root, fsBasename string, hdr *tar.Header) error {
if hdr.PAXRecords == nil {
hdr.PAXRecords = make(map[string]string)
}
for _, xattr := range []string{"security.capability", "security.ima"} {
var capability []byte
var err error
if !ta.runningInMinimalChroot {
capability, err = system.RootLgetxattr(root, fsPath, xattr)
if xattrHandle != nil {
capability, err = xattrHandle.Getxattr(xattr)
} else {
capability, err = system.Lgetxattr(filepath.Join(root.Name(), filepath.FromSlash(fsPath)), xattr)
capability, err = xattrs.Lgetxattr(filepath.Join(parentRoot.Name(), filepath.FromSlash(fsBasename)), xattr)
}
if err != nil && !errors.Is(err, system.ENOTSUP) && err != system.ErrNotSupportedPlatform {
return fmt.Errorf("failed to read %q attribute from %q in %q: %w", xattr, fsPath, root.Name(), err)
return fmt.Errorf("failed to read %q attribute from %q in %q: %w", xattr, fsBasename, parentRoot.Name(), err)
}
if capability == nil {
continue
@ -492,7 +494,7 @@ func (ta *tarWriter) readSecurityXattrToTarHeader(root *os.Root, fsPath string,
if xattr == "security.capability" {
capability, err = normalizeCapabilityRootID(ta.IDMappings, capability)
if err != nil {
return fmt.Errorf("failed to normalize %q attribute from %q: %w", xattr, fsPath, err)
return fmt.Errorf("failed to normalize %q attribute from %q: %w", xattr, fsBasename, err)
}
}
hdr.PAXRecords[PaxSchilyXattr+xattr] = string(capability)
@ -500,30 +502,31 @@ func (ta *tarWriter) readSecurityXattrToTarHeader(root *os.Root, fsPath string,
return nil
}
// readUserXattrToTarHeader reads user.* xattr from filesystem to a tar header
func (ta *tarWriter) readUserXattrToTarHeader(root *os.Root, fsPath string, hdr *tar.Header) error {
var xattrs []string
// readUserXattrToTarHeader reads user.* xattr from filesystem to a tar header,
// using xattrHandle (if not nil) or a non-os.Root access to parentRoot/fsBasename.
func (ta *tarWriter) readUserXattrToTarHeader(xattrHandle *xattrs.Handle, parentRoot *os.Root, fsBasename string, hdr *tar.Header) error {
var xattrNames []string
var err error
if !ta.runningInMinimalChroot {
xattrs, err = system.RootLlistxattr(root, fsPath)
if xattrHandle != nil {
xattrNames, err = xattrHandle.Listxattr()
} else {
xattrs, err = system.Llistxattr(filepath.Join(root.Name(), filepath.FromSlash(fsPath)))
xattrNames, err = xattrs.Llistxattr(filepath.Join(parentRoot.Name(), filepath.FromSlash(fsBasename)))
}
if err != nil && !errors.Is(err, system.ENOTSUP) && err != system.ErrNotSupportedPlatform {
return err
}
for _, key := range xattrs {
for _, key := range xattrNames {
if strings.HasPrefix(key, "user.") && !strings.HasPrefix(key, "user.overlay.") {
var value []byte
var err error
if !ta.runningInMinimalChroot {
value, err = system.RootLgetxattr(root, fsPath, key)
if xattrHandle != nil {
value, err = xattrHandle.Getxattr(key)
} else {
value, err = system.Lgetxattr(filepath.Join(root.Name(), filepath.FromSlash(fsPath)), key)
value, err = xattrs.Lgetxattr(filepath.Join(parentRoot.Name(), filepath.FromSlash(fsBasename)), key)
}
if err != nil {
if errors.Is(err, system.E2BIG) {
logrus.Errorf("archive: Skipping xattr for file %q in %q since value is too big: %s", fsPath, root.Name(), key)
logrus.Errorf("archive: Skipping xattr for file %q in %q since value is too big: %s", fsBasename, parentRoot.Name(), key)
continue
}
return err
@ -554,12 +557,15 @@ type TarWhiteoutConverter interface {
type tarWhiteoutConverter interface {
TarWhiteoutConverter
convertWrite(hdr *tar.Header, root *os.Root, fsPath string, fi os.FileInfo) (*tar.Header, error)
// convertWrite updates hdr from on-disk format to the desired tar format, based on fi,
// and reading xattrs from xattrHandle (if not nil) or a non-os.Root access to parentRoot/fsBasename.
// If representing the whiteout requires an extra tar entry, it returns one.
convertWrite(hdr *tar.Header, xattrHandle *xattrs.Handle, parentRoot *os.Root, fsBasename string, fi os.FileInfo) (*tar.Header, error)
}
// GetWhiteoutConverter has no documented way to be called externally. Do not add any users outside of c/storage.
func GetWhiteoutConverter(format WhiteoutFormat, data any) TarWhiteoutConverter {
return getWhiteoutConverter(format, data, nil)
return getWhiteoutConverter(format, data)
}
type tarWriter struct {
@ -623,7 +629,7 @@ func canonicalTarName(name string, isDir bool) (string, error) {
type addFileData struct {
// The path within a separately-provided root from which to read contents.
fsPath string
fsBasename string
// os.Stat for the above.
fi os.FileInfo
@ -636,29 +642,24 @@ type addFileData struct {
}
// prepareAddFile generates the tar file header(s) for adding a file
// from fsPath within root as tarName to the tar archive, without writing to the
// from fsBasename in parentRoot as tarName to the tar archive, without writing to the
// tar stream. Thus, any error may be ignored without corrupting the
// tar file. A (nil, nil) return means that the file should be
// ignored for non-error reasons.
func (ta *tarWriter) prepareAddFile(root *os.Root, fsPath, tarName string) (*addFileData, error) {
func (ta *tarWriter) prepareAddFile(parentRoot *os.Root, fsBasename string, fi os.FileInfo, tarName string, rootCache *rootlookupcache.Cache) (*addFileData, error) {
// WARNING: This function is called in contexts where the contents of root may be maliciously
// concurrently modified.
fi, err := root.Lstat(fsPath) // FIXME: can we eliminate this and Lstat only once, e.g. from fs.WalkDir()?
if err != nil {
return nil, err
}
var link string
if fi.Mode()&os.ModeSymlink != 0 {
var err error
link, err = root.Readlink(fsPath)
link, err = parentRoot.Readlink(fsBasename)
if err != nil {
return nil, err
}
}
if fi.Mode()&os.ModeSocket != 0 {
logrus.Infof("archive: skipping %q in %q since it is a socket", fsPath, root.Name())
logrus.Infof("archive: skipping %q in %q since it is a socket", fsBasename, parentRoot.Name())
return nil, nil
}
@ -666,10 +667,19 @@ func (ta *tarWriter) prepareAddFile(root *os.Root, fsPath, tarName string) (*add
if err != nil {
return nil, err
}
if err := ta.readSecurityXattrToTarHeader(root, fsPath, hdr); err != nil {
var xattrHandle *xattrs.Handle
if !ta.runningInMinimalChroot {
h, err := xattrs.NewLHandle(parentRoot, fsBasename, rootCache)
if err != nil {
return nil, err
}
defer h.Close()
xattrHandle = h
}
if err := ta.readSecurityXattrToTarHeader(xattrHandle, parentRoot, fsBasename, hdr); err != nil {
return nil, err
}
if err := ta.readUserXattrToTarHeader(root, fsPath, hdr); err != nil {
if err := ta.readUserXattrToTarHeader(xattrHandle, parentRoot, fsBasename, hdr); err != nil {
return nil, err
}
if err := readFileFlagsToTarHeader(fi, hdr); err != nil {
@ -726,9 +736,9 @@ func (ta *tarWriter) prepareAddFile(root *os.Root, fsPath, tarName string) (*add
maybeTruncateHeaderModTime(hdr)
result := &addFileData{
fsPath: fsPath,
hdr: hdr,
fi: fi,
fsBasename: fsBasename,
hdr: hdr,
fi: fi,
}
if ta.whiteoutConverter != nil {
// The whiteoutConverter suggests a generic mechanism,
@ -742,7 +752,7 @@ func (ta *tarWriter) prepareAddFile(root *os.Root, fsPath, tarName string) (*add
// should be represented as a directory containing a
// magic whiteout empty regular file, hence the
// extraWhiteout header returned here.
result.extraWhiteout, err = ta.whiteoutConverter.convertWrite(hdr, root, fsPath, fi)
result.extraWhiteout, err = ta.whiteoutConverter.convertWrite(hdr, xattrHandle, parentRoot, fsBasename, fi)
if err != nil {
return nil, err
}
@ -752,7 +762,7 @@ func (ta *tarWriter) prepareAddFile(root *os.Root, fsPath, tarName string) (*add
}
// addFile performs the write. An error here corrupts the tar file.
func (ta *tarWriter) addFile(root *os.Root, headers *addFileData) error {
func (ta *tarWriter) addFile(parentRoot *os.Root, headers *addFileData) error {
// WARNING: This function is called in contexts where the contents of root may be maliciously
// concurrently modified.
@ -777,7 +787,7 @@ func (ta *tarWriter) addFile(root *os.Root, headers *addFileData) error {
}
if hdr.Typeflag == tar.TypeReg && hdr.Size > 0 {
file, err := root.Open(headers.fsPath)
file, err := parentRoot.Open(headers.fsBasename)
if err != nil {
return err
}
@ -1046,7 +1056,7 @@ func tarWithOptionsTo(dest io.Writer, srcPath string, options *TarOptions) (resu
options.Timestamp,
options.InternalRunningInMinimalChroot,
)
ta.whiteoutConverter = getWhiteoutConverter(options.WhiteoutFormat, options.WhiteoutData, options)
ta.whiteoutConverter = getWhiteoutConverter(options.WhiteoutFormat, options.WhiteoutData)
ta.CopyPass = options.CopyPass
includeFiles := options.IncludeFiles
@ -1090,6 +1100,8 @@ func tarWithOptionsTo(dest io.Writer, srcPath string, options *TarOptions) (resu
return err
}
defer root.Close()
rootCache := rootlookupcache.NewCache(root)
defer rootCache.Close()
if len(includeFiles) == 0 {
includeFiles = []string{"."}
@ -1185,11 +1197,22 @@ func tarWithOptionsTo(dest io.Writer, srcPath string, options *TarOptions) (resu
relFilePath = strings.Replace(relFilePath, include, replacement, 1)
}
headers, err := ta.prepareAddFile(root, fsFilePath, relFilePath)
fi, err := d.Info() // This is free and never fails, root.FS().ReadDir() always calls lstatat to get this data.
if err != nil {
logrus.Errorf("Can't add file %q in %q to tar: %s; skipping", fsFilePath, root.Name(), err)
return nil
}
parentRoot, fsBasename, err := rootCache.PreparePath(fsFilePath)
if err != nil {
logrus.Errorf("Can't add file %q in %q to tar: %s; skipping", fsFilePath, root.Name(), err)
return nil
}
headers, err := ta.prepareAddFile(parentRoot, fsBasename, fi, relFilePath, rootCache)
if err != nil {
logrus.Errorf("Can't add file %q in %q to tar: %s; skipping", fsFilePath, root.Name(), err)
} else if headers != nil {
if err := ta.addFile(root, headers); err != nil {
if err := ta.addFile(parentRoot, headers); err != nil {
return err
}
}

View file

@ -10,6 +10,7 @@ import (
"strings"
"syscall"
"go.podman.io/storage/internal/xattrs"
"go.podman.io/storage/pkg/idtools"
"go.podman.io/storage/pkg/system"
"golang.org/x/sys/unix"
@ -19,15 +20,14 @@ func getOverlayOpaqueXattrName() string {
return GetOverlayXattrName("opaque")
}
func getWhiteoutConverter(format WhiteoutFormat, data any, options *TarOptions) tarWhiteoutConverter {
func getWhiteoutConverter(format WhiteoutFormat, data any) tarWhiteoutConverter {
if format == OverlayWhiteoutFormat {
var roLayers []string = nil
if rolayers, ok := data.([]string); ok && len(rolayers) > 0 {
roLayers = rolayers
}
return overlayWhiteoutConverter{
rolayers: roLayers,
runningInMinimalChroot: options != nil && options.InternalRunningInMinimalChroot,
rolayers: roLayers,
}
}
return nil
@ -35,13 +35,6 @@ func getWhiteoutConverter(format WhiteoutFormat, data any, options *TarOptions)
type overlayWhiteoutConverter struct {
rolayers []string
// runningInMinimalChroot indicates that we are confined to a fairly strict chroot,
// so we don’t need to worry about Lgetxattr / Llistxattr escaping the source directory.
//
// We need this because:
// - getxattrat() would be ideal, but requires Linux 6.13, and as of 2026-05 that might still be too new
// - Our fallback is to open "/proc/self/fd/$fd" of an O_PATH file handle, but /proc is not available in these chroots.
runningInMinimalChroot bool
}
func (o overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os.FileInfo) (*tar.Header, error) {
@ -50,14 +43,14 @@ func (o overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi
})
}
func (o overlayWhiteoutConverter) convertWrite(hdr *tar.Header, root *os.Root, fsPath string, fi os.FileInfo) (*tar.Header, error) {
if !o.runningInMinimalChroot {
func (o overlayWhiteoutConverter) convertWrite(hdr *tar.Header, xattrHandle *xattrs.Handle, parentRoot *os.Root, fsBasename string, fi os.FileInfo) (*tar.Header, error) {
if xattrHandle != nil {
return o.convertWriteWithGetxattr(hdr, fi, func(attrName string) ([]byte, error) {
return system.RootLgetxattr(root, fsPath, attrName)
return xattrHandle.Getxattr(attrName)
})
} else {
return o.convertWriteWithGetxattr(hdr, fi, func(attrName string) ([]byte, error) {
return system.Lgetxattr(filepath.Join(root.Name(), filepath.FromSlash(fsPath)), attrName)
return system.Lgetxattr(filepath.Join(parentRoot.Name(), filepath.FromSlash(fsBasename)), attrName)
})
}
}

View file

@ -2,7 +2,7 @@
package archive
func getWhiteoutConverter(_ WhiteoutFormat, _ any, _ *TarOptions) tarWhiteoutConverter {
func getWhiteoutConverter(_ WhiteoutFormat, _ any) tarWhiteoutConverter {
return nil
}

View file

@ -18,6 +18,7 @@ import (
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/sirupsen/logrus"
"go.podman.io/storage/internal/rootlookupcache"
"go.podman.io/storage/pkg/fileutils"
"go.podman.io/storage/pkg/idtools"
"go.podman.io/storage/pkg/pools"
@ -94,7 +95,7 @@ func aufsMetadataSkip(fsPath string) (bool, error) {
return skip, err
}
func aufsDeletedFile(root *os.Root, fsPath string, fi os.FileInfo) (string, error) {
func aufsDeletedFile(rootCache *rootlookupcache.Cache, fsPath string, fi os.FileInfo) (string, error) {
f := path.Base(fsPath)
// If there is a whiteout, then the file was removed
@ -142,14 +143,14 @@ func isENOTDIR(err error) bool {
type (
skipChange func(string) (bool, error)
deleteChange func(*os.Root, string, os.FileInfo) (string, error)
deleteChange func(*rootlookupcache.Cache, string, os.FileInfo) (string, error)
whiteoutChange func(string, string) (bool, error)
)
// changes walks the path rw and determines changes for the files in the path,
// with respect to the parent layers.
//
// deleteConverter returns "" for most files; if path (relative to rw, per fs.ValidPath) indicates a deletion,
// deleteConverter returns "" for most files; if fsPath (relative to rw, per fs.ValidPath) indicates a deletion,
// it returns the path (relative to rw, per fs.ValidPath) of the file that was deleted.
//
// skipCondition, if not nil, should return true if a file path (relative to rw, per fs.ValidPath) should be skipped.
@ -173,6 +174,8 @@ func changes(layers []string, rw string, deleteConverter deleteChange, skipCondi
return nil, err
}
defer root.Close()
rootCache := rootlookupcache.NewCache(root)
defer rootCache.Close()
err = fs.WalkDir(root.FS(), ".", func(fsPath string, dirEntry fs.DirEntry, err error) error {
if err != nil {
@ -198,7 +201,9 @@ func changes(layers []string, rw string, deleteConverter deleteChange, skipCondi
Path: filepath.FromSlash("/" + fsPath), // We have skipped ".", and no other fsPath values start with "." or "/", so blindly prepending "/" is safe.
}
deletedFile, err := deleteConverter(root, fsPath, f)
// We _usually_ don’t do I/O on root/fsPath; even overlayDeletedFile does it only for directories.
// So we don’t unconditionally do rootCache.PreparePath(), but let the deleteConverter do it if necessary.
deletedFile, err := deleteConverter(rootCache, fsPath, f)
if err != nil {
return err
}
@ -466,6 +471,8 @@ func ChangesSizeWithError(newDir string, changes []Change) (int64, error) {
return -1, err
}
defer root.Close()
rootCache := rootlookupcache.NewCache(root)
defer rootCache.Close()
var (
size int64
@ -473,7 +480,16 @@ func ChangesSizeWithError(newDir string, changes []Change) (int64, error) {
)
for _, change := range changes {
if change.Kind == ChangeModify || change.Kind == ChangeAdd {
fileInfo, err := root.Lstat(strings.TrimPrefix(filepath.ToSlash(change.Path), "/"))
fsFilePath := strings.TrimPrefix(filepath.ToSlash(change.Path), "/")
parentRoot, fsBasename, err := rootCache.PreparePath(fsFilePath)
if err != nil {
// We don’t _fail_ on these errors, this is intended to be at least minimally-useful
// with concurrent modifications happening.
logrus.Errorf("Can't prepare %q in %q: %s", fsFilePath, newDir, err)
continue
}
fileInfo, err := parentRoot.Lstat(fsBasename)
if err != nil {
// We don’t _fail_ on these errors, this is intended to be at least minimally-useful
// with concurrent modifications happening.
@ -514,7 +530,7 @@ func ExportChanges(dir string, changes []Change, uidMaps, gidMaps []idtools.IDMa
// concurrently modified.
reader, writer := io.Pipe()
go func() {
producer := func() error {
ta := newTarWriter(idtools.NewIDMappingsFromMaps(uidMaps, gidMaps), writer, nil, nil, false)
// this buffer is needed for the duration of this piped stream
@ -524,10 +540,11 @@ func ExportChanges(dir string, changes []Change, uidMaps, gidMaps []idtools.IDMa
root, err := os.OpenRoot(dir)
if err != nil {
writer.CloseWithError(err)
return
return err
}
defer root.Close()
rootCache := rootlookupcache.NewCache(root)
defer rootCache.Close()
// In general we log errors here but ignore them because
// during e.g. a diff operation the container can continue
@ -551,13 +568,27 @@ func ExportChanges(dir string, changes []Change, uidMaps, gidMaps []idtools.IDMa
}
} else {
relPath := change.Path[1:]
headers, err := ta.prepareAddFile(root, filepath.ToSlash(relPath), relPath)
fsPath := filepath.ToSlash(relPath)
parentRoot, fsBasename, err := rootCache.PreparePath(fsPath)
if err != nil {
logrus.Debugf("Can't open parent of %q in %q: %s", change.Path, root.Name(), err)
continue
}
fi, err := parentRoot.Lstat(fsBasename)
if err != nil {
logrus.Debugf("Can't add file %q in %q to tar: %s", change.Path, root.Name(), err)
} else if headers != nil {
if err := ta.addFile(root, headers); err != nil {
writer.CloseWithError(err)
return
continue
}
headers, err := ta.prepareAddFile(parentRoot, fsBasename, fi, relPath, rootCache)
if err != nil {
logrus.Debugf("Can't add file %q in %q to tar: %s", change.Path, root.Name(), err)
continue
}
if headers != nil {
if err := ta.addFile(parentRoot, headers); err != nil {
return err
}
}
}
@ -567,6 +598,13 @@ func ExportChanges(dir string, changes []Change, uidMaps, gidMaps []idtools.IDMa
if err := ta.TarWriter.Close(); err != nil {
logrus.Debugf("Can't close layer: %s", err)
}
return nil
}
go func() {
if err := producer(); err != nil {
writer.CloseWithError(err)
return
}
if err := writer.Close(); err != nil {
logrus.Debugf("failed close Changes writer: %s", err)
}

View file

@ -17,6 +17,9 @@ import (
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/cyphar/filepath-securejoin/pathrs-lite"
"github.com/sirupsen/logrus"
"go.podman.io/storage/internal/rootlookupcache"
"go.podman.io/storage/internal/stat"
"go.podman.io/storage/internal/xattrs"
"go.podman.io/storage/pkg/idtools"
"go.podman.io/storage/pkg/system"
"golang.org/x/sys/unix"
@ -31,10 +34,10 @@ import (
// directly. Eliminating stat calls in this way can save up to seconds on large
// images.
type walker struct {
root1 *os.Root
root2 *os.Root
rootFileInfo1 *FileInfo
rootFileInfo2 *FileInfo
rootCache1 *rootlookupcache.Cache
rootCache2 *rootlookupcache.Cache
}
// collectFileInfoForChanges returns a complete representation of the trees
@ -52,17 +55,21 @@ func collectFileInfoForChanges(dir1, dir2 string, idmap1, idmap2 *idtools.IDMapp
return nil, nil, err
}
defer root1.Close()
rootCache1 := rootlookupcache.NewCache(root1)
defer rootCache1.Close()
root2, err := os.OpenRoot(dir2)
if err != nil {
return nil, nil, err
}
defer root2.Close()
rootCache2 := rootlookupcache.NewCache(root2)
defer rootCache2.Close()
w := &walker{
root1: root1,
root2: root2,
rootFileInfo1: newRootFileInfo(idmap1),
rootFileInfo2: newRootFileInfo(idmap2),
rootCache1: rootCache1,
rootCache2: rootCache2,
}
i1, err := root1.Lstat(".")
@ -74,16 +81,18 @@ func collectFileInfoForChanges(dir1, dir2 string, idmap1, idmap2 *idtools.IDMapp
return nil, nil, err
}
if err := w.walk(".", i1, i2); err != nil {
if err := w.walk(".", ".", root1, root2, i1, i2); err != nil {
return nil, nil, err
}
return w.rootFileInfo1, w.rootFileInfo2, nil
}
// Given a FileInfo, its path info, and a reference to the root of the tree
// being constructed, register this file with the tree.
func walkchunk(root *os.Root, fsPath string, fi os.FileInfo, rootFI *FileInfo) error {
// Register file fsPath with rootFI, which is accessible as parentRoot/fsBasename (coming from rootCache)
// and for which fi is already available.
//
// parentRoot+fi can be simultaneously nil, in that case do nothing.
func walkchunk(rootFI *FileInfo, fsPath string, parentRoot *os.Root, fsBasename string, fi os.FileInfo, rootCache *rootlookupcache.Cache) error {
// WARNING: This is called in contexts where the contents of root may be maliciously
// concurrently modified.
@ -102,12 +111,14 @@ func walkchunk(root *os.Root, fsPath string, fi os.FileInfo, rootFI *FileInfo) e
idMappings: rootFI.idMappings,
target: "",
}
stat, err := system.FromStatT(fi.Sys().(*syscall.Stat_t))
info.stat = stat.FromFileInfo(fi)
var err error
xattrHandle, err := xattrs.NewLHandle(parentRoot, fsBasename, rootCache)
if err != nil {
return err
}
info.stat = stat
info.capability, err = system.RootLgetxattr(root, fsPath, "security.capability") // lgetxattr(2): fs access
defer xattrHandle.Close()
info.capability, err = xattrHandle.Getxattr("security.capability") // lgetxattr(2): fs access
if err != nil && !errors.Is(err, system.ENOTSUP) {
return err
}
@ -115,16 +126,16 @@ func walkchunk(root *os.Root, fsPath string, fi os.FileInfo, rootFI *FileInfo) e
if err != nil {
return err
}
xattrs, err := system.RootLlistxattr(root, fsPath)
xattrs, err := xattrHandle.Listxattr()
if err != nil && !errors.Is(err, system.ENOTSUP) {
return err
}
for _, key := range xattrs {
if strings.HasPrefix(key, "user.") {
value, err := system.RootLgetxattr(root, fsPath, key)
value, err := xattrHandle.Getxattr(key)
if err != nil {
if errors.Is(err, system.E2BIG) {
logrus.Errorf("archive: Skipping xattr for file %q in %q since value is too big: %s", fsPath, root.Name(), key)
logrus.Errorf("archive: Skipping xattr for file %q in %q since value is too big: %s", fsBasename, parentRoot.Name(), key)
continue
}
return err
@ -136,7 +147,7 @@ func walkchunk(root *os.Root, fsPath string, fi os.FileInfo, rootFI *FileInfo) e
}
}
if fi.Mode()&os.ModeSymlink != 0 {
info.target, err = root.Readlink(fsPath)
info.target, err = parentRoot.Readlink(fsBasename)
if err != nil {
return err
}
@ -147,17 +158,22 @@ func walkchunk(root *os.Root, fsPath string, fi os.FileInfo, rootFI *FileInfo) e
// Walk a subtree rooted at the same path in both trees being iterated. For
// example, /docker/overlay/1234/a/b/c/d and /docker/overlay/8888/a/b/c/d
func (w *walker) walk(fsPath string, i1, i2 os.FileInfo) (err error) {
// parentRoot1+i1, and parentRoot2+i2, may be nil.
// fsPath is path within the subtree, per fs.ValidPath;
// the files are accessible as parentRootN/fsBasename.
//
// NOTE: parentRootN comes from w.rootcacheN, and may be invalidated by this function using the cache.
func (w *walker) walk(fsPath string, fsBasename string, parentRoot1, parentRoot2 *os.Root, i1, i2 os.FileInfo) (err error) {
// WARNING: This is called in contexts where the contents of w.root1 or w.root2 may be maliciously
// concurrently modified.
// Register these nodes with the return trees, unless we're still at the
// (already-created) roots:
if fsPath != "." {
if err := walkchunk(w.root1, fsPath, i1, w.rootFileInfo1); err != nil {
if err := walkchunk(w.rootFileInfo1, fsPath, parentRoot1, fsBasename, i1, w.rootCache1); err != nil {
return err
}
if err := walkchunk(w.root2, fsPath, i2, w.rootFileInfo2); err != nil {
if err := walkchunk(w.rootFileInfo2, fsPath, parentRoot2, fsBasename, i2, w.rootCache2); err != nil {
return err
}
}
@ -182,13 +198,13 @@ func (w *walker) walk(fsPath string, i1, i2 os.FileInfo) (err error) {
// Fetch the names of all the files contained in both directories being walked:
var names1, names2 []nameIno
if is1Dir {
names1, err = readdirnames(w.root1, fsPath) // getdents(2): fs access
names1, err = readdirnames(parentRoot1, fsBasename) // getdents(2): fs access
if err != nil {
return err
}
}
if is2Dir {
names2, err = readdirnames(w.root2, fsPath) // getdents(2): fs access
names2, err = readdirnames(parentRoot2, fsBasename) // getdents(2): fs access
if err != nil {
return err
}
@ -231,24 +247,42 @@ func (w *walker) walk(fsPath string, i1, i2 os.FileInfo) (err error) {
ix2++
}
// NOTE: parentRootN can be invalidated from now on, and can no longer be used.
// For each of the names present in either or both of the directories being
// iterated, stat the name under each root, and recurse the pair of them:
for _, name := range names {
fsFname := path.Join(fsPath, name)
// We need to look up childRootN from the cache in each iteration because w.walk might have invalidated them.
//
// Locally, w.walk() could return a value indicating whether it used the cache, and we could avoid the cache lookup
// for leaf directories.
//
// Ideally, the cache could be directly controlled by our walk, e.g. we know when we are leaving a directory never to return,
// and we know when we are returning to a previously-used parent.
var childRoot1, childRoot2 *os.Root
var cInfo1, cInfo2 os.FileInfo
if is1Dir {
cInfo1, err = w.root1.Lstat(fsFname) // lstat(2): fs access
childRoot1, err = w.rootCache1.RootForDir(fsPath)
if err != nil {
return err
}
cInfo1, err = childRoot1.Lstat(name) // lstat(2): fs access
if err != nil && !os.IsNotExist(err) {
return err
}
}
if is2Dir {
cInfo2, err = w.root2.Lstat(fsFname) // lstat(2): fs access
childRoot2, err = w.rootCache2.RootForDir(fsPath)
if err != nil {
return err
}
cInfo2, err = childRoot2.Lstat(name) // lstat(2): fs access
if err != nil && !os.IsNotExist(err) {
return err
}
}
if err = w.walk(fsFname, cInfo1, cInfo2); err != nil {
if err = w.walk(fsFname, name, childRoot1, childRoot2, cInfo1, cInfo2); err != nil { // May invalidate childRootN
return err
}
}
@ -340,8 +374,8 @@ func OverlayChanges(layers []string, rw string) ([]Change, error) {
// WARNING: This is called in contexts where the contents of rw (but not layers) may be maliciously
// concurrently modified.
dc := func(root *os.Root, fsPath string, fi os.FileInfo) (string, error) {
r, err := overlayDeletedFile(layers, root, fsPath, fi)
dc := func(rootCache *rootlookupcache.Cache, fsPath string, fi os.FileInfo) (string, error) {
r, err := overlayDeletedFile(layers, rootCache, fsPath, fi)
if err != nil {
return "", fmt.Errorf("overlay deleted file query: %w", err)
}
@ -370,7 +404,7 @@ func overlayLowerContainsWhiteout(root, fsPath string) (bool, error) {
return false, nil
}
func overlayDeletedFile(layers []string, root *os.Root, fsPath string, fi os.FileInfo) (string, error) {
func overlayDeletedFile(layers []string, rootCache *rootlookupcache.Cache, fsPath string, fi os.FileInfo) (string, error) {
// If it's a whiteout item, then a file or directory with that name is removed by this layer.
if fi.Mode()&os.ModeCharDevice != 0 {
if isWhiteOut(fi) {
@ -382,7 +416,18 @@ func overlayDeletedFile(layers []string, root *os.Root, fsPath string, fi os.Fil
return "", nil
}
// If the directory isn't marked as opaque, then it's just a normal directory.
opaque, err := system.RootLgetxattr(root, fsPath, getOverlayOpaqueXattrName())
parentRoot, fsBasename, err := rootCache.PreparePath(fsPath)
if err != nil {
return "", err
}
opaque, err := func() ([]byte, error) { // A scope for defer
xattrHandle, err := xattrs.NewLHandle(parentRoot, fsBasename, rootCache)
if err != nil {
return nil, err
}
defer xattrHandle.Close()
return xattrHandle.Getxattr(getOverlayOpaqueXattrName())
}()
if err != nil {
return "", fmt.Errorf("failed querying overlay opaque xattr: %w", err)
}

View file

@ -8,6 +8,9 @@ import (
"os"
"path/filepath"
"go.podman.io/storage/internal/rootlookupcache"
"go.podman.io/storage/internal/stat"
"go.podman.io/storage/internal/xattrs"
"go.podman.io/storage/pkg/idtools"
"go.podman.io/storage/pkg/system"
)
@ -49,6 +52,8 @@ func collectFileInfo(sourceDir string, idMappings *idtools.IDMappings) (*FileInf
return nil, err
}
defer root.Close()
rootCache := rootlookupcache.NewCache(root)
defer rootCache.Close()
rootFileInfo := newRootFileInfo(idMappings)
@ -81,10 +86,11 @@ func collectFileInfo(sourceDir string, idMappings *idtools.IDMappings) (*FileInf
idMappings: idMappings,
}
s, err := system.RootLstat(root, fsPath)
fi, err := d.Info() // This is free and never fails, root.FS().ReadDir() always calls lstatat() to get this data.
if err != nil {
return err
}
s := stat.FromFileInfo(fi)
// Don't cross mount points. This ignores file mounts to avoid
// generating a diff which deletes all files following the
@ -93,10 +99,21 @@ func collectFileInfo(sourceDir string, idMappings *idtools.IDMappings) (*FileInf
return filepath.SkipDir
}
parentRoot, fsBasename, err := rootCache.PreparePath(fsPath)
if err != nil {
return err
}
info.stat = s
info.capability, _ = system.RootLgetxattr(root, fsPath, "security.capability")
info.capability, _ = func() ([]byte, error) { // A scope for defer
h, err := xattrs.NewLHandle(parentRoot, fsBasename, rootCache)
if err != nil {
return nil, err
}
defer h.Close()
return h.Getxattr("security.capability")
}()
if s.IsSymlink() {
info.target, err = root.Readlink(fsPath)
info.target, err = parentRoot.Readlink(fsBasename)
if err != nil {
return err
}

View file

@ -156,23 +156,28 @@ type IDMappings struct {
gids []IDMap
}
// NewIDMappings takes a requested user and group name and
// using the data from /etc/sub{uid,gid} ranges, creates the
// proper uid and gid remapping ranges for that user/group pair
func NewIDMappings(username, groupname string) (*IDMappings, error) {
subuidRanges, err := readSubuid(username)
// NewIDMappings takes the username (or user ID) for each sub{uid,gid}
// mapping. Note that both files are indexed via username (or user ID),
// and NOT the group name even for the subgid files.
// It reads the data from the /etc/sub{uid,gid} files or
// when using the libsubid build tag it then uses the libsubid functions
// to query the user ranges which resolves them according to the system NSS
// configuration, potentially fetching the ranges from a central network source.
// It returns proper uid and gid remapping ranges for the given names.
func NewIDMappings(usernameForUIDs, usernameForGIDs string) (*IDMappings, error) {
subuidRanges, err := readSubuid(usernameForUIDs)
if err != nil {
return nil, err
}
subgidRanges, err := readSubgid(groupname)
subgidRanges, err := readSubgid(usernameForGIDs)
if err != nil {
return nil, err
}
if len(subuidRanges) == 0 {
return nil, fmt.Errorf("no subuid ranges found for user %q in %s", username, subuidFileName)
return nil, fmt.Errorf("no subuid ranges found for user %q in %s", usernameForUIDs, subuidFileName)
}
if len(subgidRanges) == 0 {
return nil, fmt.Errorf("no subgid ranges found for group %q in %s", groupname, subgidFileName)
return nil, fmt.Errorf("no subgid ranges found for user %q in %s", usernameForGIDs, subgidFileName)
}
return &IDMappings{

View file

@ -5,6 +5,7 @@ package idtools
import (
"errors"
"os/user"
"runtime"
"sync"
"unsafe"
)
@ -20,22 +21,30 @@ struct subid_range get_range(struct subid_range *ranges, int i)
return ranges[i];
}
// helper for stderr to avoid referencing C.stderr from Go code,
// which breaks cgo on musl due to stderr being declared as FILE *const
static FILE *subid_stderr(void) {
return stderr;
}
#if !defined(SUBID_ABI_MAJOR) || (SUBID_ABI_MAJOR < 4)
# define subid_init libsubid_init
# define subid_get_uid_ranges get_subuid_ranges
# define subid_get_gid_ranges get_subgid_ranges
#endif
#if !defined(SUBID_ABI_MAJOR) || (SUBID_ABI_MAJOR < 5)
# define subid_free free
#endif
// Define our own static init function here instead of calling subid_init
// from the go code so we can have the storage string inlined here.
// static variables cannot be referenced from the go code.
static void containers_storage_subid_init() {
subid_init("storage", stderr);
}
*/
import "C"
var onceInit sync.Once
var (
libsubidLock sync.Mutex
subidInitialized bool
)
func readSubid(username string, isUser bool) ([]subIDRange, error) {
var ret []subIDRange
@ -49,9 +58,21 @@ func readSubid(username string, isUser bool) ([]subIDRange, error) {
uidstr = u.Uid
}
onceInit.Do(func() {
C.subid_init(C.CString("storage"), C.subid_stderr())
})
// libsubid is not thread safe currently, concurrent calls often fail.
// https://github.com/shadow-maint/shadow/issues/1362
libsubidLock.Lock()
defer libsubidLock.Unlock()
// Avoid the goroutine being switched to different threads during our calls.
// While right now the library does not seem to depend on any thread local state
// lets be future proof in case it will be.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if !subidInitialized {
C.containers_storage_subid_init()
subidInitialized = true
}
cUsername := C.CString(username)
defer C.free(unsafe.Pointer(cUsername))
@ -75,10 +96,10 @@ func readSubid(username string, isUser bool) ([]subIDRange, error) {
if nRanges < 0 {
return nil, errors.New("cannot read subids")
}
defer C.free(unsafe.Pointer(cRanges))
defer C.subid_free(unsafe.Pointer(cRanges))
for i := 0; i < int(nRanges); i++ {
r := C.get_range(cRanges, C.int(i))
for i := C.int(0); i < nRanges; i++ {
r := C.get_range(cRanges, i)
newRange := subIDRange{
Start: int(r.start),
Length: int(r.count),

View file

@ -1,8 +1,6 @@
package system
import (
"errors"
)
import "go.podman.io/storage/internal/xattrs"
// ErrNotSupportedPlatform means the platform is not supported.
var ErrNotSupportedPlatform = errors.New("platform and architecture is not supported")
var ErrNotSupportedPlatform = xattrs.ErrNotSupportedPlatform

View file

@ -3,10 +3,7 @@
package system
import (
"os"
"strconv"
"unsafe"
"go.podman.io/storage/internal/xattrs"
"golang.org/x/sys/unix"
)
@ -16,107 +13,24 @@ const (
EXTATTR_NAMESPACE_SYSTEM = unix.EXTATTR_NAMESPACE_SYSTEM
)
// extattrGet is the logic underlying ExtattrGetLink and extattrGetFd.
// Returns a []byte slice if the extattr is set and nil otherwise.
func extattrGet(syscallName string, pathInError string, getSyscall func(dest uintptr, nbytes int) (int, error)) ([]byte, error) {
size, errno := getSyscall(uintptr(unsafe.Pointer(nil)), 0)
if errno != nil {
if errno == unix.ENOATTR {
return nil, nil
}
return nil, &os.PathError{Op: syscallName, Path: pathInError, Err: errno}
}
if size == 0 {
return []byte{}, nil
}
dest := make([]byte, size)
size, errno = getSyscall(uintptr(unsafe.Pointer(&dest[0])), size)
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError, Err: errno}
}
return dest[:size], nil
}
// ExtattrGetLink retrieves the value of the extended attribute identified by attrname
// in the given namespace and associated with the given path in the file system.
// If the path is a symbolic link, the extended attribute is retrieved from the link itself.
// Returns a []byte slice if the extattr is set and nil otherwise.
func ExtattrGetLink(path string, attrnamespace int, attrname string) ([]byte, error) {
return extattrGet("extattr_get_link", path, func(dest uintptr, nbytes int) (int, error) {
return unix.ExtattrGetLink(path, attrnamespace, attrname, dest, nbytes)
})
}
// extattrGetFd retrieves the value of the extended attribute identified by attrname
// in the given namespace and associated with the given file descriptor.
// Returns a []byte slice if the extattr is set and nil otherwise.
func extattrGetFd(fd int, attrnamespace int, attrname string) ([]byte, error) {
return extattrGet("extattr_get_fd", strconv.Itoa(fd), func(dest uintptr, nbytes int) (int, error) {
return unix.ExtattrGetFd(fd, attrnamespace, attrname, dest, nbytes)
})
return xattrs.ExtattrGetLink(path, attrnamespace, attrname)
}
// ExtattrSetLink sets the value of extended attribute identified by attrname
// in the given namespace and associated with the given path in the file system.
// If the path is a symbolic link, the extended attribute is set on the link itself.
func ExtattrSetLink(path string, attrnamespace int, attrname string, data []byte) error {
if len(data) == 0 {
data = []byte{} // ensure non-nil for empty data
}
if _, errno := unix.ExtattrSetLink(path, attrnamespace, attrname,
uintptr(unsafe.Pointer(&data[0])), len(data)); errno != nil {
return &os.PathError{Op: "extattr_set_link", Path: path, Err: errno}
}
return nil
}
// extattrList is the logic underlying ExtattrListLink and extattrListFd.
func extattrList(syscallName string, pathInError string, listSyscall func(dest uintptr, nbytes int) (int, error)) ([]string, error) {
size, errno := listSyscall(uintptr(unsafe.Pointer(nil)), 0)
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError, Err: errno}
}
if size == 0 {
return []string{}, nil
}
dest := make([]byte, size)
size, errno = listSyscall(uintptr(unsafe.Pointer(&dest[0])), size)
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError, Err: errno}
}
var attrs []string
for i := 0; i < size; {
// Each attribute is preceded by a single byte length
length := int(dest[i])
i++
if i+length > size {
break
}
attrs = append(attrs, string(dest[i:i+length]))
i += length
}
return attrs, nil
return xattrs.ExtattrSetLink(path, attrnamespace, attrname, data)
}
// ExtattrListLink lists extended attributes associated with the given path
// in the specified namespace. If the path is a symbolic link, the attributes
// are listed from the link itself.
func ExtattrListLink(path string, attrnamespace int) ([]string, error) {
return extattrList("extattr_list_link", path, func(dest uintptr, nbytes int) (int, error) {
return unix.ExtattrListLink(path, attrnamespace, dest, nbytes)
})
}
// extattrListFd lists extended attributes associated with fd
// in the specified namespace.
func extattrListFd(fd int, attrnamespace int) ([]string, error) {
return extattrList("extattr_list_fd", strconv.Itoa(fd), func(dest uintptr, nbytes int) (int, error) {
return unix.ExtattrListFd(fd, attrnamespace, dest, nbytes)
})
return xattrs.ExtattrListLink(path, attrnamespace)
}

17
vendor/go.podman.io/storage/pkg/system/lstat.go generated vendored Normal file
View file

@ -0,0 +1,17 @@
package system
import (
"os"
"go.podman.io/storage/internal/stat"
)
// RootLstat takes fsPath within root and returns
// a system.StatT type pertaining to that file.
func RootLstat(root *os.Root, fsPath string) (*StatT, error) {
fi, err := root.Lstat(fsPath)
if err != nil {
return nil, err
}
return stat.FromFileInfo(fi), nil
}

View file

@ -5,6 +5,8 @@ package system
import (
"os"
"syscall"
"go.podman.io/storage/internal/stat"
)
// Lstat takes a path to a file and returns
@ -16,15 +18,5 @@ func Lstat(path string) (*StatT, error) {
if err := syscall.Lstat(path, s); err != nil {
return nil, &os.PathError{Op: "Lstat", Path: path, Err: err}
}
return fromStatT(s)
}
// RootLstat takes fsPath within root and returns
// a system.StatT type pertaining to that file.
func RootLstat(root *os.Root, fsPath string) (*StatT, error) {
fi, err := root.Lstat(fsPath)
if err != nil {
return nil, err
}
return fromStatT(fi.Sys().(*syscall.Stat_t))
return stat.FromStatT(s), nil
}

View file

@ -1,6 +1,10 @@
package system
import "os"
import (
"os"
"go.podman.io/storage/internal/stat"
)
// Lstat calls os.Lstat to get a fileinfo interface back.
// This is then copied into our own locally defined structure.
@ -10,15 +14,5 @@ func Lstat(path string) (*StatT, error) {
return nil, err
}
return fromStatT(&fi)
}
// RootLstat takes fsPath within root and returns
// a system.StatT type pertaining to that file.
func RootLstat(root *os.Root, fsPath string) (*StatT, error) {
fi, err := root.Lstat(fsPath)
if err != nil {
return nil, err
}
return fromStatT(&fi)
return stat.FromFileInfo(fi), nil
}

View file

@ -1,22 +1,13 @@
package system
import "syscall"
import (
"syscall"
// fromStatT converts a syscall.Stat_t type to a system.Stat_t type
func fromStatT(s *syscall.Stat_t) (*StatT, error) {
return &StatT{
size: s.Size,
mode: s.Mode,
uid: s.Uid,
gid: s.Gid,
rdev: uint64(s.Rdev), //nolint:unconvert
mtim: s.Mtim,
dev: uint64(s.Dev), //nolint:unconvert
}, nil
}
"go.podman.io/storage/internal/stat"
)
// FromStatT converts a syscall.Stat_t type to a system.Stat_t type
// This is exposed on Linux as pkg/archive/changes uses it.
func FromStatT(s *syscall.Stat_t) (*StatT, error) {
return fromStatT(s)
return stat.FromStatT(s), nil
}

View file

@ -7,64 +7,12 @@ import (
"strconv"
"syscall"
"golang.org/x/sys/unix"
"go.podman.io/storage/internal/stat"
)
// StatT type contains status of a file. It contains metadata
// like permission, owner, group, size, etc about a file.
type StatT struct {
mode uint32
uid uint32
gid uint32
rdev uint64
size int64
mtim syscall.Timespec
dev uint64
platformStatT
}
// Mode returns file's permission mode.
func (s StatT) Mode() uint32 {
return s.mode
}
// UID returns file's user id of owner.
func (s StatT) UID() uint32 {
return s.uid
}
// GID returns file's group id of owner.
func (s StatT) GID() uint32 {
return s.gid
}
// Rdev returns file's device ID (if it's special file).
func (s StatT) Rdev() uint64 {
return s.rdev
}
// Size returns file's size.
func (s StatT) Size() int64 {
return s.size
}
// Mtim returns file's last modification time.
func (s StatT) Mtim() syscall.Timespec {
return s.mtim
}
// Dev returns a unique identifier for owning filesystem
func (s StatT) Dev() uint64 {
return s.dev
}
func (s StatT) IsDir() bool {
return (s.mode & unix.S_IFDIR) != 0
}
func (s StatT) IsSymlink() bool {
return (s.mode & unix.S_IFMT) == unix.S_IFLNK
}
type StatT = stat.StatT
// Stat takes a path to a file and returns
// a system.StatT type pertaining to that file.
@ -75,7 +23,7 @@ func Stat(path string) (*StatT, error) {
if err := syscall.Stat(path, s); err != nil {
return nil, &os.PathError{Op: "Stat", Path: path, Err: err}
}
return fromStatT(s)
return stat.FromStatT(s), nil
}
// Fstat takes an open file descriptor and returns
@ -87,5 +35,5 @@ func Fstat(fd int) (*StatT, error) {
if err := syscall.Fstat(fd, s); err != nil {
return nil, &os.PathError{Op: "Fstat", Path: strconv.Itoa(fd), Err: err}
}
return fromStatT(s)
return stat.FromStatT(s), nil
}

View file

@ -2,59 +2,13 @@ package system
import (
"os"
"time"
"go.podman.io/storage/internal/stat"
)
// StatT type contains status of a file. It contains metadata
// like permission, size, etc about a file.
type StatT struct {
mode os.FileMode
size int64
mtim time.Time
platformStatT
}
// Size returns file's size.
func (s StatT) Size() int64 {
return s.size
}
// Mode returns file's permission mode.
func (s StatT) Mode() os.FileMode {
return os.FileMode(s.mode)
}
// Mtim returns file's last modification time.
func (s StatT) Mtim() time.Time {
return time.Time(s.mtim)
}
// UID returns file's user id of owner.
//
// on windows this is always 0 because there is no concept of UID
func (s StatT) UID() uint32 {
return 0
}
// GID returns file's group id of owner.
//
// on windows this is always 0 because there is no concept of GID
func (s StatT) GID() uint32 {
return 0
}
// Dev returns a unique identifier for owning filesystem
func (s StatT) Dev() uint64 {
return 0
}
func (s StatT) IsDir() bool {
return s.Mode().IsDir()
}
func (s StatT) IsSymlink() bool {
return s.Mode()&os.ModeSymlink != 0
}
type StatT = stat.StatT
// Stat takes a path to a file and returns
// a system.StatT type pertaining to that file.
@ -65,14 +19,5 @@ func Stat(path string) (*StatT, error) {
if err != nil {
return nil, err
}
return fromStatT(&fi)
}
// fromStatT converts a os.FileInfo type to a system.StatT type
func fromStatT(fi *os.FileInfo) (*StatT, error) {
return &StatT{
size: (*fi).Size(),
mode: (*fi).Mode(),
mtim: (*fi).ModTime(),
}, nil
return stat.FromFileInfo(fi), nil
}

View file

@ -1,24 +1,39 @@
package system
import "os"
import (
"os"
// syscallConnControl calls fn with the file descriptor of fd,
// simplifying the boilerplate of f.SyscallConn().Control().
func syscallConnControl[T any](fd *os.File, fn func(uintptr) (T, error)) (T, error) {
var zeroRes T
conn, err := fd.SyscallConn()
if err != nil {
return zeroRes, err
}
var res T
var resErr error
if err := conn.Control(func(fd uintptr) {
res, resErr = fn(fd)
}); err != nil {
return zeroRes, err
}
if resErr != nil {
return zeroRes, resErr
}
return res, nil
"go.podman.io/storage/internal/xattrs"
)
// Lgetxattr retrieves the value of the extended attribute identified by attr
// and associated with the given path in the file system.
// Returns a []byte slice if the xattr is set and nil otherwise.
func Lgetxattr(path string, attr string) ([]byte, error) {
return xattrs.Lgetxattr(path, attr)
}
// RootLgetxattr retrieves the value of the extended attribute identified by attr
// in fsPath (per fs.ValidPath) under root.
// Returns a []byte slice if the xattr is set and nil otherwise.
func RootLgetxattr(root *os.Root, fsPath string, attr string) ([]byte, error) {
return xattrs.RootLgetxattr(root, fsPath, attr)
}
// Lsetxattr sets the value of the extended attribute identified by attr
// and associated with the given path in the file system.
func Lsetxattr(path string, attr string, data []byte, flags int) error {
return xattrs.Lsetxattr(path, attr, data, flags)
}
// Llistxattr lists extended attributes associated with the given path
// in the file system.
func Llistxattr(path string) ([]string, error) {
return xattrs.Llistxattr(path)
}
// RootLlistxattr lists extended attributes associated with
// fsPath (per fs.ValidPath) under root.
func RootLlistxattr(root *os.Root, fsPath string) ([]string, error) {
return xattrs.RootLlistxattr(root, fsPath)
}

View file

@ -1,10 +1,6 @@
package system
import (
"bytes"
"os"
"path/filepath"
"golang.org/x/sys/unix"
)
@ -18,133 +14,3 @@ const (
// Not in x/sys/unix as of v0.40.0.
O_RESOLVE_BENEATH = 0x00001000
)
// getxattr is the logic underlying Lgetxattr and Fgetxattr.
// Returns a []byte slice if the xattr is set and nil otherwise.
func getxattr(syscallName string, pathInError string, getSyscall func(dest []byte) (int, error)) ([]byte, error) {
// Start with a 128 length byte array
dest := make([]byte, 128)
sz, errno := getSyscall(dest)
for errno == unix.ERANGE {
// Buffer too small, use zero-sized buffer to get the actual size
sz, errno = getSyscall([]byte{})
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError, Err: errno}
}
dest = make([]byte, sz)
sz, errno = getSyscall(dest)
}
switch {
case errno == unix.ENOATTR:
return nil, nil
case errno != nil:
return nil, &os.PathError{Op: syscallName, Path: pathInError, Err: errno}
}
return dest[:sz], nil
}
// Lgetxattr retrieves the value of the extended attribute identified by attr
// and associated with the given path in the file system.
// Returns a []byte slice if the xattr is set and nil otherwise.
func Lgetxattr(path string, attr string) ([]byte, error) {
return getxattr("lgetxattr", path, func(dest []byte) (int, error) {
return unix.Lgetxattr(path, attr, dest)
})
}
// RootLgetxattr retrieves the value of the extended attribute identified by attr
// in fsPath (per fs.ValidPath) under root.
// Returns a []byte slice if the xattr is set and nil otherwise.
func RootLgetxattr(root *os.Root, fsPath string, attr string) ([]byte, error) {
// We can’t use root.Open(fsPath) because it follows trailing symlinks.
rootFD, err := root.Open(".")
if err != nil {
return nil, err
}
defer rootFD.Close()
fd, err := syscallConnControl(rootFD, func(rootFD uintptr) (int, error) {
// macOS does not have O_PATH, so hope the user has enough permissions.
return unix.Openat(int(rootFD), filepath.FromSlash(fsPath), unix.O_RDONLY|unix.O_CLOEXEC|unix.O_SYMLINK|O_RESOLVE_BENEATH, 0)
})
if err != nil {
return nil, err
}
defer unix.Close(fd)
return getxattr("RootLgetxattr", fsPath, func(dest []byte) (int, error) {
return unix.Fgetxattr(fd, attr, dest)
})
}
// Lsetxattr sets the value of the extended attribute identified by attr
// and associated with the given path in the file system.
func Lsetxattr(path string, attr string, data []byte, flags int) error {
if err := unix.Lsetxattr(path, attr, data, flags); err != nil {
return &os.PathError{Op: "lsetxattr", Path: path, Err: err}
}
return nil
}
// listxattr is the logic underlying Llistxattr and RootLlistxattr.
func listxattr(syscallName string, pathInError string, listSyscall func(dest []byte) (int, error)) ([]string, error) {
dest := make([]byte, 128)
sz, errno := listSyscall(dest)
for errno == unix.ERANGE {
// Buffer too small, use zero-sized buffer to get the actual size
sz, errno = listSyscall([]byte{})
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError, Err: errno}
}
dest = make([]byte, sz)
sz, errno = listSyscall(dest)
}
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError, Err: errno}
}
var attrs []string
for token := range bytes.SplitSeq(dest[:sz], []byte{0}) {
if len(token) > 0 {
attrs = append(attrs, string(token))
}
}
return attrs, nil
}
// Llistxattr lists extended attributes associated with the given path
// in the file system.
func Llistxattr(path string) ([]string, error) {
return listxattr("llistxattr", path, func(dest []byte) (int, error) {
return unix.Llistxattr(path, dest)
})
}
// RootLlistxattr lists extended attributes associated with
// fsPath (per fs.ValidPath) under root.
func RootLlistxattr(root *os.Root, fsPath string) ([]string, error) {
// We can’t use root.Open(fsPath) because it follows trailing symlinks.
rootFD, err := root.Open(".")
if err != nil {
return nil, err
}
defer rootFD.Close()
// macOS does not have O_PATH, so hope the user has enough permissions.
fd, err := syscallConnControl(rootFD, func(rootFD uintptr) (int, error) {
return unix.Openat(int(rootFD), filepath.FromSlash(fsPath), unix.O_RDONLY|unix.O_CLOEXEC|unix.O_SYMLINK|O_RESOLVE_BENEATH, 0)
})
if err != nil {
return nil, err
}
defer unix.Close(fd)
return listxattr("RootLlistxattr", fsPath, func(dest []byte) (int, error) {
return unix.Flistxattr(fd, dest)
})
}

View file

@ -1,12 +1,6 @@
package system
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"golang.org/x/sys/unix"
)
@ -20,140 +14,3 @@ const (
// Value is too small or too large for maximum size allowed
EOVERFLOW unix.Errno = unix.EOVERFLOW
)
var namespaceMap = map[string]int{
"user": EXTATTR_NAMESPACE_USER,
"system": EXTATTR_NAMESPACE_SYSTEM,
}
func xattrToExtattr(xattr string) (namespace int, extattr string, err error) {
namespaceName, extattr, found := strings.Cut(xattr, ".")
if !found {
return -1, "", ENOTSUP
}
namespace, ok := namespaceMap[namespaceName]
if !ok {
return -1, "", ENOTSUP
}
return namespace, extattr, nil
}
// Lgetxattr retrieves the value of the extended attribute identified by attr
// and associated with the given path in the file system.
// Returns a []byte slice if the xattr is set and nil otherwise.
func Lgetxattr(path string, attr string) ([]byte, error) {
namespace, extattr, err := xattrToExtattr(attr)
if err != nil {
return nil, err
}
return ExtattrGetLink(path, namespace, extattr)
}
// RootLgetxattr retrieves the value of the extended attribute identified by attr
// in fsPath (per fs.ValidPath) under root.
// Returns a []byte slice if the xattr is set and nil otherwise.
func RootLgetxattr(root *os.Root, fsPath string, attr string) ([]byte, error) {
// O_PATH value on freebsd. We must define O_PATH ourselves
// until https://github.com/golang/go/issues/54355 is fixed.
const O_PATH = 0x00400000 //nolint:staticcheck // ST1003: should not use ALL_CAPS
// We can’t use root.Open(fsPath) because it follows trailing symlinks.
parentDir, err := root.Open(path.Dir(fsPath))
if err != nil {
return nil, err
}
defer parentDir.Close()
// A path per fs.ValidPath should not contain a ".."; reject it so that we can ensure no escape from parentDir.
fsBase := path.Base(fsPath)
if fsBase == ".." {
return nil, fmt.Errorf("trailing .. in RootLgetxattr(%q)", fsPath)
}
fd, err := syscallConnControl(parentDir, func(parentDir uintptr) (int, error) {
return unix.Openat(int(parentDir), filepath.FromSlash(fsBase), O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
})
if err != nil {
return nil, err
}
defer unix.Close(fd)
namespace, extattr, err := xattrToExtattr(attr)
if err != nil {
return nil, err
}
return extattrGetFd(fd, namespace, extattr)
}
// Lsetxattr sets the value of the extended attribute identified by attr
// and associated with the given path in the file system.
func Lsetxattr(path string, attr string, value []byte, flags int) error {
if flags != 0 {
// FIXME: Flags are not supported on FreeBSD, but we can implement
// them mimicking the behavior of the Linux implementation.
// See lsetxattr(2) on Linux for more information.
return ENOTSUP
}
namespace, extattr, err := xattrToExtattr(attr)
if err != nil {
return err
}
return ExtattrSetLink(path, namespace, extattr, value)
}
// listxattr is the logic underlying Llistxattr and RootLlistxattr.
func listxattr(listOperation func(namespace int) ([]string, error)) ([]string, error) {
attrs := []string{}
for namespaceName, namespace := range namespaceMap {
namespaceAttrs, err := listOperation(namespace)
if err != nil {
return nil, err
}
for _, attr := range namespaceAttrs {
attrs = append(attrs, namespaceName+"."+attr)
}
}
return attrs, nil
}
// Llistxattr lists extended attributes associated with the given path
// in the file system.
func Llistxattr(path string) ([]string, error) {
return listxattr(func(namespace int) ([]string, error) {
return ExtattrListLink(path, namespace)
})
}
// RootLlistxattr lists extended attributes associated with
// fsPath (per fs.ValidPath) under root.
func RootLlistxattr(root *os.Root, fsPath string) ([]string, error) {
// O_PATH value on freebsd. We must define O_PATH ourselves
// until https://github.com/golang/go/issues/54355 is fixed.
const O_PATH = 0x00400000 //nolint:staticcheck // ST1003: should not use ALL_CAPS
// We can’t use root.Open(fsPath) because it follows trailing symlinks.
parentDir, err := root.Open(path.Dir(fsPath))
if err != nil {
return nil, err
}
defer parentDir.Close()
// A path per fs.ValidPath should not contain a ".."; reject it so that we can ensure no escape from parentDir.
fsBase := path.Base(fsPath)
if fsBase == ".." {
return nil, fmt.Errorf("trailing .. in RootLlistxattr(%q)", fsPath)
}
fd, err := syscallConnControl(parentDir, func(parentDir uintptr) (int, error) {
return unix.Openat(int(parentDir), filepath.FromSlash(fsBase), O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
})
if err != nil {
return nil, err
}
defer unix.Close(fd)
return listxattr(func(namespace int) ([]string, error) {
return extattrListFd(fd, namespace)
})
}

View file

@ -1,12 +1,6 @@
package system
import (
"bytes"
"fmt"
"os"
"path"
"path/filepath"
"golang.org/x/sys/unix"
)
@ -20,143 +14,3 @@ const (
// Value is too small or too large for maximum size allowed
EOVERFLOW unix.Errno = unix.EOVERFLOW
)
// getxattr is the logic underlying Lgetxattr and RootLgetxattr.
// Returns a []byte slice if the xattr is set and nil otherwise.
func getxattr(syscallName string, pathInError string, getSyscall func(dest []byte) (int, error)) ([]byte, error) {
// Start with a 128 length byte array
dest := make([]byte, 128)
sz, errno := getSyscall(dest)
for errno == unix.ERANGE {
// Buffer too small, use zero-sized buffer to get the actual size
sz, errno = getSyscall([]byte{})
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError, Err: errno}
}
dest = make([]byte, sz)
sz, errno = getSyscall(dest)
}
switch {
case errno == unix.ENODATA:
return nil, nil
case errno != nil:
return nil, &os.PathError{Op: syscallName, Path: pathInError, Err: errno}
}
return dest[:sz], nil
}
// Lgetxattr retrieves the value of the extended attribute identified by attr
// and associated with the given path in the file system.
// Returns a []byte slice if the xattr is set and nil otherwise.
func Lgetxattr(path string, attr string) ([]byte, error) {
return getxattr("lgetxattr", path, func(dest []byte) (int, error) {
return unix.Lgetxattr(path, attr, dest)
})
}
// RootLgetxattr retrieves the value of the extended attribute identified by attr
// in fsPath (per fs.ValidPath) under root.
// Returns a []byte slice if the xattr is set and nil otherwise.
func RootLgetxattr(root *os.Root, fsPath string, attr string) ([]byte, error) {
// We can use neither root.Open nor pathrs.OpenatInRoot to get the target file descriptor because they follow trailing symlinks.
parentDir, err := root.Open(path.Dir(fsPath))
if err != nil {
return nil, err
}
defer parentDir.Close()
// A path per fs.ValidPath should not contain a ".."; reject it so that we can ensure no escape from parentDir.
fsBase := path.Base(fsPath)
if fsBase == ".." {
return nil, fmt.Errorf("trailing .. in RootLgetxattr(%q)", fsPath)
}
// Ideally we would use getxattrat() here, but as of 2026-05 that might be too recent.
fd, err := syscallConnControl(parentDir, func(parentDir uintptr) (int, error) {
return unix.Openat(int(parentDir), filepath.FromSlash(fsBase), unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
})
if err != nil {
return nil, err
}
defer unix.Close(fd)
return getxattr("RootLgetxattr", fsPath, func(dest []byte) (int, error) {
return unix.Getxattr(fmt.Sprintf("/proc/self/fd/%d", fd), attr, dest)
})
}
// Lsetxattr sets the value of the extended attribute identified by attr
// and associated with the given path in the file system.
func Lsetxattr(path string, attr string, data []byte, flags int) error {
if err := unix.Lsetxattr(path, attr, data, flags); err != nil {
return &os.PathError{Op: "lsetxattr", Path: path, Err: err}
}
return nil
}
// listxattr is the logic underlying Llistxattr and RootLlistxattr.
func listxattr(syscallName string, pathInError string, listSyscall func(dest []byte) (int, error)) ([]string, error) {
dest := make([]byte, 128)
sz, errno := listSyscall(dest)
for errno == unix.ERANGE {
// Buffer too small, use zero-sized buffer to get the actual size
sz, errno = listSyscall([]byte{})
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError, Err: errno}
}
dest = make([]byte, sz)
sz, errno = listSyscall(dest)
}
if errno != nil {
return nil, &os.PathError{Op: syscallName, Path: pathInError, Err: errno}
}
var attrs []string
for token := range bytes.SplitSeq(dest[:sz], []byte{0}) {
if len(token) > 0 {
attrs = append(attrs, string(token))
}
}
return attrs, nil
}
// Llistxattr lists extended attributes associated with the given path
// in the file system.
func Llistxattr(path string) ([]string, error) {
return listxattr("llistxattr", path, func(dest []byte) (int, error) {
return unix.Llistxattr(path, dest)
})
}
// RootLlistxattr lists extended attributes associated with
// fsPath (per fs.ValidPath) under root.
func RootLlistxattr(root *os.Root, fsPath string) ([]string, error) {
// We can use neither root.Open nor pathrs.OpenatInRoot to get the target file descriptor because they follow trailing symlinks.
parentDir, err := root.Open(path.Dir(fsPath))
if err != nil {
return nil, err
}
defer parentDir.Close()
// A path per fs.ValidPath should not contain a ".."; reject it so that we can ensure no escape from parentDir.
fsBase := path.Base(fsPath)
if fsBase == ".." {
return nil, fmt.Errorf("trailing .. in RootLlistxattr(%q)", fsPath)
}
// Ideally we would use listxattrat() here, but as of 2026-05 that might be too recent.
fd, err := syscallConnControl(parentDir, func(parentDir uintptr) (int, error) {
return unix.Openat(int(parentDir), filepath.FromSlash(fsBase), unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
})
if err != nil {
return nil, err
}
defer unix.Close(fd)
return listxattr("RootLlistxattr", fsPath, func(dest []byte) (int, error) {
return unix.Listxattr(fmt.Sprintf("/proc/self/fd/%d", fd), dest)
})
}

View file

@ -3,7 +3,6 @@
package system
import (
"os"
"syscall"
)
@ -17,31 +16,3 @@ const (
// Value is too small or too large for maximum size allowed
EOVERFLOW syscall.Errno = syscall.Errno(0)
)
// Lgetxattr is not supported on platforms other than linux.
func Lgetxattr(path string, attr string) ([]byte, error) {
return nil, ErrNotSupportedPlatform
}
// RootLgetxattr retrieves the value of the extended attribute identified by attr
// in fsPath (per fs.ValidPath) under root.
// Returns a []byte slice if the xattr is set and nil otherwise.
func RootLgetxattr(root *os.Root, fsPath string, attr string) ([]byte, error) {
return nil, ErrNotSupportedPlatform
}
// Lsetxattr is not supported on platforms other than linux.
func Lsetxattr(path string, attr string, data []byte, flags int) error {
return ErrNotSupportedPlatform
}
// Llistxattr is not supported on platforms other than linux.
func Llistxattr(path string) ([]string, error) {
return nil, ErrNotSupportedPlatform
}
// RootLlistxattr lists extended attributes associated with
// fsPath (per fs.ValidPath) under root.
func RootLlistxattr(root *os.Root, fsPath string) ([]string, error) {
return nil, ErrNotSupportedPlatform
}

View file

@ -703,11 +703,17 @@ func GetHostIDMappings(pid string) ([]specs.LinuxIDMapping, []specs.LinuxIDMappi
return uidmap, gidmap, nil
}
// GetSubIDMappings reads mappings from /etc/subuid and /etc/subgid.
func GetSubIDMappings(user, group string) ([]specs.LinuxIDMapping, []specs.LinuxIDMapping, error) {
mappings, err := idtools.NewIDMappings(user, group)
// GetSubIDMappings reads the uid and gid mappings for the given usernames.
// It reads the data from the /etc/sub{uid,gid} files or
// when using the libsubid build tag it then uses the libsubid functions
// to query the user ranges which resolves them according to the system NSS
// configuration, potentially fetching the ranges from a central network source.
// It returns the uid mapping as first return value and the gid mapping as second
// or an error otherwise.
func GetSubIDMappings(usernameForUIDs, usernameForGIDs string) ([]specs.LinuxIDMapping, []specs.LinuxIDMapping, error) {
mappings, err := idtools.NewIDMappings(usernameForUIDs, usernameForGIDs)
if err != nil {
return nil, nil, fmt.Errorf("reading subuid mappings for user %q and subgid mappings for group %q: %w", user, group, err)
return nil, nil, err
}
var uidmap, gidmap []specs.LinuxIDMapping
for _, m := range mappings.UIDs() {

7
vendor/modules.txt vendored
View file

@ -741,7 +741,7 @@ go.opentelemetry.io/otel/trace
go.opentelemetry.io/otel/trace/embedded
go.opentelemetry.io/otel/trace/internal/telemetry
go.opentelemetry.io/otel/trace/noop
# go.podman.io/buildah v1.45.1
# go.podman.io/buildah v1.45.2
## explicit; go 1.26.0
go.podman.io/buildah
go.podman.io/buildah/bind
@ -922,7 +922,7 @@ go.podman.io/image/v5/transports
go.podman.io/image/v5/transports/alltransports
go.podman.io/image/v5/types
go.podman.io/image/v5/version
# go.podman.io/storage v1.64.1
# go.podman.io/storage v1.64.2
## explicit; go 1.25.0
go.podman.io/storage
go.podman.io/storage/drivers
@ -938,8 +938,11 @@ go.podman.io/storage/internal/createpath
go.podman.io/storage/internal/dedup
go.podman.io/storage/internal/driver
go.podman.io/storage/internal/rawfilelock
go.podman.io/storage/internal/rootlookupcache
go.podman.io/storage/internal/staging_lockfile
go.podman.io/storage/internal/stat
go.podman.io/storage/internal/tempdir
go.podman.io/storage/internal/xattrs
go.podman.io/storage/pkg/archive
go.podman.io/storage/pkg/chrootarchive
go.podman.io/storage/pkg/chunked