Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

interfaces,dirs: add ldconfig backend #14926

Merged

Conversation

alfonsosanchezbeato
Copy link
Member

This backend will expose libraries coming from snaps to either the
rootfs or to other snaps (currently supporting only the former).

Copy link

github-actions bot commented Jan 14, 2025

Mon Feb 3 14:25:56 UTC 2025
The following results are from: https://github.com/canonical/snapd/actions/runs/13077391096

Failures:

Preparing:

  • openstack:debian-sid-64:tests/regression/

Executing:

  • openstack:opensuse-tumbleweed-64:tests/main/auto-refresh-gating
  • openstack:opensuse-tumbleweed-64:tests/main/auto-refresh-pre-download:restart
  • openstack:opensuse-tumbleweed-64:tests/main/snap-refresh-hold
  • openstack:opensuse-tumbleweed-64:tests/main/auto-refresh-gating-from-snap
  • openstack:opensuse-tumbleweed-64:tests/main/auto-refresh-backoff
  • openstack:opensuse-tumbleweed-64:tests/main/auto-refresh-retry
  • openstack:opensuse-tumbleweed-64:tests/main/auto-refresh:parallel
  • openstack:opensuse-tumbleweed-64:tests/main/refresh-app-awareness-notify
  • openstack:opensuse-tumbleweed-64:tests/main/auto-refresh-pre-download:close_mid_restart
  • openstack:opensuse-tumbleweed-64:tests/main/auto-refresh-pre-download:close
  • openstack:opensuse-tumbleweed-64:tests/main/auto-refresh:regular
  • openstack:opensuse-tumbleweed-64:tests/main/auto-refresh-pre-download:ignore

Restoring:

  • openstack:opensuse-tumbleweed-64:tests/main/refresh-app-awareness-notify

Meulengracht
Meulengracht previously approved these changes Jan 15, 2025
Copy link
Member

@Meulengracht Meulengracht left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks sensible to me, and clean. Thank you

Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One main question.

Further usually interfaces decide to do different things depending if the slot is implicit (see the usage of the implicitSystemPermanent/ConnectedSlot helpers) but maybe here we want to do something at the backend level?

dirs/dirs.go Outdated
@@ -465,6 +466,7 @@ func SetRootDir(rootdir string) {

SnapDataDir = filepath.Join(rootdir, "/var/snap")
SnapAppArmorDir = filepath.Join(rootdir, snappyDir, "apparmor", "profiles")
SnapLdconfigDir = filepath.Join(rootdir, "etc/ld.so.conf.d")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: the style here seems to be /etc...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed

// the rootfs. For snaps, we will create files in
// /var/lib/snapd/ldconfig/ that will be used to generate a cache
// specific to each snap.
ldconfigPath := filepath.Join(dir, "snapd.conf")
Copy link
Collaborator

@pedronis pedronis Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused, specification are per snap app set but here we reuse the same file? won't multiple snap stomp on each ther?

Copy link
Member Author

@alfonsosanchezbeato alfonsosanchezbeato Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have changed this. However I started to re-think about this and I am not sure anymore, maybe I'm missing some piece. I created snapd.conf thinking of it as the only config file for the "system" (rootfs) snap (the implementation is not considering exporting libraries to snap yet). That is, the "snap app set" here is the rootfs. Afaiu any change in interfaces would trigger the call to Setup(), and having multiple files will help with knowing where libraries come from (this could still be done by adding comments in snapd.conf), but there is no other practical effect as the call to ldconfig will process all files in here.

My plan for the implementation for the snaps case, is to have also only one config file per snap, that will be seen in ld.so.conf.d/ but only for the mount namespace of the snap, that would be a mix of the contents of the app base and of these files generated by snapd (that will be found also in the rootfs in, maybe, /var/lib/snapd/ldconfig/).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pedronis as discussed, I've looked with a bit more of detail into this and how it works now is

  1. Tasks like "connect" and "setup-profiles" connect the interfaces. In a refresh case, the later does a set of operations (
    // 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
    ) that in the end implies that repo.Connect is called for all connections in the state, filling the interfaces repo appropriately.
  2. setupSecurityByBackend is then called in all these tasks, which calls interfaces.SetupMany for all backends of the affected snaps
  3. When the backend's Setup is called it calls repo.SnapSpecification, which adds the connected plug for all connected slots to a plug, which covers the greedy plug case (

    snapd/interfaces/repo.go

    Lines 941 to 952 in b85fde1

    // plug side
    for _, plugInfo := range r.plugs[snapName] {
    iface := r.ifaces[plugInfo.Interface]
    if err := spec.AddPermanentPlug(iface, plugInfo); err != nil {
    return nil, err
    }
    for _, conn := range r.plugSlots[plugInfo] {
    if err := spec.AddConnectedPlug(iface, conn.Plug, conn.Slot); err != nil {
    return nil, err
    }
    }
    }
    ). This is symmetric to slots with multiple connections. Therefore the specification should contain the right information even on refreshing snaps due to step 1.

I think then that greedy plugs in principle work as expected, and the unit test that I have implemented for the ldconfig backend does similar things to what is done by the taks. I lean now more towards the option of having just one configuration file in /etc/ld.conf.so.d/ for hybrid, to avoid polluting too much that user-exposed folder. Inside it we could add comments to specify what parts come from a given snap/slot. Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if I follow if we have a greedy plug on connected to many slot snaps <slot-snap1,2...>, when e.g. slot-snap2 refreshes:

    1. will add to affected snaps
  • When we construct SnapSpecification for 3. will call AddConnectedPlug for all the slots is connected to and not just the one for slot-snap2?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r.plugSlots is filled by calls to Repository.Connect. Afaiu in a refresh all connections are restored in the repository by InterfaceManager.reloadConnections() (

if _, err := m.repo.Connect(connRef, staticPlugAttrs, connState.DynamicPlugAttrs, staticSlotAttrs, connState.DynamicSlotAttrs, nil); err != nil {
).

So unless I got something wrong, the specification should contain information for all the connected slots, that is, for slot-snap1 and 2.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Afaiu in a refresh all connections are restored in the repository by InterfaceManager.reloadConnections()

all connections for the given snap

I created the tests we were discussing:

#14985

the conclusion from there is that we should probably create a file per consumer side

Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment

// Methods called by interfaces

// AddLibDirs add dirs with libraries to the specification.
func (spec *Specification) AddLibDirs(snapName, slotName string, dirs []string) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can capture the contextual info on the spec in AddConnectedPlug, I would expect that's the place we want this to work from? then we should error or panic if it's called with the contextual info unse

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see something like setScope in the apparmor backend, though what we need here is simpler

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done now

Copy link
Member Author

@alfonsosanchezbeato alfonsosanchezbeato left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pedronis thanks, comments addressed now

// Methods called by interfaces

// AddLibDirs add dirs with libraries to the specification.
func (spec *Specification) AddLibDirs(snapName, slotName string, dirs []string) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done now

Copy link

codecov bot commented Jan 21, 2025

Codecov Report

Attention: Patch coverage is 68.92655% with 55 lines in your changes missing coverage. Please review.

Please upload report for BASE (master@6fa2df6). Learn more about missing BASE report.
Report is 59 commits behind head on master.

Files with missing lines Patch % Lines
interfaces/ldconfig/backend.go 68.88% 23 Missing and 5 partials ⚠️
interfaces/ifacetest/testiface.go 0.00% 20 Missing ⚠️
interfaces/ldconfig/spec.go 86.79% 6 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##             master   #14926   +/-   ##
=========================================
  Coverage          ?   78.22%           
=========================================
  Files             ?     1167           
  Lines             ?   154243           
  Branches          ?        0           
=========================================
  Hits              ?   120656           
  Misses            ?    26155           
  Partials          ?     7432           
Flag Coverage Δ
unittests 78.22% <68.92%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

content += "\n"

// File name is snap.<snap_name>.<slot_name>.conf
ldconfigPath := fmt.Sprintf("snap.%s.conf", key)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

based on what I found we want this to be based on the plug snap name I think. OTOH if the plug snap is snapd we could use a special name like snapd.conf or snap.system.conf ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed as discussed, this is ready for review now

@alfonsosanchezbeato
Copy link
Member Author

@Meulengracht I've re-asked you for review as there have been a few changes

Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion

type Specification struct {
// libDirs is the list of directories with libraries coming from
// different slots. The key has the form "<snap_name>.<slot>".
libDirs map[string][]string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should use a struct:

type sourceSlot {
Snap string
Slot string
}

for the key? to avoid the string mangling?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, done now

pedronis
pedronis previously approved these changes Jan 28, 2025
Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you

@pedronis pedronis dismissed Meulengracht’s stale review January 29, 2025 08:42

this needs to be re-reviewed

// names for the snap representing the system.
func IsTheSystemSnap(snapName string) bool {
switch snapName {
case "", "snapd", "core", "ubuntu-core":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we still care about "ubuntu-core" snap?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed, thinking about this we already branched for UC16, so we do not need to care (we probably would not care even for UC16 nowadays).

libcConfPath := filepath.Join(confDir, "libc.conf")
c.Assert(os.WriteFile(libcConfPath, []byte{}, 0644), IsNil)

// Add cakkback and register the interface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Add cakkback and register the interface
// Add callback and register the interface

// names for the snap representing the system.
func IsTheSystemSnap(snapName string) bool {
switch snapName {
case "", "snapd", "core", "ubuntu-core":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have some code in repo.go in guessSystemSnapName() which maybe could reuse the list.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed so at least both share the list of the names of the system snap


// IsTheSystemSnap returns true if snapName is one of the possible
// names for the snap representing the system.
func IsTheSystemSnap(snapName string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func IsTheSystemSnap(snapName string) bool {
func IsSystemSnap(snapName string) bool {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to keep this, it makes clear that there can be only one system snap

// names for the snap representing the system.
func IsTheSystemSnap(snapName string) bool {
switch snapName {
case "", "snapd", "core", "ubuntu-core":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is "" for implicit plugs/slots? Aren't those always attached to either core or snapd?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, yes, afaiu IsTheSystemSnap could be called with "".

interfaces/ldconfig/backend.go Show resolved Hide resolved
Copy link
Member Author

@alfonsosanchezbeato alfonsosanchezbeato left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bboozzoo thanks for the review, I've addressed your comments now

interfaces/ldconfig/backend.go Show resolved Hide resolved

// IsTheSystemSnap returns true if snapName is one of the possible
// names for the snap representing the system.
func IsTheSystemSnap(snapName string) bool {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to keep this, it makes clear that there can be only one system snap

// names for the snap representing the system.
func IsTheSystemSnap(snapName string) bool {
switch snapName {
case "", "snapd", "core", "ubuntu-core":
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed, thinking about this we already branched for UC16, so we do not need to care (we probably would not care even for UC16 nowadays).

// names for the snap representing the system.
func IsTheSystemSnap(snapName string) bool {
switch snapName {
case "", "snapd", "core", "ubuntu-core":
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed so at least both share the list of the names of the system snap

// names for the snap representing the system.
func IsTheSystemSnap(snapName string) bool {
switch snapName {
case "", "snapd", "core", "ubuntu-core":
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, yes, afaiu IsTheSystemSnap could be called with "".

bboozzoo
bboozzoo previously approved these changes Jan 30, 2025
Copy link
Contributor

@bboozzoo bboozzoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@alfonsosanchezbeato
Copy link
Member Author

@pedronis @bboozzoo I had to do some changes here as spread tests were failing - I was being naive on when Setup was being called, which is basically all the time for all backends for any interface change. So I had to make sure that the setup for ldconfig only happened for the system snap, and also minimize the calls to ldconfig, otherwise we try to touch files on UC that cannot be written. Please take a look at 2c7d844

@pedronis pedronis dismissed their stale review January 31, 2025 07:49

needs re-review

@pedronis pedronis self-requested a review January 31, 2025 07:49
Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to be a bit more careful with the checks instead

@@ -87,7 +87,7 @@ func (spec *Specification) AddConnectedPlug(iface interfaces.Interface, plug *in

// AddConnectedSlot records ldconfig-specific side-effects of having a connected slot.
func (spec *Specification) AddConnectedSlot(iface interfaces.Interface, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
if !interfaces.IsTheSystemSnap(plug.Snap().InstanceName()) {
if !interfaces.IsTheSystemSnap(slot.Snap().InstanceName()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change seems definitely wrong we probably need to do something else and move the check only if the interface has the definer no?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also in either case this definitely warrants a comment

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the check really needs to happen only for interfaces supporting ldconfig, I've changed this now and reverted to check for the plug here too.

@@ -52,6 +53,12 @@ func (b *Backend) Name() interfaces.SecuritySystem {
// If the method fails it should be re-tried (with a sensible strategy) by the caller.
func (b *Backend) Setup(appSet *interfaces.SnapAppSet, opts interfaces.ConfinementOptions, repo *interfaces.Repository, tm timings.Measurer) error {
snapName := appSet.InstanceName()
if !interfaces.IsTheSystemSnap(snapName) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this might be ok for now but we probably want to instead change the rest of the code to work even if this is not here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the change in the checks in the specification, this check is not needed anymore. But that is sort of by chance as the spec is empty, if it wasn't it would try to write in /etc/ld.so.conf.d/snap.system.conf for a snap that is not the system snap. So I think it is better to leave for the moment just to make clear we do not support normal snaps for the moment.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but there two sides to things, you need to be abel to update things both when the system snap is update, but also when the providing snap with the slot which a kernel or something is updated, so I don't think we can really have this check

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed now

Copy link
Member

@Meulengracht Meulengracht left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this is a +1 from me if comments are resolved, but not approving since it's awaitings Samuele's approval and you already have maciejs

@alfonsosanchezbeato
Copy link
Member Author

@pedronis @Meulengracht this is ready for another round now

Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, another question

// This method should be called after removing a snap.
//
// If the method fails it should be re-tried (with a sensible strategy) by the caller.
func (b *Backend) Remove(snapName string) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't we need to do something here to remove the entries for a snap that had ldconfig providing slot?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alfonsosanchezbeato remarked that a slot side going away is taken care by this logic:

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
}

as the plug side will be Setup

@pedronis pedronis dismissed bboozzoo’s stale review January 31, 2025 16:28

this will need a re-review

Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks

This backend will expose libraries coming from snaps to either the
rootfs or to other snaps (currently supporting only the former).
@alfonsosanchezbeato alfonsosanchezbeato merged commit f2f4d10 into canonical:master Feb 3, 2025
65 of 67 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants