// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) 2016-2024 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package ifacestate

import (
	"errors"
	"fmt"
	"path"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"gopkg.in/tomb.v2"

	"github.com/snapcore/snapd/dirs"
	"github.com/snapcore/snapd/i18n"
	"github.com/snapcore/snapd/interfaces"
	"github.com/snapcore/snapd/interfaces/hotplug"
	"github.com/snapcore/snapd/logger"
	"github.com/snapcore/snapd/osutil"
	"github.com/snapcore/snapd/overlord/assertstate"
	"github.com/snapcore/snapd/overlord/configstate/config"
	"github.com/snapcore/snapd/overlord/hookstate"
	"github.com/snapcore/snapd/overlord/ifacestate/schema"
	"github.com/snapcore/snapd/overlord/restart"
	"github.com/snapcore/snapd/overlord/servicestate"
	"github.com/snapcore/snapd/overlord/snapstate"
	"github.com/snapcore/snapd/overlord/state"
	"github.com/snapcore/snapd/snap"
	"github.com/snapcore/snapd/snap/quota"
	"github.com/snapcore/snapd/strutil"
	"github.com/snapcore/snapd/timings"
)

var snapstateFinishRestart = snapstate.FinishRestart

// journalQuotaLayout returns the necessary journal quota mount layouts
// to mimick what systemd does for services with log namespaces.
func journalQuotaLayout(quotaGroup *quota.Group) []snap.Layout {
	if quotaGroup.JournalLimit == nil {
		return nil
	}

	// bind mount the journal namespace folder on top of the journal folder
	// /run/systemd/journal.<ns> -> /run/systemd/journal
	layouts := []snap.Layout{{
		Bind: path.Join(dirs.SnapSystemdRunDir, fmt.Sprintf("journal.%s", quotaGroup.JournalNamespaceName())),
		Path: path.Join(dirs.SnapSystemdRunDir, "journal"),
		Mode: 0755,
	}}
	return layouts
}

// getExtraLayouts helper function to dynamically calculate the extra mount layouts for
// a snap instance. These are the layouts which can change during the lifetime of a snap
// like for instance mimicking systemd journal namespace mount layouts.
func getExtraLayouts(st *state.State, snapInfo *snap.Info) ([]snap.Layout, error) {
	snapOpts, err := servicestate.SnapServiceOptions(st, snapInfo, nil)
	if err != nil {
		return nil, err
	}

	var extraLayouts []snap.Layout
	if snapOpts.QuotaGroup != nil {
		extraLayouts = append(extraLayouts, journalQuotaLayout(snapOpts.QuotaGroup)...)
	}

	return extraLayouts, nil
}

func (m *InterfaceManager) buildConfinementOptions(st *state.State, task *state.Task, snapInfo *snap.Info, flags snapstate.Flags) (interfaces.ConfinementOptions, error) {
	extraLayouts, err := getExtraLayouts(st, snapInfo)
	if err != nil {
		return interfaces.ConfinementOptions{}, fmt.Errorf("cannot get extra mount layouts of snap %q: %s", snapInfo.InstanceName(), err)
	}

	kernelSnap := ""
	deviceCtx, err := snapstate.DeviceCtx(st, task, nil)
	if err == nil {
		kernelSnap = deviceCtx.Kernel()
	}

	return interfaces.ConfinementOptions{
		DevMode:           flags.DevMode,
		JailMode:          flags.JailMode,
		Classic:           flags.Classic,
		ExtraLayouts:      extraLayouts,
		AppArmorPrompting: m.useAppArmorPrompting,
		KernelSnap:        kernelSnap,
	}, nil
}

func (m *InterfaceManager) setupAffectedSnaps(task *state.Task, affectingSnap string, affectedSnaps []string, tm timings.Measurer) error {
	st := task.State()

	// Setup security of the affected snaps.
	for _, affectedInstanceName := range affectedSnaps {
		// the snap that triggered the change needs to be skipped
		if affectedInstanceName == affectingSnap {
			continue
		}
		var snapst snapstate.SnapState
		if err := snapstate.Get(st, affectedInstanceName, &snapst); err != nil {
			task.Errorf("skipping security profiles setup for snap %q when handling snap %q: %v", affectedInstanceName, affectingSnap, err)
			continue
		}
		affectedSnapInfo, err := snapst.CurrentInfo()
		if err != nil {
			return err
		}
		if err := addImplicitInterfaces(st, affectedSnapInfo); err != nil {
			return err
		}

		affectedAppSet, err := appSetForSnapRevision(st, affectedSnapInfo)
		if err != nil {
			return fmt.Errorf("building app set for snap %q: %v", affectingSnap, err)
		}

		opts, err := m.buildConfinementOptions(st, task, affectedSnapInfo, snapst.Flags)
		if err != nil {
			return err
		}
		if err := m.setupSnapSecurity(task, affectedAppSet, opts, tm); err != nil {
			return err
		}
	}
	return nil
}

func delayedEffectsTask(chg *state.Change) *state.Task {
	for _, t := range chg.Tasks() {
		if t.Kind() == "process-delayed-security-backend-effects" {
			return t
		}
	}
	return nil
}

func (m *InterfaceManager) doSetupProfiles(task *state.Task, tomb *tomb.Tomb) error {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	perfTimings := state.TimingsForTask(task)
	defer perfTimings.Save(st)

	// Get snap.Info from bits handed by the snap manager.
	snapsup, err := snapstate.TaskSnapSetup(task)
	if err != nil {
		return err
	}

	var prepareProfiles bool
	if err := task.Get("prepare-profiles", &prepareProfiles); err != nil && !errors.Is(err, state.ErrNoState) {
		return err
	}

	var newConns []string
	if err := task.Get("new-connections", &newConns); err != nil && !errors.Is(err, state.ErrNoState) {
		return err
	}

	logger.Debugf("new connections: %v", newConns)

	delayedTask := delayedEffectsTask(task.Change())
	// we can only delay side effects if we have the delay task
	canDelay := delayedTask != nil
	logger.Debugf("has delayed effects support? %v", canDelay)

	snapInfo, err := snap.ReadInfo(snapsup.InstanceName(), snapsup.SideInfo)
	if err != nil {
		return err
	}

	if len(snapInfo.BadInterfaces) > 0 {
		st.Warnf("%s", snap.BadInterfacesSummary(snapInfo))
	}

	// We no longer do/need core-phase-2, see
	//   https://github.com/snapcore/snapd/pull/5301
	// This code is just here to deal with old state that may still
	// have the 2nd setup-profiles with this flag set.
	var corePhase2 bool
	if err := task.Get("core-phase-2", &corePhase2); err != nil && !errors.Is(err, state.ErrNoState) {
		return err
	}
	if corePhase2 {
		// nothing to do
		return nil
	}

	opts, err := m.buildConfinementOptions(st, task, snapInfo, snapsup.Flags)
	if err != nil {
		return err
	}

	if err := addImplicitInterfaces(st, snapInfo); err != nil {
		return err
	}

	// this app set is derived from the current task, which will include any
	// components that are already installed, with the addition of any new
	// components that are getting setup up by this task
	appSet, err := appSetForTask(task, snapInfo)
	if err != nil {
		return err
	}

	if prepareProfiles {
		// In prepare mode we only refresh repository state and run backend
		// preparation; full profile setup is deferred to the later
		// setup-profiles task after auto-connect.
		if _, _, err = m.refreshAppSetConnections(task, appSet); err != nil {
			return err
		}

		for _, backend := range m.repo.Backends() {
			if err := backend.Prepare(appSet); err != nil {
				return err
			}
		}

		// Keep PendingSecurity updated for restart durability while this
		// revision remains inactive.
		return setPendingProfilesSideInfo(task.State(), snapsup.InstanceName(), appSet)
	}

	delayedEffects, err := m.setupProfilesForAppSet(task, appSet, opts, newConns, canDelay, perfTimings)
	if err != nil {
		return err
	}

	if err := setPendingProfilesSideInfo(task.State(), snapsup.InstanceName(), appSet); err != nil {
		return err
	}

	if len(delayedEffects) != 0 {
		if delayedTask == nil {
			return fmt.Errorf("internal error: cannot delay backend effects without a handler task")
		}
		logger.Debugf("has delayed effects for snaps: %v", delayedEffects)
		task.Logf("delayed effects for snaps:\n%v", delayedEffects)
		err := DelayedBackendEffectsFor(delayedTask, triggeringSnap(snapsup.InstanceName()), delayedEffects)
		if err != nil {
			return err
		}
	}

	return nil
}

// setupPendingProfilesSideInfo helps updating information about any
// revision for which security profiles are set up while the snap is
// not yet active.
//
// This state is still required for correctness after the prepare-profiles
// split: it preserves restart durability while a snap is inactive during
// refresh, and lets ifacestate pick the most recent inactive
// revision/components when regenerating security for affected snaps.
func setPendingProfilesSideInfo(st *state.State, instanceName string, appSet *interfaces.SnapAppSet) error {
	var snapst snapstate.SnapState
	if err := snapstate.Get(st, instanceName, &snapst); err != nil && !errors.Is(err, state.ErrNoState) {
		return err
	}
	if !snapst.IsInstalled() {
		// not yet visible to the rest of the system, nothing to do here
		return nil
	}
	if snapst.Active {
		// nothing is pending
		return nil
	}

	if appSet != nil {
		csis := make([]*snap.ComponentSideInfo, 0, len(appSet.Components()))
		for _, ci := range appSet.Components() {
			csis = append(csis, &ci.ComponentSideInfo)
		}

		snapst.PendingSecurity = &snapstate.PendingSecurityState{
			SideInfo:   &appSet.Info().SideInfo,
			Components: csis,
		}
	} else {
		snapst.PendingSecurity = &snapstate.PendingSecurityState{}
	}

	snapstate.Set(st, instanceName, &snapst)
	return nil
}

// triggeringSnap is the snap name of a snap that triggered the side effect.
type triggeringSnap string

// affectedSnap is the name of a snap that is affected.
type affectedSnap string

// delayedEffectsForSnaps captures all affected snaps and delayed side effects for
// each security backend for each snap.
type delayedEffectsForSnaps map[affectedSnap]map[interfaces.SecuritySystem][]interfaces.DelayedSideEffect

// delayedEffects captures all delayed side effects grouped by snaps that triggered them.
type delayedEffects struct {
	// snap owning the slot -> plug snaps -> []work items
	TriggeringSnaps map[triggeringSnap]delayedEffectsForSnaps `json:"triggering-snaps"`
}

func newDelayedEffectsForSnaps() delayedEffectsForSnaps {
	return delayedEffectsForSnaps(make(map[affectedSnap]map[interfaces.SecuritySystem][]interfaces.DelayedSideEffect))
}

func (d delayedEffectsForSnaps) EnqueueFor(snapName affectedSnap, backend interfaces.SecuritySystem, item interfaces.DelayedSideEffect) {
	if d[snapName] == nil {
		d[snapName] = make(map[interfaces.SecuritySystem][]interfaces.DelayedSideEffect)
	}
	d[snapName][backend] = append(d[snapName][backend], item)
}

// refreshAppSetConnections refreshes repository connections for appSet and, on
// the setup-profiles do path, records undo data for persisted connection state
// that reloadConnections changed or dropped.
func (m *InterfaceManager) refreshAppSetConnections(task *state.Task, appSet *interfaces.SnapAppSet) ([]string, []string, error) {
	snapInfo := appSet.Info()
	snapName := appSet.InstanceName()

	// The snap may have been updated so perform the following operation to
	// ensure that we are always working on the correct state:
	//
	// - disconnect all connections to/from the given snap
	//   - remembering the snaps that were affected by this operation
	// - remove the (old) snap from the interfaces repository
	// - add the (new) snap to the interfaces repository
	// - restore connections based on what is kept in the state
	//   - if a connection cannot be restored then remove it from the state
	// - setup the security of all the affected snaps
	disconnectedSnaps, err := m.repo.DisconnectSnap(snapName)
	if err != nil {
		return nil, nil, err
	}

	// XXX: what about snap renames? We should remove the old name (or switch
	// to IDs in the interfaces repository)
	if err := m.repo.RemoveSnap(snapName); err != nil {
		return nil, nil, err
	}
	if err := m.repo.AddAppSet(appSet); err != nil {
		return nil, nil, err
	}

	if len(snapInfo.BadInterfaces) > 0 {
		task.Logf("%s", snap.BadInterfacesSummary(snapInfo))
	}

	reloadedConns, changedOrDroppedConns, err := m.reloadConnections(snapName)
	if err != nil {
		return nil, nil, err
	}

	// if this task modified any connection states, take a snapshot of the
	// original connections so that setup-profiles' undo can restore them, if
	// needed
	if task.Status() != state.UndoingStatus {
		if err := snapshotChangedConnectionsForUndo(task, snapName, changedOrDroppedConns); err != nil {
			return nil, nil, err
		}
	}

	return disconnectedSnaps, reloadedConns, nil
}

func (m *InterfaceManager) setupProfilesForAppSet(
	task *state.Task, appSet *interfaces.SnapAppSet, opts interfaces.ConfinementOptions,
	newConns []string,
	canDelay bool,
	tm timings.Measurer,
) (delayedEffects delayedEffectsForSnaps, err error) {
	st := task.State()

	snapName := appSet.InstanceName()
	disconnectedSnaps, reloadedConns, err := m.refreshAppSetConnections(task, appSet)
	if err != nil {
		return nil, err
	}

	affectedSet := make(map[string]bool)
	for _, name := range disconnectedSnaps {
		affectedSet[name] = true
	}

	snapsWithConnectedPlugs := make(map[string]bool)
	snapsWithConnectedSlots := make(map[string]bool)
	newConnectedSnaps := make(map[string]bool)
	// Identify affected snaps on either side of the connection.
	for _, connID := range reloadedConns {
		connRef, err := interfaces.ParseConnRef(connID)
		if err != nil {
			return nil, fmt.Errorf("internal error: cannot parse existing connection: %w", err)
		}

		affectedSet[connRef.PlugRef.Snap] = true
		affectedSet[connRef.SlotRef.Snap] = true

		// Snaps on the plug or slot side, other than the current one, are
		// indirectly affected.
		if connRef.PlugRef.Snap != snapName {
			snapsWithConnectedPlugs[connRef.PlugRef.Snap] = true
		}
		if connRef.SlotRef.Snap != snapName {
			snapsWithConnectedSlots[connRef.SlotRef.Snap] = true
		}
	}

	for _, connId := range newConns {
		connRef, err := interfaces.ParseConnRef(connId)
		if err != nil {
			return nil, fmt.Errorf("internal error: cannot parse new connection: %w", err)
		}
		newConnectedSnaps[connRef.PlugRef.Snap] = true
		newConnectedSnaps[connRef.SlotRef.Snap] = true
	}

	// back to a slice
	affectedNames := make([]string, 0, len(affectedSet))
	for name := range affectedSet {
		if name != snapName {
			affectedNames = append(affectedNames, name)
		}
	}
	sort.Strings(affectedNames)
	// the snap for which profiles are being set up comes first
	affectedNames = append([]string{snapName}, affectedNames...)

	// Obtain interfaces.SnapAppSet for each affected snap, skipping those that
	// cannot be found and compute the confinement options that apply to it.
	affectedSnapSets := make([]*interfaces.SnapAppSet, 0, len(affectedSet))
	confinementOpts := make([]interfaces.ConfinementOptions, 0, len(affectedSet))
	setupContexts := make(map[string]interfaces.SetupContext, len(affectedSet))

	// For the snap being setup we know exactly what was requested.
	affectedSnapSets = append(affectedSnapSets, appSet)
	confinementOpts = append(confinementOpts, opts)
	setupContexts[appSet.InstanceName()] = interfaces.SetupContext{
		// We are being updated
		Reason:          interfaces.SnapSetupReasonOwnUpdate,
		CanDelayEffects: false,
	}

	var delayedEffectsLock sync.Mutex
	var delayErr error
	delayedEffects = newDelayedEffectsForSnaps()

	// For remaining snaps we need to interrogate the state.
	for _, name := range affectedNames[1:] {
		var snapst snapstate.SnapState
		if err := snapstate.Get(st, name, &snapst); err != nil {
			task.Errorf("cannot obtain state of snap %s: %s", name, err)
			continue
		}
		snapInfo, err := snapst.CurrentInfo()
		if err != nil {
			return nil, err
		}
		if err := addImplicitInterfaces(st, snapInfo); err != nil {
			return nil, err
		}

		var appSet *interfaces.SnapAppSet
		if snapst.PendingSecurity != nil {
			// a content plug/slot may have already updated in this change, so the appSet
			// should reflect in the revision (otherwise, we may regenerate the
			// profile for the wrong revision)
			snapInfo.SideInfo = *snapst.PendingSecurity.SideInfo

			var comps []*snap.ComponentInfo
			for _, csi := range snapst.PendingSecurity.Components {
				ci, err := snapstate.ReadComponentInfo(snapInfo, csi)
				if err != nil {
					return nil, fmt.Errorf("cannot read component info when building app set %q: %v", name, err)
				}

				comps = append(comps, ci)
			}

			appSet, err = interfaces.NewSnapAppSet(snapInfo, comps)
		} else {
			appSet, err = appSetForSnapRevision(st, snapInfo)
		}

		if err != nil {
			return nil, fmt.Errorf("building app set for snap %q: %v", name, err)
		}

		opts, err := m.buildConfinementOptions(st, task, snapInfo, snapst.Flags)
		if err != nil {
			return nil, err
		}

		// The snap is affected though a connection, set the context for the
		// Setup() call, depending on which side of the connection it is.
		sctx := interfaces.SetupContext{}
		switch {
		case snapsWithConnectedPlugs[name] && snapsWithConnectedSlots[name]:
			// Same snap appears on both the plug side and slot side,
			// indicating a cyclic connection
			sctx.Reason = interfaces.SnapSetupReasonCyclicallyConnectedUpdate
		case snapsWithConnectedPlugs[name]:
			sctx.Reason = interfaces.SnapSetupReasonConnectedSlotProviderUpdate
		case snapsWithConnectedSlots[name]:
			sctx.Reason = interfaces.SnapSetupReasonConnectedPlugConsumerUpdate
		}

		// TODO: the safe bet right now is to only allow side effects when the
		// snap is on the plug side of the connection
		if sctx.Reason == interfaces.SnapSetupReasonConnectedSlotProviderUpdate {
			// for newly established connections, especially ones with plug side
			// connection hook, the side effects cannot be delayed
			_, isNewConnected := newConnectedSnaps[name]
			sctx.CanDelayEffects = canDelay && !isNewConnected
			if sctx.CanDelayEffects {
				// callback only makes sense if side effects can be delayed
				sctx.DelayEffect = func(backend interfaces.SecurityBackend, item interfaces.DelayedSideEffect) {
					delayedEffectsLock.Lock()
					defer delayedEffectsLock.Unlock()

					if backend == nil {
						// TODO:GOVERSION: use errors.Join
						delayErr = strutil.JoinErrors(delayErr,
							fmt.Errorf("internal error: attempt to delay effects without a backend"))
						return
					}
					if !interfaces.SupportsDelayingEffects(backend) {
						// TODO:GOVERSION: use errors.Join
						delayErr = strutil.JoinErrors(delayErr,
							fmt.Errorf("internal error: attempt to delay effects for backend %q without support for it",
								backend.Name()))
						return
					}
					delayedEffects.EnqueueFor(affectedSnap(snapst.InstanceName()), backend.Name(), item)
				}
			}
		}
		setupContexts[name] = sctx

		affectedSnapSets = append(affectedSnapSets, appSet)
		confinementOpts = append(confinementOpts, opts)
	}

	if err := m.setupSecurityByBackend(task, affectedSnapSets, confinementOpts, setupContexts, tm); err != nil {
		return nil, err
	}

	if delayErr != nil {
		return nil, delayErr
	}

	return delayedEffects, nil
}

func (m *InterfaceManager) doRemoveProfiles(task *state.Task, tomb *tomb.Tomb) error {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	perfTimings := state.TimingsForTask(task)
	defer perfTimings.Save(st)

	// Get SnapSetup for this snap. This is gives us the name of the snap.
	snapSetup, err := snapstate.TaskSnapSetup(task)
	if err != nil {
		return err
	}
	snapName := snapSetup.InstanceName()

	if err := m.removeProfilesForSnap(task, tomb, snapName, perfTimings); err != nil {
		return err
	}

	// no pending profiles on disk
	return setPendingProfilesSideInfo(task.State(), snapName, nil)
}

func (m *InterfaceManager) removeProfilesForSnap(task *state.Task, _ *tomb.Tomb, snapName string, tm timings.Measurer) error {
	// Disconnect the snap entirely.
	// This is required to remove the snap from the interface repository.
	// The returned list of affected snaps will need to have its security setup
	// to reflect the change.
	affectedSnaps, err := m.repo.DisconnectSnap(snapName)
	if err != nil {
		return err
	}
	if err := m.setupAffectedSnaps(task, snapName, affectedSnaps, tm); err != nil {
		return err
	}

	// Remove the snap from the interface repository.
	// This discards all the plugs and slots belonging to that snap.
	if err := m.repo.RemoveSnap(snapName); err != nil {
		return err
	}

	// Remove security artefacts of the snap.
	if err := m.removeSnapSecurity(task, snapName); err != nil {
		return err
	}

	return nil
}

// shouldUndoSetupProfiles determines whether the undo is actually required given
// the current task-set, and based on the name of the current task.
func shouldUndoSetupProfiles(task *state.Task, instanceName string) bool {
	// If there are no setup-profiles tasks marked as prepare mode in this
	// change, we're likely handling either an older change shape or a
	// component-only change; in those cases undo should run for
	// setup-profiles tasks.
	var isPrepareTask bool
	var hasPrepareProfiles bool
	for _, t := range task.Change().Tasks() {
		if t.Kind() != "setup-profiles" {
			continue
		}
		taskSnapSetup, err := snapstate.TaskSnapSetup(t)
		if err != nil || taskSnapSetup.InstanceName() != instanceName {
			continue
		}

		var prepareProfiles bool
		if err := t.Get("prepare-profiles", &prepareProfiles); err != nil && !errors.Is(err, state.ErrNoState) {
			continue
		}

		if prepareProfiles {
			// We must observe a task with prepare-profiles=true to know the new flow
			// exists for the task-set of this snap, otherwise this was created using the old
			// flow, and undo should always run
			hasPrepareProfiles = true
			if task == t {
				// this is the prepare-profiles task for the snap, undo should run for it
				isPrepareTask = true
			}
			break
		}
	}

	// With the split flow encoded as setup-profiles+flag, only the prepare-mode
	// setup-profiles task should run undo.
	return !hasPrepareProfiles || isPrepareTask
}

func (m *InterfaceManager) undoSetupProfiles(task *state.Task, tomb *tomb.Tomb) error {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	perfTimings := state.TimingsForTask(task)
	defer perfTimings.Save(st)

	var corePhase2 bool
	if err := task.Get("core-phase-2", &corePhase2); err != nil && !errors.Is(err, state.ErrNoState) {
		return err
	}
	if corePhase2 {
		// let the first setup-profiles deal with this
		return nil
	}

	snapsup, err := snapstate.TaskSnapSetup(task)
	if err != nil {
		return err
	}

	snapName := snapsup.InstanceName()
	if !shouldUndoSetupProfiles(task, snapName) {
		logger.Debugf("skipping undo of setup-profiles for task %q", task.ID())
		return nil
	}

	// The previous task's undo (link-snap) may have triggered a restart, if this
	// is the case we can only proceed once the restart has happened or we
	// may be invoking tools (like apparmor) from the wrong snapd. We set
	// the default to true as we cannot set it otherwise since the change will
	// always have been created by the old snapd (that may not have "finish-restart")
	logger.Debugf("finish restart from undoLinkSnap")
	finishOpts := snapstate.FinishRestartOptions{
		// Only default to true for snapd snap
		FinishRestartDefault: snapsup.Type == snap.TypeSnapd,
	}
	if err := snapstateFinishRestart(task, snapsup, finishOpts); err != nil {
		return err
	}

	// restore any connection state snapshot saved by refreshAppSetConnections on
	// the original setup-profiles do path before rebuilding profiles for the old
	// revision
	if err := restoreConnectionsForSetupProfiles(task); err != nil {
		return err
	}

	// Get the name from SnapSetup and use it to find the current SideInfo
	// about the snap, if there is one.
	var snapst snapstate.SnapState
	err = snapstate.Get(st, snapName, &snapst)
	if err != nil && !errors.Is(err, state.ErrNoState) {
		return err
	}
	sideInfo := snapst.CurrentSideInfo()
	if sideInfo == nil {
		// The snap was not installed before so undo should remove security profiles.
		return m.removeProfilesForSnap(task, tomb, snapName, perfTimings)
	} else {
		// The snap was installed before so undo should setup the old security profiles.
		snapInfo, err := snap.ReadInfo(snapName, sideInfo)
		if err != nil {
			return err
		}

		// When undoing, ignore any component setup data attached to the task and
		// regenerate profiles based on the components that are currently installed
		// for this revision.
		// TODO: The relevant confinement flags (devmode, jailmode and classic)
		// should be split out into a separate struct here to avoid passing all
		// of snapst.Flags.
		// OBS: It's important here that we are passing snapst.Flags
		// and not snapsup.Flags, as the latter may contain different flags optins that
		// impact profile generation while we want to be setting up the old profiles
		// based on the old flags. (Specifically the Classic flag may have changed)
		opts, err := m.buildConfinementOptions(st, task, snapInfo, snapst.Flags)
		if err != nil {
			return err
		}
		if err := addImplicitInterfaces(st, snapInfo); err != nil {
			return err
		}
		appSet, err := appSetForSnapRevision(st, snapInfo)
		if err != nil {
			return err
		}

		const canDefer = false
		if _, err := m.setupProfilesForAppSet(task, appSet, opts, nil, canDefer, perfTimings); err != nil {
			return err
		}
		return setPendingProfilesSideInfo(st, snapName, appSet)
	}
}

func (m *InterfaceManager) doDiscardConns(task *state.Task, _ *tomb.Tomb) error {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	snapSetup, err := snapstate.TaskSnapSetup(task)
	if err != nil {
		return err
	}

	instanceName := snapSetup.InstanceName()

	var snapst snapstate.SnapState
	err = snapstate.Get(st, instanceName, &snapst)
	if err != nil && !errors.Is(err, state.ErrNoState) {
		return err
	}

	if err == nil && len(snapst.Sequence.Revisions) != 0 {
		return fmt.Errorf("cannot discard connections for snap %q while it is present", instanceName)
	}
	conns, err := getConns(st)
	if err != nil {
		return err
	}
	removed := make(map[string]*schema.ConnState)
	for id := range conns {
		connRef, err := interfaces.ParseConnRef(id)
		if err != nil {
			return err
		}
		if connRef.PlugRef.Snap == instanceName || connRef.SlotRef.Snap == instanceName {
			removed[id] = conns[id]
			delete(conns, id)
		}
	}
	task.Set("removed", removed)
	setConns(st, conns)
	return nil
}

func (m *InterfaceManager) undoDiscardConns(task *state.Task, _ *tomb.Tomb) error {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	var removed map[string]*schema.ConnState
	err := task.Get("removed", &removed)
	if err != nil && !errors.Is(err, state.ErrNoState) {
		return err
	}

	conns, err := getConns(st)
	if err != nil {
		return err
	}

	for id, connState := range removed {
		conns[id] = connState
	}
	setConns(st, conns)
	task.Set("removed", nil)
	return nil
}

func getDynamicHookAttributes(task *state.Task) (plugAttrs, slotAttrs map[string]any, err error) {
	if err = task.Get("plug-dynamic", &plugAttrs); err != nil && !errors.Is(err, state.ErrNoState) {
		return nil, nil, err
	}
	if err = task.Get("slot-dynamic", &slotAttrs); err != nil && !errors.Is(err, state.ErrNoState) {
		return nil, nil, err
	}
	if plugAttrs == nil {
		plugAttrs = make(map[string]any)
	}
	if slotAttrs == nil {
		slotAttrs = make(map[string]any)
	}

	return plugAttrs, slotAttrs, nil
}

func setDynamicHookAttributes(task *state.Task, plugAttrs, slotAttrs map[string]any) {
	task.Set("plug-dynamic", plugAttrs)
	task.Set("slot-dynamic", slotAttrs)
}

func (m *InterfaceManager) doConnect(task *state.Task, _ *tomb.Tomb) (err error) {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	perfTimings := state.TimingsForTask(task)
	defer perfTimings.Save(st)

	plugRef, slotRef, err := getPlugAndSlotRefs(task)
	if err != nil {
		return err
	}

	var autoConnect bool
	if err := task.Get("auto", &autoConnect); err != nil && !errors.Is(err, state.ErrNoState) {
		return err
	}
	var byGadget bool
	if err := task.Get("by-gadget", &byGadget); err != nil && !errors.Is(err, state.ErrNoState) {
		return err
	}
	var delayedSetupProfiles bool
	if err := task.Get("delayed-setup-profiles", &delayedSetupProfiles); err != nil && !errors.Is(err, state.ErrNoState) {
		return err
	}

	deviceCtx, err := snapstate.DeviceCtx(st, task, nil)
	if err != nil {
		return err
	}

	conns, err := getConns(st)
	if err != nil {
		return err
	}

	connRef := &interfaces.ConnRef{PlugRef: plugRef, SlotRef: slotRef}

	var plugSnapst snapstate.SnapState
	if err := snapstate.Get(st, plugRef.Snap, &plugSnapst); err != nil {
		if autoConnect && errors.Is(err, state.ErrNoState) {
			// conflict logic should prevent this
			return fmt.Errorf("internal error: snap %q is no longer available for auto-connecting", plugRef.Snap)
		}
		return err
	}

	var slotSnapst snapstate.SnapState
	if err := snapstate.Get(st, slotRef.Snap, &slotSnapst); err != nil {
		if autoConnect && errors.Is(err, state.ErrNoState) {
			// conflict logic should prevent this
			return fmt.Errorf("internal error: snap %q is no longer available for auto-connecting", slotRef.Snap)
		}
		return err
	}

	plug := m.repo.Plug(connRef.PlugRef.Snap, connRef.PlugRef.Name)
	if plug == nil {
		// conflict logic should prevent this
		return fmt.Errorf("snap %q has no %q plug", connRef.PlugRef.Snap, connRef.PlugRef.Name)
	}

	plugAppSet, err := appSetForSnapRevision(st, plug.Snap)
	if err != nil {
		return fmt.Errorf("building app set for snap %q: %v", plug.Snap.InstanceName(), err)
	}

	slot := m.repo.Slot(connRef.SlotRef.Snap, connRef.SlotRef.Name)
	if slot == nil {
		// conflict logic should prevent this
		return fmt.Errorf("snap %q has no %q slot", connRef.SlotRef.Snap, connRef.SlotRef.Name)
	}

	slotAppSet, err := appSetForSnapRevision(st, slot.Snap)
	if err != nil {
		return fmt.Errorf("building app set for snap %q: %v", slot.Snap.InstanceName(), err)
	}

	// attributes are always present, even if there are no hooks (they're initialized by Connect).
	plugDynamicAttrs, slotDynamicAttrs, err := getDynamicHookAttributes(task)
	if err != nil {
		return fmt.Errorf("failed to get hook attributes: %s", err)
	}

	var policyChecker interfaces.PolicyFunc

	// manual connections and connections by the gadget obey the
	// policy "connection" rules, other auto-connections obey the
	// "auto-connection" rules
	if autoConnect && !byGadget {
		autochecker, err := newAutoConnectChecker(st, m.repo, deviceCtx)
		if err != nil {
			return err
		}
		policyChecker = func(plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) (bool, error) {
			ok, _, err := autochecker.check(plug, slot)
			return ok, err
		}
	} else {
		policyCheck, err := newConnectChecker(st, deviceCtx)
		if err != nil {
			return err
		}
		policyChecker = policyCheck.check
	}

	// static attributes of the plug and slot not provided, the ones from snap infos will be used
	conn, err := m.repo.Connect(connRef, nil, plugDynamicAttrs, nil, slotDynamicAttrs, policyChecker)
	if err != nil || conn == nil {
		return err
	}
	defer func() {
		if err != nil {
			if err := m.repo.Disconnect(plugRef.Snap, plugRef.Name, slotRef.Snap, slotRef.Name); err != nil {
				logger.Noticef("cannot undo failed connection: %v", err)
			}
		}
	}()

	if !delayedSetupProfiles {
		slotSnapInfo, err := slotSnapst.CurrentInfo()
		if err != nil {
			return err
		}
		slotOpts, err := m.buildConfinementOptions(st, task, slotSnapInfo, slotSnapst.Flags)
		if err != nil {
			return err
		}
		if err := m.setupSnapSecurity(task, slotAppSet, slotOpts, perfTimings); err != nil {
			return err
		}

		plugSnapInfo, err := plugSnapst.CurrentInfo()
		if err != nil {
			return err
		}
		plugOpts, err := m.buildConfinementOptions(st, task, plugSnapInfo, plugSnapst.Flags)
		if err != nil {
			return err
		}
		if err := m.setupSnapSecurity(task, plugAppSet, plugOpts, perfTimings); err != nil {
			return err
		}
	} else {
		logger.Debugf("Connect handler: skipping setupSnapSecurity for snaps %q and %q", plug.Snap.InstanceName(), slot.Snap.InstanceName())
	}

	// For undo handler. We need to remember old state of the connection only
	// if undesired flag is set because that means there was a remembered
	// inactive connection already and we should restore its properties
	// in case of undo. Otherwise we don't have to keep old-conn because undo
	// can simply delete any trace of the connection.
	if old, ok := conns[connRef.ID()]; ok && old.Undesired {
		task.Set("old-conn", old)
	}

	conns[connRef.ID()] = &schema.ConnState{
		Interface:        conn.Interface(),
		StaticPlugAttrs:  conn.Plug.StaticAttrs(),
		DynamicPlugAttrs: conn.Plug.DynamicAttrs(),
		StaticSlotAttrs:  conn.Slot.StaticAttrs(),
		DynamicSlotAttrs: conn.Slot.DynamicAttrs(),
		Auto:             autoConnect,
		ByGadget:         byGadget,
		HotplugKey:       slot.HotplugKey,
	}
	setConns(st, conns)

	// the dynamic attributes might have been updated by the interface's BeforeConnectPlug/Slot code,
	// so we need to update the task for connect-plug- and connect-slot- hooks to see new values.
	setDynamicHookAttributes(task, conn.Plug.DynamicAttrs(), conn.Slot.DynamicAttrs())
	return nil
}

func (m *InterfaceManager) doDisconnect(task *state.Task, _ *tomb.Tomb) error {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	perfTimings := state.TimingsForTask(task)
	defer perfTimings.Save(st)

	plugRef, slotRef, err := getPlugAndSlotRefs(task)
	if err != nil {
		return err
	}

	cref := interfaces.ConnRef{PlugRef: plugRef, SlotRef: slotRef}

	conns, err := getConns(st)
	if err != nil {
		return err
	}

	// forget flag can be passed with snap disconnect --forget
	var forget bool
	if err := task.Get("forget", &forget); err != nil && !errors.Is(err, state.ErrNoState) {
		return fmt.Errorf("internal error: cannot read 'forget' flag: %s", err)
	}

	var snapStates []snapstate.SnapState
	for _, instanceName := range []string{plugRef.Snap, slotRef.Snap} {
		var snapst snapstate.SnapState
		if err := snapstate.Get(st, instanceName, &snapst); err != nil {
			if errors.Is(err, state.ErrNoState) {
				task.Logf("skipping disconnect operation for connection %s %s, snap %q doesn't exist", plugRef, slotRef, instanceName)
				return nil
			}
			task.Errorf("skipping security profiles setup for snap %q when disconnecting %s from %s: %v", instanceName, plugRef, slotRef, err)
		} else {
			snapStates = append(snapStates, snapst)
		}
	}

	conn, ok := conns[cref.ID()]
	if !ok {
		return fmt.Errorf("internal error: connection %q not found in state", cref.ID())
	}

	// store old connection for undo
	task.Set("old-conn", conn)

	err = m.repo.Disconnect(plugRef.Snap, plugRef.Name, slotRef.Snap, slotRef.Name)
	if err != nil {
		_, notConnected := err.(*interfaces.NotConnectedError)
		_, noPlugOrSlot := err.(*interfaces.NoPlugOrSlotError)
		// not connected, just forget it.
		if forget && (notConnected || noPlugOrSlot) {
			delete(conns, cref.ID())
			setConns(st, conns)
			return nil
		}
		return fmt.Errorf("snapd changed, please retry the operation: %v", err)
	}

	for _, snapst := range snapStates {
		snapInfo, err := snapst.CurrentInfo()
		if err != nil {
			return err
		}

		appSet, err := appSetForSnapRevision(st, snapInfo)
		if err != nil {
			return fmt.Errorf("building app set for snap %q: %v", snapInfo.InstanceName(), err)
		}

		opts, err := m.buildConfinementOptions(st, task, snapInfo, snapst.Flags)
		if err != nil {
			return err
		}
		if err := m.setupSnapSecurity(task, appSet, opts, perfTimings); err != nil {
			return err
		}
	}

	// "auto-disconnect" flag indicates it's a disconnect triggered automatically as part of snap removal;
	// such disconnects should not set undesired flag and instead just remove the connection.
	var autoDisconnect bool
	if err := task.Get("auto-disconnect", &autoDisconnect); err != nil && !errors.Is(err, state.ErrNoState) {
		return fmt.Errorf("internal error: failed to read 'auto-disconnect' flag: %s", err)
	}

	// "by-hotplug" flag indicates it's a disconnect triggered by hotplug remove event;
	// we want to keep information of the connection and just mark it as hotplug-gone.
	var byHotplug bool
	if err := task.Get("by-hotplug", &byHotplug); err != nil && !errors.Is(err, state.ErrNoState) {
		return fmt.Errorf("internal error: cannot read 'by-hotplug' flag: %s", err)
	}

	switch {
	case forget:
		delete(conns, cref.ID())
	case byHotplug:
		conn.HotplugGone = true
		conns[cref.ID()] = conn
	case conn.Auto && !autoDisconnect:
		conn.Undesired = true
		conn.DynamicPlugAttrs = nil
		conn.DynamicSlotAttrs = nil
		conn.StaticPlugAttrs = nil
		conn.StaticSlotAttrs = nil
		conns[cref.ID()] = conn
	default:
		delete(conns, cref.ID())
	}
	setConns(st, conns)

	return nil
}

func (m *InterfaceManager) undoDisconnect(task *state.Task, _ *tomb.Tomb) error {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	perfTimings := state.TimingsForTask(task)
	defer perfTimings.Save(st)

	var oldconn schema.ConnState
	err := task.Get("old-conn", &oldconn)
	if errors.Is(err, state.ErrNoState) {
		return nil
	}
	if err != nil {
		return err
	}

	var forget bool
	if err := task.Get("forget", &forget); err != nil && !errors.Is(err, state.ErrNoState) {
		return fmt.Errorf("internal error: cannot read 'forget' flag: %s", err)
	}

	plugRef, slotRef, err := getPlugAndSlotRefs(task)
	if err != nil {
		return err
	}

	conns, err := getConns(st)
	if err != nil {
		return err
	}

	var plugSnapst snapstate.SnapState
	if err := snapstate.Get(st, plugRef.Snap, &plugSnapst); err != nil {
		return err
	}
	var slotSnapst snapstate.SnapState
	if err := snapstate.Get(st, slotRef.Snap, &slotSnapst); err != nil {
		return err
	}

	connRef := &interfaces.ConnRef{PlugRef: plugRef, SlotRef: slotRef}

	plug := m.repo.Plug(connRef.PlugRef.Snap, connRef.PlugRef.Name)
	slot := m.repo.Slot(connRef.SlotRef.Snap, connRef.SlotRef.Name)

	if forget && (plug == nil || slot == nil) {
		// we were trying to forget an inactive connection that was
		// referring to a non-existing plug or slot; just restore it
		// in the conns state but do not reconnect via repository.
		conns[connRef.ID()] = &oldconn
		setConns(st, conns)
		return nil
	}
	if plug == nil {
		return fmt.Errorf("snap %q has no %q plug", connRef.PlugRef.Snap, connRef.PlugRef.Name)
	}
	if slot == nil {
		return fmt.Errorf("snap %q has no %q slot", connRef.SlotRef.Snap, connRef.SlotRef.Name)
	}

	plugAppSet, err := appSetForSnapRevision(st, plug.Snap)
	if err != nil {
		return fmt.Errorf("building app set for snap %q: %v", plug.Snap.InstanceName(), err)
	}

	slotAppSet, err := appSetForSnapRevision(st, slot.Snap)
	if err != nil {
		return fmt.Errorf("building app set for snap %q: %v", slot.Snap.InstanceName(), err)
	}

	_, err = m.repo.Connect(connRef, nil, oldconn.DynamicPlugAttrs, nil, oldconn.DynamicSlotAttrs, nil)
	if err != nil {
		return err
	}

	slotSnapInfo, err := slotSnapst.CurrentInfo()
	if err != nil {
		return err
	}
	slotOpts, err := m.buildConfinementOptions(st, task, slotSnapInfo, slotSnapst.Flags)
	if err != nil {
		return err
	}
	if err := m.setupSnapSecurity(task, slotAppSet, slotOpts, perfTimings); err != nil {
		return err
	}

	plugSnapInfo, err := plugSnapst.CurrentInfo()
	if err != nil {
		return err
	}
	plugOpts, err := m.buildConfinementOptions(st, task, plugSnapInfo, plugSnapst.Flags)
	if err != nil {
		return err
	}
	if err := m.setupSnapSecurity(task, plugAppSet, plugOpts, perfTimings); err != nil {
		return err
	}

	conns[connRef.ID()] = &oldconn
	setConns(st, conns)

	return nil
}

func (m *InterfaceManager) undoConnect(task *state.Task, _ *tomb.Tomb) error {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	perfTimings := state.TimingsForTask(task)
	defer perfTimings.Save(st)

	plugRef, slotRef, err := getPlugAndSlotRefs(task)
	if err != nil {
		return err
	}
	connRef := interfaces.ConnRef{PlugRef: plugRef, SlotRef: slotRef}
	conns, err := getConns(st)
	if err != nil {
		return err
	}

	var old schema.ConnState
	err = task.Get("old-conn", &old)
	if err != nil && !errors.Is(err, state.ErrNoState) {
		return err
	}
	if err == nil {
		conns[connRef.ID()] = &old
	} else {
		delete(conns, connRef.ID())
	}
	setConns(st, conns)

	if err := m.repo.Disconnect(connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name); err != nil {
		return err
	}

	var delayedSetupProfiles bool
	if err := task.Get("delayed-setup-profiles", &delayedSetupProfiles); err != nil && !errors.Is(err, state.ErrNoState) {
		return err
	}
	if delayedSetupProfiles {
		logger.Debugf("Connect undo handler: skipping setupSnapSecurity for snaps %q and %q", connRef.PlugRef.Snap, connRef.SlotRef.Snap)
		return nil
	}

	plug := m.repo.Plug(connRef.PlugRef.Snap, connRef.PlugRef.Name)
	if plug == nil {
		return fmt.Errorf("internal error: snap %q has no %q plug", connRef.PlugRef.Snap, connRef.PlugRef.Name)
	}

	plugAppSet, err := appSetForSnapRevision(st, plug.Snap)
	if err != nil {
		return fmt.Errorf("building app set for snap %q: %v", plug.Snap.InstanceName(), err)
	}

	slot := m.repo.Slot(connRef.SlotRef.Snap, connRef.SlotRef.Name)
	if slot == nil {
		return fmt.Errorf("internal error: snap %q has no %q slot", connRef.SlotRef.Snap, connRef.SlotRef.Name)
	}

	slotAppSet, err := appSetForSnapRevision(st, slot.Snap)
	if err != nil {
		return fmt.Errorf("building app set for snap %q: %v", slot.Snap.InstanceName(), err)
	}

	var plugSnapst snapstate.SnapState
	err = snapstate.Get(st, plugRef.Snap, &plugSnapst)
	if errors.Is(err, state.ErrNoState) {
		return fmt.Errorf("internal error: snap %q is no longer available", plugRef.Snap)
	}
	if err != nil {
		return err
	}
	var slotSnapst snapstate.SnapState
	err = snapstate.Get(st, slotRef.Snap, &slotSnapst)
	if errors.Is(err, state.ErrNoState) {
		return fmt.Errorf("internal error: snap %q is no longer available", slotRef.Snap)
	}
	if err != nil {
		return err
	}

	slotSnapInfo, err := slotSnapst.CurrentInfo()
	if err != nil {
		return err
	}
	slotOpts, err := m.buildConfinementOptions(st, task, slotSnapInfo, slotSnapst.Flags)
	if err != nil {
		return err
	}
	if err := m.setupSnapSecurity(task, slotAppSet, slotOpts, perfTimings); err != nil {
		return err
	}

	plugSnapInfo, err := plugSnapst.CurrentInfo()
	if err != nil {
		return err
	}
	plugOpts, err := m.buildConfinementOptions(st, task, plugSnapInfo, plugSnapst.Flags)
	if err != nil {
		return err
	}
	if err := m.setupSnapSecurity(task, plugAppSet, plugOpts, perfTimings); err != nil {
		return err
	}

	return nil
}

// timeout for shared content retry
var contentLinkRetryTimeout = 30 * time.Second

// timeout for retrying hotplug-related tasks
var hotplugRetryTimeout = 300 * time.Millisecond

func obsoleteCorePhase2SetupProfiles(kind string, task *state.Task) (bool, error) {
	if kind != "setup-profiles" {
		return false, nil
	}

	var corePhase2 bool
	if err := task.Get("core-phase-2", &corePhase2); err != nil && !errors.Is(err, state.ErrNoState) {
		return false, err
	}
	return corePhase2, nil
}

func checkAutoconnectConflicts(st *state.State, autoconnectTask *state.Task, plugSnap, slotSnap string) error {
	for _, task := range st.Tasks() {
		if task.Status().Ready() {
			continue
		}

		k := task.Kind()
		if k == "connect" || k == "disconnect" {
			// if the task depends on the auto-connect in some way, then we assume that it is safe to go ahead
			// as it was scheduled by that exact task, and we should not block for that.
			if inSameChangeWaitChain(autoconnectTask, task) {
				continue
			}

			// retry if we found another connect/disconnect affecting same snap; note we can only encounter
			// connects/disconnects created by doAutoDisconnect / doAutoConnect here as manual interface ops
			// are rejected by conflict check logic in snapstate.
			plugRef, slotRef, err := getPlugAndSlotRefs(task)
			if err != nil {
				return err
			}
			if plugRef.Snap == plugSnap {
				return &state.Retry{After: connectRetryTimeout, Reason: fmt.Sprintf("conflicting plug snap %s, task %q", plugSnap, k)}
			}
			if slotRef.Snap == slotSnap {
				return &state.Retry{After: connectRetryTimeout, Reason: fmt.Sprintf("conflicting slot snap %s, task %q", slotSnap, k)}
			}
			continue
		}

		snapsup, err := snapstate.TaskSnapSetup(task)
		// e.g. hook tasks don't have task snap setup
		if err != nil {
			continue
		}

		otherSnapName := snapsup.InstanceName()

		// different snaps - no conflict
		if otherSnapName != plugSnap && otherSnapName != slotSnap {
			continue
		}

		// setup-profiles core-phase-2 is now no-op, we shouldn't
		// conflict on it; note, old snapd would create this task even
		// for regular snaps if installed with the dangerous flag.
		obsoleteCorePhase2, err := obsoleteCorePhase2SetupProfiles(k, task)
		if err != nil {
			return err
		}
		if obsoleteCorePhase2 {
			continue
		}

		// other snap that affects us because of plug or slot
		if k == "unlink-snap" || k == "link-snap" || k == "setup-profiles" || k == "discard-snap" {
			// discard-snap is scheduled as part of garbage collection during refresh, if multiple revsions are already installed.
			// this revision check avoids conflict with own discard tasks created as part of install/refresh.
			if k == "discard-snap" && autoconnectTask.Change() != nil && autoconnectTask.Change().ID() == task.Change().ID() {
				continue
			}

			// setup-profiles will be scheduled when we schedule the connect hooks during pre-seed, and they are postponed until
			// after pre-seeding. They will still cause a conflict here, so take that into account.
			if k == "setup-profiles" && inSameChangeWaitChain(autoconnectTask, task) {
				continue
			}

			// if snap is getting removed, we will retry but the snap will be gone and auto-connect becomes no-op
			// if snap is getting installed/refreshed - temporary conflict, retry later
			return &state.Retry{After: connectRetryTimeout, Reason: fmt.Sprintf("conflicting snap %s with task %q", otherSnapName, k)}
		}
	}
	return nil
}

func checkDisconnectConflicts(st *state.State, disconnectingSnap, plugSnap, slotSnap string) error {
	for _, task := range st.Tasks() {
		if task.Status().Ready() {
			continue
		}

		k := task.Kind()
		if k == "connect" || k == "disconnect" {
			// retry if we found another connect/disconnect affecting same snap; note we can only encounter
			// connects/disconnects created by doAutoDisconnect / doAutoConnect here as manual interface ops
			// are rejected by conflict check logic in snapstate.
			plugRef, slotRef, err := getPlugAndSlotRefs(task)
			if err != nil {
				return err
			}
			if plugRef.Snap == plugSnap || slotRef.Snap == slotSnap {
				return &state.Retry{After: connectRetryTimeout}
			}
			continue
		}

		snapsup, err := snapstate.TaskSnapSetup(task)
		// e.g. hook tasks don't have task snap setup
		if err != nil {
			continue
		}

		otherSnapName := snapsup.InstanceName()

		// different snaps - no conflict
		if otherSnapName != plugSnap && otherSnapName != slotSnap {
			continue
		}

		// another task related to same snap op (unrelated op would be blocked by snapstate conflict logic)
		if otherSnapName == disconnectingSnap {
			continue
		}

		// note, don't care about unlink-snap for the opposite end. This relies
		// on the fact that auto-disconnect will create conflicting "disconnect" tasks that
		// we will retry with the logic above.
		if k == "link-snap" || k == "setup-profiles" {
			// other snap is getting installed/refreshed - temporary conflict
			return &state.Retry{After: connectRetryTimeout}
		}
	}
	return nil
}

func checkHotplugDisconnectConflicts(st *state.State, plugSnap, slotSnap string) error {
	for _, task := range st.Tasks() {
		if task.Status().Ready() {
			continue
		}

		k := task.Kind()
		if k == "connect" || k == "disconnect" {
			plugRef, slotRef, err := getPlugAndSlotRefs(task)
			if err != nil {
				return err
			}
			if plugRef.Snap == plugSnap {
				return &state.Retry{After: connectRetryTimeout, Reason: fmt.Sprintf("conflicting plug snap %s, task %q", plugSnap, k)}
			}
			if slotRef.Snap == slotSnap {
				return &state.Retry{After: connectRetryTimeout, Reason: fmt.Sprintf("conflicting slot snap %s, task %q", slotSnap, k)}
			}
			continue
		}

		snapsup, err := snapstate.TaskSnapSetup(task)
		// e.g. hook tasks don't have task snap setup
		if err != nil {
			continue
		}
		otherSnapName := snapsup.InstanceName()

		// different snaps - no conflict
		if otherSnapName != plugSnap && otherSnapName != slotSnap {
			continue
		}

		if k == "link-snap" || k == "setup-profiles" || k == "unlink-snap" {
			// other snap is getting installed/refreshed/removed - temporary conflict
			return &state.Retry{After: connectRetryTimeout, Reason: fmt.Sprintf("conflicting snap %s with task %q", otherSnapName, k)}
		}
	}
	return nil
}

// inSameChangeWaitChains returns true if there is a wait chain so
// that `startT` is run before `searchT` in the same state.Change.
func inSameChangeWaitChain(startT, searchT *state.Task) bool {
	// Trivial case, tasks in different changes (they could in theory
	// still have cross-change waits but we don't do these today).
	// In this case, return quickly.
	if startT.Change() != searchT.Change() {
		return false
	}
	seenTasks := make(map[string]bool)
	// Do a recursive check if its in the same change
	return waitChainSearch(startT, searchT, seenTasks)
}

func waitChainSearch(startT, searchT *state.Task, seenTasks map[string]bool) bool {
	if seenTasks[startT.ID()] {
		return false
	}
	seenTasks[startT.ID()] = true
	for _, cand := range startT.HaltTasks() {
		if cand == searchT {
			return true
		}
		if waitChainSearch(cand, searchT, seenTasks) {
			return true
		}
	}

	return false
}

// batchConnectTasks creates connect tasks and interface hooks for
// conns and sets their wait chain with regard to the setupProfiles
// task.
//
// The tasks are chained so that: - prepare-plug-, prepare-slot- and
// connect tasks are all executed before setup-profiles -
// connect-plug-, connect-slot- are all executed after setup-profiles.
// The "delayed-setup-profiles" flag is set on the connect tasks to
// indicate that doConnect handler should not set security backends up
// because this will be done later by the setup-profiles task.
func batchConnectTasks(st *state.State, snapsup *snapstate.SnapSetup, conns map[string]*interfaces.ConnRef, connOpts map[string]*connectOpts) (ts *state.TaskSet, hasInterfaceHooks bool, err error) {
	setupProfiles := st.NewTask("setup-profiles",
		fmt.Sprintf(
			i18n.G("Setup snap %q (%s) security profiles"),
			snapsup.InstanceName(), snapsup.Revision()))
	setupProfiles.Set("snap-setup", snapsup)

	var newConnections []string
	ts = state.NewTaskSet()
	for connID, conn := range conns {
		var opts connectOpts
		if providedOpts := connOpts[connID]; providedOpts != nil {
			opts = *providedOpts
		} else {
			// default
			opts.AutoConnect = true
		}
		opts.DelayedSetupProfiles = true
		connectTs, err := connect(st, conn.PlugRef.Snap, conn.PlugRef.Name, conn.SlotRef.Snap, conn.SlotRef.Name, opts)
		if err != nil {
			return nil, false, fmt.Errorf("internal error: auto-connect of %q failed: %s", conn, err)
		}

		if len(connectTs.Tasks()) > 1 {
			hasInterfaceHooks = true
		}

		// setup-profiles needs to wait for the main "connect" task
		connectTask, _ := connectTs.Edge(ConnectTaskEdge)
		if connectTask == nil {
			return nil, false, fmt.Errorf("internal error: no 'connect' task found for %q", conn)
		}
		setupProfiles.WaitFor(connectTask)

		// setup-profiles must be run before the task that marks the end of connect-plug- and connect-slot- hooks
		afterConnectTask, _ := connectTs.Edge(AfterConnectHooksEdge)
		if afterConnectTask != nil {
			afterConnectTask.WaitFor(setupProfiles)
		}
		ts.AddAll(connectTs)

		newConnections = append(newConnections, conn.ID())
	}

	if len(ts.Tasks()) > 0 {
		sort.Strings(newConnections)
		setupProfiles.Set("new-connections", newConnections)
	}

	// always inject setupProfiles, we no longer generate profiles prior to making the
	// snap available on the system, so we should always make sure to do this after
	// connections, even if none needs to be made.
	ts.AddTask(setupProfiles)

	return ts, hasInterfaceHooks, nil
}

// firstTaskAfterBootWhenPreseeding finds the first task to be run for thisSnap
// on first boot after mark-preseeded task, this is always the install hook.
// It is an internal error if install hook for thisSnap cannot be found.
func firstTaskAfterBootWhenPreseeding(thisSnap string, markPreseeded *state.Task) (*state.Task, error) {
	if markPreseeded.Change() == nil {
		return nil, fmt.Errorf("internal error: %s task not in change", markPreseeded.Kind())
	}
	for _, ht := range markPreseeded.HaltTasks() {
		if ht.Kind() == "run-hook" {
			var hs hookstate.HookSetup
			if err := ht.Get("hook-setup", &hs); err != nil {
				return nil, fmt.Errorf("internal error: cannot get hook setup: %v", err)
			}
			if hs.Hook == "install" && hs.Snap == thisSnap {
				return ht, nil
			}
		}
	}
	return nil, fmt.Errorf("internal error: cannot find install hook for snap %q", thisSnap)
}

func filterForSlot(slot *snap.SlotInfo) func(candSlots []*snap.SlotInfo) []*snap.SlotInfo {
	return func(candSlots []*snap.SlotInfo) []*snap.SlotInfo {
		for _, candSlot := range candSlots {
			if candSlot.String() == slot.String() {
				return []*snap.SlotInfo{slot}
			}
		}
		return nil
	}
}

// Keep this separate to allow mocking
var isSnapVerified = func(st *state.State, snapID string) bool {
	acc, err := assertstate.Publisher(st, snapID)
	if err != nil {
		logger.Debugf("cannot retrieve publisher assertion for %q: %v", snapID, err)
		return false
	}
	return acc.Validation() == "verified"
}

func getAllowOptionAsString(inter string, tr *config.Transaction) (string, error) {
	var value any
	option := fmt.Sprintf("interface.%s.allow-auto-connection", inter)
	err := tr.Get("core", option, &value)
	if err != nil && !config.IsNoOption(err) {
		return "", err
	}

	switch option := value.(type) {
	case string:
		return option, nil
	case bool:
		return strconv.FormatBool(option), nil
	}
	return "", nil
}

func isSnapSlotAutoConnectAllowed(st *state.State, snapID string, slot *snap.SlotInfo, tr *config.Transaction) (bool, error) {
	// Until we decide that we want to support other interfaces, do a
	// quick allow check for x11 only.
	if slot.Interface != "x11" {
		return true, nil
	}

	option, err := getAllowOptionAsString(slot.Interface, tr)
	if err != nil {
		return false, err
	}
	switch option {
	case "", "true":
		return true, nil
	case "false":
		return false, nil
	case "verified":
		return isSnapVerified(st, snapID), nil
	}
	return false, fmt.Errorf(
		"internal error: invalid allow-auto-connection status %s for %s",
		option, slot.Snap.RealName)
}

func filterAllowedAutoConnectionSlots(st *state.State, snapID string, ssi []*snap.SlotInfo) ([]*snap.SlotInfo, error) {
	var filtered []*snap.SlotInfo
	tr := config.NewTransaction(st)
	for _, slot := range ssi {
		if allowed, err := isSnapSlotAutoConnectAllowed(st, snapID, slot, tr); err != nil {
			// In case there is any error of filtering auto-connections, assume something is horribly
			// wrong and return to the default behaviour. However make sure we log this error from the
			// caller.
			return ssi, err
		} else if allowed {
			filtered = append(filtered, slot)
		} else {
			logger.Debugf("slot %v interface %v disallowed auto-connection, skipping connection", slot, slot.Interface)
		}
	}
	return filtered, nil
}

// doAutoConnect creates task(s) to connect the given snap to viable candidates.
func (m *InterfaceManager) doAutoConnect(task *state.Task, _ *tomb.Tomb) error {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	snapsup, err := snapstate.TaskSnapSetup(task)
	if err != nil {
		return err
	}

	deviceCtx, err := snapstate.DeviceCtx(st, task, nil)
	if err != nil {
		return err
	}

	conns, err := getConns(st)
	if err != nil {
		return err
	}

	// The previous task (link-snap) may have triggered a restart, if this
	// is the case we can only proceed once the restart has happened or we
	// may not have all the interfaces of the new core/base snap. We set
	// the default to true as we always called FinishRestart in older
	// snapd.
	logger.Debugf("finish restart from doAutoConnect")
	if err := snapstateFinishRestart(task, snapsup,
		snapstate.FinishRestartOptions{FinishRestartDefault: true}); err != nil {
		return err
	}

	snapName := snapsup.InstanceName()

	autochecker, err := newAutoConnectChecker(st, m.repo, deviceCtx)
	if err != nil {
		return err
	}

	gadgectConnect := newGadgetConnect(st, task, m.repo, snapName, deviceCtx)

	// wait for auto-install, started by prerequisites code, for
	// the default-providers of content ifaces so we can
	// auto-connect to them; snapstate prerequisites does a bit
	// more filtering than this so defaultProviders here can
	// contain some more snaps; should not be an issue in practice
	// given the check below checks for same chain and we don't
	// forcefully wait for defaultProviders; we just retry for
	// things in the intersection between defaultProviders here and
	// snaps with not ready link-snap|setup-profiles tasks
	defaultProviders := snap.DefaultContentProviders(m.repo.Plugs(snapName))
	for _, chg := range st.Changes() {
		if chg.IsReady() {
			continue
		}
		for _, t := range chg.Tasks() {
			if t.Status().Ready() {
				continue
			}
			if t.Kind() != "link-snap" && t.Kind() != "setup-profiles" {
				continue
			}
			if snapsup, err := snapstate.TaskSnapSetup(t); err == nil {
				// Only retry if the task that installs the
				// content provider is not waiting for us
				// (or this will just hang forever).
				_, ok := defaultProviders[snapsup.InstanceName()]
				if ok && !inSameChangeWaitChain(task, t) {
					return &state.Retry{After: contentLinkRetryTimeout}
				}
			}
		}
	}

	plugs := m.repo.Plugs(snapName)
	slots := m.repo.Slots(snapName)
	newconns := make(map[string]*interfaces.ConnRef, len(plugs)+len(slots))
	var connOpts map[string]*connectOpts

	conflictError := func(retry *state.Retry, err error) error {
		if retry != nil {
			task.Logf("Waiting for conflicting change in progress: %s", retry.Reason)
			return retry // will retry
		}
		return fmt.Errorf("auto-connect conflict check failed: %v", err)
	}

	// Consider gadget connections, we want to remember them in
	// any case with "by-gadget" set, so they should be processed
	// before the auto-connection ones.
	if err := gadgectConnect.addGadgetConnections(newconns, conns, conflictError); err != nil {
		return err
	}
	if len(newconns) > 0 {
		connOpts = make(map[string]*connectOpts, len(newconns))
		byGadgetOpts := &connectOpts{AutoConnect: true, ByGadget: true}
		for key := range newconns {
			connOpts[key] = byGadgetOpts
		}
	}

	// Auto-connect all the plugs unless specifically disallowed
	checkAutoConnectAllowed := func(css []*snap.SlotInfo) []*snap.SlotInfo {
		filtered, err := filterAllowedAutoConnectionSlots(st, snapsup.SideInfo.SnapID, css)
		if err != nil {
			// Log the error that we failed to filter out the auto-connections. We do not
			// want to mess up auto-connections because we failed this 'opt-in' support.
			task.Logf("failed to filter auto-connection slots: %v", err)
		}
		return filtered
	}

	cannotAutoConnectLog := func(plug *snap.PlugInfo, candRefs []string) string {
		return fmt.Sprintf("cannot auto-connect plug %s, candidates found: %s", plug, strings.Join(candRefs, ", "))
	}
	if err := autochecker.addAutoConnections(task, newconns, plugs, checkAutoConnectAllowed, conns, cannotAutoConnectLog, conflictError); err != nil {
		return err
	}
	// Auto-connect all the slots
	for _, slot := range slots {
		candidates := m.repo.AutoConnectCandidatePlugs(snapName, slot.Name, autochecker.check)
		if len(candidates) == 0 {
			continue
		}

		cannotAutoConnectLog := func(plug *snap.PlugInfo, candRefs []string) string {
			return fmt.Sprintf("cannot auto-connect slot %s to plug %s, candidates found: %s", slot, plug, strings.Join(candRefs, ", "))
		}
		if err := autochecker.addAutoConnections(task, newconns, candidates, filterForSlot(slot), conns, cannotAutoConnectLog, conflictError); err != nil {
			return err
		}
	}
	autots, hasInterfaceHooks, err := batchConnectTasks(st, snapsup, newconns, connOpts)
	if err != nil {
		return err
	}

	// If interface hooks are not present then connects can be executed during
	// preseeding.
	// Otherwise we will run all connects, their hooks and setup-profiles after
	// preseeding (on first boot). Note, we may be facing multiple connections
	// here where only some have hooks; however there is no point in running
	// those without hooks before mark-preseeded, because only setup-profiles is
	// performance-critical and it still needs to run after those with hooks.
	if m.preseed && hasInterfaceHooks {
		// note, hasInterfaceHooks implies autots != nil, so no extra check
		for _, t := range st.Tasks() {
			if t.Kind() == "mark-preseeded" {
				markPreseeded := t
				// consistency check
				if markPreseeded.Status() != state.DoStatus {
					return fmt.Errorf("internal error: unexpected state of mark-preseeded task: %s", markPreseeded.Status())
				}

				firstTaskAfterBoot, err := firstTaskAfterBootWhenPreseeding(snapsup.InstanceName(), markPreseeded)
				if err != nil {
					return err
				}
				// first task of the snap that normally runs on first boot
				// needs to wait on connects & interface hooks.
				firstTaskAfterBoot.WaitAll(autots)

				// connect tasks and interface hooks need to wait for end of preseeding
				// (they need to run on first boot, not during preseeding).
				autots.WaitFor(markPreseeded)
				t.Change().AddAll(autots)
				task.SetStatus(state.DoneStatus)
				st.EnsureBefore(0)
				return nil
			}
		}
		return fmt.Errorf("internal error: mark-preseeded task not found in preseeding mode")
	}

	if autots != nil && len(autots.Tasks()) > 0 {
		snapstate.InjectTasks(task, autots)
		st.EnsureBefore(0)
	}

	task.SetStatus(state.DoneStatus)

	// Inject fault after the state is set to done
	osutil.MaybeInjectFault("after-auto-connect")

	return nil
}

// doAutoDisconnect creates tasks for disconnecting all interfaces of a snap and running its interface hooks.
func (m *InterfaceManager) doAutoDisconnect(task *state.Task, _ *tomb.Tomb) error {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	snapsup, err := snapstate.TaskSnapSetup(task)
	if err != nil {
		return err
	}

	snapName := snapsup.InstanceName()
	connections, err := m.repo.Connections(snapName)
	if err != nil {
		return err
	}

	// check for conflicts on all connections first before creating disconnect hooks
	for _, connRef := range connections {
		if err := checkDisconnectConflicts(st, snapName, connRef.PlugRef.Snap, connRef.SlotRef.Snap); err != nil {
			if _, retry := err.(*state.Retry); retry {
				logger.Debugf("disconnecting interfaces of snap %q will be retried because of %q - %q conflict", snapName, connRef.PlugRef.Snap, connRef.SlotRef.Snap)
				task.Logf("Waiting for conflicting change in progress...")
				return err // will retry
			}
			return fmt.Errorf("cannot check conflicts when disconnecting interfaces: %s", err)
		}
	}

	hookTasks := state.NewTaskSet()
	for _, connRef := range connections {
		conn, err := m.repo.Connection(connRef)
		if err != nil {
			break
		}
		// "auto-disconnect" flag indicates it's a disconnect triggered as part of snap removal, in which
		// case we want to skip the logic of marking auto-connections as 'undesired' and instead just remove
		// them so they can be automatically connected if the snap is installed again.
		ts, err := disconnectTasks(st, conn, disconnectOpts{
			AutoDisconnect: true,
			// Ignore errors from disconnect hooks.
			IgnoreHookError: true,
		})
		if err != nil {
			return err
		}
		hookTasks.AddAll(ts)
	}

	snapstate.InjectTasks(task, hookTasks)

	// make sure that we add tasks and mark this task done in the same atomic write, otherwise there is a risk of re-adding tasks again
	task.SetStatus(state.DoneStatus)
	return nil
}

func (m *InterfaceManager) undoAutoConnect(task *state.Task, _ *tomb.Tomb) error {
	// TODO Introduce disconnection hooks, and run them here as well to give a chance
	// for the snap to undo whatever it did when the connection was established.
	return nil
}

// transitionConnectionsCoreMigration will transition all connections
// from oldName to newName. Note that this is only useful when you
// know that newName supports everything that oldName supports,
// otherwise you will be in a world of pain.
func (m *InterfaceManager) transitionConnectionsCoreMigration(st *state.State, oldName, newName string) error {
	// transition over, ubuntu-core has only slots
	conns, err := getConns(st)
	if err != nil {
		return err
	}

	for id := range conns {
		connRef, err := interfaces.ParseConnRef(id)
		if err != nil {
			return err
		}
		if connRef.SlotRef.Snap == oldName {
			connRef.SlotRef.Snap = newName
			conns[connRef.ID()] = conns[id]
			delete(conns, id)
		}
	}
	setConns(st, conns)

	// After migrating connections in state, remove them from repo so they stay in sync and we don't
	// attempt to run disconnects on when the old core gets removed as part of the transition.
	if err := m.removeConnections(oldName); err != nil {
		return err
	}

	// The reloadConnections() just modifies the repository object, it
	// has no effect on the running system, i.e. no security profiles
	// on disk are rewritten. This is ok because core/ubuntu-core have
	// exactly the same profiles and nothing in the generated policies
	// has the core snap-name encoded.
	if _, _, err := m.reloadConnections(newName); err != nil {
		return err
	}

	return nil
}

func (m *InterfaceManager) doTransitionUbuntuCore(t *state.Task, _ *tomb.Tomb) error {
	st := t.State()
	st.Lock()
	defer st.Unlock()

	var oldName, newName string
	if err := t.Get("old-name", &oldName); err != nil {
		return err
	}
	if err := t.Get("new-name", &newName); err != nil {
		return err
	}

	return m.transitionConnectionsCoreMigration(st, oldName, newName)
}

func (m *InterfaceManager) undoTransitionUbuntuCore(t *state.Task, _ *tomb.Tomb) error {
	st := t.State()
	st.Lock()
	defer st.Unlock()

	// symmetrical to the "do" method, just reverse them again
	var oldName, newName string
	if err := t.Get("old-name", &oldName); err != nil {
		return err
	}
	if err := t.Get("new-name", &newName); err != nil {
		return err
	}

	return m.transitionConnectionsCoreMigration(st, newName, oldName)
}

// doHotplugConnect creates task(s) to (re)create old connections or auto-connect viable slots in response to hotplug "add" event.
func (m *InterfaceManager) doHotplugConnect(task *state.Task, _ *tomb.Tomb) error {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	deviceCtx, err := snapstate.DeviceCtx(st, task, nil)
	if err != nil {
		return err
	}

	conns, err := getConns(st)
	if err != nil {
		return err
	}

	ifaceName, hotplugKey, err := getHotplugAttrs(task)
	if err != nil {
		return fmt.Errorf("internal error: cannot get hotplug task attributes: %s", err)
	}

	slot, err := m.repo.SlotForHotplugKey(ifaceName, hotplugKey)
	if err != nil {
		return err
	}
	if slot == nil {
		return fmt.Errorf("cannot find hotplug slot for interface %s and hotplug key %q", ifaceName, hotplugKey)
	}

	// find old connections for slots of this device - note we can't ask the repository since we need
	// to recreate old connections that are only remembered in the state.
	connsForDevice := findConnsForHotplugKey(conns, ifaceName, hotplugKey)

	conflictError := func(retry *state.Retry, err error) error {
		if retry != nil {
			task.Logf("hotplug connect will be retried: %s", retry.Reason)
			return retry // will retry
		}
		return fmt.Errorf("hotplug-connect conflict check failed: %v", err)
	}

	// find old connections to recreate
	var recreate []*interfaces.ConnRef
	for _, id := range connsForDevice {
		conn := conns[id]
		// device was not unplugged, this is the case if snapd is restarted and we enumerate devices.
		// note, the situation where device was not unplugged but has changed is handled
		// by hotplugDeviceAdded handler - updateDevice.
		if !conn.HotplugGone || conn.Undesired {
			continue
		}

		// the device was unplugged while connected, so it had disconnect hooks run; recreate the connection
		connRef, err := interfaces.ParseConnRef(id)
		if err != nil {
			return err
		}

		if err := checkAutoconnectConflicts(st, task, connRef.PlugRef.Snap, connRef.SlotRef.Snap); err != nil {
			retry, _ := err.(*state.Retry)
			return conflictError(retry, err)
		}
		recreate = append(recreate, connRef)
	}

	// find new auto-connections
	autochecker, err := newAutoConnectChecker(st, m.repo, deviceCtx)
	if err != nil {
		return err
	}

	instanceName := slot.Snap.InstanceName()
	candidates := m.repo.AutoConnectCandidatePlugs(instanceName, slot.Name, autochecker.check)

	newconns := make(map[string]*interfaces.ConnRef, len(candidates))
	// Auto-connect the plugs
	cannotAutoConnectLog := func(plug *snap.PlugInfo, candRefs []string) string {
		return fmt.Sprintf("cannot auto-connect hotplug slot %s to plug %s, candidates found: %s", slot, plug, strings.Join(candRefs, ", "))
	}
	if err := autochecker.addAutoConnections(task, newconns, candidates, filterForSlot(slot), conns, cannotAutoConnectLog, conflictError); err != nil {
		return err
	}

	if len(recreate) == 0 && len(newconns) == 0 {
		return nil
	}

	// Create connect tasks and interface hooks for old connections
	connectTs := state.NewTaskSet()
	for _, conn := range recreate {
		wasAutoconnected := conns[conn.ID()].Auto
		ts, err := connect(st, conn.PlugRef.Snap, conn.PlugRef.Name, conn.SlotRef.Snap, conn.SlotRef.Name, connectOpts{AutoConnect: wasAutoconnected})
		if err != nil {
			return fmt.Errorf("internal error: connect of %q failed: %s", conn, err)
		}
		connectTs.AddAll(ts)
	}
	// Create connect tasks and interface hooks for new auto-connections
	for _, conn := range newconns {
		ts, err := connect(st, conn.PlugRef.Snap, conn.PlugRef.Name, conn.SlotRef.Snap, conn.SlotRef.Name, connectOpts{AutoConnect: true})
		if err != nil {
			return fmt.Errorf("internal error: auto-connect of %q failed: %s", conn, err)
		}
		connectTs.AddAll(ts)
	}

	if len(connectTs.Tasks()) > 0 {
		snapstate.InjectTasks(task, connectTs)
		st.EnsureBefore(0)
	}

	// make sure that we add tasks and mark this task done in the same atomic write, otherwise there is a risk of re-adding tasks again
	task.SetStatus(state.DoneStatus)

	return nil
}

// doHotplugUpdateSlot updates static attributes of a hotplug slot for given device.
func (m *InterfaceManager) doHotplugUpdateSlot(task *state.Task, _ *tomb.Tomb) error {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	ifaceName, hotplugKey, err := getHotplugAttrs(task)
	if err != nil {
		return fmt.Errorf("internal error: cannot get hotplug task attributes: %s", err)
	}

	var attrs map[string]any
	if err := task.Get("slot-attrs", &attrs); err != nil {
		return fmt.Errorf("internal error: cannot get slot-attrs attribute for device %s, interface %s: %s", hotplugKey, ifaceName, err)
	}

	stateSlots, err := getHotplugSlots(st)
	if err != nil {
		return fmt.Errorf("internal error: cannot obtain hotplug slots: %v", err)
	}

	slot, err := m.repo.UpdateHotplugSlotAttrs(ifaceName, hotplugKey, attrs)
	if err != nil {
		return err
	}

	if slotSpec, ok := stateSlots[slot.Name]; ok {
		slotSpec.StaticAttrs = attrs
		stateSlots[slot.Name] = slotSpec
		setHotplugSlots(st, stateSlots)
	} else {
		return fmt.Errorf("internal error: cannot find slot %s for device %q", slot.Name, hotplugKey)
	}

	return nil
}

// doHotplugRemoveSlot removes hotplug slot for given device from the repository in response to udev "remove" event.
// This task must necessarily be run after all affected slot gets disconnected in the repo.
func (m *InterfaceManager) doHotplugRemoveSlot(task *state.Task, _ *tomb.Tomb) error {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	ifaceName, hotplugKey, err := getHotplugAttrs(task)
	if err != nil {
		return fmt.Errorf("internal error: cannot get hotplug task attributes: %s", err)
	}

	slot, err := m.repo.SlotForHotplugKey(ifaceName, hotplugKey)
	if err != nil {
		return fmt.Errorf("internal error: cannot determine slots: %v", err)
	}
	if slot != nil {
		if err := m.repo.RemoveSlot(slot.Snap.InstanceName(), slot.Name); err != nil {
			return fmt.Errorf("cannot remove hotplug slot: %v", err)
		}
	}

	stateSlots, err := getHotplugSlots(st)
	if err != nil {
		return fmt.Errorf("internal error: cannot obtain hotplug slots: %v", err)
	}

	// remove the slot from hotplug-slots in the state as long as there are no connections referencing it,
	// including connection with hotplug-gone=true.
	slotDef := findHotplugSlot(stateSlots, ifaceName, hotplugKey)
	if slotDef == nil {
		return fmt.Errorf("internal error: cannot find hotplug slot for interface %s, hotplug key %q", ifaceName, hotplugKey)
	}
	conns, err := getConns(st)
	if err != nil {
		return err
	}
	for _, conn := range conns {
		if conn.Interface == slotDef.Interface && conn.HotplugKey == slotDef.HotplugKey {
			// there is a connection referencing this slot, do not remove it, only mark as "gone"
			slotDef.HotplugGone = true
			stateSlots[slotDef.Name] = slotDef
			setHotplugSlots(st, stateSlots)
			return nil
		}
	}
	delete(stateSlots, slotDef.Name)
	setHotplugSlots(st, stateSlots)

	return nil
}

// doHotplugDisconnect creates task(s) to disconnect connections and remove slots in response to hotplug "remove" event.
func (m *InterfaceManager) doHotplugDisconnect(task *state.Task, _ *tomb.Tomb) error {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	ifaceName, hotplugKey, err := getHotplugAttrs(task)
	if err != nil {
		return fmt.Errorf("internal error: cannot get hotplug task attributes: %s", err)
	}

	connections, err := m.repo.ConnectionsForHotplugKey(ifaceName, hotplugKey)
	if err != nil {
		return err
	}
	if len(connections) == 0 {
		return nil
	}

	// check for conflicts on all connections first before creating disconnect hooks
	for _, connRef := range connections {
		if err := checkHotplugDisconnectConflicts(st, connRef.PlugRef.Snap, connRef.SlotRef.Snap); err != nil {
			if retry, ok := err.(*state.Retry); ok {
				task.Logf("Waiting for conflicting change in progress: %s", retry.Reason)
				return err // will retry
			}
			return fmt.Errorf("cannot check conflicts when disconnecting interfaces: %s", err)
		}
	}

	dts := state.NewTaskSet()
	for _, connRef := range connections {
		conn, err := m.repo.Connection(connRef)
		if err != nil {
			// this should never happen since we get all connections from the repo
			return fmt.Errorf("internal error: cannot get connection %q: %s", connRef, err)
		}
		// "by-hotplug" flag indicates it's a disconnect triggered as part of hotplug removal.
		ts, err := disconnectTasks(st, conn, disconnectOpts{ByHotplug: true})
		if err != nil {
			return fmt.Errorf("internal error: cannot create disconnect tasks: %s", err)
		}
		dts.AddAll(ts)
	}

	snapstate.InjectTasks(task, dts)
	st.EnsureBefore(0)

	// make sure that we add tasks and mark this task done in the same atomic write, otherwise there is a risk of re-adding tasks again
	task.SetStatus(state.DoneStatus)

	return nil
}

func (m *InterfaceManager) doHotplugAddSlot(task *state.Task, _ *tomb.Tomb) error {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	systemSnap, err := systemSnapInfo(st)
	if err != nil {
		return fmt.Errorf("system snap not available")
	}

	ifaceName, hotplugKey, err := getHotplugAttrs(task)
	if err != nil {
		return fmt.Errorf("internal error: cannot get hotplug task attributes: %s", err)
	}

	var proposedSlot hotplug.ProposedSlot
	if err := task.Get("proposed-slot", &proposedSlot); err != nil {
		return fmt.Errorf("internal error: cannot get proposed hotplug slot from task attributes: %s", err)
	}
	var devinfo hotplug.HotplugDeviceInfo
	if err := task.Get("device-info", &devinfo); err != nil {
		return fmt.Errorf("internal error: cannot get hotplug device info from task attributes: %s", err)
	}

	stateSlots, err := getHotplugSlots(st)
	if err != nil {
		return fmt.Errorf("internal error obtaining hotplug slots: %v", err.Error())
	}

	iface := m.repo.Interface(ifaceName)
	if iface == nil {
		return fmt.Errorf("internal error: cannot find interface %s", ifaceName)
	}

	slot := findHotplugSlot(stateSlots, ifaceName, hotplugKey)

	// if we know this slot already, restore / update it.
	if slot != nil {
		if slot.HotplugGone {
			// hotplugGone means the device was unplugged, so its disconnect hooks were run and can now
			// simply recreate the slot with potentially new attributes, and old connections will be re-created
			newSlot := &snap.SlotInfo{
				Name:       slot.Name,
				Label:      proposedSlot.Label,
				Snap:       systemSnap,
				Interface:  ifaceName,
				Attrs:      proposedSlot.Attrs,
				HotplugKey: hotplugKey,
			}
			return addHotplugSlot(st, m.repo, stateSlots, iface, newSlot)
		}

		// else - not gone, restored already by reloadConnections, but may need updating.
		if !reflect.DeepEqual(proposedSlot.Attrs, slot.StaticAttrs) {
			ts := updateDevice(st, iface.Name(), hotplugKey, proposedSlot.Attrs)
			snapstate.InjectTasks(task, ts)
			st.EnsureBefore(0)
			task.SetStatus(state.DoneStatus)
		} // else - nothing to do
		return nil
	}

	// New slot.
	slotName := hotplugSlotName(hotplugKey, systemSnap.InstanceName(), proposedSlot.Name, iface.Name(), &devinfo, m.repo, stateSlots)
	newSlot := &snap.SlotInfo{
		Name:       slotName,
		Label:      proposedSlot.Label,
		Snap:       systemSnap,
		Interface:  iface.Name(),
		Attrs:      proposedSlot.Attrs,
		HotplugKey: hotplugKey,
	}
	return addHotplugSlot(st, m.repo, stateSlots, iface, newSlot)
}

// doHotplugSeqWait returns Retry error if there is another change for same hotplug key and a lower sequence number.
// Sequence numbers control the order of execution of hotplug-related changes, which would otherwise be executed in
// arbitrary order by task runner, leading to unexpected results if multiple events for same device are in flight
// (e.g. plugging, followed by immediate unplugging, or snapd restart with pending hotplug changes).
// The handler expects "hotplug-key" and "hotplug-seq" values set on own and other hotplug-related changes.
func (m *InterfaceManager) doHotplugSeqWait(task *state.Task, _ *tomb.Tomb) error {
	st := task.State()
	st.Lock()
	defer st.Unlock()

	chg := task.Change()
	if chg == nil || !isHotplugChange(chg) {
		return fmt.Errorf("internal error: task %q not in a hotplug change", task.Kind())
	}

	seq, hotplugKey, err := getHotplugChangeAttrs(chg)
	if err != nil {
		return err
	}

	for _, otherChg := range st.Changes() {
		if otherChg.IsReady() || otherChg.ID() == chg.ID() {
			continue
		}

		// only inspect hotplug changes
		if !isHotplugChange(otherChg) {
			continue
		}

		otherSeq, otherKey, err := getHotplugChangeAttrs(otherChg)
		if err != nil {
			return err
		}

		// conflict with retry if there another change affecting same device and has lower sequence number
		if hotplugKey == otherKey && otherSeq < seq {
			task.Logf("Waiting processing of earlier hotplug event change %q affecting device with hotplug key %q", otherChg.Kind(), hotplugKey)
			// TODO: consider introducing a new task that runs last and does EnsureBefore(0) for hotplug changes
			return &state.Retry{After: hotplugRetryTimeout}
		}
	}

	// no conflicting change for same hotplug key found
	return nil
}

func (m *InterfaceManager) doRegenerateAllSecurityProfiles(task *state.Task, _ *tomb.Tomb) error {
	st := task.State()

	st.Lock()
	defer st.Unlock()

	perfTimings := state.TimingsForTask(task)
	defer perfTimings.Save(task.State())

	// the reported system key change may have an effect on the security
	// backends, give them a chance to update their view of the system
	if err := m.reinitializeBackends(perfTimings); err != nil {
		return err
	}

	// regenerating and reloading profiles is time consuming, so allow unlocking
	// of state for the duration of security backend operations
	const unlockState = true
	return m.regenerateAllSecurityProfiles(perfTimings, unlockState)
}

// DelayedBackendEffectsFor queues delayed backend effects for a bunch of
// consumer snaps that were triggered by the provided provider snap.
func DelayedBackendEffectsFor(deferTask *state.Task, snapWithSlot triggeringSnap, newDelayedSnapsWork delayedEffectsForSnaps) error {
	scheduledDelayedEffects, err := getDelayedEffectsForSnaps(deferTask)
	if err != nil {
		return err
	}

	if scheduledDelayedEffects.TriggeringSnaps == nil {
		scheduledDelayedEffects.TriggeringSnaps = make(map[triggeringSnap]delayedEffectsForSnaps)
	}

	forSnap := scheduledDelayedEffects.TriggeringSnaps[snapWithSlot]

	if forSnap == nil {
		forSnap = newDelayedEffectsForSnaps()
	}

	// if there's already some work, merge new things
	for sn := range newDelayedSnapsWork {
		if len(forSnap[sn]) == 0 {
			forSnap[sn] = make(map[interfaces.SecuritySystem][]interfaces.DelayedSideEffect, len(newDelayedSnapsWork[sn]))
		}

		for b := range newDelayedSnapsWork[sn] {
			forSnap[sn][b] = append(forSnap[sn][b], newDelayedSnapsWork[sn][b]...)
		}
	}

	scheduledDelayedEffects.TriggeringSnaps[snapWithSlot] = forSnap
	deferTask.Set("delayed-effects-for-snaps", scheduledDelayedEffects)

	return nil
}

// getDelayedEffectsForSnaps returns delayed backend effects.
func getDelayedEffectsForSnaps(deferTask *state.Task) (delayed delayedEffects, err error) {
	if err := deferTask.Get("delayed-effects-for-snaps", &delayed); err != nil && !errors.Is(err, state.ErrNoState) {
		return delayedEffects{}, err
	}
	return delayed, nil
}

var delayedEffectsCoordinationRetryTimeout = time.Second / 2

func (m *InterfaceManager) doProcessDelayedSecurityBackendEffects(task *state.Task, _ *tomb.Tomb) error {
	// Single catch all task handler for all delayed side effects for snaps.
	// Looks through all the tasks to identify which snaps are affected, and
	// whether the snaps that triggered the side effect were successfully
	// processed. Then schedules new tasks, one for each affected snap to apply
	// its queued effects.

	st := task.State()

	st.Lock()
	defer st.Unlock()

	var snapLanes []int
	var applyInLane int

	if err := task.Get("monitored-lanes", &snapLanes); err != nil && !errors.Is(err, state.ErrNoState) {
		return err
	}
	if err := task.Get("apply-in-lane", &applyInLane); err != nil && !errors.Is(err, state.ErrNoState) {
		return err
	}

	logger.Debugf("delayed effects coordination, monitoring lanes: %v", snapLanes)

	if len(snapLanes) == 0 {
		task.SetStatus(state.DoneStatus)
		return nil
	}

	// We are going to apply the effects only for triggering snaps in lanes
	// which were fully successful. Depending on the scenario there could be one
	// or more snaps in the lane. Before we start doing any work, all lanes must
	// have completed.
	successfulTriggeringSnaps := map[triggeringSnap]bool{}
	chg := task.Change()
	considerRestartTriggeringTasks := make(map[string]bool, len(chg.Tasks()))

	unreadyTasks := false
	for _, lane := range snapLanes {
		laneFailed := false
		var seenSnaps []string
		for _, tsk := range chg.LaneTasks(lane) {
			considerRestartTriggeringTasks[tsk.ID()] = true
			switch {
			case tsk.ID() == task.ID():
				// our task should only be present in the default '0' lane, not
				// a dedicated one, unless we're part of a all-snap transaction
				// which already runs in a dedicated lane
				if lanes := tsk.Lanes(); len(lanes) >= 2 || (len(lanes) == 1 && lanes[0] != applyInLane) {
					logger.Noticef("process delayed effects in unexpected lanes: %v", lanes)
				}
				delete(considerRestartTriggeringTasks, tsk.ID())
				continue
			case tsk.Kind() == "check-rerefresh":
				// same thing applies to check-rerefresh, although lane
				// verification should be done in snapstate
				delete(considerRestartTriggeringTasks, tsk.ID())
				continue
			case !tsk.Status().Ready():
				// Continue iterating to collect all task IDs for restart checking
				unreadyTasks = true
			case tsk.Status() != state.DoneStatus:
				laneFailed = true
				// keep scanning the lane even after it has failed so restart
				// detection still sees later wait-for-restart tasks in the same
				// transactional lane.
				continue
			case tsk.Kind() == "link-snap":
				sup, err := snapstate.TaskSnapSetup(tsk)
				if err != nil {
					return fmt.Errorf("internal error: task snap setup not found through link-snap task")
				}
				seenSnaps = append(seenSnaps, sup.InstanceName())
			}
		}
		if laneFailed {
			logger.Debugf("lane %v failed", lane)
		} else {
			for _, seenSnap := range seenSnaps {
				successfulTriggeringSnaps[triggeringSnap(seenSnap)] = true
			}
		}
	}

	if unreadyTasks {
		if restart.PendingForChangeTasks(st, chg, considerRestartTriggeringTasks) {
			// Tasks in monitored lanes may have requested a restart, in which case we
			// must put ourselves into the waiting state.
			return restart.TaskWaitForRestart(task)
		}

		return &state.Retry{After: delayedEffectsCoordinationRetryTimeout, Reason: "pending snap work"}
	}

	delayed, err := getDelayedEffectsForSnaps(task)
	if err != nil {
		return err
	}

	if len(delayed.TriggeringSnaps) == 0 {
		return nil
	}

	// delayed effects are grouped by snap providing the slot, regroup them by
	// the affected snap only of the snap that triggered it has been successfully
	// processed
	snapsWithDelayedEffects := newDelayedEffectsForSnaps()
	for triggeredBySnap, affectedSnaps := range delayed.TriggeringSnaps {
		if _, ok := successfulTriggeringSnaps[triggeredBySnap]; !ok {
			task.Logf("skipping effects triggered by failed snap %q", triggeredBySnap)
			continue
		}

		for affected, perBackendEffects := range affectedSnaps {
			alreadyCollected := snapsWithDelayedEffects[affected]
			if alreadyCollected == nil {
				alreadyCollected = make(map[interfaces.SecuritySystem][]interfaces.DelayedSideEffect)
			}

			for b, items := range perBackendEffects {
				alreadyCollected[b] = append(alreadyCollected[b], items...)
			}

			snapsWithDelayedEffects[affected] = alreadyCollected
		}
	}

	var perSnapTasks []*state.Task
	for affectedSnap, backendEffects := range snapsWithDelayedEffects {
		if len(backendEffects) == 0 {
			// unlikely
			continue
		}

		task.Logf("scheduling delayed effects for snap %q", affectedSnap)

		// One task per connected snap instance
		updateTask := st.NewTask("apply-delayed-snap-security-backend-effects",
			fmt.Sprintf("Apply delayed security backend side effects for snap %q", affectedSnap))
		updateTask.Set("effects-data", delayedEffectsForSnapData{
			AffectedSnapInstance: affectedSnap,
			Effects:              backendEffects,
		})

		perSnapTasks = append(perSnapTasks, updateTask)
	}

	if len(perSnapTasks) > 0 {
		// unless apply-in-lane is set to a non-0 value indicating an all-snap
		// transaction, place each task in a dedicated lane, such that their
		// errors are not affecting anything else (neither the default lane, nor
		// any other lanes where the triggering snaps are being processed)

		for _, tsk := range perSnapTasks {
			if applyInLane != 0 {
				tsk.JoinLane(applyInLane)
			} else {
				tsk.JoinLane(st.NewLane())
			}
		}

		ts := state.NewTaskSet(perSnapTasks...)
		ts.WaitFor(task)
		task.Change().AddAll(ts)
	}

	// Update the status before we unlock
	task.SetStatus(state.DoneStatus)

	return nil
}

type delayedEffectsForSnapData struct {
	AffectedSnapInstance affectedSnap                                                 `json:"affected-snap-instance"`
	Effects              map[interfaces.SecuritySystem][]interfaces.DelayedSideEffect `json:"effects"`
}

func (m *InterfaceManager) doApplyDelayedSnapSecurityBackendEffects(task *state.Task, _ *tomb.Tomb) error {
	// Process and apply delayed effects to a snap

	st := task.State()

	st.Lock()
	defer st.Unlock()

	perfTimings := state.TimingsForTask(task)
	defer perfTimings.Save(task.State())

	var effectsData delayedEffectsForSnapData
	if err := task.Get("effects-data", &effectsData); err != nil {
		return err
	}

	instanceName := string(effectsData.AffectedSnapInstance)
	delayed := effectsData.Effects
	logger.Debugf("delayed update for snap %v, effects %v", instanceName, delayed)

	// Get the snap state to build an app set
	var snapst snapstate.SnapState
	if err := snapstate.Get(st, instanceName, &snapst); err != nil {
		return err
	}

	// Get the current snap info
	snapInfo, err := snapst.CurrentInfo()
	if err != nil {
		return err
	}

	// Add implicit interfaces if needed
	if err := addImplicitInterfaces(st, snapInfo); err != nil {
		return err
	}

	// Build an app set for the current snap revision
	appSet, err := appSetForSnapRevision(st, snapInfo)
	if err != nil {
		return fmt.Errorf("building app set for snap %q: %v", instanceName, err)
	}

	// Unlock state for the duration of security backend operations
	st.Unlock()
	defer st.Lock()

	// Call SetupDelayed on each backend
	for _, backend := range m.repo.Backends() {
		effects := delayed[backend.Name()]
		if len(effects) == 0 {
			continue
		}

		if err := interfaces.ApplyDelayedEffects(m.repo, backend, appSet, effects, perfTimings); err != nil {
			return err
		}
	}

	return nil
}
