1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
|
// Copyright (c) 2020, Control Command Inc. All rights reserved.
// Copyright (c) 2019-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE.md file distributed with the sources of this project regarding your
// rights to use or distribute this software.
package unpacker
import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"github.com/sylabs/singularity/v4/internal/pkg/util/bin"
"github.com/sylabs/singularity/v4/pkg/sylog"
"github.com/sylabs/singularity/v4/pkg/util/namespaces"
"golang.org/x/sys/unix"
)
const (
stdinFile = "/proc/self/fd/0"
// exclude 'dev/' directory from extraction for non root users
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)
// 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) {
args := []string{}
args = append(args, opts...)
// remove the destination directory if any, if the directory is
// not empty (typically during image build), the unsafe option -f is
// set, this is unfortunately required by image build
if err := os.Remove(dest); err != nil && !os.IsNotExist(err) {
if !os.IsExist(err) {
return nil, fmt.Errorf("failed to remove %s: %s", dest, err)
}
// unsafe mode
args = append(args, "-f")
}
args = append(args, "-d", dest, filename)
if filter != "" {
args = append(args, filter)
}
sylog.Debugf("Calling %s %v", unsquashfs, args)
return exec.Command(unsquashfs, args...), nil
}
// Squashfs represents a squashfs unpacker.
type Squashfs struct {
UnsquashfsPath string
}
// NewSquashfs initializes and returns a Squahfs unpacker instance
func NewSquashfs() *Squashfs {
s := &Squashfs{}
s.UnsquashfsPath, _ = bin.FindBin("unsquashfs")
return s
}
// HasUnsquashfs returns if unsquashfs binary has been found or not
func (s *Squashfs) HasUnsquashfs() bool {
return s.UnsquashfsPath != ""
}
func (s *Squashfs) extract(files []string, reader io.Reader, dest string) (err error) {
if !s.HasUnsquashfs() {
return fmt.Errorf("could not extract squashfs data, unsquashfs not found")
}
// pipe over stdin by default
stdin := true
filename := stdinFile
if _, ok := reader.(*os.File); !ok {
// use the destination parent directory to store the
// temporary archive
tmpdir := filepath.Dir(dest)
// unsquashfs doesn't support to send file content over
// a stdin pipe since it use lseek for every read it does
tmp, err := os.CreateTemp(tmpdir, "archive-")
if err != nil {
return fmt.Errorf("failed to create staging file: %s", err)
}
filename = tmp.Name()
stdin = false
defer os.Remove(filename)
if _, err := io.Copy(tmp, reader); err != nil {
return fmt.Errorf("failed to copy content in staging file: %s", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("failed to close staging file: %s", err)
}
}
// First we try unsquashfs with appropriate xattr options. If we are in
// rootless mode we need "-user-xattrs" so we don't try to set system xattrs
// that require root. However we must check (user) xattrs are supported on
// the FS as unsquashfs >=4.4 will give a non-zero error code if it cannot
// set them, e.g. on tmpfs (#5668)
opts := []string{}
hostuid, err := namespaces.HostUID()
if err != nil {
return fmt.Errorf("could not get host UID: %s", err)
}
rootless := hostuid != 0
// Does our target filesystem support user xattrs?
ok, err := TestUserXattr(filepath.Dir(dest))
if err != nil {
return err
}
// If we are in rootless mode & we support user xattrs, set -user-xattrs so that user xattrs are extracted, but
// system xattrs are ignored (needs root).
if ok && rootless {
opts = append(opts, "-user-xattrs")
}
// If user-xattrs aren't supported we need to disable setting of all xattrs.
if !ok {
opts = append(opts, "-no-xattrs")
}
// non real root users could not create pseudo devices so we compare
// the host UID (to include fake root user) and apply a filter at extraction (#5690)
filter := ""
// exclude dev directory only if there no specific files provided for extraction
// as globbing won't work with POSIX regex enabled
if rootless && len(files) == 0 {
sylog.Debugf("Excluding /dev directory during root filesystem extraction (non root user)")
// filter requires POSIX regex
opts = append(opts, "-r")
filter = excludeDevRegex
}
defer func() {
if err != nil || filter == "" {
return
}
// create $rootfs/dev as it has been excluded
rootfsDev := filepath.Join(dest, "dev")
devErr := os.Mkdir(rootfsDev, 0o755)
if devErr != nil && !os.IsExist(devErr) {
err = fmt.Errorf("could not create %s: %s", rootfsDev, devErr)
}
}()
// Now run unsquashfs with our 'best' options
sylog.Debugf("Trying unsquashfs options: %v", opts)
cmd, err := cmdFunc(s.UnsquashfsPath, dest, filename, filter, opts...)
if err != nil {
return fmt.Errorf("command error: %s", err)
}
cmd.Args = append(cmd.Args, files...)
if stdin {
cmd.Stdin = reader
}
o, err := cmd.CombinedOutput()
if os.Getenv("SINGULARITY_DEBUG") != "" {
sylog.Debugf("*** BEGIN WRAPPED UNSQUASHFS OUTPUT ***")
sylog.Debugf(string(o))
sylog.Debugf("*** END WRAPPED UNSQUASHFS OUTPUT ***")
}
if err != nil {
return fmt.Errorf("extract command failed: %s: %s", string(o), err)
}
return nil
}
// ExtractAll extracts a squashfs filesystem read from reader to a
// destination directory.
func (s *Squashfs) ExtractAll(reader io.Reader, dest string) error {
return s.extract(nil, reader, dest)
}
// ExtractFiles extracts provided files from a squashfs filesystem
// read from reader to a destination directory.
func (s *Squashfs) ExtractFiles(files []string, reader io.Reader, dest string) error {
if len(files) == 0 {
return fmt.Errorf("no files to extract")
}
return s.extract(files, reader, dest)
}
// TestUserXattr tries to set a user xattr on PATH to ensure they are supported on this fs
func TestUserXattr(path string) (ok bool, err error) {
tmp, err := os.CreateTemp(path, "uxattr-")
defer os.Remove(tmp.Name())
tmp.Close()
err = unix.Setxattr(tmp.Name(), "user.singularity", []byte{}, 0)
if err == unix.ENOTSUP || err == unix.EOPNOTSUPP {
return false, nil
} else if err != nil {
return false, fmt.Errorf("while testing user xattr support at %s: %v", tmp.Name(), err)
}
return true, nil
}
|