Skip to content

Commit 2a1be68

Browse files
fix umount cleanup (#27)
* fix umount cleanup with new metadata location Since eaa7b43, Umount was not cleaning up backing devices correctly, but this was not caught in tests. Stacker's test suite had a test that noticed. It isn't clear that Umount as used in the `atomfs` cli tool was correct before eaa7b43 either, because the underlying atom mount points were put under the overmounted final mount point, and would thus not be shared by other molecule mounts. This also wasn't being tested though. This commit fixes Umount, and: - consolidates all unmounting logic - out of `cmd/umount` and into the package in `atomfs.Umount`. - adds a version of the test from Stacker that noticed this - that test belongs here instead of in stacker. Also fixed a couple seeming typos in that test that hid failures. - fixes the lock path used in Umount to match that used in Mount, which was out of sync since eaa7b43 - splits out the verity.go squashfs.Umount code to allow for getting backing devices of an atom without unmounting it, in order to only clean up the backing dev if it is no longer in use Signed-off-by: Michael McCracken <[email protected]> * test: extend coverage tracking to bats tests - updates codecov action version - make batstest now builds with coverage and runs all bats tests with coverage, then merges into a file codecov knows about Signed-off-by: Michael McCracken <[email protected]> --------- Signed-off-by: Michael McCracken <[email protected]>
1 parent 787f5fe commit 2a1be68

11 files changed

+271
-133
lines changed

.github/workflows/build.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@ jobs:
6767
run: |
6868
make batstest
6969
- name: Upload code coverage
70-
uses: codecov/codecov-action@v4
70+
uses: codecov/codecov-action@v5
7171
with:
7272
fail_ci_if_error: true # optional (default = false)
73-
files: ./coverage.txt
73+
files: ./unit-coverage.txt,./integ-coverage.txt
7474
token: ${{ secrets.CODECOV_TOKEN }} # required
7575
- name: Release
7676
uses: softprops/action-gh-release@v1

Makefile

+21-6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ BATS_VERSION := v1.10.0
1414
STACKER = $(TOOLS_D)/bin/stacker
1515
STACKER_VERSION := v1.0.0
1616
TOOLS_D := $(ROOT)/tools
17+
GOCOVERDIR ?= $(ROOT)
1718

1819
export PATH := $(TOOLS_D)/bin:$(PATH)
1920

@@ -25,11 +26,13 @@ gofmt: .made-gofmt
2526
{ echo "gofmt made changes: $$o" 1>&2; exit 1; }
2627
@touch $@
2728

28-
atomfs: .made-gofmt $(GO_SRC)
29-
cd $(ROOT)/cmd/atomfs && go build -buildvcs=false -ldflags "$(VERSION_LDFLAGS)" -o $(ROOT)/bin/atomfs ./...
29+
atomfs atomfs-cover: .made-gofmt $(GO_SRC)
30+
cd $(ROOT)/cmd/atomfs && go build $(BUILDCOVERFLAGS) -buildvcs=false -ldflags "$(VERSION_LDFLAGS)" -o $(ROOT)/bin/$@ ./...
31+
32+
atomfs-cover: BUILDCOVERFLAGS=-cover
3033

3134
gotest: $(GO_SRC)
32-
go test -coverprofile=coverage.txt -ldflags "$(VERSION_LDFLAGS)" ./...
35+
go test -coverprofile=unit-coverage.txt -ldflags "$(VERSION_LDFLAGS)" ./...
3336

3437
$(STACKER):
3538
mkdir -p $(TOOLS_D)/bin
@@ -46,9 +49,21 @@ $(BATS):
4649
git clone --depth 1 https://github.com/bats-core/bats-assert $(ROOT)/test/test_helper/bats-assert
4750
git clone --depth 1 https://github.com/bats-core/bats-file $(ROOT)/test/test_helper/bats-file
4851

49-
batstest: $(BATS) $(STACKER) atomfs test/random.txt
50-
cd $(ROOT)/test; sudo $(BATS) --tap --timing priv-*.bats
51-
cd $(ROOT)/test; $(BATS) --tap --timing unpriv-*.bats
52+
batstest: $(BATS) $(STACKER) atomfs-cover test/random.txt testimages
53+
cd $(ROOT)/test; sudo GOCOVERDIR=$(GOCOVERDIR) $(BATS) --tap --timing priv-*.bats
54+
cd $(ROOT)/test; GOCOVERDIR=$(GOCOVERDIR) $(BATS) --tap --timing unpriv-*.bats
55+
go tool covdata textfmt -i $(GOCOVERDIR) -o integ-coverage.txt
56+
57+
covreport: gotest batstest
58+
go tool cover -func=unit-coverage.txt
59+
go tool cover -func=integ-coverage.txt
60+
61+
testimages: /tmp/atomfs-test-oci/.copydone
62+
@echo "busybox image exists at /tmp/atomfs-test-oci already"
63+
64+
/tmp/atomfs-test-oci/.copydone:
65+
skopeo copy docker://public.ecr.aws/docker/library/busybox:stable oci:/tmp/atomfs-test-oci:busybox
66+
touch $@
5267

5368
test/random.txt:
5469
dd if=/dev/random of=/dev/stdout count=2048 | base64 > test/random.txt

cmd/atomfs/umount.go

+1-53
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ package main
22

33
import (
44
"fmt"
5-
"os"
65
"path/filepath"
7-
"syscall"
86

97
"github.com/urfave/cli"
108
"machinerun.io/atomfs"
@@ -39,9 +37,7 @@ func doUmount(ctx *cli.Context) error {
3937
}
4038

4139
mountpoint := ctx.Args()[0]
42-
4340
var err error
44-
var errs []error
4541

4642
if !filepath.IsAbs(mountpoint) {
4743
mountpoint, err = filepath.Abs(mountpoint)
@@ -50,53 +46,5 @@ func doUmount(ctx *cli.Context) error {
5046
}
5147
}
5248

53-
// We expect the argument to be the mountpoint of the overlay
54-
err = syscall.Unmount(mountpoint, 0)
55-
if err != nil {
56-
errs = append(errs, fmt.Errorf("Failed unmounting %s: %v", mountpoint, err))
57-
}
58-
59-
// We expect the following in the metadir
60-
//
61-
// $metadir/mounts/* - the original squashfs mounts
62-
// $metadir/meta/config.json
63-
64-
// TODO: want to know mountnsname for a target mountpoint... not for our current proc???
65-
mountNSName, err := atomfs.GetMountNSName()
66-
if err != nil {
67-
errs = append(errs, fmt.Errorf("Failed to get mount namespace name"))
68-
}
69-
metadir := filepath.Join(atomfs.RuntimeDir(ctx.String("metadir")), "meta", mountNSName, atomfs.ReplacePathSeparators(mountpoint))
70-
71-
mountsdir := filepath.Join(metadir, "mounts")
72-
mounts, err := os.ReadDir(mountsdir)
73-
if err != nil {
74-
errs = append(errs, fmt.Errorf("Failed reading list of mounts: %v", err))
75-
return fmt.Errorf("Encountered errors: %v", errs)
76-
}
77-
78-
for _, m := range mounts {
79-
p := filepath.Join(mountsdir, m.Name())
80-
if !m.IsDir() || !isMountpoint(p) {
81-
continue
82-
}
83-
84-
err = syscall.Unmount(p, 0)
85-
if err != nil {
86-
errs = append(errs, fmt.Errorf("Failed unmounting squashfs dir %s: %v", p, err))
87-
}
88-
}
89-
90-
if len(errs) != 0 {
91-
for i, e := range errs {
92-
fmt.Printf("Error %d: %v\n", i, e)
93-
}
94-
return fmt.Errorf("Encountered errors %d: %v", len(errs), errs)
95-
}
96-
97-
if err := os.RemoveAll(metadir); err != nil {
98-
return fmt.Errorf("Failed removing %q: %v", metadir, err)
99-
}
100-
101-
return nil
49+
return atomfs.UmountWithMetadir(mountpoint, ctx.String("metadir"))
10250
}

molecule.go

+71-37
Original file line numberDiff line numberDiff line change
@@ -287,14 +287,32 @@ func (m Molecule) Mount(dest string) error {
287287
return nil
288288
}
289289

290+
// Default Umount passes "" and uses /run/atomfs metadir, see RuntimeDir().
290291
func Umount(dest string) error {
292+
return UmountWithMetadir(dest, "")
293+
}
294+
295+
func UmountWithMetadir(dest, metadirArg string) error {
291296
var err error
292297
dest, err = filepath.Abs(dest)
293298
if err != nil {
294299
return errors.Wrapf(err, "couldn't create abs path for %v", dest)
295300
}
296301

297-
lockfile, err := makeLock(dest)
302+
// recreate molecule config as much as possible, for MetadataPath():
303+
mol := Molecule{
304+
config: MountOCIOpts{
305+
Target: dest,
306+
MetadataDir: metadirArg,
307+
},
308+
}
309+
310+
metadir, err := mol.MetadataPath()
311+
if err != nil {
312+
return errors.WithStack(err)
313+
}
314+
315+
lockfile, err := makeLock(metadir)
298316
if err != nil {
299317
return errors.WithStack(err)
300318
}
@@ -310,77 +328,93 @@ func Umount(dest string) error {
310328
return err
311329
}
312330

313-
underlyingAtoms := []string{}
331+
// Find all mountpoints underlying the current top Overlay MP
332+
underlyingAtomRelPaths := []string{}
314333
for _, m := range mounts {
315334
if m.FSType != "overlay" {
316335
continue
317336
}
318337

319-
if m.Target != dest { // TODO is this still right
338+
if m.Target != dest {
320339
continue
321340
}
322341

323-
underlyingAtoms, err = m.GetOverlayDirs()
342+
underlyingAtomRelPaths, err = m.GetOverlayDirs()
324343
if err != nil {
325344
return err
326345
}
327346
break
328347
}
329348

349+
underlyingAtoms := []string{}
350+
// Ensure abs paths, as we compare it to the abs path of dest
351+
for _, m := range underlyingAtomRelPaths {
352+
abspath, err := filepath.Abs(m)
353+
if err != nil {
354+
return errors.Wrapf(err, "failed to get abs path for %q", m)
355+
}
356+
underlyingAtoms = append(underlyingAtoms, abspath)
357+
}
358+
330359
if len(underlyingAtoms) == 0 {
331360
return errors.Errorf("%s is not an atomfs mountpoint", dest)
332361
}
333362

363+
// Unmount the top Overlay MP
334364
if err := unix.Unmount(dest, 0); err != nil {
335-
return err
365+
return errors.Wrapf(err, "failed to unmount dest, %q", dest)
336366
}
337367

338-
// now, "refcount" the remaining atoms and see if any of ours are
339-
// unused
340-
usedAtoms := map[string]int{}
341-
342-
mounts, err = mount.ParseMounts("/proc/self/mountinfo")
343-
if err != nil {
344-
return err
345-
}
368+
// For each underlying dir, we need to find the corresponding
369+
// squashfs-verity device mounted on it, then unconditionally unmount the
370+
// underlying dir (because its mount point is specific to `dest`) - then
371+
// find the underlying device and optionally clean it up too
372+
for _, a := range underlyingAtoms {
373+
// the workaround dir isn't really a mountpoint, so don't unmount it
374+
if path.Base(a) == "workaround" {
375+
continue
376+
}
346377

347-
for _, m := range mounts {
348-
if m.FSType != "overlay" {
378+
// overlaydirs includes the top level mount, ignore it
379+
if a == dest {
349380
continue
350381
}
351382

352-
dirs, err := m.GetOverlayDirs()
383+
backingDevice, err := squashfs.GetBackingDevice(a)
353384
if err != nil {
354385
return err
355386
}
356-
for _, d := range dirs {
357-
usedAtoms[d]++
387+
log.Debugf("Unmounting underlying atom =%q", a)
388+
if err := unix.Unmount(a, 0); err != nil {
389+
return err
358390
}
359-
}
360391

361-
// If any of the atoms underlying the target mountpoint are now unused,
362-
// let's unmount them too.
363-
for _, a := range underlyingAtoms {
364-
_, used := usedAtoms[a]
365-
if used {
366-
continue
392+
// if that was the last mountpoint for the dev, we can clean it up too
393+
mounts, err = mount.ParseMounts("/proc/self/mountinfo")
394+
if err != nil {
395+
return err
367396
}
368-
/* TODO: some kind of logging
369-
if !used {
370-
log.Warnf("unused atom %s was part of this molecule?")
371-
continue
397+
backingDevIsUsed := false
398+
for _, m := range mounts {
399+
if m.Source == backingDevice {
400+
backingDevIsUsed = true
401+
}
372402
}
373-
*/
374403

375-
// the workaround dir isn't really a mountpoint, so don't unmount it
376-
if path.Base(a) == "workaround" {
377-
continue
404+
if !backingDevIsUsed {
405+
if err := squashfs.MaybeCleanupBackingDevice(backingDevice); err != nil {
406+
return err
407+
}
378408
}
409+
}
379410

380-
err = squashfs.Umount(a)
381-
if err != nil {
382-
return err
383-
}
411+
mountNSName, err := GetMountNSName()
412+
if err != nil {
413+
return err
414+
}
415+
destMetaDir := filepath.Join(RuntimeDir(metadir), "meta", mountNSName, ReplacePathSeparators(dest))
416+
if err := os.RemoveAll(destMetaDir); err != nil {
417+
return err
384418
}
385419

386420
return nil

squashfs/verity.go

+34-13
Original file line numberDiff line numberDiff line change
@@ -449,25 +449,42 @@ func findLoopBackingVerity(device string) (int64, error) {
449449
return deviceNo, nil
450450
}
451451

452+
// unmounts a squash mountpoint and detaches any verity / loop devices that back
453+
// it. Only use this if you are sure the underlying devices aren't in use by
454+
// other mount points.
452455
func Umount(mountpoint string) error {
456+
devPath, err := GetBackingDevice(mountpoint)
457+
458+
err = unix.Unmount(mountpoint, 0)
459+
if err != nil {
460+
return errors.Wrapf(err, "failed unmounting %v", mountpoint)
461+
}
462+
463+
return MaybeCleanupBackingDevice(devPath)
464+
}
465+
466+
func GetBackingDevice(mountpoint string) (string, error) {
453467
mounts, err := mount.ParseMounts("/proc/self/mountinfo")
454468
if err != nil {
455-
return err
469+
return "", err
456470
}
457471

458-
// first, find the verity device that backs the mount
459472
theMount, found := mounts.FindMount(mountpoint)
460473
if !found {
461-
return errors.Errorf("%s is not a mountpoint", mountpoint)
462-
}
463-
464-
err = unix.Unmount(mountpoint, 0)
465-
if err != nil {
466-
return errors.Wrapf(err, "failed unmounting %v", mountpoint)
474+
return "", errors.Errorf("%s is not a mountpoint", mountpoint)
467475
}
476+
return theMount.Source, nil
477+
}
468478

469-
if _, err := os.Stat(theMount.Source); err != nil {
479+
// given a device path that had been used as the backing device for a squash
480+
// mountpoint, cleans up and detaches verity device if it still exists.
481+
//
482+
// If the device path does not exist, that is OK - this happens if the device
483+
// was a regular loopback and not -verity.
484+
func MaybeCleanupBackingDevice(devPath string) error {
485+
if _, err := os.Stat(devPath); err != nil {
470486
if os.IsNotExist(err) {
487+
// It's been detached, this is fine.
471488
return nil
472489
}
473490
return errors.WithStack(err)
@@ -476,9 +493,9 @@ func Umount(mountpoint string) error {
476493
// was this a verity mount or a regular loopback mount? (if it's a
477494
// regular loopback mount, we detached it above, so need to do anything
478495
// special here; verity doesn't play as nicely)
479-
if strings.HasSuffix(theMount.Source, veritySuffix) {
496+
if strings.HasSuffix(devPath, veritySuffix) {
480497
// find the loop device that backs the verity device
481-
deviceNo, err := findLoopBackingVerity(theMount.Source)
498+
deviceNo, err := findLoopBackingVerity(devPath)
482499
if err != nil {
483500
return err
484501
}
@@ -488,18 +505,22 @@ func Umount(mountpoint string) error {
488505
// above). the cryptsetup API allows us to pass NULL for the crypt
489506
// device, but go-cryptsetup doesn't have a way to initialize a NULL
490507
// crypt device short of making the struct by hand like this.
491-
err = (&cryptsetup.Device{}).Deactivate(theMount.Source)
508+
err = (&cryptsetup.Device{}).Deactivate(devPath)
492509
if err != nil {
493510
return errors.WithStack(err)
494511
}
495512

496513
// finally, kill the loop dev
497514
err = loopDev.Detach()
498515
if err != nil {
499-
return errors.Wrapf(err, "failed to detach loop dev for %v", theMount.Source)
516+
return errors.Wrapf(err, "failed to detach loop dev for %v", devPath)
500517
}
501518
}
502519

520+
// NOTE: because of lazy device destruction, it is possible for a non-verity
521+
// backing loop dev to be auto-detached but still exist. There's nothing for
522+
// us to do in that case, so we return nil.
523+
503524
return nil
504525
}
505526

0 commit comments

Comments
 (0)