Skip to content

Commit

Permalink
fix: honor --userns in unsquashfs wrapping
Browse files Browse the repository at this point in the history
If singularity is executed with `--userns/-u` then where possible it
should also use a user namespace where it executes `unsquashfs` in a
wrapped manner.

Previously the `unsquashfs` wrapping was without `--userns/-u` in a
setuid installation. This caused extraction to fail from within a
non-root-mapped user namespace (e.g. `unshare -c`).

Part of #2698
  • Loading branch information
dtrudg committed Mar 1, 2024
1 parent 877b762 commit 829c9fe
Show file tree
Hide file tree
Showing 8 changed files with 42 additions and 25 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +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
- 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
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

0 comments on commit 829c9fe

Please sign in to comment.