github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/interfaces/builtin/content.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package builtin
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"path/filepath"
    26  	"strings"
    27  
    28  	"github.com/snapcore/snapd/interfaces"
    29  	"github.com/snapcore/snapd/interfaces/apparmor"
    30  	"github.com/snapcore/snapd/interfaces/mount"
    31  	"github.com/snapcore/snapd/osutil"
    32  	"github.com/snapcore/snapd/snap"
    33  )
    34  
    35  const contentSummary = `allows sharing code and data with other snaps`
    36  
    37  const contentBaseDeclarationSlots = `
    38    content:
    39      allow-installation:
    40        slot-snap-type:
    41          - app
    42          - gadget
    43      allow-connection:
    44        plug-attributes:
    45          content: $SLOT(content)
    46      allow-auto-connection:
    47        plug-publisher-id:
    48          - $SLOT_PUBLISHER_ID
    49        plug-attributes:
    50          content: $SLOT(content)
    51  `
    52  
    53  // contentInterface allows sharing content between snaps
    54  type contentInterface struct{}
    55  
    56  func (iface *contentInterface) Name() string {
    57  	return "content"
    58  }
    59  
    60  func (iface *contentInterface) StaticInfo() interfaces.StaticInfo {
    61  	return interfaces.StaticInfo{
    62  		Summary:              contentSummary,
    63  		BaseDeclarationSlots: contentBaseDeclarationSlots,
    64  	}
    65  }
    66  
    67  func cleanSubPath(path string) bool {
    68  	return filepath.Clean(path) == path && path != ".." && !strings.HasPrefix(path, "../")
    69  }
    70  
    71  func (iface *contentInterface) BeforePrepareSlot(slot *snap.SlotInfo) error {
    72  	content, ok := slot.Attrs["content"].(string)
    73  	if !ok || len(content) == 0 {
    74  		if slot.Attrs == nil {
    75  			slot.Attrs = make(map[string]interface{})
    76  		}
    77  		// content defaults to "slot" name if unspecified
    78  		slot.Attrs["content"] = slot.Name
    79  	}
    80  
    81  	// Error if "read" or "write" are present alongside "source".
    82  	// TODO: use slot.Lookup() once PR 4510 lands.
    83  	var unused map[string]interface{}
    84  	if err := slot.Attr("source", &unused); err == nil {
    85  		var unused []interface{}
    86  		if err := slot.Attr("read", &unused); err == nil {
    87  			return fmt.Errorf(`move the "read" attribute into the "source" section`)
    88  		}
    89  		if err := slot.Attr("write", &unused); err == nil {
    90  			return fmt.Errorf(`move the "write" attribute into the "source" section`)
    91  		}
    92  	}
    93  
    94  	// check that we have either a read or write path
    95  	rpath := iface.path(slot, "read")
    96  	wpath := iface.path(slot, "write")
    97  	if len(rpath) == 0 && len(wpath) == 0 {
    98  		return fmt.Errorf("read or write path must be set")
    99  	}
   100  
   101  	// go over both paths
   102  	paths := rpath
   103  	paths = append(paths, wpath...)
   104  	for _, p := range paths {
   105  		if !cleanSubPath(p) {
   106  			return fmt.Errorf("content interface path is not clean: %q", p)
   107  		}
   108  	}
   109  	return nil
   110  }
   111  
   112  func (iface *contentInterface) BeforePreparePlug(plug *snap.PlugInfo) error {
   113  	content, ok := plug.Attrs["content"].(string)
   114  	if !ok || len(content) == 0 {
   115  		if plug.Attrs == nil {
   116  			plug.Attrs = make(map[string]interface{})
   117  		}
   118  		// content defaults to "plug" name if unspecified
   119  		plug.Attrs["content"] = plug.Name
   120  	}
   121  	target, ok := plug.Attrs["target"].(string)
   122  	if !ok || len(target) == 0 {
   123  		return fmt.Errorf("content plug must contain target path")
   124  	}
   125  	if !cleanSubPath(target) {
   126  		return fmt.Errorf("content interface target path is not clean: %q", target)
   127  	}
   128  
   129  	return nil
   130  }
   131  
   132  // path is an internal helper that extract the "read" and "write" attribute
   133  // of the slot
   134  func (iface *contentInterface) path(attrs interfaces.Attrer, name string) []string {
   135  	if name != "read" && name != "write" {
   136  		panic("internal error, path can only be used with read/write")
   137  	}
   138  
   139  	var paths []interface{}
   140  	var source map[string]interface{}
   141  
   142  	if err := attrs.Attr("source", &source); err == nil {
   143  		// Access either "source.read" or "source.write" attribute.
   144  		var ok bool
   145  		if paths, ok = source[name].([]interface{}); !ok {
   146  			return nil
   147  		}
   148  	} else {
   149  		// Access either "read" or "write" attribute directly (legacy).
   150  		if err := attrs.Attr(name, &paths); err != nil {
   151  			return nil
   152  		}
   153  	}
   154  
   155  	out := make([]string, len(paths))
   156  	for i, p := range paths {
   157  		var ok bool
   158  		out[i], ok = p.(string)
   159  		if !ok {
   160  			return nil
   161  		}
   162  	}
   163  	return out
   164  }
   165  
   166  // resolveSpecialVariable resolves one of the three $SNAP* variables at the
   167  // beginning of a given path.  The variables are $SNAP, $SNAP_DATA and
   168  // $SNAP_COMMON. If there are no variables then $SNAP is implicitly assumed
   169  // (this is the behavior that was used before the variables were supporter).
   170  func resolveSpecialVariable(path string, snapInfo *snap.Info) string {
   171  	// Content cannot be mounted at arbitrary locations, validate the path
   172  	// for extra safety.
   173  	if err := snap.ValidatePathVariables(path); err == nil && strings.HasPrefix(path, "$") {
   174  		// The path starts with $ and ValidatePathVariables() ensures
   175  		// path contains only $SNAP, $SNAP_DATA, $SNAP_COMMON, and no
   176  		// other $VARs are present. It is ok to use
   177  		// ExpandSnapVariables() since it only expands $SNAP, $SNAP_DATA
   178  		// and $SNAP_COMMON
   179  		return snapInfo.ExpandSnapVariables(path)
   180  	}
   181  	// Always prefix with $SNAP if nothing else is provided or the path
   182  	// contains invalid variables.
   183  	return snapInfo.ExpandSnapVariables(filepath.Join("$SNAP", path))
   184  }
   185  
   186  func sourceTarget(plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot, relSrc string) (string, string) {
   187  	var target string
   188  	// The 'target' attribute has already been verified in BeforePreparePlug.
   189  	_ = plug.Attr("target", &target)
   190  	source := resolveSpecialVariable(relSrc, slot.Snap())
   191  	target = resolveSpecialVariable(target, plug.Snap())
   192  
   193  	// Check if the "source" section is present.
   194  	var unused map[string]interface{}
   195  	if err := slot.Attr("source", &unused); err == nil {
   196  		_, sourceName := filepath.Split(source)
   197  		target = filepath.Join(target, sourceName)
   198  	}
   199  	return source, target
   200  }
   201  
   202  func mountEntry(plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot, relSrc string, extraOptions ...string) osutil.MountEntry {
   203  	options := make([]string, 0, len(extraOptions)+1)
   204  	options = append(options, "bind")
   205  	options = append(options, extraOptions...)
   206  	source, target := sourceTarget(plug, slot, relSrc)
   207  	return osutil.MountEntry{
   208  		Name:    source,
   209  		Dir:     target,
   210  		Options: options,
   211  	}
   212  }
   213  
   214  func (iface *contentInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
   215  	contentSnippet := bytes.NewBuffer(nil)
   216  	writePaths := iface.path(slot, "write")
   217  	emit := spec.AddUpdateNSf
   218  	if len(writePaths) > 0 {
   219  		fmt.Fprintf(contentSnippet, `
   220  # In addition to the bind mount, add any AppArmor rules so that
   221  # snaps may directly access the slot implementation's files. Due
   222  # to a limitation in the kernel's LSM hooks for AF_UNIX, these
   223  # are needed for using named sockets within the exported
   224  # directory.
   225  `)
   226  		for i, w := range writePaths {
   227  			fmt.Fprintf(contentSnippet, "%s/** mrwklix,\n",
   228  				resolveSpecialVariable(w, slot.Snap()))
   229  			source, target := sourceTarget(plug, slot, w)
   230  			emit("  # Read-write content sharing %s -> %s (w#%d)\n", plug.Ref(), slot.Ref(), i)
   231  			emit("  mount options=(bind, rw) %s/ -> %s{,-[0-9]*}/,\n", source, target)
   232  			emit("  mount options=(rprivate) -> %s{,-[0-9]*}/,\n", target)
   233  			emit("  umount %s{,-[0-9]*}/,\n", target)
   234  			// TODO: The assumed prefix depth could be optimized to be more
   235  			// precise since content sharing can only take place in a fixed
   236  			// list of places with well-known paths (well, constrained set of
   237  			// paths). This can be done when the prefix is actually consumed.
   238  			apparmor.GenWritableProfile(emit, source, 1)
   239  			apparmor.GenWritableProfile(emit, target, 1)
   240  			apparmor.GenWritableProfile(emit, fmt.Sprintf("%s-[0-9]*", target), 1)
   241  		}
   242  	}
   243  
   244  	readPaths := iface.path(slot, "read")
   245  	if len(readPaths) > 0 {
   246  		fmt.Fprintf(contentSnippet, `
   247  # In addition to the bind mount, add any AppArmor rules so that
   248  # snaps may directly access the slot implementation's files
   249  # read-only.
   250  `)
   251  		for i, r := range readPaths {
   252  			fmt.Fprintf(contentSnippet, "%s/** mrkix,\n",
   253  				resolveSpecialVariable(r, slot.Snap()))
   254  
   255  			source, target := sourceTarget(plug, slot, r)
   256  			emit("  # Read-only content sharing %s -> %s (r#%d)\n", plug.Ref(), slot.Ref(), i)
   257  			emit("  mount options=(bind) %s/ -> %s{,-[0-9]*}/,\n", source, target)
   258  			emit("  remount options=(bind, ro) %s{,-[0-9]*}/,\n", target)
   259  			emit("  mount options=(rprivate) -> %s{,-[0-9]*}/,\n", target)
   260  			emit("  umount %s{,-[0-9]*}/,\n", target)
   261  			// Look at the TODO comment above.
   262  			apparmor.GenWritableProfile(emit, source, 1)
   263  			apparmor.GenWritableProfile(emit, target, 1)
   264  			apparmor.GenWritableProfile(emit, fmt.Sprintf("%s-[0-9]*", target), 1)
   265  		}
   266  	}
   267  
   268  	spec.AddSnippet(contentSnippet.String())
   269  	return nil
   270  }
   271  
   272  func (iface *contentInterface) AppArmorConnectedSlot(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
   273  	contentSnippet := bytes.NewBuffer(nil)
   274  	writePaths := iface.path(slot, "write")
   275  	if len(writePaths) > 0 {
   276  		fmt.Fprintf(contentSnippet, `
   277  # When the content interface is writable, allow this slot
   278  # implementation to access the slot's exported files at the plugging
   279  # snap's mountpoint to accommodate software where the plugging app
   280  # tells the slotting app about files to share.
   281  `)
   282  		for _, w := range writePaths {
   283  			_, target := sourceTarget(plug, slot, w)
   284  			fmt.Fprintf(contentSnippet, "%s/** mrwklix,\n",
   285  				target)
   286  		}
   287  	}
   288  
   289  	spec.AddSnippet(contentSnippet.String())
   290  	return nil
   291  }
   292  
   293  func (iface *contentInterface) AutoConnect(plug *snap.PlugInfo, slot *snap.SlotInfo) bool {
   294  	// allow what declarations allowed
   295  	return true
   296  }
   297  
   298  // Interactions with the mount backend.
   299  
   300  func (iface *contentInterface) MountConnectedPlug(spec *mount.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
   301  	for _, r := range iface.path(slot, "read") {
   302  		err := spec.AddMountEntry(mountEntry(plug, slot, r, "ro"))
   303  		if err != nil {
   304  			return err
   305  		}
   306  	}
   307  	for _, w := range iface.path(slot, "write") {
   308  		err := spec.AddMountEntry(mountEntry(plug, slot, w))
   309  		if err != nil {
   310  			return err
   311  		}
   312  	}
   313  	return nil
   314  }
   315  
   316  func init() {
   317  	registerIface(&contentInterface{})
   318  }