Skip to content

Commit

Permalink
Merge pull request #5611 from dctrud/36-sec
Browse files Browse the repository at this point in the history
Merge GHSA-7gcp-w6ww-2xv9 fixes for release 3.6.4
  • Loading branch information
dtrudg authored Oct 13, 2020
2 parents d603455 + 34b3192 commit eba3dea
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 13 deletions.
16 changes: 14 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,23 @@ _With the release of `v3.0.0`, we're introducing a new changelog format in an at

_The old changelog can be found in the `release-2.6` branch_

# Changes since v3.6.3
# v3.6.4 - [2020-10-13]

## Security related fixes

Singularity 3.6.4 addresses the following security issue.

- [CVE-2020-15229](https://github.com/hpcng/singularity/security/advisories/GHSA-7gcp-w6ww-2xv9):
Due to insecure handling of path traversal and the lack of path
sanitization within unsquashfs (a distribution provided utility
used by Singularity), it is possible to overwrite/create files on
the host filesystem during the extraction of a crafted squashfs
filesystem. Affects unprivileged execution of SIF / SquashFS
images, and image builds from SIF / SquashFS images.

## Bug Fixes

- Update scs-library-client to support library:// backends using an
- Update scs-library-client to support `library://` backends using an
3rd party S3 object store that does not strictly conform to v4
signature spec.

Expand Down
6 changes: 3 additions & 3 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ $ mkdir -p ${GOPATH}/src/github.com/sylabs && \
To build a stable version of Singularity, check out a [release tag](https://github.com/sylabs/singularity/tags) before compiling:

```
$ git checkout v3.6.3
$ git checkout v3.6.4
```

## Compiling Singularity
Expand Down Expand Up @@ -132,7 +132,7 @@ as shown above. Then download the latest
and use it to install the RPM like this:

```
$ export VERSION=3.6.3 # this is the singularity version, change as you need
$ export VERSION=3.6.4 # this is the singularity version, change as you need
$ wget https://github.com/sylabs/singularity/releases/download/v${VERSION}/singularity-${VERSION}.tar.gz && \
rpmbuild -tb singularity-${VERSION}.tar.gz && \
Expand All @@ -148,7 +148,7 @@ tarball and use it to install Singularity:
$ cd $GOPATH/src/github.com/sylabs/singularity && \
./mconfig && \
make -C builddir rpm && \
sudo rpm -ivh ~/rpmbuild/RPMS/x86_64/singularity-3.6.2*.x86_64.rpm # or whatever version you built
sudo rpm -ivh ~/rpmbuild/RPMS/x86_64/singularity-3.6.4*.x86_64.rpm # or whatever version you built
```

To build an rpm with an alternative install prefix set RPMPREFIX on the
Expand Down
49 changes: 41 additions & 8 deletions pkg/image/unpacker/squashfs.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Copyright (c) 2020, Control Command Inc. All rights reserved.
// Copyright (c) 2019, 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
Expand All @@ -17,6 +18,33 @@ import (
"github.com/sylabs/singularity/pkg/sylog"
)

const (
stdinFile = "/proc/self/fd/0"
)

var cmdFunc func(unsquashfs string, dest string, filename string, rootless bool) (*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, rootless bool) (*exec.Cmd, error) {
args := make([]string, 0)
if rootless {
args = append(args, "-user-xattrs")
}
// 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)
return exec.Command(unsquashfs, args...), nil
}

// Squashfs represents a squashfs unpacker.
type Squashfs struct {
UnsquashfsPath string
Expand All @@ -41,7 +69,7 @@ func (s *Squashfs) extract(files []string, reader io.Reader, dest string) error

// pipe over stdin by default
stdin := true
filename := "/proc/self/fd/0"
filename := stdinFile

if _, ok := reader.(*os.File); !ok {
// use the destination parent directory to store the
Expand Down Expand Up @@ -71,9 +99,12 @@ func (s *Squashfs) extract(files []string, reader io.Reader, dest string) error
// have to fall back to not using that option on failure.
if os.Geteuid() != 0 {
sylog.Debugf("Rootless extraction. Trying -user-xattrs for unsquashfs")
args := []string{"-user-xattrs", "-f", "-d", dest, filename}
args = append(args, files...)
cmd := exec.Command(s.UnsquashfsPath, args...)

cmd, err := cmdFunc(s.UnsquashfsPath, dest, filename, true)
if err != nil {
return fmt.Errorf("command error: %s", err)
}
cmd.Args = append(cmd.Args, files...)
if stdin {
cmd.Stdin = reader
}
Expand All @@ -85,17 +116,19 @@ func (s *Squashfs) extract(files []string, reader io.Reader, dest string) error

// Invalid options give output...
// SYNTAX: unsquashfs [options] filesystem [directories or files to extract]
if bytes.HasPrefix(o, []byte("SYNTAX")) {
if bytes.Contains(o, []byte("SYNTAX")) {
sylog.Warningf("unsquashfs does not support -user-xattrs. Images with system xattrs may fail to extract")
} else {
// A different error is fatal
return fmt.Errorf("extract command failed: %s: %s", string(o), err)
}
}

args := []string{"-f", "-d", dest, filename}
args = append(args, files...)
cmd := exec.Command(s.UnsquashfsPath, args...)
cmd, err := cmdFunc(s.UnsquashfsPath, dest, filename, false)
if err != nil {
return fmt.Errorf("command error: %s", err)
}
cmd.Args = append(cmd.Args, files...)
if stdin {
cmd.Stdin = reader
}
Expand Down
12 changes: 12 additions & 0 deletions pkg/image/unpacker/squashfs_no_singularity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) 2020, Control Command 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.

// +build !singularity_engine

package unpacker

func init() {
cmdFunc = unsquashfsCmd
}
228 changes: 228 additions & 0 deletions pkg/image/unpacker/squashfs_singularity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// Copyright (c) 2020, Control Command 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.

// +build singularity_engine

package unpacker

import (
"bytes"
"debug/elf"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"

"github.com/sylabs/singularity/internal/pkg/buildcfg"
)

func init() {
cmdFunc = unsquashfsSandboxCmd
}

// getLibraries returns the libraries required by the elf binary,
// the binary path must be absolute.
func getLibraries(binary string) ([]string, error) {
libs := make([]string, 0)

exe, err := elf.Open(binary)
if err != nil {
return nil, err
}
defer exe.Close()

interp := ""

// look for the interpreter
for _, p := range exe.Progs {
if p.Type != elf.PT_INTERP {
continue
}
buf := make([]byte, 4096)
n, err := p.ReadAt(buf, 0)
if err != nil && err != io.EOF {
return nil, err
} else if n > cap(buf) {
return nil, fmt.Errorf("buffer too small to store interpreter")
}
// trim null byte to avoid an execution failure with
// an invalid argument error
interp = string(bytes.Trim(buf, "\x00"))
}

// this is a static binary, nothing to do
if interp == "" {
return libs, nil
}

// run interpreter to list library dependencies for the
// corresponding binary, eg:
// /lib64/ld-linux-x86-64.so.2 --list <program>
// /lib/ld-musl-x86_64.so.1 --list <program>
errBuf := new(bytes.Buffer)
buf := new(bytes.Buffer)

cmd := exec.Command(interp, "--list", binary)
cmd.Stdout = buf
cmd.Stderr = errBuf

if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("while getting library dependencies: %s\n%s", err, errBuf.String())
}

// parse the output to get matches for ' /an/absolute/path ('
re := regexp.MustCompile(`[[:blank:]]?(\/.*)[[:blank:]]\(`)

match := re.FindAllStringSubmatch(buf.String(), -1)
for _, m := range match {
if len(m) < 2 {
continue
}
lib := m[1]
has := false
for _, l := range libs {
if l == lib {
has = true
break
}
}
if !has {
libs = append(libs, lib)
}
}

return libs, nil
}

// unsquashfsSandboxCmd is the command instance for executing unsquashfs command
// in a sandboxed environment with singularity.
func unsquashfsSandboxCmd(unsquashfs string, dest string, filename string, rootless bool) (*exec.Cmd, error) {
const (
// will contain both dest and filename inside the sandbox
rootfsImageDir = "/image"
)

// create the sandbox temporary directory
tmpdir := filepath.Dir(dest)
rootfs, err := ioutil.TempDir(tmpdir, "tmp-rootfs-")
if err != nil {
return nil, fmt.Errorf("failed to create chroot directory: %s", err)
}

overwrite := false

// 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)
}
overwrite = true
}

// map destination into the sandbox
rootfsDest := filepath.Join(rootfsImageDir, filepath.Base(dest))

// sandbox required directories
rootfsDirs := []string{
// unsquashfs get available CPU from /sys/devices/system/cpu/online
filepath.Join(rootfs, "/sys"),
filepath.Join(rootfs, "/dev"),
filepath.Join(rootfs, rootfsImageDir),
}

for _, d := range rootfsDirs {
if err := os.Mkdir(d, 0700); err != nil {
return nil, fmt.Errorf("while creating %s: %s", d, err)
}
}

// 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{
"-q",
"exec",
"--no-home",
"--no-nv",
"--no-rocm",
"-C",
"--no-init",
"--writable",
"-B", fmt.Sprintf("%s:%s", tmpdir, rootfsImageDir),
}

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

// get the library dependencies of unsquashfs
libs, err := getLibraries(unsquashfs)
if err != nil {
return nil, err
}
libraryPath := make([]string, 0)

roFiles := []string{
unsquashfs,
}

// add libraries for bind mount and also generate
// LD_LIBRARY_PATH
for _, l := range libs {
dir := filepath.Dir(l)
roFiles = append(roFiles, l)
has := false
for _, lp := range libraryPath {
if lp == dir {
has = true
break
}
}
if !has {
libraryPath = append(libraryPath, dir)
}
}

// create files and directories in the sandbox and
// add singularity bind mount options
for _, b := range roFiles {
file := filepath.Join(rootfs, b)
dir := filepath.Dir(file)
if err := os.MkdirAll(dir, 0700); err != nil {
return nil, fmt.Errorf("while creating %s: %s", dir, err)
}
if err := ioutil.WriteFile(file, []byte(""), 0600); err != nil {
return nil, fmt.Errorf("while creating %s: %s", file, err)
}
args = append(args, "-B", fmt.Sprintf("%s:%s:ro", b, b))
}

// singularity sandbox
args = append(args, rootfs)

// unsquashfs execution arguments
args = append(args, unsquashfs)
if rootless {
args = append(args, "-user-xattrs")
}
if overwrite {
args = append(args, "-f")
}
args = append(args, "-d", rootfsDest, filename)

cmd := exec.Command(filepath.Join(buildcfg.BINDIR, "singularity"), args...)
cmd.Dir = "/"
cmd.Env = []string{
fmt.Sprintf("LD_LIBRARY_PATH=%s", strings.Join(libraryPath, string(os.PathListSeparator))),
}

return cmd, nil
}
6 changes: 6 additions & 0 deletions pkg/image/unpacker/squashfs_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Copyright (c) 2020, Control Command Inc. All rights reserved.
// Copyright (c) 2019, 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
Expand Down Expand Up @@ -106,3 +107,8 @@ func TestSquashfs(t *testing.T) {
t.Errorf("file extraction failed, %s is missing", path)
}
}

func TestMain(m *testing.M) {
cmdFunc = unsquashfsCmd
os.Exit(m.Run())
}

0 comments on commit eba3dea

Please sign in to comment.