Skip to content

Commit e977002

Browse files
systemd, tests: RemoveMountUnitFile unmounts even if mount unit file is missing (#17038)
1 parent c305c75 commit e977002

4 files changed

Lines changed: 167 additions & 17 deletions

File tree

systemd/emulation.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,9 @@ func (s *emulation) EnsureMountUnitFile(unitOptions *MountUnitOptions) (string,
185185
}
186186

187187
func (s *emulation) RemoveMountUnitFile(mountedDir string) error {
188-
unit := MountUnitPath(dirs.StripRootDir(mountedDir))
189-
if !osutil.FileExists(unit) {
190-
return nil
191-
}
192-
188+
// unmount regardless of whether the unit file exists as
189+
// the unit file may have been deleted while the mount is
190+
// still active
193191
isMounted, err := osutilIsMounted(mountedDir)
194192
if err != nil {
195193
return err
@@ -201,6 +199,11 @@ func (s *emulation) RemoveMountUnitFile(mountedDir string) error {
201199
}
202200
}
203201

202+
unit := MountUnitPath(dirs.StripRootDir(mountedDir))
203+
if !osutil.FileExists(unit) {
204+
return nil
205+
}
206+
204207
if err := s.DisableNoReload([]string{filepath.Base(unit)}); err != nil {
205208
return err
206209
}

systemd/systemd.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1620,6 +1620,23 @@ func (s *systemd) EnsureMountUnitFile(unitOptions *MountUnitOptions) (string, er
16201620
}
16211621

16221622
func (s *systemd) RemoveMountUnitFile(mountedDir string) error {
1623+
// unmount regardless of whether the unit file exists as
1624+
// the unit file may have been deleted while the mount is
1625+
// still active
1626+
isMounted, err := osutilIsMounted(mountedDir)
1627+
if err != nil {
1628+
return err
1629+
}
1630+
if isMounted {
1631+
// use umount -d (cleanup loopback devices) -l (lazy) to ensure
1632+
// that even busy mount points can be unmounted.
1633+
// note that the long option --lazy is not supported on trusty.
1634+
// the explicit -d is only needed on trusty.
1635+
if output, err := exec.Command("umount", "-d", "-l", mountedDir).CombinedOutput(); err != nil {
1636+
return osutil.OutputErr(output, err)
1637+
}
1638+
}
1639+
16231640
daemonReloadLock.Lock()
16241641
defer daemonReloadLock.Unlock()
16251642

@@ -1628,20 +1645,8 @@ func (s *systemd) RemoveMountUnitFile(mountedDir string) error {
16281645
return nil
16291646
}
16301647

1631-
// use umount -d (cleanup loopback devices) -l (lazy) to ensure that even busy mount points
1632-
// can be unmounted.
1633-
// note that the long option --lazy is not supported on trusty.
1634-
// the explicit -d is only needed on trusty.
1635-
isMounted, err := osutilIsMounted(mountedDir)
1636-
if err != nil {
1637-
return err
1638-
}
16391648
units := []string{filepath.Base(unit)}
16401649
if isMounted {
1641-
if output, err := exec.Command("umount", "-d", "-l", mountedDir).CombinedOutput(); err != nil {
1642-
return osutil.OutputErr(output, err)
1643-
}
1644-
16451650
if err := s.Stop(units); err != nil {
16461651
return err
16471652
}

systemd/systemd_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2088,6 +2088,58 @@ func (s *SystemdTestSuite) TestRemoveMountUnit(c *C) {
20882088
})
20892089
}
20902090

2091+
func (s *SystemdTestSuite) TestRemoveMountUnitWhenIsMountedButDeletedUnitFile(c *C) {
2092+
// When the unit file is missing but the directory is still mounted
2093+
// (e.g. a user manually deleted the unit file), RemoveMountUnitFile
2094+
// must still unmount the directory and return nil error
2095+
rootDir := dirs.GlobalRootDir
2096+
mountDir := rootDir + "/snap/foo/42"
2097+
2098+
restore := MockOsutilIsMounted(func(path string) (bool, error) {
2099+
c.Check(path, Equals, mountDir)
2100+
return true, nil
2101+
})
2102+
defer restore()
2103+
2104+
mockUmountCmd := testutil.MockCommand(c, "umount", "")
2105+
defer mockUmountCmd.Restore()
2106+
2107+
// No unit file on disk
2108+
err := NewUnderRoot(rootDir, SystemMode, nil).RemoveMountUnitFile(mountDir)
2109+
c.Assert(err, IsNil)
2110+
2111+
// umount was called
2112+
c.Check(mockUmountCmd.Calls(), HasLen, 1)
2113+
c.Check(mockUmountCmd.Calls()[0], DeepEquals, []string{"umount", "-d", "-l", mountDir})
2114+
// no systemctl calls: no unit file to disable or daemon-reload for
2115+
c.Check(s.argses, HasLen, 0)
2116+
}
2117+
2118+
func (s *SystemdTestSuite) TestRemoveMountUnitWhenIsNotMountedAndDeletedUnitFile(c *C) {
2119+
// When the unit file is missing and the directory is not mounted
2120+
// RemoveMountUnitFile must return nil error without calling umount
2121+
// or systemctl
2122+
rootDir := dirs.GlobalRootDir
2123+
mountDir := rootDir + "/snap/foo/42"
2124+
2125+
restore := MockOsutilIsMounted(func(path string) (bool, error) {
2126+
c.Check(path, Equals, mountDir)
2127+
return false, nil
2128+
})
2129+
defer restore()
2130+
2131+
mockUmountCmd := testutil.MockCommand(c, "umount", "")
2132+
defer mockUmountCmd.Restore()
2133+
2134+
// No unit file on disk
2135+
err := NewUnderRoot(rootDir, SystemMode, nil).RemoveMountUnitFile(mountDir)
2136+
c.Assert(err, IsNil)
2137+
2138+
// nothing to unmount or disable
2139+
c.Check(mockUmountCmd.Calls(), HasLen, 0)
2140+
c.Check(s.argses, HasLen, 0)
2141+
}
2142+
20912143
func (s *SystemdTestSuite) TestDaemonReloadMutex(c *C) {
20922144
s.testDaemonOpWithMutex(c, Systemd.DaemonReload)
20932145
}
@@ -2684,6 +2736,56 @@ func (s *SystemdTestSuite) TestPreseedModeRemoveMountUnitUnmounted(c *C) {
26842736
c.Check(mockUmountCmd.Calls(), HasLen, 0)
26852737
}
26862738

2739+
func (s *SystemdTestSuite) TestPreseedModeRemoveMountUnitMountedButNoUnitFile(c *C) {
2740+
// When the unit file is missing but the directory is still mounted,
2741+
// emulation-mode RemoveMountUnitFile must still unmount and return nil
2742+
// error.
2743+
mountDir := dirs.GlobalRootDir + "/snap/foo/42"
2744+
2745+
restore := MockOsutilIsMounted(func(path string) (bool, error) {
2746+
c.Check(path, Equals, mountDir)
2747+
return true, nil
2748+
})
2749+
defer restore()
2750+
2751+
mockUmountCmd := testutil.MockCommand(c, "umount", "")
2752+
defer mockUmountCmd.Restore()
2753+
2754+
// No unit file on disk
2755+
sysd := NewEmulationMode(dirs.GlobalRootDir)
2756+
c.Assert(sysd.RemoveMountUnitFile(mountDir), IsNil)
2757+
2758+
// umount was called
2759+
c.Check(mockUmountCmd.Calls(), HasLen, 1)
2760+
c.Check(mockUmountCmd.Calls()[0], DeepEquals, []string{"umount", "-d", "-l", mountDir})
2761+
// no systemctl calls: no unit file to disable
2762+
c.Check(s.argses, HasLen, 0)
2763+
}
2764+
2765+
func (s *SystemdTestSuite) TestPreseedModeRemoveMountUnitUnmountedAndNoUnitFile(c *C) {
2766+
// When the unit file is missing and the directory is not mounted
2767+
// RemoveMountUnitFile must return nil error without calling umount
2768+
// or systemctl
2769+
mountDir := dirs.GlobalRootDir + "/snap/foo/42"
2770+
2771+
restore := MockOsutilIsMounted(func(path string) (bool, error) {
2772+
c.Check(path, Equals, mountDir)
2773+
return false, nil
2774+
})
2775+
defer restore()
2776+
2777+
mockUmountCmd := testutil.MockCommand(c, "umount", "")
2778+
defer mockUmountCmd.Restore()
2779+
2780+
// No unit file on disk
2781+
sysd := NewEmulationMode(dirs.GlobalRootDir)
2782+
c.Assert(sysd.RemoveMountUnitFile(mountDir), IsNil)
2783+
2784+
// nothing to unmount or disable
2785+
c.Check(mockUmountCmd.Calls(), HasLen, 0)
2786+
c.Check(s.argses, HasLen, 0)
2787+
}
2788+
26872789
func (s *SystemdTestSuite) TestPreseedModeBindmountNotSupported(c *C) {
26882790
sysd := NewEmulationMode(dirs.GlobalRootDir)
26892791

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
summary: Snap remove with missing snap mount unit file
2+
3+
details: |
4+
The test verifies that snap remove succeeds even if the snap's mount unit file
5+
is missing (e.g. manually deleted by a user) while the mount is still active.
6+
7+
environment:
8+
SNAP_NAME: test-snapd-tools
9+
10+
prepare: |
11+
"$TESTSTOOLS"/snaps-state install-local $SNAP_NAME
12+
tests.cleanup defer snap remove --purge $SNAP_NAME
13+
14+
execute: |
15+
snap_mount_dir="$(os.paths snap-mount-dir)"
16+
revision=$(snap list $SNAP_NAME | awk 'NR==2 {print $3}')
17+
mount_dir="$snap_mount_dir/$SNAP_NAME/$revision"
18+
mount_unit_name="$(systemd-escape --path "$mount_dir").mount"
19+
mount_unit_file="/etc/systemd/system/$mount_unit_name"
20+
21+
echo "Verify that snap is currently mounted"
22+
MATCH "$mount_dir" < /proc/self/mountinfo
23+
24+
echo "Verify that the mount unit file exists"
25+
test -f "$mount_unit_file"
26+
27+
echo "Simulate user deleting the mount unit file"
28+
rm "$mount_unit_file"
29+
systemctl daemon-reload
30+
31+
echo "Verify that the mount unit file is gone but the mount is still active"
32+
test ! -f "$mount_unit_file"
33+
MATCH "$mount_dir" < /proc/self/mountinfo
34+
35+
echo "Snap remove must succeed even with the mount unit file missing"
36+
snap remove --purge $SNAP_NAME
37+
38+
echo "Verify that the snap is fully removed"
39+
NOMATCH "$mount_dir" < /proc/self/mountinfo
40+
not snap list $SNAP_NAME &>/dev/null

0 commit comments

Comments
 (0)