File: overlay_item_linux.go

package info (click to toggle)
singularity-container 4.0.3%2Bds1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 21,672 kB
  • sloc: asm: 3,857; sh: 2,125; ansic: 1,677; awk: 414; makefile: 110; python: 99
file content (332 lines) | stat: -rw-r--r-- 9,677 bytes parent folder | download
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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
// Copyright (c) 2019-2023, 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 overlay

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"syscall"

	"github.com/sylabs/singularity/v4/internal/pkg/util/fs"
	fsfuse "github.com/sylabs/singularity/v4/internal/pkg/util/fs/fuse"
	"github.com/sylabs/singularity/v4/pkg/image"
	"github.com/sylabs/singularity/v4/pkg/sylog"
)

// Item represents information about a single overlay item (as specified,
// for example, in a single --overlay argument)
type Item struct {
	// Type represents what type of overlay this is (from among the values in
	// pkg/image)
	Type int

	// Readonly represents whether this is a readonly overlay
	Readonly bool

	// SourcePath is the path of the overlay item, stripped of any
	// colon-prefixed options (like ":ro")
	SourcePath string

	// StagingDir is the directory on which this overlay item is staged, to be
	// used as a source for an overlayfs mount as part of an overlay.Set
	StagingDir string

	// parentDir is the (optional) location of a secure parent-directory in
	// which to create mount directories if needed. If empty, one will be
	// created with os.MkdirTemp()
	parentDir string

	// allowSetuid is set to true to mount the overlay item without the "nosuid" option.
	allowSetuid bool

	// allowDev is set to true to mount the overlay item without the "nodev" option.
	allowDev bool
}

// NewItemFromString takes a string argument, as passed to --overlay, and
// returns an Item struct describing the requested overlay.
func NewItemFromString(overlayString string) (*Item, error) {
	item := Item{Readonly: false}

	var err error
	splitted := strings.SplitN(overlayString, ":", 2)
	item.SourcePath, err = filepath.Abs(splitted[0])
	if err != nil {
		return nil, fmt.Errorf("error while trying to convert overlay path %q to absolute path: %w", splitted[0], err)
	}

	if len(splitted) > 1 {
		if splitted[1] == "ro" {
			item.Readonly = true
		}
	}

	s, err := os.Stat(item.SourcePath)
	if (err != nil) && os.IsNotExist(err) {
		return nil, fmt.Errorf("specified overlay %q does not exist", item.SourcePath)
	}
	if err != nil {
		return nil, err
	}

	if s.IsDir() {
		item.Type = image.SANDBOX
	} else if err := item.analyzeImageFile(); err != nil {
		return nil, fmt.Errorf("while examining image file %s: %w", item.SourcePath, err)
	}

	return &item, nil
}

// analyzeImageFile attempts to determine the format of an image file based on
// its header
func (i *Item) analyzeImageFile() error {
	img, err := image.Init(i.SourcePath, false)
	if err != nil {
		return err
	}

	switch img.Type {
	case image.SQUASHFS:
		i.Type = image.SQUASHFS
		// squashfs image must be readonly
		i.Readonly = true
	case image.EXT3:
		i.Type = image.EXT3
	default:
		return fmt.Errorf("image %s is of a type that is not currently supported as overlay", i.SourcePath)
	}

	return nil
}

// SetParentDir sets the parent-dir in which to create overlay-specific mount
// directories.
func (i *Item) SetParentDir(d string) {
	i.parentDir = d
}

// SetAllowDev sets whether to allow devices on the mount for this item.
func (i *Item) SetAllowDev(a bool) {
	i.allowDev = a
}

// SetAllowSetuid sets whether to allow setuid binaries on the mount for this item.
func (i *Item) SetAllowSetuid(a bool) {
	i.allowSetuid = a
}

// GetParentDir gets a parent-dir in which to create overlay-specific mount
// directories. If one has not been set using SetParentDir(), one will be
// created using os.MkdirTemp().
func (i *Item) GetParentDir() (string, error) {
	// Check if we've already been given a parentDir value; if not, create
	// one using os.MkdirTemp()
	if len(i.parentDir) > 0 {
		return i.parentDir, nil
	}

	d, err := os.MkdirTemp("", "overlay-parent-")
	if err != nil {
		return d, err
	}

	i.parentDir = d
	return i.parentDir, nil
}

// Mount performs the necessary steps to mount an individual Item. Note that
// this method does not mount the assembled overlay itself. That happens in
// Set.Mount().
func (i *Item) Mount() error {
	var err error
	switch i.Type {
	case image.SANDBOX:
		err = i.mountDir()

	case image.SQUASHFS, image.EXT3:
		err = i.mountWithFuse()

	default:
		return fmt.Errorf("internal error: unrecognized image type in overlay.Item.Mount() (type: %v)", i.Type)
	}

	if err != nil {
		return err
	}

	if !i.Readonly {
		return i.prepareWritableOverlay()
	}

	return nil
}

// GetMountDir returns the path to the directory that will actually be mounted
// for this overlay. For squashfs overlays, this is equivalent to the
// Item.StagingDir field. But for all other overlays, it is the "upper"
// subdirectory of Item.StagingDir.
func (i Item) GetMountDir() string {
	switch i.Type {
	case image.SQUASHFS:
		return i.StagingDir

	case image.SANDBOX:
		if (!i.Readonly) || fs.IsDir(i.Upper()) {
			return i.Upper()
		}
		return i.StagingDir

	default:
		return i.Upper()
	}
}

// mountDir mounts directory-based Items. This involves bind-mounting followed
// by remounting of the directory onto itself. This pattern of bind-mount
// followed by remount allows application of more restrictive mount flags than
// are in force on the underlying filesystem.
func (i *Item) mountDir() error {
	var err error
	if len(i.StagingDir) < 1 {
		i.StagingDir = i.SourcePath
	}

	if err = EnsureOverlayDir(i.StagingDir, false, 0); err != nil {
		return fmt.Errorf("error accessing directory %s: %w", i.StagingDir, err)
	}

	sylog.Debugf("Performing identity bind-mount of %q", i.StagingDir)
	if err = syscall.Mount(i.StagingDir, i.StagingDir, "", syscall.MS_BIND, ""); err != nil {
		return fmt.Errorf("failed to bind %s: %w", i.StagingDir, err)
	}

	// Best effort to cleanup mount
	defer func() {
		if err != nil {
			sylog.Debugf("Encountered error with current overlay set; attempting to unmount %q", i.StagingDir)
			syscall.Unmount(i.StagingDir, syscall.MNT_DETACH)
		}
	}()

	// Try to perform remount to apply restrictive flags.
	var remountOpts uintptr = syscall.MS_REMOUNT | syscall.MS_BIND
	if i.Readonly {
		// Not strictly necessary as will be read-only in assembled overlay,
		// however this stops any erroneous writes through the stagingDir.
		remountOpts |= syscall.MS_RDONLY
	}
	if !i.allowDev {
		remountOpts |= syscall.MS_NODEV
	}
	if !i.allowSetuid {
		remountOpts |= syscall.MS_NOSUID
	}
	sylog.Debugf("Performing remount of %q", i.StagingDir)
	if err = syscall.Mount("", i.StagingDir, "", remountOpts, ""); err != nil {
		return fmt.Errorf("failed to remount %s: %w", i.StagingDir, err)
	}

	return nil
}

// mountWithFuse mounts an image to a temporary directory
func (i *Item) mountWithFuse() error {
	parentDir, err := i.GetParentDir()
	if err != nil {
		return err
	}

	im := fsfuse.ImageMount{
		Type:         i.Type,
		Readonly:     i.Readonly,
		SourcePath:   i.SourcePath,
		EnclosingDir: parentDir,
		AllowSetuid:  i.allowSetuid,
		AllowDev:     i.allowDev,
	}

	if err := im.Mount(); err != nil {
		return err
	}

	i.StagingDir = im.GetMountPoint()

	return nil
}

// Unmount performs the necessary steps to unmount an individual Item. Note that
// this method does not unmount the overlay itself. That happens in
// Set.Unmount().
func (i Item) Unmount() error {
	switch i.Type {
	case image.SANDBOX:
		return i.unmountDir()

	case image.SQUASHFS, image.EXT3:
		return i.unmountFuse()

	default:
		return fmt.Errorf("internal error: unrecognized image type in overlay.Item.Unmount() (type: %v)", i.Type)
	}
}

// unmountDir unmounts directory-based Items.
func (i Item) unmountDir() error {
	return DetachMount(i.StagingDir)
}

// unmountFuse unmounts FUSE-based Items.
func (i Item) unmountFuse() error {
	defer os.Remove(i.StagingDir)
	err := fsfuse.UnmountWithFuse(i.StagingDir)
	if err != nil {
		return fmt.Errorf("error while trying to unmount image %q from %s: %w", i.SourcePath, i.StagingDir, err)
	}
	return nil
}

// PrepareWritableOverlay ensures that the upper and work subdirs of a writable
// overlay dir exist, and if not, creates them.
func (i *Item) prepareWritableOverlay() error {
	switch i.Type {
	case image.SANDBOX:
		i.StagingDir = i.SourcePath
		fallthrough
	case image.EXT3:
		if err := EnsureOverlayDir(i.StagingDir, true, 0o755); err != nil {
			return err
		}
		sylog.Debugf("Ensuring %q exists", i.Upper())
		if err := EnsureOverlayDir(i.Upper(), true, 0o755); err != nil {
			sylog.Errorf("Could not create overlay upper dir. If using an overlay image ensure it contains 'upper' and 'work' directories")
			return fmt.Errorf("err encountered while preparing upper subdir of overlay dir %q: %w", i.Upper(), err)
		}
		sylog.Debugf("Ensuring %q exists", i.Work())
		if err := EnsureOverlayDir(i.Work(), true, 0o700); err != nil {
			sylog.Errorf("Could not create overlay work dir. If using an overlay image ensure it contains 'upper' and 'work' directories")
			return fmt.Errorf("err encountered while preparing work subdir of overlay dir %q: %w", i.Work(), err)
		}
	default:
		return fmt.Errorf("unsupported image type in prepareWritableOverlay() (type: %v)", i.Type)
	}

	return nil
}

// Upper returns the "upper"-subdir of the Item's DirToMount field.
// Useful for computing options strings for overlay-related mount system calls.
func (i Item) Upper() string {
	return filepath.Join(i.StagingDir, "upper")
}

// Work returns the "work"-subdir of the Item's DirToMount field. Useful
// for computing options strings for overlay-related mount system calls.
func (i Item) Work() string {
	return filepath.Join(i.StagingDir, "work")
}