Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Address layer / image extraction issues in user namespaces #2699

Merged
merged 2 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
the original image.
- Fix `target: no such file or directory` error in native mode when extracting
layers from certain OCI images that manipulate hard links across layers.
- Fix extraction of OCI layers when run in a root mapped user namespace
(e.g.. `unshare -r`).
- Use user namespace for wrapping of `unsquashfs` when singularity is run with
`--userns / -u` flag. Fixes temporary sandbox extraction of images in non-root
mapped user namespace (e.g. `unshare -c`).

## 4.1.1 \[2024-02-01\]

Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/build/sources/packer_sif.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func unpackSIF(b *types.Bundle, img *image.Image) (err error) {
return fmt.Errorf("could not extract root filesystem: %s", err)
}

s := unpacker.NewSquashfs()
s := unpacker.NewSquashfs(false)

// extract root filesystem
if err := s.ExtractAll(reader, b.RootfsPath); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/build/sources/packer_squashfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func (p *SquashfsPacker) Pack(context.Context) (*types.Bundle, error) {
return nil, fmt.Errorf("could not extract root filesystem: %s", err)
}

s := unpacker.NewSquashfs()
s := unpacker.NewSquashfs(false)

// extract root filesystem
if err := s.ExtractAll(reader, p.b.RootfsPath); err != nil {
Expand Down
21 changes: 13 additions & 8 deletions internal/pkg/image/unpacker/squashfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ const (
excludeDevRegex = `^(.{0}[^d]|.{1}[^e]|.{2}[^v]|.{3}[^\x2f]).*$`
)

var cmdFunc func(unsquashfs string, dest string, filename string, filter string, opts ...string) (*exec.Cmd, error)
var cmdFunc func(squashfs *Squashfs, dest string, filename string, filter string, opts ...string) (*exec.Cmd, error)

// unsquashfsCmd is the command instance for executing unsquashfs command
// in a non sandboxed environment when this package is used for unit tests.
func unsquashfsCmd(unsquashfs string, dest string, filename string, filter string, opts ...string) (*exec.Cmd, error) {
func unsquashfsCmd(squashfs *Squashfs, dest string, filename string, filter string, opts ...string) (*exec.Cmd, error) {
args := []string{}
args = append(args, opts...)
// remove the destination directory if any, if the directory is
Expand All @@ -49,18 +49,23 @@ func unsquashfsCmd(unsquashfs string, dest string, filename string, filter strin
args = append(args, filter)
}

sylog.Debugf("Calling %s %v", unsquashfs, args)
return exec.Command(unsquashfs, args...), nil
sylog.Debugf("Calling %s %v", squashfs.UnsquashfsPath, args)
return exec.Command(squashfs.UnsquashfsPath, args...), nil
}

// Squashfs represents a squashfs unpacker.
type Squashfs struct {
// Path to the unsquashfs executable
UnsquashfsPath string
// ForceUserns sets --userns when unsquashfs is being run wrapped by singularity.
ForceUserns bool
}

// NewSquashfs initializes and returns a Squahfs unpacker instance
func NewSquashfs() *Squashfs {
s := &Squashfs{}
func NewSquashfs(userns bool) *Squashfs {
s := &Squashfs{
ForceUserns: userns,
}
s.UnsquashfsPath, _ = bin.FindBin("unsquashfs")
return s
}
Expand Down Expand Up @@ -112,7 +117,7 @@ func (s *Squashfs) extract(files []string, reader io.Reader, dest string) (err e
if err != nil {
return fmt.Errorf("could not get host UID: %s", err)
}
rootless := hostuid != 0
rootless := s.ForceUserns || hostuid != 0

// Does our target filesystem support user xattrs?
ok, err := TestUserXattr(filepath.Dir(dest))
Expand Down Expand Up @@ -155,7 +160,7 @@ func (s *Squashfs) extract(files []string, reader io.Reader, dest string) (err e

// Now run unsquashfs with our 'best' options
sylog.Debugf("Trying unsquashfs options: %v", opts)
cmd, err := cmdFunc(s.UnsquashfsPath, dest, filename, filter, opts...)
cmd, err := cmdFunc(s, dest, filename, filter, opts...)
if err != nil {
return fmt.Errorf("command error: %s", err)
}
Expand Down
15 changes: 8 additions & 7 deletions internal/pkg/image/unpacker/squashfs_singularity.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func parseLibraryBinds(buf io.Reader) ([]libBind, error) {

// unsquashfsSandboxCmd is the command instance for executing unsquashfs command
// in a sandboxed environment with singularity.
func unsquashfsSandboxCmd(unsquashfs string, dest string, filename string, filter string, opts ...string) (*exec.Cmd, error) {
func unsquashfsSandboxCmd(squashfs *Squashfs, dest string, filename string, filter string, opts ...string) (*exec.Cmd, error) {
const (
// will contain both dest and filename inside the sandbox
rootfsImageDir = "/image"
Expand Down Expand Up @@ -186,9 +186,6 @@ func unsquashfsSandboxCmd(unsquashfs string, dest string, filename string, filte
}
}

// the decision to use user namespace is left to singularity
// which will detect automatically depending of the configuration
// what workflow it could use
args := []string{
"exec",
"--no-home",
Expand All @@ -200,16 +197,20 @@ func unsquashfsSandboxCmd(unsquashfs string, dest string, filename string, filte
"-B", fmt.Sprintf("%s:%s", tmpdir, rootfsImageDir),
}

if squashfs.ForceUserns {
args = append(args, "--userns")
}

if filename != stdinFile {
filename = filepath.Join(rootfsImageDir, filepath.Base(filename))
}

roFiles := []string{
unsquashfs,
squashfs.UnsquashfsPath,
}

// get the library dependencies of unsquashfs
libs, err := getLibraryBinds(unsquashfs)
libs, err := getLibraryBinds(squashfs.UnsquashfsPath)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -262,7 +263,7 @@ func unsquashfsSandboxCmd(unsquashfs string, dest string, filename string, filte
args = append(args, rootfs)

// unsquashfs execution arguments
args = append(args, unsquashfs)
args = append(args, squashfs.UnsquashfsPath)
args = append(args, opts...)

if overwrite {
Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/image/unpacker/squashfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestSquashfs(t *testing.T) {
}

func testSquashfs(t *testing.T, tmpParent string) {
s := NewSquashfs()
s := NewSquashfs(false)

if !s.HasUnsquashfs() {
t.Skip("unsquashfs not found")
Expand Down
4 changes: 3 additions & 1 deletion internal/pkg/ociimage/unpack.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/opencontainers/umoci/pkg/idtools"
"github.com/sylabs/singularity/v4/internal/pkg/util/fs"
"github.com/sylabs/singularity/v4/pkg/sylog"
"github.com/sylabs/singularity/v4/pkg/util/namespaces"
)

// isExtractable checks if we have extractable layers in the image. Shouldn't be
Expand Down Expand Up @@ -69,7 +70,8 @@ func UnpackRootfs(_ context.Context, srcImage v1.Image, destDir string) (err err
}

// Allow unpacking as non-root
if os.Geteuid() != 0 {
insideUserNs, _ := namespaces.IsInsideUserNamespace(os.Getpid())
if os.Geteuid() != 0 || insideUserNs {
mapOptions.Rootless = true

uidMap, err := idtools.ParseMapping(fmt.Sprintf("0:%d:1", os.Geteuid()))
Expand Down
18 changes: 13 additions & 5 deletions internal/pkg/runtime/launcher/native/launcher_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -1074,7 +1074,14 @@ func (l *Launcher) prepareSquashfs(ctx context.Context, img *imgutil.Image, tryF
sylog.Warningf("--writable applies to temporary sandbox only, changes will not be written to the original image.")
}

err = extractImage(img, imageDir)
// Due to path traversal issues in older unsquashfs versions, we run it
// wrapped under singularity. If the user has requested --userns/-u then
// that wrapping should also use a user namespace (to support
// container/namespace nesting). An exception is when running as root. As
// root, unsquashfs would attempt chown and fail with the single uid/gid
// mapping.
extractUserns := l.cfg.Namespaces.User && os.Getuid() != 0
err = extractImage(img, imageDir, extractUserns)
if err == nil {
l.engineConfig.SetImage(imageDir)
l.engineConfig.SetDeleteTempDir(tempDir)
Expand Down Expand Up @@ -1165,9 +1172,10 @@ func mkContainerDirs() (tempDir, imageDir string, err error) {
}

// extractImage extracts img to directory dir within a temporary directory
// tempDir. It is the caller's responsibility to remove tempDir
// when no longer needed.
func extractImage(img *imgutil.Image, imageDir string) error {
// tempDir. It is the caller's responsibility to remove tempDir when no longer
// needed. If userns is true, then where unsquashfs is wrapped with singularity,
// a user namespace will be used.
func extractImage(img *imgutil.Image, imageDir string, userns bool) error {
sylog.Infof("Converting SIF file to temporary sandbox...")
unsquashfsPath, err := bin.FindBin("unsquashfs")
if err != nil {
Expand All @@ -1179,7 +1187,7 @@ func extractImage(img *imgutil.Image, imageDir string) error {
if err != nil {
return fmt.Errorf("could not extract root filesystem: %s", err)
}
s := unpacker.NewSquashfs()
s := unpacker.NewSquashfs(userns)
if !s.HasUnsquashfs() && unsquashfsPath != "" {
s.UnsquashfsPath = unsquashfsPath
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/image/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func checkPartition(t *testing.T, reader io.Reader) error {
extracted := "/bin/busybox"
dir := t.TempDir()

s := unpacker.NewSquashfs()
s := unpacker.NewSquashfs(false)
if s.HasUnsquashfs() {
if err := s.ExtractFiles([]string{extracted}, reader, dir); err != nil {
return fmt.Errorf("extraction failed: %s", err)
Expand Down