Skip to content

Commit 98ad49a

Browse files
authored
Merge pull request #5575 from dctrud/3.6.3-security-local
Merge tempdir security fixes to release-3.6
2 parents 505a4b5 + 74f0352 commit 98ad49a

File tree

8 files changed

+115
-50
lines changed

8 files changed

+115
-50
lines changed

Diff for: CHANGELOG.md

+25-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,31 @@ _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.2
12+
# v3.6.3 - [2020-09-15]
13+
14+
## Security related fixes
15+
16+
Singularity 3.6.3 addresses the following security issues.
17+
18+
- [CVE-2020-25039](https://github.com/hpcng/singularity/security/advisories/GHSA-w6v2-qchm-grj7):
19+
When a Singularity action command (run, shell, exec) is run with
20+
the fakeroot or user namespace option, Singularity will extract a
21+
container image to a temporary sandbox directory. Due to insecure
22+
permissions on the temporary directory it is possible for any user
23+
with access to the system to read the contents of the
24+
image. Additionally, if the image contains a world-writable file
25+
or directory, it is possible for a user to inject arbitrary
26+
content into the running container.
27+
28+
- [CVE-2020-25040](https://github.com/hpcng/singularity/security/advisories/GHSA-jv9c-w74q-6762):
29+
When a Singularity command that results in a container build
30+
operation is executed, it is possible for a user with access to
31+
the system to read the contents of the image during the
32+
build. Additionally, if the image contains a world-writable file
33+
or directory, it is possible for a user to inject arbitrary
34+
content into the running build, which in certain circumstances may
35+
enable arbitrary code execution during the build and/or when the
36+
built container is run.
1337

1438
## Bug Fixes
1539

Diff for: INSTALL.md

+2-2
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.2
92+
$ git checkout v3.6.3
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.2 # this is the singularity version, change as you need
135+
$ export VERSION=3.6.3 # 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 && \

Diff for: cmd/internal/cli/actions_linux.go

+28-15
Original file line numberDiff line numberDiff line change
@@ -45,27 +45,30 @@ import (
4545
"golang.org/x/sys/unix"
4646
)
4747

48-
func convertImage(filename string, unsquashfsPath string) (string, error) {
48+
// convertImage extracts the image found at filename to directory dir within a temporary directory
49+
// tempDir. If the unsquashfs binary is not located, the binary at unsquashfsPath is used. It is
50+
// the caller's responsibility to remove tempDir when no longer needed.
51+
func convertImage(filename string, unsquashfsPath string) (tempDir, imageDir string, err error) {
4952
img, err := imgutil.Init(filename, false)
5053
if err != nil {
51-
return "", fmt.Errorf("could not open image %s: %s", filename, err)
54+
return "", "", fmt.Errorf("could not open image %s: %s", filename, err)
5255
}
5356
defer img.File.Close()
5457

5558
part, err := img.GetRootFsPartition()
5659
if err != nil {
57-
return "", fmt.Errorf("while getting root filesystem in %s: %s", filename, err)
60+
return "", "", fmt.Errorf("while getting root filesystem in %s: %s", filename, err)
5861
}
5962

6063
// squashfs only
6164
if part.Type != imgutil.SQUASHFS {
62-
return "", fmt.Errorf("not a squashfs root filesystem")
65+
return "", "", fmt.Errorf("not a squashfs root filesystem")
6366
}
6467

6568
// create a reader for rootfs partition
6669
reader, err := imgutil.NewPartitionReader(img, "", 0)
6770
if err != nil {
68-
return "", fmt.Errorf("could not extract root filesystem: %s", err)
71+
return "", "", fmt.Errorf("could not extract root filesystem: %s", err)
6972
}
7073
s := unpacker.NewSquashfs()
7174
if !s.HasUnsquashfs() && unsquashfsPath != "" {
@@ -82,18 +85,28 @@ func convertImage(filename string, unsquashfsPath string) (string, error) {
8285
}
8386

8487
// create temporary sandbox
85-
dir, err := ioutil.TempDir(tmpdir, "rootfs-")
88+
tempDir, err = ioutil.TempDir(tmpdir, "rootfs-")
8689
if err != nil {
87-
return "", fmt.Errorf("could not create temporary sandbox: %s", err)
90+
return "", "", fmt.Errorf("could not create temporary sandbox: %s", err)
91+
}
92+
defer func() {
93+
if err != nil {
94+
os.RemoveAll(tempDir)
95+
}
96+
}()
97+
98+
// create an inner dir to extract to, so we don't clobber the secure permissions on the tmpDir.
99+
imageDir = filepath.Join(tempDir, "root")
100+
if err := os.Mkdir(imageDir, 0755); err != nil {
101+
return "", "", fmt.Errorf("could not create root directory: %s", err)
88102
}
89103

90104
// extract root filesystem
91-
if err := s.ExtractAll(reader, dir); err != nil {
92-
os.RemoveAll(dir)
93-
return "", fmt.Errorf("root filesystem extraction failed: %s", err)
105+
if err := s.ExtractAll(reader, imageDir); err != nil {
106+
return "", "", fmt.Errorf("root filesystem extraction failed: %s", err)
94107
}
95108

96-
return dir, err
109+
return tempDir, imageDir, err
97110
}
98111

99112
// checkHidepid checks if hidepid is set on /proc mount point, when this
@@ -650,13 +663,13 @@ func execStarter(cobraCmd *cobra.Command, image string, args []string, name stri
650663
}
651664
sylog.Verbosef("User namespace requested, convert image %s to sandbox", image)
652665
sylog.Infof("Convert SIF file to sandbox...")
653-
dir, err := convertImage(image, unsquashfsPath)
666+
tempDir, imageDir, err := convertImage(image, unsquashfsPath)
654667
if err != nil {
655668
sylog.Fatalf("while extracting %s: %s", image, err)
656669
}
657-
engineConfig.SetImage(dir)
658-
engineConfig.SetDeleteImage(true)
659-
generator.AddProcessEnv("SINGULARITY_CONTAINER", dir)
670+
engineConfig.SetImage(imageDir)
671+
engineConfig.SetDeleteTempDir(tempDir)
672+
generator.AddProcessEnv("SINGULARITY_CONTAINER", imageDir)
660673

661674
// if '--disable-cache' flag, then remove original SIF after converting to sandbox
662675
if disableCache {

Diff for: internal/pkg/build/build.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import (
1313
"os"
1414
"os/signal"
1515
"path/filepath"
16+
"strings"
1617
"syscall"
1718

1819
"github.com/sylabs/singularity/internal/pkg/util/fs"
1920
"github.com/sylabs/singularity/pkg/util/fs/proc"
2021
"github.com/sylabs/singularity/pkg/util/singularityconf"
2122

22-
uuid "github.com/satori/go.uuid"
2323
"github.com/sylabs/singularity/internal/pkg/build/apps"
2424
"github.com/sylabs/singularity/internal/pkg/build/assemblers"
2525
"github.com/sylabs/singularity/internal/pkg/build/sources"
@@ -113,14 +113,16 @@ func newBuild(defs []types.Definition, conf Config) (*Build, error) {
113113
if conf.Format == "sandbox" {
114114
rootfsParent = filepath.Dir(conf.Dest)
115115
}
116-
rootfs := filepath.Join(rootfsParent, "rootfs-"+uuid.NewV1().String())
116+
parentPath, err := ioutil.TempDir(rootfsParent, "build-temp-")
117+
if err != nil {
118+
return nil, fmt.Errorf("failed to create build parent dir: %w", err)
119+
}
117120

118121
var s stage
119-
var err error
120122
if conf.Opts.EncryptionKeyInfo != nil {
121-
s.b, err = types.NewEncryptedBundle(rootfs, conf.Opts.TmpDir, conf.Opts.EncryptionKeyInfo)
123+
s.b, err = types.NewEncryptedBundle(parentPath, conf.Opts.TmpDir, conf.Opts.EncryptionKeyInfo)
122124
} else {
123-
s.b, err = types.NewBundle(rootfs, conf.Opts.TmpDir)
125+
s.b, err = types.NewBundle(parentPath, conf.Opts.TmpDir)
124126
}
125127
if err != nil {
126128
return nil, err
@@ -134,7 +136,7 @@ func newBuild(defs []types.Definition, conf Config) (*Build, error) {
134136
// the old behavior which is to create the temporary rootfs inside
135137
// $TMPDIR and copy the final root filesystem to the destination
136138
// provided
137-
if s.b.RootfsPath != rootfs {
139+
if !strings.HasPrefix(s.b.RootfsPath, parentPath) {
138140
sandboxCopy = true
139141
sylog.Warningf("The underlying filesystem on which resides %q won't allow to set ownership, "+
140142
"as a consequence the sandbox could not preserve image's files/directories ownerships", conf.Dest)

Diff for: internal/pkg/build/sources/conveyorPacker_oci.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ func (cp *OCIConveyorPacker) Get(ctx context.Context, b *sytypes.Bundle) (err er
9696
cp.srcRef, err = ociarchive.ParseReference(ref)
9797
} else {
9898
// As non-root we need to do a dumb tar extraction first
99-
tmpDir, err := ioutil.TempDir(cp.b.Opts.TmpDir, "temp-oci-")
99+
tmpDir, err := ioutil.TempDir(b.TmpDir, "temp-oci-")
100100
if err != nil {
101101
return fmt.Errorf("could not create temporary oci directory: %v", err)
102102
}

Diff for: internal/pkg/runtime/engine/singularity/cleanup_linux.go

+5-6
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,8 @@ func (e *EngineOperations) CleanupContainer(ctx context.Context, fatal error, st
5050
}
5151
}
5252

53-
if e.EngineConfig.GetDeleteImage() {
54-
image := e.EngineConfig.GetImage()
55-
sylog.Verbosef("Removing image %s", image)
53+
if tempDir := e.EngineConfig.GetDeleteTempDir(); tempDir != "" {
54+
sylog.Verbosef("Removing image tempDir %s", tempDir)
5655
sylog.Infof("Cleaning up image...")
5756

5857
var err error
@@ -63,12 +62,12 @@ func (e *EngineOperations) CleanupContainer(ctx context.Context, fatal error, st
6362
// context and can get permission denied error during
6463
// image removal, so we execute "rm -rf /tmp/image" via
6564
// the fakeroot engine
66-
err = fakerootCleanup(image)
65+
err = fakerootCleanup(tempDir)
6766
} else {
68-
err = os.RemoveAll(image)
67+
err = os.RemoveAll(tempDir)
6968
}
7069
if err != nil {
71-
sylog.Errorf("failed to delete container image %s: %s", image, err)
70+
sylog.Errorf("failed to delete container image tempDir %s: %s", tempDir, err)
7271
}
7372
}
7473

Diff for: pkg/build/types/bundle.go

+37-12
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ type Bundle struct {
3030

3131
RootfsPath string `json:"rootfsPath"` // where actual fs to chroot will appear
3232
TmpDir string `json:"tmpPath"` // where temp files required during build will appear
33+
34+
parentPath string // parent directory for RootfsPath
3335
}
3436

3537
// Options defines build time behavior to be executed on the bundle.
@@ -73,13 +75,13 @@ type Options struct {
7375
}
7476

7577
// NewEncryptedBundle creates an Encrypted Bundle environment.
76-
func NewEncryptedBundle(rootfs, tempDir string, keyInfo *crypt.KeyInfo) (b *Bundle, err error) {
77-
return newBundle(rootfs, tempDir, keyInfo)
78+
func NewEncryptedBundle(parentPath, tempDir string, keyInfo *crypt.KeyInfo) (b *Bundle, err error) {
79+
return newBundle(parentPath, tempDir, keyInfo)
7880
}
7981

8082
// NewBundle creates a Bundle environment.
81-
func NewBundle(rootfs, tempDir string) (b *Bundle, err error) {
82-
return newBundle(rootfs, tempDir, nil)
83+
func NewBundle(parentPath, tempDir string) (b *Bundle, err error) {
84+
return newBundle(parentPath, tempDir, nil)
8385
}
8486

8587
// RunSection iterates through the sections specified in a bundle
@@ -100,7 +102,7 @@ func (b *Bundle) RunSection(s string) bool {
100102
// Remove cleans up any bundle files.
101103
func (b *Bundle) Remove() error {
102104
var errors []string
103-
for _, dir := range []string{b.TmpDir, b.RootfsPath} {
105+
for _, dir := range []string{b.TmpDir, b.parentPath} {
104106
if err := fs.ForceRemoveAll(dir); err != nil {
105107
errors = append(errors, fmt.Sprintf("could not remove %q: %v", dir, err))
106108
}
@@ -141,11 +143,19 @@ func cleanupDir(path string) {
141143
}
142144
}
143145

144-
// newBundle creates a minimum bundle with root filesystem in rootfs.
146+
// newBundle creates a minimum bundle with root filesystem in parentPath.
145147
// Any temporary files created during build process will be in tempDir/bundle-temp-*
146148
// directory, that will be cleaned up after successful build.
147-
func newBundle(rootfs, tempDir string, keyInfo *crypt.KeyInfo) (*Bundle, error) {
148-
rootfsPath := rootfs
149+
150+
//
151+
// TODO: much of the logic in this func should likely be re-factored to func newBuild in the
152+
// internal/pkg/build package, since it is the sole caller and has conditional logic which depends
153+
// on implementation details of this package. In particular, chown() handling should be done at the
154+
// build level, rather than the bundle level, to avoid repetition during multi-stage builds, and
155+
// clarify responsibility for cleanup of the various directories that are created during the build
156+
// process.
157+
func newBundle(parentPath, tempDir string, keyInfo *crypt.KeyInfo) (*Bundle, error) {
158+
rootfsPath := filepath.Join(parentPath, "rootfs")
149159

150160
tmpPath, err := ioutil.TempDir(tempDir, "bundle-temp-")
151161
if err != nil {
@@ -155,6 +165,7 @@ func newBundle(rootfs, tempDir string, keyInfo *crypt.KeyInfo) (*Bundle, error)
155165

156166
if err := os.MkdirAll(rootfsPath, 0755); err != nil {
157167
cleanupDir(tmpPath)
168+
cleanupDir(parentPath)
158169
return nil, fmt.Errorf("could not create %q: %v", rootfsPath, err)
159170
}
160171

@@ -164,14 +175,25 @@ func newBundle(rootfs, tempDir string, keyInfo *crypt.KeyInfo) (*Bundle, error)
164175
if err != nil {
165176
cleanupDir(tmpPath)
166177
cleanupDir(rootfsPath)
178+
cleanupDir(parentPath)
167179
return nil, err
168180
} else if !can {
169-
defer cleanupDir(rootfsPath)
181+
cleanupDir(rootfsPath)
182+
cleanupDir(parentPath)
170183

171-
rootfsNewPath := filepath.Join(tempDir, filepath.Base(rootfsPath))
172-
if rootfsNewPath != rootfsPath {
173-
if err := os.MkdirAll(rootfsNewPath, 0755); err != nil {
184+
// If the supplied rootfs was not inside tempDir (as is the case during a sandbox build),
185+
// try tempDir as a fallback.
186+
if !strings.HasPrefix(parentPath, tempDir) {
187+
parentPath, err = ioutil.TempDir(tempDir, "build-temp-")
188+
if err != nil {
189+
cleanupDir(tmpPath)
190+
return nil, fmt.Errorf("failed to create rootfs directory: %v", err)
191+
}
192+
// Create an inner dir, so we don't clobber the secure permissions on the surrounding dir.
193+
rootfsNewPath := filepath.Join(parentPath, "rootfs")
194+
if err := os.Mkdir(rootfsNewPath, 0755); err != nil {
174195
cleanupDir(tmpPath)
196+
cleanupDir(parentPath)
175197
return nil, fmt.Errorf("could not create rootfs dir in %q: %v", rootfsNewPath, err)
176198
}
177199
// check that chown works with the underlying filesystem pointed
@@ -180,10 +202,12 @@ func newBundle(rootfs, tempDir string, keyInfo *crypt.KeyInfo) (*Bundle, error)
180202
if err != nil {
181203
cleanupDir(tmpPath)
182204
cleanupDir(rootfsNewPath)
205+
cleanupDir(parentPath)
183206
return nil, err
184207
} else if !can {
185208
cleanupDir(tmpPath)
186209
cleanupDir(rootfsNewPath)
210+
cleanupDir(parentPath)
187211
sylog.Errorf("Could not set files/directories ownership, if %s is on a network filesystem, "+
188212
"you must set TMPDIR to a local path (eg: TMPDIR=/var/tmp singularity build ...)", rootfsNewPath)
189213
return nil, fmt.Errorf("ownership change not allowed in %s, aborting", tempDir)
@@ -195,6 +219,7 @@ func newBundle(rootfs, tempDir string, keyInfo *crypt.KeyInfo) (*Bundle, error)
195219
sylog.Debugf("Created directory %q for the bundle", rootfsPath)
196220

197221
return &Bundle{
222+
parentPath: parentPath,
198223
RootfsPath: rootfsPath,
199224
TmpDir: tmpPath,
200225
JSONObjects: make(map[string][]byte),

Diff for: pkg/runtime/engine/singularity/config/config.go

+9-7
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ type JSONConfig struct {
148148
NoPrivs bool `json:"noPrivs,omitempty"`
149149
NoHome bool `json:"noHome,omitempty"`
150150
NoInit bool `json:"noInit,omitempty"`
151-
DeleteImage bool `json:"deleteImage,omitempty"`
151+
DeleteTempDir string `json:"deleteTempDir,omitempty"`
152152
Fakeroot bool `json:"fakeroot,omitempty"`
153153
SignalPropagation bool `json:"signalPropagation,omitempty"`
154154
}
@@ -711,14 +711,16 @@ func (e *EngineConfig) GetFakeroot() bool {
711711
return e.JSON.Fakeroot
712712
}
713713

714-
// GetDeleteImage returns if container image must be deleted after use.
715-
func (e *EngineConfig) GetDeleteImage() bool {
716-
return e.JSON.DeleteImage
714+
// GetDeleteTempDir returns the path of the temporary directory containing the root filesystem
715+
// which must be deleted after use. If no deletion is required, the empty string is returned.
716+
func (e *EngineConfig) GetDeleteTempDir() string {
717+
return e.JSON.DeleteTempDir
717718
}
718719

719-
// SetDeleteImage sets if container image must be deleted after use.
720-
func (e *EngineConfig) SetDeleteImage(delete bool) {
721-
e.JSON.DeleteImage = delete
720+
// SetDeleteTempDir sets dir as the path of the temporary directory containing the root filesystem,
721+
// which must be deleted after use.
722+
func (e *EngineConfig) SetDeleteTempDir(dir string) {
723+
e.JSON.DeleteTempDir = dir
722724
}
723725

724726
// SetSignalPropagation sets if engine must propagate signals from

0 commit comments

Comments
 (0)