diff --git a/dirs/dirs.go b/dirs/dirs.go index f1d441ee33f..7081f86a05c 100644 --- a/dirs/dirs.go +++ b/dirs/dirs.go @@ -48,6 +48,7 @@ var ( snapDataHomeGlob []string SnapDownloadCacheDir string SnapAppArmorDir string + SnapLdconfigDir string SnapSeccompBase string SnapSeccompDir string SnapMountPolicyDir string @@ -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") SnapDownloadCacheDir = filepath.Join(rootdir, snappyDir, "cache") SnapSeccompBase = filepath.Join(rootdir, snappyDir, "seccomp") SnapSeccompDir = filepath.Join(SnapSeccompBase, "bpf") diff --git a/interfaces/backends/backends.go b/interfaces/backends/backends.go index b65300ad519..7fe0402c03c 100644 --- a/interfaces/backends/backends.go +++ b/interfaces/backends/backends.go @@ -24,6 +24,7 @@ import ( "github.com/snapcore/snapd/interfaces/apparmor" "github.com/snapcore/snapd/interfaces/dbus" "github.com/snapcore/snapd/interfaces/kmod" + "github.com/snapcore/snapd/interfaces/ldconfig" "github.com/snapcore/snapd/interfaces/mount" "github.com/snapcore/snapd/interfaces/polkit" "github.com/snapcore/snapd/interfaces/seccomp" @@ -45,6 +46,7 @@ func All() []interfaces.SecurityBackend { &mount.Backend{}, &kmod.Backend{}, &polkit.Backend{}, + &ldconfig.Backend{}, } // TODO use something like: diff --git a/interfaces/core.go b/interfaces/core.go index c915e6c55f1..65b9111b0ac 100644 --- a/interfaces/core.go +++ b/interfaces/core.go @@ -310,6 +310,8 @@ const ( SecuritySystemd SecuritySystem = "systemd" // SecurityPolkit identifies the polkit security system. SecurityPolkit SecuritySystem = "polkit" + // SecurityLdconfig identifies the ldconfig security system. + SecurityLdconfig SecuritySystem = "ldconfig" ) var isValidBusName = regexp.MustCompile(`^[a-zA-Z_-][a-zA-Z0-9_-]*(\.[a-zA-Z_-][a-zA-Z0-9_-]*)+$`).MatchString diff --git a/interfaces/ifacetest/testiface.go b/interfaces/ifacetest/testiface.go index 2bbcd06e98d..bb65b6f9225 100644 --- a/interfaces/ifacetest/testiface.go +++ b/interfaces/ifacetest/testiface.go @@ -25,6 +25,7 @@ import ( "github.com/snapcore/snapd/interfaces/dbus" "github.com/snapcore/snapd/interfaces/hotplug" "github.com/snapcore/snapd/interfaces/kmod" + "github.com/snapcore/snapd/interfaces/ldconfig" "github.com/snapcore/snapd/interfaces/mount" "github.com/snapcore/snapd/interfaces/polkit" "github.com/snapcore/snapd/interfaces/seccomp" @@ -84,6 +85,13 @@ type TestInterface struct { KModPermanentPlugCallback func(spec *kmod.Specification, plug *snap.PlugInfo) error KModPermanentSlotCallback func(spec *kmod.Specification, slot *snap.SlotInfo) error + // Support for interacting with the ldconfig backend. + + LdconfigConnectedPlugCallback func(spec *ldconfig.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error + LdconfigConnectedSlotCallback func(spec *ldconfig.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error + LdconfigPermanentPlugCallback func(spec *ldconfig.Specification, plug *snap.PlugInfo) error + LdconfigPermanentSlotCallback func(spec *ldconfig.Specification, slot *snap.SlotInfo) error + // Support for interacting with the seccomp backend. SecCompConnectedPlugCallback func(spec *seccomp.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error @@ -360,6 +368,36 @@ func (t *TestInterface) KModPermanentSlot(spec *kmod.Specification, slot *snap.S return nil } +// Support for interacting with the ldconfig backend. + +func (t *TestInterface) LdconfigConnectedPlug(spec *ldconfig.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + if t.LdconfigConnectedPlugCallback != nil { + return t.LdconfigConnectedPlugCallback(spec, plug, slot) + } + return nil +} + +func (t *TestInterface) LdconfigConnectedSlot(spec *ldconfig.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + if t.LdconfigConnectedSlotCallback != nil { + return t.LdconfigConnectedSlotCallback(spec, plug, slot) + } + return nil +} + +func (t *TestInterface) LdconfigPermanentPlug(spec *ldconfig.Specification, plug *snap.PlugInfo) error { + if t.LdconfigPermanentPlugCallback != nil { + return t.LdconfigPermanentPlugCallback(spec, plug) + } + return nil +} + +func (t *TestInterface) LdconfigPermanentSlot(spec *ldconfig.Specification, slot *snap.SlotInfo) error { + if t.LdconfigPermanentSlotCallback != nil { + return t.LdconfigPermanentSlotCallback(spec, slot) + } + return nil +} + // Support for interacting with the dbus backend. func (t *TestInterface) DBusConnectedPlug(spec *dbus.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { diff --git a/interfaces/ldconfig/backend.go b/interfaces/ldconfig/backend.go new file mode 100644 index 00000000000..b16c22c3347 --- /dev/null +++ b/interfaces/ldconfig/backend.go @@ -0,0 +1,121 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package ldconfig + +import ( + "fmt" + "os" + "strings" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/timings" +) + +// Backend is responsible for maintaining ldconfig cache. +type Backend struct{} + +var _ = interfaces.SecurityBackend(&Backend{}) + +// Initialize does nothing for this backend. +func (b *Backend) Initialize(opts *interfaces.SecurityBackendOptions) error { + return nil +} + +// Name returns the name of the backend. +func (b *Backend) Name() interfaces.SecuritySystem { + return "ldconfig" +} + +// Setup will make the ldconfig backend generate the needed +// configuration files and re-create the ld cache. +// +// 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() + // Get the snippets that apply to this snap + spec, err := repo.SnapSpecification(b.Name(), appSet, opts) + if err != nil { + return fmt.Errorf("cannot obtain ldconfig specification for snap %q: %s", + snapName, err) + } + + return b.setupLdconfigCache(spec.(*Specification)) +} + +// Remove removes modules ldconfig files specific to a given snap. +// 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 { + // TODO this will not be called for the rootfs case where the system + // has an implicit plug, but needs to be revisited for snaps. + return nil +} + +// NewSpecification returns a new specification associated with this backend. +func (b *Backend) NewSpecification(*interfaces.SnapAppSet, + interfaces.ConfinementOptions) interfaces.Specification { + return &Specification{} +} + +// SandboxFeatures returns the list of features supported by snapd for ldconfig. +func (b *Backend) SandboxFeatures() []string { + return []string{"mediated-ldconfig"} +} + +func (b *Backend) setupLdconfigCache(spec *Specification) error { + ldConfigDir := dirs.SnapLdconfigDir + if err := os.MkdirAll(ldConfigDir, 0755); err != nil { + return fmt.Errorf("cannot create directory for ldconfig files %q: %s", ldConfigDir, err) + } + + // TODO this considers only the case when the libraries are exposed to + // 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. + + allContent := map[string]osutil.FileState{} + for key, dirs := range spec.libDirs { + content := "# This file is automatically generated by snapd\n" + content += strings.Join(dirs, "\n") + content += "\n" + + // File name is snap...conf + ldconfigPath := fmt.Sprintf("snap.%s.conf", key) + allContent[ldconfigPath] = &osutil.MemoryFileState{ + Content: []byte(content), + Mode: 0644, + } + } + _, _, err := osutil.EnsureDirStateGlobs(ldConfigDir, []string{"snap.*.conf"}, allContent) + if err != nil { + return err + } + + // Re-create cache + out, stderr, err := osutil.RunSplitOutput("ldconfig") + if err != nil { + return osutil.OutputErrCombine(out, stderr, err) + } + + return nil +} diff --git a/interfaces/ldconfig/backend_test.go b/interfaces/ldconfig/backend_test.go new file mode 100644 index 00000000000..db05ff0d007 --- /dev/null +++ b/interfaces/ldconfig/backend_test.go @@ -0,0 +1,123 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package ldconfig_test + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/ifacetest" + "github.com/snapcore/snapd/interfaces/ldconfig" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +func Test(t *testing.T) { + TestingT(t) +} + +type backendSuite struct { + ifacetest.BackendSuite + ldconfigCmd *testutil.MockCmd +} + +var _ = Suite(&backendSuite{}) + +func (s *backendSuite) SetUpTest(c *C) { + s.Backend = &ldconfig.Backend{} + s.BackendSuite.SetUpTest(c) + c.Assert(s.Repo.AddBackend(s.Backend), IsNil) + s.ldconfigCmd = testutil.MockCommand(c, "ldconfig", "") +} + +func (s *backendSuite) TearDownTest(c *C) { + s.ldconfigCmd.Restore() + s.BackendSuite.TearDownTest(c) +} + +func (s *backendSuite) TestName(c *C) { + c.Check(s.Backend.Name(), Equals, interfaces.SecurityLdconfig) +} + +func checkLdconfigFile(c *C, snapName, slot string, libDirs []string) { + path := filepath.Join(dirs.GlobalRootDir, "etc", "ld.so.conf.d", + fmt.Sprintf("snap.%s.%s.conf", snapName, slot)) + content := "# This file is automatically generated by snapd\n" + content += strings.Join(libDirs, "\n") + content += "\n" + c.Assert(path, testutil.FileEquals, content) +} + +func (s *backendSuite) TestInstallingCreatesLdconf(c *C) { + s.Iface.LdconfigPermanentSlotCallback = func(spec *ldconfig.Specification, + slot *snap.SlotInfo) error { + spec.AddLibDirs("snap1", "slot1", []string{"/dir1/lib1", "/dir1/lib11"}) + spec.AddLibDirs("snap1", "slot2", []string{"/dir1/lib2", "/dir1/lib22"}) + spec.AddLibDirs("snap2", "slot1", []string{"/dir2/lib1"}) + return nil + } + snapInfo := s.InstallSnap(c, interfaces.ConfinementOptions{}, "", ifacetest.SambaYamlV1, 0) + + confDir := filepath.Join(dirs.GlobalRootDir, "etc", "ld.so.conf.d") + libcConfPath := filepath.Join(confDir, "libc.conf") + c.Assert(os.WriteFile(libcConfPath, []byte{}, 0644), IsNil) + + checkLdconfigFile(c, "snap1", "slot1", []string{"/dir1/lib1", "/dir1/lib11"}) + checkLdconfigFile(c, "snap1", "slot2", []string{"/dir1/lib2", "/dir1/lib22"}) + checkLdconfigFile(c, "snap2", "slot1", []string{"/dir2/lib1"}) + + c.Assert(s.ldconfigCmd.Calls(), DeepEquals, [][]string{ + {"ldconfig"}, + }) + s.ldconfigCmd.ForgetCalls() + + // When refreshing a slot is removed + s.Iface.LdconfigPermanentSlotCallback = func(spec *ldconfig.Specification, + slot *snap.SlotInfo) error { + spec.AddLibDirs("snap1", "slot1", []string{"/dir1/lib1", "/dir1/lib11"}) + spec.AddLibDirs("snap2", "slot1", []string{"/dir2/lib1"}) + return nil + } + s.UpdateSnap(c, snapInfo, interfaces.ConfinementOptions{}, ifacetest.SambaYamlV1, 1) + + checkLdconfigFile(c, "snap1", "slot1", []string{"/dir1/lib1", "/dir1/lib11"}) + c.Check(filepath.Join(confDir, "snap.snap1.slot2.conf"), testutil.FileAbsent) + checkLdconfigFile(c, "snap2", "slot1", []string{"/dir2/lib1"}) + + c.Assert(s.ldconfigCmd.Calls(), DeepEquals, [][]string{ + {"ldconfig"}, + }) + + // libc config has not been touched + c.Check(libcConfPath, testutil.FilePresent) + + s.RemoveSnap(c, snapInfo) +} + +func (s *backendSuite) TestSandboxFeatures(c *C) { + c.Assert(s.Backend.SandboxFeatures(), DeepEquals, []string{"mediated-ldconfig"}) +} diff --git a/interfaces/ldconfig/spec.go b/interfaces/ldconfig/spec.go new file mode 100644 index 00000000000..3497ce02b51 --- /dev/null +++ b/interfaces/ldconfig/spec.go @@ -0,0 +1,99 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package ldconfig + +import ( + "fmt" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/snap" +) + +// Specification assists in collecting library directories associated with an +// interface. +// +// Unlike the Backend itself (which is stateless and non-persistent) this type +// holds internal state that is used by the ldconfig backend during the +// interface setup process. +type Specification struct { + // libDirs is the list of directories with libraries coming from + // different slots. The key has the form ".". + libDirs map[string][]string +} + +// Methods called by interfaces + +// AddLibDirs add dirs with libraries to the specification. +func (spec *Specification) AddLibDirs(snapName, slotName string, dirs []string) { + if spec.libDirs == nil { + spec.libDirs = make(map[string][]string) + } + spec.libDirs[fmt.Sprintf("%s.%s", snapName, slotName)] = dirs +} + +func (spec *Specification) LibDirs() map[string][]string { + return spec.libDirs +} + +// Implementation of methods required by interfaces.Specification + +// AddConnectedPlug records ldconfig-specific side-effects of having a connected plug. +func (spec *Specification) AddConnectedPlug(iface interfaces.Interface, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + type definer interface { + LdconfigConnectedPlug(spec *Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error + } + if iface, ok := iface.(definer); ok { + return iface.LdconfigConnectedPlug(spec, plug, slot) + } + return nil +} + +// 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 { + type definer interface { + LdconfigConnectedSlot(spec *Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error + } + if iface, ok := iface.(definer); ok { + return iface.LdconfigConnectedSlot(spec, plug, slot) + } + return nil +} + +// AddPermanentPlug records ldconfig-specific side-effects of having a plug. +func (spec *Specification) AddPermanentPlug(iface interfaces.Interface, plug *snap.PlugInfo) error { + type definer interface { + LdconfigPermanentPlug(spec *Specification, plug *snap.PlugInfo) error + } + if iface, ok := iface.(definer); ok { + return iface.LdconfigPermanentPlug(spec, plug) + } + return nil +} + +// AddPermanentSlot records ldconfig-specific side-effects of having a slot. +func (spec *Specification) AddPermanentSlot(iface interfaces.Interface, slot *snap.SlotInfo) error { + type definer interface { + LdconfigPermanentSlot(spec *Specification, slot *snap.SlotInfo) error + } + if iface, ok := iface.(definer); ok { + return iface.LdconfigPermanentSlot(spec, slot) + } + return nil +} diff --git a/interfaces/ldconfig/spec_test.go b/interfaces/ldconfig/spec_test.go new file mode 100644 index 00000000000..a0cbd3769a7 --- /dev/null +++ b/interfaces/ldconfig/spec_test.go @@ -0,0 +1,112 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 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 . + * + */ + +package ldconfig_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/ifacetest" + "github.com/snapcore/snapd/interfaces/ldconfig" + "github.com/snapcore/snapd/snap" +) + +type specSuite struct { + spec *ldconfig.Specification + iface1 *ifacetest.TestInterface + plugInfo *snap.PlugInfo + plug *interfaces.ConnectedPlug + slotInfo *snap.SlotInfo + slot *interfaces.ConnectedSlot +} + +var _ = Suite(&specSuite{ + iface1: &ifacetest.TestInterface{ + InterfaceName: "test", + LdconfigConnectedPlugCallback: func(spec *ldconfig.Specification, + plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + spec.AddLibDirs("snap1", "slot1", []string{"/dir1/lib1"}) + return nil + }, + LdconfigConnectedSlotCallback: func(spec *ldconfig.Specification, + plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + spec.AddLibDirs("snap1", "slot2", []string{"/dir1/lib2"}) + return nil + }, + LdconfigPermanentPlugCallback: func(spec *ldconfig.Specification, + plug *snap.PlugInfo) error { + spec.AddLibDirs("snap2", "slot1", []string{"/dir2/lib3"}) + return nil + }, + LdconfigPermanentSlotCallback: func(spec *ldconfig.Specification, + slot *snap.SlotInfo) error { + spec.AddLibDirs("snap2", "slot2", []string{"/dir2/lib4"}) + return nil + }, + }, +}) + +func (s *specSuite) SetUpTest(c *C) { + s.spec = &ldconfig.Specification{} + const plugYaml = `name: snap +version: 1 +apps: + app: + plugs: [name] +` + s.plug, s.plugInfo = ifacetest.MockConnectedPlug(c, plugYaml, nil, "name") + + const slotYaml = `name: snap +version: 1 +slots: + name: + interface: test +` + s.slot, s.slotInfo = ifacetest.MockConnectedSlot(c, slotYaml, nil, "name") +} + +// AddLibDirs is not broken +func (s *specSuite) TestSmoke(c *C) { + dirs1 := []string{"/dir1/lib1", "/dir1/lib2"} + dirs2 := []string{"/dir2/lib1", "/dir2/lib2"} + s.spec.AddLibDirs("snap1", "slot1", dirs1) + s.spec.AddLibDirs("snap2", "slot2", dirs2) + // no duplication of entries + s.spec.AddLibDirs("snap2", "slot2", dirs2) + c.Assert(s.spec.LibDirs(), DeepEquals, map[string][]string{ + "snap1.slot1": {"/dir1/lib1", "/dir1/lib2"}, + "snap2.slot2": {"/dir2/lib1", "/dir2/lib2"}, + }) +} + +// The ldconfig.Specification can be used through the interfaces.Specification interface +func (s *specSuite) TestSpecificationIface(c *C) { + var r interfaces.Specification = s.spec + c.Assert(r.AddConnectedPlug(s.iface1, s.plug, s.slot), IsNil) + c.Assert(r.AddConnectedSlot(s.iface1, s.plug, s.slot), IsNil) + c.Assert(r.AddPermanentPlug(s.iface1, s.plugInfo), IsNil) + c.Assert(r.AddPermanentSlot(s.iface1, s.slotInfo), IsNil) + c.Assert(s.spec.LibDirs(), DeepEquals, map[string][]string{ + "snap1.slot1": {"/dir1/lib1"}, + "snap1.slot2": {"/dir1/lib2"}, + "snap2.slot1": {"/dir2/lib3"}, + "snap2.slot2": {"/dir2/lib4"}, + }) +}