cve-2021-2021-44730-44731-4120-auto-remove
authorMichael Hudson-Doyle <mwhudson@debian.org>
Mon, 28 Nov 2022 10:37:00 +0000 (10:37 +0000)
committerAlex Murray <alex.murray@canonical.com>
Mon, 28 Nov 2022 10:37:00 +0000 (10:37 +0000)
===================================================================

Gbp-Pq: Name 0016-cve-2021-2021-44730-44731-4120-auto-remove.patch

overlord/devicestate/firstboot_test.go
overlord/managers_test.go
overlord/snapstate/snapmgr.go
overlord/snapstate/snapstate_config_defaults_test.go
overlord/snapstate/snapstate_test.go
tests/nested/manual/snapd-removes-vulnerable-snap-confine-revs/task.yaml [new file with mode: 0644]

index 6bdf6688eb953ef05eb11b0b1e83fa141a405f52..250c2118f9751e83ac7e07a62794c6eaf604e772 100644 (file)
@@ -1215,7 +1215,14 @@ type: base`
        snapdYaml := `name: snapd
 version: 1.0
 `
-       snapdFname, snapdDecl, snapdRev := s.MakeAssertedSnap(c, snapdYaml, nil, snap.R(2), "canonical")
+       // the info file is needed by the Ensure() loop of snapstate manager
+       snapdSnapFiles := [][]string{
+               {"usr/lib/snapd/info", `
+VERSION=2.54.3+git1.g479e745-dirty
+SNAPD_APPARMOR_REEXEC=0
+`},
+       }
+       snapdFname, snapdDecl, snapdRev := s.MakeAssertedSnap(c, snapdYaml, snapdSnapFiles, snap.R(2), "canonical")
        s.WriteAssertions("snapd.asserts", snapdRev, snapdDecl)
 
        var kernelFname string
index 2ec269610aaa3dcd543ad2cb9727a9c8d0dc1f7f..94bd8af355747aedd3d30080893da3a77962f95b 100644 (file)
@@ -347,6 +347,21 @@ func (s *baseMgrsSuite) SetUpTest(c *C) {
                },
        })
 
+       // commonly used core and snapd revisions in tests
+       defaultInfoFile := `
+VERSION=2.54.3+git1.g479e745-dirty
+SNAPD_APPARMOR_REEXEC=0
+`
+       for _, snapName := range []string{"snapd", "core"} {
+               for _, rev := range []string{"1", "11", "30"} {
+                       infoFile := filepath.Join(dirs.GlobalRootDir, "snap", snapName, rev, dirs.CoreLibExecDir, "info")
+                       err = os.MkdirAll(filepath.Dir(infoFile), 0755)
+                       c.Assert(err, IsNil)
+                       err = ioutil.WriteFile(infoFile, []byte(defaultInfoFile), 0644)
+                       c.Assert(err, IsNil)
+               }
+       }
+
        // don't actually try to talk to the store on snapstate.Ensure
        // needs doing after the call to devicestate.Manager (which happens in overlord.New)
        snapstate.CanAutoRefresh = nil
@@ -520,8 +535,10 @@ hooks:
 
        snapdirs, err := filepath.Glob(filepath.Join(dirs.SnapMountDir, "*"))
        c.Assert(err, IsNil)
-       // just README and bin
-       c.Check(snapdirs, HasLen, 2)
+       // just README, bin, snapd, and core (snapd and core are there because we
+       // have info files for those snaps which need to be read from the snapstate
+       // Ensure loop)
+       c.Check(snapdirs, HasLen, 4)
        for _, d := range snapdirs {
                c.Check(filepath.Base(d), Not(Equals), "foo")
        }
index f7cb8c165b9050c6dc0e16357416c27098eeea54..d7b8ff413dd5e31a2c144a83081a3198aa07e2ad 100644 (file)
@@ -25,6 +25,7 @@ import (
        "fmt"
        "io"
        "os"
+       "path/filepath"
        "strings"
        "time"
 
@@ -43,7 +44,9 @@ import (
        "github.com/snapcore/snapd/snap"
        "github.com/snapcore/snapd/snap/channel"
        "github.com/snapcore/snapd/snapdenv"
+       "github.com/snapcore/snapd/snapdtool"
        "github.com/snapcore/snapd/store"
+       "github.com/snapcore/snapd/strutil"
 )
 
 var (
@@ -574,6 +577,120 @@ func (m *SnapManager) EnsureAutoRefreshesAreDelayed(delay time.Duration) ([]*sta
        return autoRefreshChgsInFlight, nil
 }
 
+func (m *SnapManager) ensureVulnerableSnapRemoved(name string) error {
+       var removedYet bool
+       key := fmt.Sprintf("%s-snap-cve-2021-44731-vuln-removed", name)
+       if err := m.state.Get(key, &removedYet); err != nil && err != state.ErrNoState {
+               return err
+       }
+       if removedYet {
+               return nil
+       }
+       var snapSt SnapState
+       err := Get(m.state, name, &snapSt)
+       if err != nil && err != state.ErrNoState {
+               return err
+       }
+       if err == state.ErrNoState {
+               // not installed, nothing to do
+               return nil
+       }
+
+       // check if the installed, active version is fixed
+       fixedVersionInstalled := false
+       inactiveVulnRevisions := []snap.Revision{}
+       for _, si := range snapSt.Sequence {
+               // check this version
+               s := snap.Info{SideInfo: *si}
+               ver, err := snapdtool.SnapdVersionFromInfoFile(filepath.Join(s.MountDir(), dirs.CoreLibExecDir, "info"))
+               if err != nil {
+                       return err
+               }
+               // res is < 0 if "ver" is lower than "2.54.3"
+               res, err := strutil.VersionCompare(ver, "2.54.3")
+               if err != nil {
+                       return err
+               }
+               revIsVulnerable := (res < 0)
+               switch {
+               case !revIsVulnerable && si.Revision == snapSt.Current:
+                       fixedVersionInstalled = true
+               case revIsVulnerable && si.Revision == snapSt.Current:
+                       // the active installed revision is not fixed, we can break out
+                       // early since we know we won't be able to remove old revisions
+                       return nil
+               case revIsVulnerable && si.Revision != snapSt.Current:
+                       // si revision is not fixed, but is not active, so it is a candidate
+                       // for removal
+                       inactiveVulnRevisions = append(inactiveVulnRevisions, si.Revision)
+               default:
+                       // si revision is not active, but it is fixed, so just ignore it
+               }
+       }
+
+       if !fixedVersionInstalled {
+               return nil
+       }
+       // TODO: should we use one change for removing all the snap revisions?
+
+       // remove all the inactive vulnerable revisions
+       for _, rev := range inactiveVulnRevisions {
+               tss, err := Remove(m.state, name, rev, nil)
+
+               if err != nil {
+                       // in case of conflict, just trigger another ensure in a little
+                       // bit and try again later
+                       if _, ok := err.(*ChangeConflictError); ok {
+                               m.state.EnsureBefore(time.Minute)
+                               return nil
+                       }
+                       return fmt.Errorf("cannot make task set for removing %s snap: %v", name, err)
+               }
+
+               msg := fmt.Sprintf(i18n.G("Remove vulnerable %q snap"), name)
+
+               chg := m.state.NewChange("remove-snap", msg)
+               chg.AddAll(tss)
+               chg.Set("snap-names", []string{name})
+       }
+
+       // TODO: is it okay to set state here as done or should we do this
+       // elsewhere after the change is done somehow?
+
+       // mark state as done
+       m.state.Set(key, true)
+
+       // not strictly necessary, but does not hurt to ensure anyways
+       m.state.EnsureBefore(0)
+
+       return nil
+}
+
+func (m *SnapManager) ensureVulnerableSnapConfineVersionsRemovedOnClassic() error {
+       // only remove snaps on classic
+       if !release.OnClassic {
+               return nil
+       }
+
+       m.state.Lock()
+       defer m.state.Unlock()
+
+       // we have to remove vulnerable versions of both the core and snapd snaps
+       // only when we now have fixed versions installed / active
+       // the fixed version is 2.54.3, so if the version of the current core/snapd
+       // snap is that or higher, then we proceed (if we didn't already do this)
+
+       if err := m.ensureVulnerableSnapRemoved("snapd"); err != nil {
+               return err
+       }
+
+       if err := m.ensureVulnerableSnapRemoved("core"); err != nil {
+               return err
+       }
+
+       return nil
+}
+
 // ensureForceDevmodeDropsDevmodeFromState undoes the forced devmode
 // in snapstate for forced devmode distros.
 func (m *SnapManager) ensureForceDevmodeDropsDevmodeFromState() error {
@@ -873,6 +990,7 @@ func (m *SnapManager) Ensure() error {
                m.refreshHints.Ensure(),
                m.catalogRefresh.Ensure(),
                m.localInstallCleanup(),
+               m.ensureVulnerableSnapConfineVersionsRemovedOnClassic(),
        }
 
        //FIXME: use firstErr helper
index 44cb0775ec5aeebcfcf29e4b1b3580b8911071ab..afe03f40b6fa1c720cb22aabf50d7396ab632870 100644 (file)
@@ -271,7 +271,7 @@ func (s *snapmgrTestSuite) TestTransitionCoreTasksNoUbuntuCore(c *C) {
 
        snapstate.Set(s.state, "core", &snapstate.SnapState{
                Active:   true,
-               Sequence: []*snap.SideInfo{{RealName: "corecore", SnapID: "core-snap-id", Revision: snap.R(1)}},
+               Sequence: []*snap.SideInfo{{RealName: "core", SnapID: "core-snap-id", Revision: snap.R(1)}},
                Current:  snap.R(1),
                SnapType: "os",
        })
index 374ee958548f8fd2800c077adad9e571d144086f..5eb8326f73e9ec8dd620a7eb30305ef646b08c04 100644 (file)
@@ -213,6 +213,22 @@ func (s *snapmgrTestSuite) SetUpTest(c *C) {
                Current:  snap.R(1),
                SnapType: "os",
        })
+
+       // commonly used revisions in tests
+       defaultInfoFile := `
+VERSION=2.54.3+git1.g479e745-dirty
+SNAPD_APPARMOR_REEXEC=0
+`
+       for _, snapName := range []string{"snapd", "core"} {
+               for _, rev := range []string{"1", "11"} {
+                       infoFile := filepath.Join(dirs.GlobalRootDir, "snap", snapName, rev, dirs.CoreLibExecDir, "info")
+                       err = os.MkdirAll(filepath.Dir(infoFile), 0755)
+                       c.Assert(err, IsNil)
+                       err = ioutil.WriteFile(infoFile, []byte(defaultInfoFile), 0644)
+                       c.Assert(err, IsNil)
+               }
+       }
+
        s.state.Unlock()
 
        snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) {
@@ -2668,6 +2684,228 @@ func (s *snapmgrTestSuite) TestErrreportDisable(c *C) {
        // no failure report was generated
 }
 
+func (s *snapmgrTestSuite) TestEnsureRemovesVulnerableCoreSnap(c *C) {
+       s.testEnsureRemovesVulnerableSnap(c, "core")
+}
+
+func (s *snapmgrTestSuite) TestEnsureRemovesVulnerableSnapdSnap(c *C) {
+       s.testEnsureRemovesVulnerableSnap(c, "snapd")
+}
+
+func (s *snapmgrTestSuite) testEnsureRemovesVulnerableSnap(c *C, snapName string) {
+       // make the currently installed snap info file fixed but an old version
+       // vulnerable
+       fixedInfoFile := `
+VERSION=2.54.3+git1.g479e745-dirty
+SNAPD_APPARMOR_REEXEC=0
+`
+       vulnInfoFile := `
+VERSION=2.54.2+git1.g479e745-dirty
+SNAPD_APPARMOR_REEXEC=0
+`
+
+       // revision 1 vulnerable
+       infoFile := filepath.Join(dirs.GlobalRootDir, "snap", snapName, "1", dirs.CoreLibExecDir, "info")
+       err := os.MkdirAll(filepath.Dir(infoFile), 0755)
+       c.Assert(err, IsNil)
+       err = ioutil.WriteFile(infoFile, []byte(vulnInfoFile), 0644)
+       c.Assert(err, IsNil)
+
+       // revision 2 fixed
+       infoFile2 := filepath.Join(dirs.GlobalRootDir, "snap", snapName, "2", dirs.CoreLibExecDir, "info")
+       err = os.MkdirAll(filepath.Dir(infoFile2), 0755)
+       c.Assert(err, IsNil)
+       err = ioutil.WriteFile(infoFile2, []byte(fixedInfoFile), 0644)
+       c.Assert(err, IsNil)
+
+       // revision 11 fixed
+       infoFile11 := filepath.Join(dirs.GlobalRootDir, "snap", snapName, "11", dirs.CoreLibExecDir, "info")
+       err = os.MkdirAll(filepath.Dir(infoFile11), 0755)
+       c.Assert(err, IsNil)
+       err = ioutil.WriteFile(infoFile11, []byte(fixedInfoFile), 0644)
+       c.Assert(err, IsNil)
+
+       // use generic classic model
+       r := snapstatetest.UseFallbackDeviceModel()
+       defer r()
+
+       st := s.state
+       st.Lock()
+       // ensure that only this specific snap is installed
+       snapstate.Set(s.state, "core", nil)
+       snapstate.Set(s.state, "snapd", nil)
+
+       snapSt := &snapstate.SnapState{
+               Active: true,
+               Sequence: []*snap.SideInfo{
+                       {RealName: snapName, Revision: snap.R(1)},
+                       {RealName: snapName, Revision: snap.R(2)},
+                       {RealName: snapName, Revision: snap.R(11)},
+               },
+               Current:  snap.R(11),
+               SnapType: "os",
+       }
+       if snapName == "snapd" {
+               snapSt.SnapType = "snapd"
+       }
+       snapstate.Set(s.state, snapName, snapSt)
+       st.Unlock()
+
+       // special policy only on classic
+       r = release.MockOnClassic(true)
+       defer r()
+       ensureErr := s.snapmgr.Ensure()
+       c.Assert(ensureErr, IsNil)
+
+       // we should have created a single remove change for revision 1, revision 2
+       // should have been left alone
+       st.Lock()
+       defer st.Unlock()
+
+       allChgs := st.Changes()
+       c.Assert(allChgs, HasLen, 1)
+       removeChg := allChgs[0]
+       c.Assert(removeChg.Status(), Equals, state.DoStatus)
+       c.Assert(removeChg.Kind(), Equals, "remove-snap")
+       c.Assert(removeChg.Summary(), Equals, fmt.Sprintf(`Remove vulnerable %q snap`, snapName))
+
+       c.Assert(removeChg.Tasks(), HasLen, 2)
+       clearSnap := removeChg.Tasks()[0]
+       discardSnap := removeChg.Tasks()[1]
+       c.Assert(clearSnap.Kind(), Equals, "clear-snap")
+       c.Assert(discardSnap.Kind(), Equals, "discard-snap")
+       var snapsup snapstate.SnapSetup
+       err = clearSnap.Get("snap-setup", &snapsup)
+       c.Assert(err, IsNil)
+       c.Assert(snapsup.SideInfo.Revision, Equals, snap.R(1))
+
+       // and we set the appropriate key in the state
+       var removeDone bool
+       st.Get(snapName+"-snap-cve-2021-44731-vuln-removed", &removeDone)
+       c.Assert(removeDone, Equals, true)
+}
+
+func (s *snapmgrTestSuite) TestEnsureChecksSnapdInfoFileOnClassicOnly(c *C) {
+       // delete the core/snapd snap info files - they should always exist in real
+       // devices, but deleting them here makes it so we can see the failure
+       // trying to read the files easily
+
+       infoFile := filepath.Join(dirs.GlobalRootDir, "snap", "core", "1", dirs.CoreLibExecDir, "info")
+       err := os.Remove(infoFile)
+       c.Assert(err, IsNil)
+
+       // special policy only on classic
+       r := release.MockOnClassic(true)
+       defer r()
+       ensureErr := s.snapmgr.Ensure()
+       c.Assert(ensureErr, ErrorMatches, "cannot open snapd info file.*")
+
+       // if we are not on classic nothing happens
+       r = release.MockOnClassic(false)
+       defer r()
+
+       ensureErr = s.snapmgr.Ensure()
+       c.Assert(ensureErr, IsNil)
+}
+
+func (s *snapmgrTestSuite) TestEnsureSkipsCheckingSnapdSnapInfoFileWhenStateSet(c *C) {
+       // we default from SetUp to having the core snap installed, remove it so we
+       // only have the snapd snap available
+       s.state.Lock()
+       snapstate.Set(s.state, "core", nil)
+       snapstate.Set(s.state, "snapd", &snapstate.SnapState{
+               Active: true,
+               Sequence: []*snap.SideInfo{
+                       {RealName: "snapd", Revision: snap.R(1)},
+               },
+               Current:  snap.R(1),
+               SnapType: "snapd",
+       })
+       s.state.Unlock()
+
+       s.testEnsureSkipsCheckingSnapdInfoFileWhenStateSet(c, "snapd")
+}
+
+func (s *snapmgrTestSuite) TestEnsureSkipsCheckingCoreSnapInfoFileWhenStateSet(c *C) {
+       s.testEnsureSkipsCheckingSnapdInfoFileWhenStateSet(c, "core")
+}
+
+func (s *snapmgrTestSuite) TestEnsureSkipsCheckingBothCoreAndSnapdSnapsInfoFileWhenStateSet(c *C) {
+       // special policy only on classic
+       r := release.MockOnClassic(true)
+       defer r()
+
+       st := s.state
+       // also set snapd snapd as installed
+       st.Lock()
+       snapstate.Set(st, "snapd", &snapstate.SnapState{
+               Active: true,
+               Sequence: []*snap.SideInfo{
+                       {RealName: "snapd", Revision: snap.R(1)},
+               },
+               Current:  snap.R(1),
+               SnapType: "snapd",
+       })
+       st.Unlock()
+
+       infoFileFor := func(snapName string) string {
+               return filepath.Join(dirs.GlobalRootDir, "snap", snapName, "1", dirs.CoreLibExecDir, "info")
+       }
+
+       // delete both snapd and core snap info files
+       for _, snapName := range []string{"core", "snapd"} {
+               err := os.Remove(infoFileFor(snapName))
+               c.Assert(err, IsNil)
+       }
+
+       // make sure Ensure makes a whole hearted attempt to read both files - snapd
+       // is tried first
+       ensureErr := s.snapmgr.Ensure()
+       c.Assert(ensureErr, ErrorMatches, fmt.Sprintf(`cannot open snapd info file "%s".*`, infoFileFor("snapd")))
+
+       st.Lock()
+       st.Set("snapd-snap-cve-2021-44731-vuln-removed", true)
+       st.Unlock()
+
+       // still unhappy about core file missing
+       ensureErr = s.snapmgr.Ensure()
+       c.Assert(ensureErr, ErrorMatches, fmt.Sprintf(`cannot open snapd info file "%s".*`, infoFileFor("core")))
+
+       // but with core state flag set too, we are now happy
+       st.Lock()
+       st.Set("core-snap-cve-2021-44731-vuln-removed", true)
+       st.Unlock()
+
+       ensureErr = s.snapmgr.Ensure()
+       c.Assert(ensureErr, IsNil)
+}
+
+func (s *snapmgrTestSuite) testEnsureSkipsCheckingSnapdInfoFileWhenStateSet(c *C, snapName string) {
+       // special policy only on classic
+       r := release.MockOnClassic(true)
+       defer r()
+
+       // delete the snap info file for this snap - they should always exist in
+       // real devices, but deleting them here makes it so we can see the failure
+       // trying to read the files easily
+       infoFile := filepath.Join(dirs.GlobalRootDir, "snap", snapName, "1", dirs.CoreLibExecDir, "info")
+       err := os.Remove(infoFile)
+       c.Assert(err, IsNil)
+
+       // make sure it makes a whole hearted attempt to read it
+       ensureErr := s.snapmgr.Ensure()
+       c.Assert(ensureErr, ErrorMatches, "cannot open snapd info file.*")
+
+       // now it should stop trying to check if state says so
+       st := s.state
+       st.Lock()
+       st.Set(snapName+"-snap-cve-2021-44731-vuln-removed", true)
+       st.Unlock()
+
+       ensureErr = s.snapmgr.Ensure()
+       c.Assert(ensureErr, IsNil)
+}
+
 func (s *snapmgrTestSuite) TestEnsureRefreshesAtSeedPolicy(c *C) {
        // special policy only on classic
        r := release.MockOnClassic(true)
@@ -4782,7 +5020,7 @@ func (s *snapmgrTestSuite) TestTransitionSnapdSnapDoesNotRunWhenNotEnabled(c *C)
 
        snapstate.Set(s.state, "core", &snapstate.SnapState{
                Active:   true,
-               Sequence: []*snap.SideInfo{{RealName: "corecore", SnapID: "core-snap-id", Revision: snap.R(1), Channel: "beta"}},
+               Sequence: []*snap.SideInfo{{RealName: "core", SnapID: "core-snap-id", Revision: snap.R(1), Channel: "beta"}},
                Current:  snap.R(1),
                SnapType: "os",
        })
@@ -4801,7 +5039,7 @@ func (s *snapmgrTestSuite) TestTransitionSnapdSnapStartsAutomaticallyWhenEnabled
 
        snapstate.Set(s.state, "core", &snapstate.SnapState{
                Active:   true,
-               Sequence: []*snap.SideInfo{{RealName: "corecore", SnapID: "core-snap-id", Revision: snap.R(1), Channel: "beta"}},
+               Sequence: []*snap.SideInfo{{RealName: "core", SnapID: "core-snap-id", Revision: snap.R(1), Channel: "beta"}},
                Current:  snap.R(1),
                SnapType: "os",
        })
@@ -4832,7 +5070,7 @@ func (s *snapmgrTestSuite) TestTransitionSnapdSnapWithCoreRunthrough(c *C) {
 
        snapstate.Set(s.state, "core", &snapstate.SnapState{
                Active:   true,
-               Sequence: []*snap.SideInfo{{RealName: "corecore", SnapID: "core-snap-id", Revision: snap.R(1), Channel: "edge"}},
+               Sequence: []*snap.SideInfo{{RealName: "core", SnapID: "core-snap-id", Revision: snap.R(1), Channel: "edge"}},
                Current:  snap.R(1),
                SnapType: "os",
                // TrackingChannel
diff --git a/tests/nested/manual/snapd-removes-vulnerable-snap-confine-revs/task.yaml b/tests/nested/manual/snapd-removes-vulnerable-snap-confine-revs/task.yaml
new file mode 100644 (file)
index 0000000..bc7fd05
--- /dev/null
@@ -0,0 +1,110 @@
+summary: Check that refreshing snapd to a fixed version removes vulnerable revs
+
+# just focal is fine for this test - we only need to check that things happen on
+# classic
+systems: [ubuntu-20.04-*]
+
+environment:
+  # which snap snapd comes from in this test
+  SNAPD_SOURCE_SNAP/snapd: snapd
+  SNAPD_SOURCE_SNAP/core: core
+
+  # needed to get a custom image
+  NESTED_IMAGE_ID: vuln-$SNAPD_SOURCE_SNAP-auto-removed
+
+  # where we mount the image
+  IMAGE_MOUNTPOINT: /mnt/cloudimg
+
+  # we don't actually use snapd from the branch in the seed of the VM initially,
+  # but we have to define this in order to get a custom image, see
+  # nested_get_image_name in nested.sh for this logic
+  NESTED_BUILD_SNAPD_FROM_CURRENT: true
+
+  # meh snap-state doesn't have a consistent naming for these snaps, so we can't
+  # just do "$SNAPD_SOURCE_SNAP-from-deb.snap"
+  REPACKED_SNAP_NAME/core: core-from-snapd-deb.snap
+  REPACKED_SNAP_NAME/snapd: snapd-from-deb.snap
+
+  # TODO: use more up to date, but still vulnerable revisions of core and snapd
+  # for this test to reduce the delta in functionality we end up testing here
+
+  # a specific vulnerable snapd version we can base our image on
+  VULN_SNAP_REV_URL/snapd: https://storage.googleapis.com/snapd-spread-tests/snaps/snapd_2.49.1_11402.snap
+
+  # a specific vulnerable core version we can base our image on
+  VULN_SNAP_REV_URL/core: https://storage.googleapis.com/snapd-spread-tests/snaps/core_2.45_9289.snap
+
+prepare: |
+  #shellcheck source=tests/lib/preseed.sh
+  . "$TESTSLIB/preseed.sh"
+
+  # create a VM and mount a cloud image
+  tests.nested build-image classic
+  mkdir -p "$IMAGE_MOUNTPOINT"
+  IMAGE_NAME=$(tests.nested get image-name classic)
+  mount_ubuntu_image "$(tests.nested get images-path)/$IMAGE_NAME" "$IMAGE_MOUNTPOINT"
+
+  # repack the deb into the snap we want
+  "$TESTSTOOLS"/snaps-state repack_snapd_deb_into_snap "$SNAPD_SOURCE_SNAP"
+
+  # add the known vulnerable version of snapd into the seed, dangerously
+  curl -s -o "$SNAPD_SOURCE_SNAP-vuln.snap" "$VULN_SNAP_REV_URL"
+
+  # repack to ensure it is a dangerous revision
+  unsquashfs -d "$SNAPD_SOURCE_SNAP-vuln" "$SNAPD_SOURCE_SNAP-vuln.snap"
+  rm "$SNAPD_SOURCE_SNAP-vuln.snap"
+  snap pack "$SNAPD_SOURCE_SNAP-vuln" --filename="$SNAPD_SOURCE_SNAP.snap"
+
+  # inject the vulnerable snap into the seed
+  inject_snap_into_seed "$IMAGE_MOUNTPOINT" "$SNAPD_SOURCE_SNAP"
+
+  # undo any preseeding, the images may have been preseeded without our snaps
+  # so we want to undo that to ensure our snaps are on them
+  SNAPD_DEBUG=1 /usr/lib/snapd/snap-preseed --reset "$IMAGE_MOUNTPOINT"
+
+  # unmount the image and start the VM
+  umount_ubuntu_image "$IMAGE_MOUNTPOINT"
+  tests.nested create-vm classic
+
+execute: |
+  # check the current snapd snap is vulnerable
+  tests.nested exec cat /snap/$SNAPD_SOURCE_SNAP/current/usr/lib/snapd/info | MATCH '^VERSION=2\.4.*'
+  VULN_REV=$(tests.nested exec "snap list $SNAPD_SOURCE_SNAP" | tail -n +2 | awk '{print $3}')
+
+  # now install our snapd deb from the branch - this is so we know the patched
+  # snapd is always executing, regardless of which snapd/core snap re-exec 
+  # nonsense is going on
+  SNAPD_DEB_ARR=( "$SPREAD_PATH"/../snapd_*.deb )
+  SNAPD_DEB=${SNAPD_DEB_ARR[0]}
+  tests.nested copy "$SNAPD_DEB"
+  tests.nested exec "sudo dpkg -i $(basename "$SNAPD_DEB")"
+
+  # now send the snap version of snapd under test to the VM
+  tests.nested copy "$REPACKED_SNAP_NAME"
+  tests.nested exec "sudo snap install $REPACKED_SNAP_NAME --dangerous"
+
+  # there is a race between the snap install finishing and removing the 
+  # vulnerable revision, so we have to wait a bit
+  VULN_SNAP_REMOVED=false
+  #shellcheck disable=SC2034
+  for i in $(seq 1 60); do
+    if tests.nested exec "snap list $SNAPD_SOURCE_SNAP --all" | NOMATCH "$VULN_REV"; then
+      VULN_SNAP_REMOVED=true
+      break
+    fi
+    sleep 1
+  done
+
+  if [ "$VULN_SNAP_REMOVED" != "true" ]; then
+    echo "vulnerable snap was not automatically removed"
+    exit 1
+  fi
+
+  # check that the current revision is not vulnerable
+  tests.nested exec cat /snap/$SNAPD_SOURCE_SNAP/current/usr/lib/snapd/info | NOMATCH '^VERSION=2\.4.*'
+
+  # and there are no other revisions
+  if [ "$(tests.nested exec "snap list $SNAPD_SOURCE_SNAP" | tail -n +2 | wc -l)"  != "1" ]; then
+    echo "unexpected extra revision of $SNAPD_SOURCE_SNAP installed"
+    exit 1
+  fi