Skip to content

Commit

Permalink
chrootarchive(unix): Pass root via fd
Browse files Browse the repository at this point in the history
I'd like to support passing a file descriptor root for the
container storage, and not an absolute path.

In the bootc codebase (partially a philosophy inherited
from ostree) we've heavily invested in fd-relative accesses,
primarily because it's common for us to operate in different
namespaces/roots, and fd-relative access avoids a lot of
possible footguns when dealing with absolute paths. It's
also more efficient, avoiding the need for the kernel to
traverse full paths a lot.

This is just one of a few preparatory changes necessary
in making it work to do:

`podman --root=/proc/self/fd/3 --runroot=... pull busybox`

That was breaking because the fd was being closed when forking
the child untar process here. Fix this by switching over
to always passing the root via fd on Unix.

Signed-off-by: Colin Walters <[email protected]>
  • Loading branch information
cgwalters committed Jul 31, 2024
1 parent 39d469c commit 0d2975e
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 23 deletions.
7 changes: 6 additions & 1 deletion pkg/chrootarchive/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ func untarHandler(tarArchive io.Reader, dest string, options *archive.TarOptions
}
}

rootFd, dest, err := splitPathWithRoot(root, dest)
if err != nil {
return err
}

r := tarArchive
if decompress {
decompressedArchive, err := archive.DecompressStream(tarArchive)
Expand All @@ -93,7 +98,7 @@ func untarHandler(tarArchive io.Reader, dest string, options *archive.TarOptions
r = decompressedArchive
}

return invokeUnpack(r, dest, options, root)
return invokeUnpack(r, rootFd, dest, options)
}

// Tar tars the requested path while chrooted to the specified root.
Expand Down
11 changes: 9 additions & 2 deletions pkg/chrootarchive/archive_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@ package chrootarchive

import (
"io"
"os"

"github.com/containers/storage/pkg/archive"
)

// A no-op on this platform
func splitPathWithRoot(root, dest string) (*os.File, string, error) {
return nil, dest, nil
}

func invokeUnpack(decompressedArchive io.Reader,
rootFd *os.File,
dest string,
options *archive.TarOptions, root string,
options *archive.TarOptions,
) error {
_ = root // Restricting the operation to this root is not implemented on macOS
_ = rootFd // Restricting the operation to this root is not implemented on macOS
return archive.Unpack(decompressedArchive, dest, options)
}

Expand Down
62 changes: 43 additions & 19 deletions pkg/chrootarchive/archive_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ import (
"runtime"
"strings"

"golang.org/x/sys/unix"

"github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/reexec"
)

// rootFileDescriptor is passed as an extra file
const rootFileDescriptor = 4

// untar is the entry-point for storage-untar on re-exec. This is not used on
// Windows as it does not support chroot, hence no point sandboxing through
// chroot and rexec.
Expand All @@ -38,7 +43,17 @@ func untar() {
root = flag.Arg(1)
}

if root == "" {
// FreeBSD doesn't have proc/self, but we can handle it here
if root == "/proc/self/fd/4" {
// Take ownership to ensure it's closed; no need to leak
// this afterwards.
rootFd := os.NewFile(rootFileDescriptor, "rootfs")
defer rootFd.Close()
if err := unix.Fchdir(int(rootFd.Fd())); err != nil {
fatal(err)
}
root = "."
} else if root == "" {
root = dst
}

Expand All @@ -57,11 +72,32 @@ func untar() {
os.Exit(0)
}

func invokeUnpack(decompressedArchive io.Reader, dest string, options *archive.TarOptions, root string) error {
// splitPathWithRoot takes a root directory and a destination, and
// returns an open file descriptor for the given root and the new
// destination path.
func splitPathWithRoot(root, dest string) (*os.File, string, error) {
if root == "" {
return errors.New("must specify a root to chroot to")
return nil, "", errors.New("must specify a root to chroot to")
}
relDest, err := filepath.Rel(root, dest)
if err != nil {
return nil, "", err
}
if relDest == "." {
relDest = "/"
}
if relDest[0] != '/' {
relDest = "/" + relDest
}

rootfdRaw, err := unix.Open(root, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
if err != nil {
return nil, "", err
}
return os.NewFile(uintptr(rootfdRaw), "rootfs"), relDest, nil
}

func invokeUnpack(decompressedArchive io.Reader, root *os.File, dest string, options *archive.TarOptions) error {
// We can't pass a potentially large exclude list directly via cmd line
// because we easily overrun the kernel's max argument/environment size
// when the full image list is passed (e.g. when this is used by
Expand All @@ -72,24 +108,12 @@ func invokeUnpack(decompressedArchive io.Reader, dest string, options *archive.T
return fmt.Errorf("untar pipe failure: %w", err)
}

if root != "" {
relDest, err := filepath.Rel(root, dest)
if err != nil {
return err
}
if relDest == "." {
relDest = "/"
}
if relDest[0] != '/' {
relDest = "/" + relDest
}
dest = relDest
}

cmd := reexec.Command("storage-untar", dest, root)
cmd := reexec.Command("storage-untar", dest, "/proc/self/fd/4")
cmd.Stdin = decompressedArchive

cmd.ExtraFiles = append(cmd.ExtraFiles, r)
cmd.ExtraFiles = append(cmd.ExtraFiles, r) // fd 3
// If you change this, change rootFileDescriptor above too
cmd.ExtraFiles = append(cmd.ExtraFiles, root) // fd 4
output := bytes.NewBuffer(nil)
cmd.Stdout = output
cmd.Stderr = output
Expand Down
10 changes: 9 additions & 1 deletion pkg/chrootarchive/archive_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,28 @@ package chrootarchive

import (
"io"
"os"

"github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/longpath"
)

// A no-op on this platform
func splitPathWithRoot(root, dest string) (*os.File, string, error) {
return nil, dest, nil
}

// chroot is not supported by Windows
func chroot(path string) error {
return nil
}

func invokeUnpack(decompressedArchive io.Reader,
rootFd *os.File,
dest string,
options *archive.TarOptions, root string,
options *archive.TarOptions,
) error {
_ = rootFd // Restricting the operation to this root is not implemented on macOS
// Windows is different to Linux here because Windows does not support
// chroot. Hence there is no point sandboxing a chrooted process to
// do the unpack. We call inline instead within the daemon process.
Expand Down

0 comments on commit 0d2975e

Please sign in to comment.