"fmt"
"io"
"os"
+ "path/filepath"
"strings"
"time"
"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 (
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 {
m.refreshHints.Ensure(),
m.catalogRefresh.Ensure(),
m.localInstallCleanup(),
+ m.ensureVulnerableSnapConfineVersionsRemovedOnClassic(),
}
//FIXME: use firstErr helper
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) {
// 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)
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",
})
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",
})
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
--- /dev/null
+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