Skip to content

Commit

Permalink
o/c/configcore,o/ifacestate: check for presence of handler-service on…
Browse files Browse the repository at this point in the history
… startup

During snapd startup, if the "apparmor-prompting" flag is enabled but
there is no snap with an interfaces-requests-control connection and an
app app declared as a "handler-service", then do not start the prompting
backends, and record a warning that prompting will be inactive until
snapd is restarted.

This avoids a situation where prompting is enabled but there is no
client installed, resulting in snaps blocking while waiting for prompt
replies which will never be received. Now, if there's no client
installed, prompting will be inactive until a client is installed and
snapd is restarted.

There is a similar check for the presence of a handler-service client
when one attempts to enable the "apparmor-prompting" feature flag in the
first place. If there is no handler-service app present, or it cannot be
successfully started, then the flag is left disabled. Before this
commit, however, there are no subsequent checks for the presence of a
handler-service, so snapd can end up in the situation described above if
the user uninstalls the client snap without disabling the
"apparmor-prompting" flag.

We don't want killing the handler-service client to be sufficient to
disable prompting. Instead, prompting can only be simultaneously enabled
but inactive if the client snap either has disconnected the plug for the
interfaces-requests-control interface, or has been uninstalled entirely.
Both of these operations operations require elevated permissions, so
confined snaps which lack the snapd-control interface should be unable
to disable prompting.

Signed-off-by: Oliver Calder <[email protected]>
  • Loading branch information
olivercalder committed Dec 11, 2024
1 parent ecef4af commit dabea71
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 44 deletions.
42 changes: 1 addition & 41 deletions overlord/configstate/configcore/prompting.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,10 @@ import (

"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/features"
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/overlord/configstate/config"
"github.com/snapcore/snapd/overlord/ifacestate"
"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/release"
"github.com/snapcore/snapd/snap"
Expand Down Expand Up @@ -88,45 +86,7 @@ func findPromptingRequestsHandlers(st *state.State) ([]*snap.AppInfo, error) {
st.Lock()
defer st.Unlock()

conns, err := ifacestate.ConnectionStates(st)
if err != nil {
return nil, fmt.Errorf("internal error: cannot get connections: %w", err)
}

var handlers []*snap.AppInfo

for connId, connState := range conns {
if connState.Interface != "snap-interfaces-requests-control" || !connState.Active() {
continue
}

connRef, err := interfaces.ParseConnRef(connId)
if err != nil {
return nil, err
}

handler, ok := connState.StaticPlugAttrs["handler-service"].(string)
if !ok {
// does not have a handler service
continue
}

sn := connRef.PlugRef.Snap
si, err := snapstate.CurrentInfo(st, sn)
if err != nil {
return nil, err
}

// this should not fail as plug's before prepare should have validated that such app exists
app := si.Apps[handler]
if app == nil {
return nil, fmt.Errorf("internal error: cannot find app %q in snap %q", app, sn)
}

handlers = append(handlers, app)
}

return handlers, nil
return ifacestate.InterfacesRequestsControlHandlerServices(st)
}

// Trigger a security profile regeneration by restarting snapd if the
Expand Down
4 changes: 4 additions & 0 deletions overlord/ifacestate/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ func MockAssessAppArmorPrompting(new func(m *InterfaceManager) bool) (restore fu
return testutil.Mock(&assessAppArmorPrompting, new)
}

func MockInterfacesRequestsControlHandlerServicePresent(new func(m *InterfaceManager) (bool, error)) (restore func()) {
return testutil.Mock(&interfacesRequestsControlHandlerServicePresentImpl, new)
}

func MockUDevInitRetryTimeout(t time.Duration) (restore func()) {
old := udevInitRetryTimeout
udevInitRetryTimeout = t
Expand Down
91 changes: 89 additions & 2 deletions overlord/ifacestate/ifacemgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,23 @@ func (m *InterfaceManager) StartUp() error {
}

if m.useAppArmorPrompting {
// Check if there is at least one snap on the system which has a
// connection using the "snap-interfaces-requests-control" plug
// with a "handler-service" attribute declared.
present, err := m.interfacesRequestsControlHandlerServicePresent()
if err != nil {
// Internal error, should not occur
logger.Noticef("failed to check the presence of a interfaces-requests-control handler service: %v", err)
m.useAppArmorPrompting = false
} else if !present {
m.useAppArmorPrompting = false
m.state.AddWarning(`"apparmor-prompting" feature flag enabled but no prompting client is present; prompting will be inactive until a prompting client is installed and snapd is restarted`, nil)
}

func() {
if !m.useAppArmorPrompting {
return
}
// Must not hold state lock while starting interfaces requests
// manager, so that notices can be recorded if needed.
m.state.Unlock()
Expand Down Expand Up @@ -552,8 +568,79 @@ func (m *InterfaceManager) initUDevMonitor() error {
return nil
}

// initInterfacesRequestsManager should only be called if prompting is
// supported and enabled.
// interfacesRequestsControlHandlerServicePresent returns true if there is at
// least one snap which has a "snap-interfaces-requests-control" connection
// with an app declared by the "handler-service" attribute.
//
// The caller must ensure that the state lock is held.
func (m *InterfaceManager) interfacesRequestsControlHandlerServicePresent() (bool, error) {
return interfacesRequestsControlHandlerServicePresentImpl(m)
}

var interfacesRequestsControlHandlerServicePresentImpl = func(m *InterfaceManager) (bool, error) {
handlers, err := InterfacesRequestsControlHandlerServices(m.state)
if err != nil {
return false, err
}
return len(handlers) > 0, nil

Check warning on line 585 in overlord/ifacestate/ifacemgr.go

View check run for this annotation

Codecov / codecov/patch

overlord/ifacestate/ifacemgr.go#L580-L585

Added lines #L580 - L585 were not covered by tests
}

// InterfacesRequestsControlHandlerServices returns the list of all apps which
// are defined as "handler-service" for a snap which has a connected plug for
// the "snap-interfaces-requests-control" interface.
//
// The caller must ensure that the state lock is held.
func InterfacesRequestsControlHandlerServices(st *state.State) ([]*snap.AppInfo, error) {
conns, err := ConnectionStates(st)
if err != nil {
return nil, fmt.Errorf("internal error: cannot get connections: %w", err)
}

Check warning on line 597 in overlord/ifacestate/ifacemgr.go

View check run for this annotation

Codecov / codecov/patch

overlord/ifacestate/ifacemgr.go#L593-L597

Added lines #L593 - L597 were not covered by tests

var handlers []*snap.AppInfo

for connId, connState := range conns {
if connState.Interface != "snap-interfaces-requests-control" || !connState.Active() {
continue

Check warning on line 603 in overlord/ifacestate/ifacemgr.go

View check run for this annotation

Codecov / codecov/patch

overlord/ifacestate/ifacemgr.go#L599-L603

Added lines #L599 - L603 were not covered by tests
}

connRef, err := interfaces.ParseConnRef(connId)
if err != nil {
return nil, err
}

Check warning on line 609 in overlord/ifacestate/ifacemgr.go

View check run for this annotation

Codecov / codecov/patch

overlord/ifacestate/ifacemgr.go#L606-L609

Added lines #L606 - L609 were not covered by tests

handler, ok := connState.StaticPlugAttrs["handler-service"].(string)
if !ok {
// does not have a handler service
continue

Check warning on line 614 in overlord/ifacestate/ifacemgr.go

View check run for this annotation

Codecov / codecov/patch

overlord/ifacestate/ifacemgr.go#L611-L614

Added lines #L611 - L614 were not covered by tests
}

sn := connRef.PlugRef.Snap
si, err := snapstate.CurrentInfo(st, sn)
if err != nil {
return nil, err
}

Check warning on line 621 in overlord/ifacestate/ifacemgr.go

View check run for this annotation

Codecov / codecov/patch

overlord/ifacestate/ifacemgr.go#L617-L621

Added lines #L617 - L621 were not covered by tests

// this should not fail as plug's before prepare should have validated that such app exists
app := si.Apps[handler]
if app == nil {
return nil, fmt.Errorf("internal error: cannot find app %q in snap %q", app, sn)
}

Check warning on line 627 in overlord/ifacestate/ifacemgr.go

View check run for this annotation

Codecov / codecov/patch

overlord/ifacestate/ifacemgr.go#L624-L627

Added lines #L624 - L627 were not covered by tests

handlers = append(handlers, app)

Check warning on line 629 in overlord/ifacestate/ifacemgr.go

View check run for this annotation

Codecov / codecov/patch

overlord/ifacestate/ifacemgr.go#L629

Added line #L629 was not covered by tests
}

return handlers, nil

Check warning on line 632 in overlord/ifacestate/ifacemgr.go

View check run for this annotation

Codecov / codecov/patch

overlord/ifacestate/ifacemgr.go#L632

Added line #L632 was not covered by tests
}

// initInterfacesRequestsManager initializes the prompting backends which make
// up the interfaces requests manager.
//
// This function should only be called if prompting is supported and enabled,
// and at least one installed snap has a "snap-interfaces-requests-control"
// connection with the "handler-service" attribute declared.
//
// The state lock must not be held when this function is called, so that
// notices can be recorded if necessary.
func (m *InterfaceManager) initInterfacesRequestsManager() error {
m.interfacesRequestsManagerMu.Lock()
defer m.interfacesRequestsManagerMu.Unlock()
Expand Down
119 changes: 118 additions & 1 deletion overlord/ifacestate/ifacestate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,12 @@ func (s *interfaceManagerSuite) TestSmokeAppArmorPromptingEnabled(c *C) {
return true
})
defer restore()
checkCount := 0
restore = ifacestate.MockInterfacesRequestsControlHandlerServicePresent(func(m *ifacestate.InterfaceManager) (bool, error) {
checkCount++
return true, nil
})
defer restore()
createCount := 0
fakeManager := &apparmorprompting.InterfacesRequestsManager{}
restore = ifacestate.MockCreateInterfacesRequestsManager(func(s *state.State) (*apparmorprompting.InterfacesRequestsManager, error) {
Expand All @@ -397,6 +403,7 @@ func (s *interfaceManagerSuite) TestSmokeAppArmorPromptingEnabled(c *C) {
defer restore()

mgr := s.manager(c)
c.Check(checkCount, Equals, 1)
c.Check(createCount, Equals, 1)
c.Check(mgr.AppArmorPromptingRunning(), Equals, true)
c.Check(mgr.InterfacesRequestsManager(), Equals, fakeManager)
Expand All @@ -411,11 +418,19 @@ func (s *interfaceManagerSuite) TestSmokeAppArmorPromptingDisabled(c *C) {
return false
})
defer restore()
checkCount := 0
restore = ifacestate.MockInterfacesRequestsControlHandlerServicePresent(func(m *ifacestate.InterfaceManager) (bool, error) {
c.Errorf("unexpectedly called m.interfacesRequestsControlHandlerServicePresent")
checkCount++
return true, nil
})
defer restore()
createCount := 0
fakeManager := &apparmorprompting.InterfacesRequestsManager{}
restore = ifacestate.MockCreateInterfacesRequestsManager(func(s *state.State) (*apparmorprompting.InterfacesRequestsManager, error) {
c.Errorf("unexpectedly called m.initInterfacesRequestsManager")
createCount++
return fakeManager, fmt.Errorf("should not have been called")
return fakeManager, nil
})
defer restore()
stopCount := 0
Expand All @@ -426,6 +441,7 @@ func (s *interfaceManagerSuite) TestSmokeAppArmorPromptingDisabled(c *C) {
defer restore()

mgr := s.manager(c)
c.Check(checkCount, Equals, 0)
c.Check(createCount, Equals, 0)
c.Check(mgr.AppArmorPromptingRunning(), Equals, false)
c.Check(mgr.InterfacesRequestsManager(), testutil.IsInterfaceNil)
Expand Down Expand Up @@ -6985,12 +7001,109 @@ func (s *interfaceManagerSuite) TestConnectHandlesAutoconnect(c *C) {
})
}

func (s *interfaceManagerSuite) TestInterfacesRequestsManagerNoHandlerService(c *C) {
restore := ifacestate.MockAssessAppArmorPrompting(func(m *ifacestate.InterfaceManager) bool {
return true
})
defer restore()

checkCount := 0
restore = ifacestate.MockInterfacesRequestsControlHandlerServicePresent(func(m *ifacestate.InterfaceManager) (bool, error) {
checkCount++
return false, nil
})
defer restore()

createCount := 0
fakeManager := &apparmorprompting.InterfacesRequestsManager{}
restore = ifacestate.MockCreateInterfacesRequestsManager(func(s *state.State) (*apparmorprompting.InterfacesRequestsManager, error) {
c.Errorf("unexpectedly called m.initInterfacesRequestsManager")
createCount++
return fakeManager, nil
})
defer restore()

mgr, err := ifacestate.Manager(s.state, nil, s.o.TaskRunner(), nil, nil)
c.Assert(err, IsNil)

logbuf, restore := logger.MockLogger()
defer restore()

err = mgr.StartUp()
c.Check(err, IsNil)

// Check that error caused AppArmorPromptingRunning() to now return false
running := mgr.AppArmorPromptingRunning()
c.Check(running, Equals, false)

c.Check(checkCount, Equals, 1)
c.Check(createCount, Equals, 0)

logger.WithLoggerLock(func() {
logStr := logbuf.String()
c.Check(logStr, Not(testutil.Contains), "failed to check the presence of a interfaces-requests-control handler service")
c.Check(logStr, Not(testutil.Contains), "failed to start interfaces requests manager")
})

s.state.Lock()
defer s.state.Unlock()
warns := s.state.AllWarnings()
c.Check(warns, HasLen, 1)
c.Check(warns[0].String(), Matches, `"apparmor-prompting" feature flag enabled but no prompting client is present; prompting will be inactive until a prompting client is installed and snapd is restarted`)
}

func (s *interfaceManagerSuite) TestInterfacesRequestsManagerHandlerServicePresentError(c *C) {
restore := ifacestate.MockAssessAppArmorPrompting(func(m *ifacestate.InterfaceManager) bool {
return true
})
defer restore()

checkError := fmt.Errorf("custom error")
restore = ifacestate.MockInterfacesRequestsControlHandlerServicePresent(func(m *ifacestate.InterfaceManager) (bool, error) {
return false, checkError
})
defer restore()

createCount := 0
fakeManager := &apparmorprompting.InterfacesRequestsManager{}
restore = ifacestate.MockCreateInterfacesRequestsManager(func(s *state.State) (*apparmorprompting.InterfacesRequestsManager, error) {
c.Errorf("unexpectedly called m.initInterfacesRequestsManager")
createCount++
return fakeManager, nil
})
defer restore()

mgr, err := ifacestate.Manager(s.state, nil, s.o.TaskRunner(), nil, nil)
c.Assert(err, IsNil)

logbuf, restore := logger.MockLogger()
defer restore()

err = mgr.StartUp()
c.Check(err, IsNil)

// Check that error caused AppArmorPromptingRunning() to now return false
running := mgr.AppArmorPromptingRunning()
c.Check(running, Equals, false)

c.Check(createCount, Equals, 0)

logger.WithLoggerLock(func() {
c.Check(logbuf.String(), testutil.Contains, "failed to check the presence of a interfaces-requests-control handler service")
})
}

func (s *interfaceManagerSuite) TestInitInterfacesRequestsManagerError(c *C) {
restore := ifacestate.MockAssessAppArmorPrompting(func(m *ifacestate.InterfaceManager) bool {
return true
})
defer restore()

restore = ifacestate.MockInterfacesRequestsControlHandlerServicePresent(func(m *ifacestate.InterfaceManager) (bool, error) {
return true, nil
})
defer restore()

createError := fmt.Errorf("custom error")
restore = ifacestate.MockCreateInterfacesRequestsManager(func(s *state.State) (*apparmorprompting.InterfacesRequestsManager, error) {
return nil, createError
Expand Down Expand Up @@ -7020,6 +7133,10 @@ func (s *interfaceManagerSuite) TestStopInterfacesRequestsManagerError(c *C) {
return true
})
defer restore()
restore = ifacestate.MockInterfacesRequestsControlHandlerServicePresent(func(m *ifacestate.InterfaceManager) (bool, error) {
return true, nil
})
defer restore()
fakeManager := &apparmorprompting.InterfacesRequestsManager{}
restore = ifacestate.MockCreateInterfacesRequestsManager(func(s *state.State) (*apparmorprompting.InterfacesRequestsManager, error) {
return fakeManager, nil
Expand Down

0 comments on commit dabea71

Please sign in to comment.