Skip to content

Commit

Permalink
Merge pull request #2699 from dtrudg/unshare-fixes
Browse files Browse the repository at this point in the history
fix: Address layer / image extraction issues in user namespaces
dtrudg authored Mar 4, 2024
2 parents 4dd319e + 829c9fe commit 5db897a
Showing 9 changed files with 46 additions and 25 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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\]

2 changes: 1 addition & 1 deletion internal/pkg/build/sources/packer_sif.go
Original file line number Diff line number Diff line change
@@ -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 {
2 changes: 1 addition & 1 deletion internal/pkg/build/sources/packer_squashfs.go
Original file line number Diff line number Diff line change
@@ -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 {
21 changes: 13 additions & 8 deletions internal/pkg/image/unpacker/squashfs.go
Original file line number Diff line number Diff line change
@@ -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
@@ -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
}
@@ -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))
@@ -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)
}
15 changes: 8 additions & 7 deletions internal/pkg/image/unpacker/squashfs_singularity.go
Original file line number Diff line number Diff line change
@@ -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"
@@ -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",
@@ -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
}
@@ -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 {
2 changes: 1 addition & 1 deletion internal/pkg/image/unpacker/squashfs_test.go
Original file line number Diff line number Diff line change
@@ -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")
4 changes: 3 additions & 1 deletion internal/pkg/ociimage/unpack.go
Original file line number Diff line number Diff line change
@@ -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
@@ -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()))
18 changes: 13 additions & 5 deletions internal/pkg/runtime/launcher/native/launcher_linux.go
Original file line number Diff line number Diff line change
@@ -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)
@@ -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 {
@@ -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
}
2 changes: 1 addition & 1 deletion pkg/image/image_test.go
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 5db897a

Please sign in to comment.