Files
snapd/interfaces/mount/spec.go
Ernest Lotter fdc90dfe41 many: ensure-dir mounts for personal-files missing dirs (#13260)
* many: ensure-dir mount entries from personal-files write attrs

* many: review improvements

* strutil: make pathiter current path slash trimming use existing method

* osutil, strutil: more review fixes

* i, i/apparmor, i/builtin, osutil: improve unit test coverage

* i, i/apparmor, i/builtin, i/mount: review improvements

* strutil: improve comment

* interfaces/apparmor: allow snap-update-ns to open home directory

* tests: revert interfaces-personal-files changes to simplify merge

* interfaces/builtin: improve plug connect error message

Co-authored-by: Miguel Pires <miguelpires94@gmail.com>

* interfaces/builtin: fixed ut

---------

Co-authored-by: Miguel Pires <miguelpires94@gmail.com>
2023-12-15 15:25:49 +02:00

397 lines
13 KiB
Go

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2016-2017 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 mount
import (
"fmt"
"path"
"sort"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/strutil"
)
// Specification assists in collecting mount entries associated with an interface.
//
// Unlike the Backend itself (which is stateless and non-persistent) this type
// holds internal state that is used by the mount backend during the interface
// setup process.
type Specification struct {
// The mount profile is internally re-sorted by snap-update-ns based on
// the source of given mount entry and MountEntry.Dir. See
// cmd/snap-update-ns/sorting.go for details.
layout []osutil.MountEntry
general []osutil.MountEntry
user []osutil.MountEntry
overname []osutil.MountEntry
}
// AddMountEntry adds a new mount entry.
func (spec *Specification) AddMountEntry(e osutil.MountEntry) error {
spec.general = append(spec.general, e)
return nil
}
// AddUserMountEntry adds a new user mount entry.
func (spec *Specification) AddUserMountEntry(e osutil.MountEntry) error {
spec.user = append(spec.user, e)
return nil
}
// AddOvernameMountEntry adds a new overname mount entry.
func (spec *Specification) AddOvernameMountEntry(e osutil.MountEntry) error {
spec.overname = append(spec.overname, e)
return nil
}
func mountEntryFromLayout(layout *snap.Layout) osutil.MountEntry {
var entry osutil.MountEntry
mountPoint := layout.Snap.ExpandSnapVariables(layout.Path)
entry.Dir = mountPoint
// XXX: what about ro mounts?
if layout.Bind != "" {
mountSource := layout.Snap.ExpandSnapVariables(layout.Bind)
entry.Options = []string{"rbind", "rw"}
entry.Name = mountSource
}
if layout.BindFile != "" {
mountSource := layout.Snap.ExpandSnapVariables(layout.BindFile)
entry.Options = []string{"bind", "rw", osutil.XSnapdKindFile()}
entry.Name = mountSource
}
if layout.Type == "tmpfs" {
entry.Type = "tmpfs"
entry.Name = "tmpfs"
}
if layout.Symlink != "" {
oldname := layout.Snap.ExpandSnapVariables(layout.Symlink)
entry.Options = []string{osutil.XSnapdKindSymlink(), osutil.XSnapdSymlink(oldname)}
}
var uid uint32
// Only root is allowed here until we support custom users. Root is default.
switch layout.User {
case "root", "":
uid = 0
}
if uid != 0 {
entry.Options = append(entry.Options, osutil.XSnapdUser(uid))
}
var gid uint32
// Only root is allowed here until we support custom groups. Root is default.
// This is validated in spec.go.
switch layout.Group {
case "root", "":
gid = 0
}
if gid != 0 {
entry.Options = append(entry.Options, osutil.XSnapdGroup(gid))
}
if layout.Mode != 0755 {
entry.Options = append(entry.Options, osutil.XSnapdMode(uint32(layout.Mode)))
}
// Indicate that this is a layout mount entry.
entry.Options = append(entry.Options, osutil.XSnapdOriginLayout())
return entry
}
// AddLayout adds mount entries based on the layout of the snap.
func (spec *Specification) AddLayout(si *snap.Info) {
// TODO: handle layouts in base snaps as well as in this snap.
// walk the layout elements in deterministic order, by mount point name
paths := make([]string, 0, len(si.Layout))
for path := range si.Layout {
paths = append(paths, path)
}
sort.Strings(paths)
for _, path := range paths {
entry := mountEntryFromLayout(si.Layout[path])
spec.layout = append(spec.layout, entry)
}
}
// AddExtraLayouts adds mount entries based on additional layouts that
// are provided for the snap.
func (spec *Specification) AddExtraLayouts(layouts []snap.Layout) {
for _, layout := range layouts {
entry := mountEntryFromLayout(&layout)
spec.layout = append(spec.layout, entry)
}
}
// createEnsureDirMountEntry creates a mount entry from the given ensure directory spec
// that instruct snap-update-ns to create missing directories if required.
func createEnsureDirMountEntry(ensureDirSpec *interfaces.EnsureDirSpec) osutil.MountEntry {
return osutil.MountEntry{
Options: []string{osutil.XSnapdKindEnsureDir(), osutil.XSnapdMustExistDir(ensureDirSpec.MustExistDir)},
Dir: ensureDirSpec.EnsureDir,
}
}
// AddUserEnsureDirs adds user mount entries to ensure the existence of directories according to the
// given ensure directory specs.
func (spec *Specification) AddUserEnsureDirs(ensureDirSpecs []*interfaces.EnsureDirSpec) error {
// Walk the path specs in deterministic order, by EnsureDir (the mount point).
sort.Slice(ensureDirSpecs, func(i, j int) bool {
return ensureDirSpecs[i].EnsureDir < ensureDirSpecs[j].EnsureDir
})
for _, ensureDirSpec := range ensureDirSpecs {
if err := ensureDirSpec.Validate(); err != nil {
return fmt.Errorf("internal error: cannot use ensure-dir mount specification: %v", err)
}
}
for _, ensureDirSpec := range ensureDirSpecs {
entry := createEnsureDirMountEntry(ensureDirSpec)
spec.user = append(spec.user, entry)
}
return nil
}
// MountEntries returns a copy of the added mount entries.
func (spec *Specification) MountEntries() []osutil.MountEntry {
result := make([]osutil.MountEntry, 0, len(spec.overname)+len(spec.layout)+len(spec.general))
// overname is the mappings that were added to support parallel
// installation of snaps and must come first, as they establish the base
// namespace for any further operations
result = append(result, spec.overname...)
result = append(result, spec.layout...)
result = append(result, spec.general...)
return unclashMountEntries(result)
}
// UserMountEntries returns a copy of the added user mount entries.
func (spec *Specification) UserMountEntries() []osutil.MountEntry {
result := make([]osutil.MountEntry, len(spec.user))
copy(result, spec.user)
return unclashMountEntries(result)
}
// Assuming that two mount entries have the same source, target and type, this
// function computes the mount options that should be used when performing the
// mount, so that the most permissive options are kept.
// The following flags are considered (of course the operation is commutative):
// - "ro" + "rw" = "rw"
// - "bind" + "rbind" = "rbind
func mergeOptions(options ...[]string) []string {
mergedOptions := make([]string, 0, len(options[0]))
foundWritableEntry := false
foundRBindEntry := false
firstEntryIsBindMount := false
for i, opts := range options {
isReadOnly := false
isRBind := false
for _, o := range opts {
switch o {
case "ro":
isReadOnly = true
case "rbind":
isRBind = true
fallthrough
case "bind":
// We know that the passed entries will either be all
// bind-mounts, or none will be a bind-mount (because
// unclashMountEntries() invokes us only if the source, target,
// and FS type are the same). That's why we check only the
// first entry here.
if i == 0 {
firstEntryIsBindMount = true
}
case "rw", "async":
// these are default options for mount, do nothing
default:
// write all other options
if !strutil.ListContains(mergedOptions, o) {
mergedOptions = append(mergedOptions, o)
}
}
}
if !isReadOnly {
foundWritableEntry = true
}
if isRBind {
foundRBindEntry = true
}
}
if !foundWritableEntry {
mergedOptions = append(mergedOptions, "ro")
}
if firstEntryIsBindMount {
if foundRBindEntry {
mergedOptions = append(mergedOptions, "rbind")
} else {
mergedOptions = append(mergedOptions, "bind")
}
}
return mergedOptions
}
// unclashMountEntries renames mount points if they clash with other entries.
//
// Subsequent entries get suffixed with -2, -3, etc.
// The initial entry is unaltered (and does not become -1).
func unclashMountEntries(entries []osutil.MountEntry) []osutil.MountEntry {
result := make([]osutil.MountEntry, 0, len(entries))
// The clashingEntry structure contains the information about different
// mount entries which use the same mount point.
type clashingEntry struct {
// Index in the `entries` array to the first entry of this clashing group
FirstIndex int
// Number of entries having this same mount point
Count int
}
entriesByMountPoint := make(map[string]*clashingEntry, len(entries))
var ensureDirEntries []osutil.MountEntry
for i := range entries {
// Gather ensure-dir mounts, do not unclash
if entries[i].XSnapdKind() == "ensure-dir" {
ensureDirEntries = append(ensureDirEntries, entries[i])
continue
}
mountPoint := entries[i].Dir
entryInMap, found := entriesByMountPoint[mountPoint]
if !found {
index := len(result)
result = append(result, entries[i])
entriesByMountPoint[mountPoint] = &clashingEntry{
FirstIndex: index,
Count: 1,
}
continue
}
// If the source and the FS type is the same, we do not consider
// this to be a clash, and instead will try to combine the mount
// flags in a way that fulfils the permissions required by all
// requesting entries
firstEntry := &result[entryInMap.FirstIndex]
if firstEntry.Name == entries[i].Name && firstEntry.Type == entries[i].Type &&
// Only merge entries that have no origin, or snap-update-ns will
// get confused
firstEntry.XSnapdOrigin() == "" && entries[i].XSnapdOrigin() == "" {
firstEntry.Options = mergeOptions(firstEntry.Options, entries[i].Options)
} else {
entryInMap.Count++
newDir := fmt.Sprintf("%s-%d", entries[i].Dir, entryInMap.Count)
logger.Noticef("renaming mount entry for directory %q to %q to avoid a clash", entries[i].Dir, newDir)
entries[i].Dir = newDir
result = append(result, entries[i])
}
}
// Add all ensure-dir mounts
result = append(result, ensureDirEntries...)
return result
}
// Implementation of methods required by interfaces.Specification
// AddConnectedPlug records mount-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 {
MountConnectedPlug(spec *Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error
}
if iface, ok := iface.(definer); ok {
return iface.MountConnectedPlug(spec, plug, slot)
}
return nil
}
// AddConnectedSlot records mount-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 {
MountConnectedSlot(spec *Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error
}
if iface, ok := iface.(definer); ok {
return iface.MountConnectedSlot(spec, plug, slot)
}
return nil
}
// AddPermanentPlug records mount-specific side-effects of having a plug.
func (spec *Specification) AddPermanentPlug(iface interfaces.Interface, plug *snap.PlugInfo) error {
type definer interface {
MountPermanentPlug(spec *Specification, plug *snap.PlugInfo) error
}
if iface, ok := iface.(definer); ok {
return iface.MountPermanentPlug(spec, plug)
}
return nil
}
// AddPermanentSlot records mount-specific side-effects of having a slot.
func (spec *Specification) AddPermanentSlot(iface interfaces.Interface, slot *snap.SlotInfo) error {
type definer interface {
MountPermanentSlot(spec *Specification, slot *snap.SlotInfo) error
}
if iface, ok := iface.(definer); ok {
return iface.MountPermanentSlot(spec, slot)
}
return nil
}
// AddOvername records mappings of snap directories.
//
// When the snap is installed with an instance key, set up its mount namespace
// such that it appears as a non-instance key snap. This ensures compatibility
// with code making assumptions about $SNAP{,_DATA,_COMMON} locations. That is,
// given a snap foo_bar, the mappings added are:
//
// - /snap/foo_bar -> /snap/foo
// - /var/snap/foo_bar -> /var/snap/foo
func (spec *Specification) AddOvername(info *snap.Info) {
if info.InstanceKey == "" {
return
}
// /snap/foo_bar -> /snap/foo
spec.AddOvernameMountEntry(osutil.MountEntry{
Name: path.Join(dirs.CoreSnapMountDir, info.InstanceName()),
Dir: path.Join(dirs.CoreSnapMountDir, info.SnapName()),
Options: []string{"rbind", osutil.XSnapdOriginOvername()},
})
// /var/snap/foo_bar -> /var/snap/foo
spec.AddOvernameMountEntry(osutil.MountEntry{
Name: path.Join(dirs.SnapDataDir, info.InstanceName()),
Dir: path.Join(dirs.SnapDataDir, info.SnapName()),
Options: []string{"rbind", osutil.XSnapdOriginOvername()},
})
}