Skip to content

Commit eba3dea

Browse files
authored
Merge pull request #5611 from dctrud/36-sec
Merge GHSA-7gcp-w6ww-2xv9 fixes for release 3.6.4
2 parents d603455 + 34b3192 commit eba3dea

File tree

6 files changed

+304
-13
lines changed

6 files changed

+304
-13
lines changed

Diff for: CHANGELOG.md

+14-2
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,23 @@ _With the release of `v3.0.0`, we're introducing a new changelog format in an at
99

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

12-
# Changes since v3.6.3
12+
# v3.6.4 - [2020-10-13]
13+
14+
## Security related fixes
15+
16+
Singularity 3.6.4 addresses the following security issue.
17+
18+
- [CVE-2020-15229](https://github.com/hpcng/singularity/security/advisories/GHSA-7gcp-w6ww-2xv9):
19+
Due to insecure handling of path traversal and the lack of path
20+
sanitization within unsquashfs (a distribution provided utility
21+
used by Singularity), it is possible to overwrite/create files on
22+
the host filesystem during the extraction of a crafted squashfs
23+
filesystem. Affects unprivileged execution of SIF / SquashFS
24+
images, and image builds from SIF / SquashFS images.
1325

1426
## Bug Fixes
1527

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

Diff for: INSTALL.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ $ mkdir -p ${GOPATH}/src/github.com/sylabs && \
8989
To build a stable version of Singularity, check out a [release tag](https://github.com/sylabs/singularity/tags) before compiling:
9090

9191
```
92-
$ git checkout v3.6.3
92+
$ git checkout v3.6.4
9393
```
9494

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

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

154154
To build an rpm with an alternative install prefix set RPMPREFIX on the

Diff for: pkg/image/unpacker/squashfs.go

+41-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Copyright (c) 2020, Control Command Inc. All rights reserved.
12
// Copyright (c) 2019, Sylabs Inc. All rights reserved.
23
// This software is licensed under a 3-clause BSD license. Please consult the
34
// LICENSE.md file distributed with the sources of this project regarding your
@@ -17,6 +18,33 @@ import (
1718
"github.com/sylabs/singularity/pkg/sylog"
1819
)
1920

21+
const (
22+
stdinFile = "/proc/self/fd/0"
23+
)
24+
25+
var cmdFunc func(unsquashfs string, dest string, filename string, rootless bool) (*exec.Cmd, error)
26+
27+
// unsquashfsCmd is the command instance for executing unsquashfs command
28+
// in a non sandboxed environment when this package is used for unit tests.
29+
func unsquashfsCmd(unsquashfs string, dest string, filename string, rootless bool) (*exec.Cmd, error) {
30+
args := make([]string, 0)
31+
if rootless {
32+
args = append(args, "-user-xattrs")
33+
}
34+
// remove the destination directory if any, if the directory is
35+
// not empty (typically during image build), the unsafe option -f is
36+
// set, this is unfortunately required by image build
37+
if err := os.Remove(dest); err != nil && !os.IsNotExist(err) {
38+
if !os.IsExist(err) {
39+
return nil, fmt.Errorf("failed to remove %s: %s", dest, err)
40+
}
41+
// unsafe mode
42+
args = append(args, "-f")
43+
}
44+
args = append(args, "-d", dest, filename)
45+
return exec.Command(unsquashfs, args...), nil
46+
}
47+
2048
// Squashfs represents a squashfs unpacker.
2149
type Squashfs struct {
2250
UnsquashfsPath string
@@ -41,7 +69,7 @@ func (s *Squashfs) extract(files []string, reader io.Reader, dest string) error
4169

4270
// pipe over stdin by default
4371
stdin := true
44-
filename := "/proc/self/fd/0"
72+
filename := stdinFile
4573

4674
if _, ok := reader.(*os.File); !ok {
4775
// use the destination parent directory to store the
@@ -71,9 +99,12 @@ func (s *Squashfs) extract(files []string, reader io.Reader, dest string) error
7199
// have to fall back to not using that option on failure.
72100
if os.Geteuid() != 0 {
73101
sylog.Debugf("Rootless extraction. Trying -user-xattrs for unsquashfs")
74-
args := []string{"-user-xattrs", "-f", "-d", dest, filename}
75-
args = append(args, files...)
76-
cmd := exec.Command(s.UnsquashfsPath, args...)
102+
103+
cmd, err := cmdFunc(s.UnsquashfsPath, dest, filename, true)
104+
if err != nil {
105+
return fmt.Errorf("command error: %s", err)
106+
}
107+
cmd.Args = append(cmd.Args, files...)
77108
if stdin {
78109
cmd.Stdin = reader
79110
}
@@ -85,17 +116,19 @@ func (s *Squashfs) extract(files []string, reader io.Reader, dest string) error
85116

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

96-
args := []string{"-f", "-d", dest, filename}
97-
args = append(args, files...)
98-
cmd := exec.Command(s.UnsquashfsPath, args...)
127+
cmd, err := cmdFunc(s.UnsquashfsPath, dest, filename, false)
128+
if err != nil {
129+
return fmt.Errorf("command error: %s", err)
130+
}
131+
cmd.Args = append(cmd.Args, files...)
99132
if stdin {
100133
cmd.Stdin = reader
101134
}

Diff for: pkg/image/unpacker/squashfs_no_singularity.go

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) 2020, Control Command Inc. All rights reserved.
2+
// This software is licensed under a 3-clause BSD license. Please consult the
3+
// LICENSE.md file distributed with the sources of this project regarding your
4+
// rights to use or distribute this software.
5+
6+
// +build !singularity_engine
7+
8+
package unpacker
9+
10+
func init() {
11+
cmdFunc = unsquashfsCmd
12+
}

Diff for: pkg/image/unpacker/squashfs_singularity.go

+228
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// Copyright (c) 2020, Control Command Inc. All rights reserved.
2+
// This software is licensed under a 3-clause BSD license. Please consult the
3+
// LICENSE.md file distributed with the sources of this project regarding your
4+
// rights to use or distribute this software.
5+
6+
// +build singularity_engine
7+
8+
package unpacker
9+
10+
import (
11+
"bytes"
12+
"debug/elf"
13+
"fmt"
14+
"io"
15+
"io/ioutil"
16+
"os"
17+
"os/exec"
18+
"path/filepath"
19+
"regexp"
20+
"strings"
21+
22+
"github.com/sylabs/singularity/internal/pkg/buildcfg"
23+
)
24+
25+
func init() {
26+
cmdFunc = unsquashfsSandboxCmd
27+
}
28+
29+
// getLibraries returns the libraries required by the elf binary,
30+
// the binary path must be absolute.
31+
func getLibraries(binary string) ([]string, error) {
32+
libs := make([]string, 0)
33+
34+
exe, err := elf.Open(binary)
35+
if err != nil {
36+
return nil, err
37+
}
38+
defer exe.Close()
39+
40+
interp := ""
41+
42+
// look for the interpreter
43+
for _, p := range exe.Progs {
44+
if p.Type != elf.PT_INTERP {
45+
continue
46+
}
47+
buf := make([]byte, 4096)
48+
n, err := p.ReadAt(buf, 0)
49+
if err != nil && err != io.EOF {
50+
return nil, err
51+
} else if n > cap(buf) {
52+
return nil, fmt.Errorf("buffer too small to store interpreter")
53+
}
54+
// trim null byte to avoid an execution failure with
55+
// an invalid argument error
56+
interp = string(bytes.Trim(buf, "\x00"))
57+
}
58+
59+
// this is a static binary, nothing to do
60+
if interp == "" {
61+
return libs, nil
62+
}
63+
64+
// run interpreter to list library dependencies for the
65+
// corresponding binary, eg:
66+
// /lib64/ld-linux-x86-64.so.2 --list <program>
67+
// /lib/ld-musl-x86_64.so.1 --list <program>
68+
errBuf := new(bytes.Buffer)
69+
buf := new(bytes.Buffer)
70+
71+
cmd := exec.Command(interp, "--list", binary)
72+
cmd.Stdout = buf
73+
cmd.Stderr = errBuf
74+
75+
if err := cmd.Run(); err != nil {
76+
return nil, fmt.Errorf("while getting library dependencies: %s\n%s", err, errBuf.String())
77+
}
78+
79+
// parse the output to get matches for ' /an/absolute/path ('
80+
re := regexp.MustCompile(`[[:blank:]]?(\/.*)[[:blank:]]\(`)
81+
82+
match := re.FindAllStringSubmatch(buf.String(), -1)
83+
for _, m := range match {
84+
if len(m) < 2 {
85+
continue
86+
}
87+
lib := m[1]
88+
has := false
89+
for _, l := range libs {
90+
if l == lib {
91+
has = true
92+
break
93+
}
94+
}
95+
if !has {
96+
libs = append(libs, lib)
97+
}
98+
}
99+
100+
return libs, nil
101+
}
102+
103+
// unsquashfsSandboxCmd is the command instance for executing unsquashfs command
104+
// in a sandboxed environment with singularity.
105+
func unsquashfsSandboxCmd(unsquashfs string, dest string, filename string, rootless bool) (*exec.Cmd, error) {
106+
const (
107+
// will contain both dest and filename inside the sandbox
108+
rootfsImageDir = "/image"
109+
)
110+
111+
// create the sandbox temporary directory
112+
tmpdir := filepath.Dir(dest)
113+
rootfs, err := ioutil.TempDir(tmpdir, "tmp-rootfs-")
114+
if err != nil {
115+
return nil, fmt.Errorf("failed to create chroot directory: %s", err)
116+
}
117+
118+
overwrite := false
119+
120+
// remove the destination directory if any, if the directory is
121+
// not empty (typically during image build), the unsafe option -f is
122+
// set, this is unfortunately required by image build
123+
if err := os.Remove(dest); err != nil && !os.IsNotExist(err) {
124+
if !os.IsExist(err) {
125+
return nil, fmt.Errorf("failed to remove %s: %s", dest, err)
126+
}
127+
overwrite = true
128+
}
129+
130+
// map destination into the sandbox
131+
rootfsDest := filepath.Join(rootfsImageDir, filepath.Base(dest))
132+
133+
// sandbox required directories
134+
rootfsDirs := []string{
135+
// unsquashfs get available CPU from /sys/devices/system/cpu/online
136+
filepath.Join(rootfs, "/sys"),
137+
filepath.Join(rootfs, "/dev"),
138+
filepath.Join(rootfs, rootfsImageDir),
139+
}
140+
141+
for _, d := range rootfsDirs {
142+
if err := os.Mkdir(d, 0700); err != nil {
143+
return nil, fmt.Errorf("while creating %s: %s", d, err)
144+
}
145+
}
146+
147+
// the decision to use user namespace is left to singularity
148+
// which will detect automatically depending of the configuration
149+
// what workflow it could use
150+
args := []string{
151+
"-q",
152+
"exec",
153+
"--no-home",
154+
"--no-nv",
155+
"--no-rocm",
156+
"-C",
157+
"--no-init",
158+
"--writable",
159+
"-B", fmt.Sprintf("%s:%s", tmpdir, rootfsImageDir),
160+
}
161+
162+
if filename != stdinFile {
163+
filename = filepath.Join(rootfsImageDir, filepath.Base(filename))
164+
}
165+
166+
// get the library dependencies of unsquashfs
167+
libs, err := getLibraries(unsquashfs)
168+
if err != nil {
169+
return nil, err
170+
}
171+
libraryPath := make([]string, 0)
172+
173+
roFiles := []string{
174+
unsquashfs,
175+
}
176+
177+
// add libraries for bind mount and also generate
178+
// LD_LIBRARY_PATH
179+
for _, l := range libs {
180+
dir := filepath.Dir(l)
181+
roFiles = append(roFiles, l)
182+
has := false
183+
for _, lp := range libraryPath {
184+
if lp == dir {
185+
has = true
186+
break
187+
}
188+
}
189+
if !has {
190+
libraryPath = append(libraryPath, dir)
191+
}
192+
}
193+
194+
// create files and directories in the sandbox and
195+
// add singularity bind mount options
196+
for _, b := range roFiles {
197+
file := filepath.Join(rootfs, b)
198+
dir := filepath.Dir(file)
199+
if err := os.MkdirAll(dir, 0700); err != nil {
200+
return nil, fmt.Errorf("while creating %s: %s", dir, err)
201+
}
202+
if err := ioutil.WriteFile(file, []byte(""), 0600); err != nil {
203+
return nil, fmt.Errorf("while creating %s: %s", file, err)
204+
}
205+
args = append(args, "-B", fmt.Sprintf("%s:%s:ro", b, b))
206+
}
207+
208+
// singularity sandbox
209+
args = append(args, rootfs)
210+
211+
// unsquashfs execution arguments
212+
args = append(args, unsquashfs)
213+
if rootless {
214+
args = append(args, "-user-xattrs")
215+
}
216+
if overwrite {
217+
args = append(args, "-f")
218+
}
219+
args = append(args, "-d", rootfsDest, filename)
220+
221+
cmd := exec.Command(filepath.Join(buildcfg.BINDIR, "singularity"), args...)
222+
cmd.Dir = "/"
223+
cmd.Env = []string{
224+
fmt.Sprintf("LD_LIBRARY_PATH=%s", strings.Join(libraryPath, string(os.PathListSeparator))),
225+
}
226+
227+
return cmd, nil
228+
}

Diff for: pkg/image/unpacker/squashfs_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Copyright (c) 2020, Control Command Inc. All rights reserved.
12
// Copyright (c) 2019, Sylabs Inc. All rights reserved.
23
// This software is licensed under a 3-clause BSD license. Please consult the
34
// LICENSE.md file distributed with the sources of this project regarding your
@@ -106,3 +107,8 @@ func TestSquashfs(t *testing.T) {
106107
t.Errorf("file extraction failed, %s is missing", path)
107108
}
108109
}
110+
111+
func TestMain(m *testing.M) {
112+
cmdFunc = unsquashfsCmd
113+
os.Exit(m.Run())
114+
}

0 commit comments

Comments
 (0)