gitee.com/mysnapcore/mysnapd@v0.1.0/interfaces/builtin/shared_memory.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2022 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  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"path/filepath"
    28  	"strings"
    29  
    30  	"gitee.com/mysnapcore/mysnapd/dirs"
    31  	"gitee.com/mysnapcore/mysnapd/interfaces"
    32  	"gitee.com/mysnapcore/mysnapd/interfaces/apparmor"
    33  	"gitee.com/mysnapcore/mysnapd/interfaces/mount"
    34  	"gitee.com/mysnapcore/mysnapd/osutil"
    35  	"gitee.com/mysnapcore/mysnapd/snap"
    36  )
    37  
    38  const sharedMemorySummary = `allows two snaps to use predefined shared memory objects`
    39  
    40  // The plug side of shared-memory can operate in two modes: if the
    41  // private attribute is set to true, then it can be connected to the
    42  // implicit system slot to be given a private version of /dev/shm.
    43  //
    44  // For a plug without that attribute set, it will connect to a
    45  // matching application snap slot - this is permitted even though the
    46  // interface is super-privileged because using a slot requires a store
    47  // declaration anyways so just declaring a plug will not grant access
    48  // unless a slot was also granted at some point.
    49  const sharedMemoryBaseDeclarationPlugs = `
    50    shared-memory:
    51      allow-connection:
    52        -
    53          plug-attributes:
    54            private: false
    55          slot-attributes:
    56            shared-memory: $PLUG(shared-memory)
    57        -
    58          plug-attributes:
    59            private: true
    60          slot-snap-type:
    61            - core
    62      allow-auto-connection:
    63        -
    64          plug-attributes:
    65            private: false
    66          slot-publisher-id:
    67            - $PLUG_PUBLISHER_ID
    68          slot-attributes:
    69            shared-memory: $PLUG(shared-memory)
    70        -
    71          plug-attributes:
    72            private: true
    73          slot-snap-type:
    74            - core
    75  `
    76  
    77  // shared-memory slots can appear either as an implicit system slot,
    78  // or as a slot on an application snap.
    79  //
    80  // The implicit version of the slot is intended to auto-connect with
    81  // plugs that have the private attribute set to true.
    82  //
    83  // Slots on app snaps connect to non-private plugs. They are are
    84  // super-privileged and thus denied to any snap except those that get
    85  // a store declaration to do so, but the intent is for application or
    86  // gadget snaps to use the slot much like the content interface.
    87  const sharedMemoryBaseDeclarationSlots = `
    88    shared-memory:
    89      allow-installation:
    90        slot-snap-type:
    91          - app
    92          - gadget
    93          - core
    94        slot-snap-id:
    95          - PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4
    96          - 99T7MUlRhtI3U0QFgl5mXXESAiSwt776
    97      deny-auto-connection: true
    98  `
    99  
   100  const sharedMemoryPrivateConnectedPlugAppArmor = `
   101  # Description: Allow access to everything in private /dev/shm
   102  "/dev/shm/*" mrwlkix,
   103  `
   104  
   105  func validateSharedMemoryPath(path string) error {
   106  	if len(path) == 0 {
   107  		return fmt.Errorf("shared-memory interface path is empty")
   108  	}
   109  
   110  	if strings.TrimSpace(path) != path {
   111  		return fmt.Errorf("shared-memory interface path has leading or trailing spaces: %q", path)
   112  	}
   113  
   114  	// allow specifically only "*" globbing character, but disallow all other
   115  	// AARE characters
   116  
   117  	// same as from ValidateNoAppArmorRegexp, but with globbing
   118  	const aareWithoutGlob = `?[]{}^"` + "\x00"
   119  	if strings.ContainsAny(path, aareWithoutGlob) {
   120  		return fmt.Errorf("shared-memory interface path is invalid: %q contains a reserved apparmor char from %s", path, aareWithoutGlob)
   121  	}
   122  
   123  	// in addition to only allowing "*", we don't want to allow double "**"
   124  	// because "**" can traverse sub-directories as well which we don't want
   125  	if strings.Contains(path, "**") {
   126  		return fmt.Errorf("shared-memory interface path is invalid: %q contains ** which is unsupported", path)
   127  	}
   128  
   129  	// TODO: consider whether we should remove this check and allow full SHM path
   130  	if strings.Contains(path, "/") {
   131  		return fmt.Errorf("shared-memory interface path should not contain '/': %q", path)
   132  	}
   133  
   134  	// The check above protects from most unclean paths, but one could still specify ".."
   135  	if !cleanSubPath(path) {
   136  		return fmt.Errorf("shared-memory interface path is not clean: %q", path)
   137  	}
   138  
   139  	return nil
   140  }
   141  
   142  func stringListAttribute(attrer interfaces.Attrer, key string) ([]string, error) {
   143  	var stringList []string
   144  	err := attrer.Attr(key, &stringList)
   145  	if err != nil && !errors.Is(err, snap.AttributeNotFoundError{}) {
   146  		value, _ := attrer.Lookup(key)
   147  		return nil, fmt.Errorf(`shared-memory %q attribute must be a list of strings, not "%v"`, key, value)
   148  	}
   149  
   150  	return stringList, nil
   151  }
   152  
   153  // sharedMemoryInterface allows sharing sharedMemory between snaps
   154  type sharedMemoryInterface struct{}
   155  
   156  func (iface *sharedMemoryInterface) Name() string {
   157  	return "shared-memory"
   158  }
   159  
   160  func (iface *sharedMemoryInterface) StaticInfo() interfaces.StaticInfo {
   161  	return interfaces.StaticInfo{
   162  		Summary:              sharedMemorySummary,
   163  		BaseDeclarationPlugs: sharedMemoryBaseDeclarationPlugs,
   164  		BaseDeclarationSlots: sharedMemoryBaseDeclarationSlots,
   165  		AffectsPlugOnRefresh: true,
   166  		ImplicitOnCore:       true,
   167  		ImplicitOnClassic:    true,
   168  	}
   169  }
   170  
   171  func (iface *sharedMemoryInterface) BeforePrepareSlot(slot *snap.SlotInfo) error {
   172  	sharedMemoryAttr, isSet := slot.Attrs["shared-memory"]
   173  	sharedMemory, ok := sharedMemoryAttr.(string)
   174  	if isSet && !ok {
   175  		return fmt.Errorf(`shared-memory "shared-memory" attribute must be a string, not %v`,
   176  			slot.Attrs["shared-memory"])
   177  	}
   178  	if sharedMemory == "" {
   179  		if slot.Attrs == nil {
   180  			slot.Attrs = make(map[string]interface{})
   181  		}
   182  		// shared-memory defaults to "slot" name if unspecified
   183  		slot.Attrs["shared-memory"] = slot.Name
   184  	}
   185  
   186  	readPaths, err := stringListAttribute(slot, "read")
   187  	if err != nil {
   188  		return err
   189  	}
   190  
   191  	writePaths, err := stringListAttribute(slot, "write")
   192  	if err != nil {
   193  		return err
   194  	}
   195  
   196  	// We perform the same validation for read-only and writable paths, so
   197  	// let's just put them all in the same array
   198  	allPaths := append(readPaths, writePaths...)
   199  	if len(allPaths) == 0 {
   200  		return errors.New(`shared memory interface requires at least a valid "read" or "write" attribute`)
   201  	}
   202  
   203  	for _, path := range allPaths {
   204  		if err := validateSharedMemoryPath(path); err != nil {
   205  			return err
   206  		}
   207  	}
   208  
   209  	return nil
   210  }
   211  
   212  type sharedMemorySnippetType int
   213  
   214  const (
   215  	snippetForSlot sharedMemorySnippetType = iota
   216  	snippetForPlug
   217  )
   218  
   219  func writeSharedMemoryPaths(w io.Writer, slot *interfaces.ConnectedSlot,
   220  	snippetType sharedMemorySnippetType) {
   221  	emitWritableRule := func(path string) {
   222  		// Ubuntu 14.04 uses /run/shm instead of the most common /dev/shm
   223  		fmt.Fprintf(w, "\"/{dev,run}/shm/%s\" mrwlk,\n", path)
   224  	}
   225  
   226  	// All checks were already done in BeforePrepare{Plug,Slot}
   227  	writePaths, _ := stringListAttribute(slot, "write")
   228  	for _, path := range writePaths {
   229  		emitWritableRule(path)
   230  	}
   231  	readPaths, _ := stringListAttribute(slot, "read")
   232  	for _, path := range readPaths {
   233  		if snippetType == snippetForPlug {
   234  			// grant read-only access
   235  			fmt.Fprintf(w, "\"/{dev,run}/shm/%s\" r,\n", path)
   236  		} else {
   237  			// the slot must still be granted write access, because the "read"
   238  			// and "write" attributes are meant to affect the plug only
   239  			emitWritableRule(path)
   240  		}
   241  	}
   242  }
   243  
   244  func (iface *sharedMemoryInterface) BeforePreparePlug(plug *snap.PlugInfo) error {
   245  	privateAttr, isPrivateSet := plug.Attrs["private"]
   246  	private, ok := privateAttr.(bool)
   247  	if isPrivateSet && !ok {
   248  		return fmt.Errorf(`shared-memory "private" attribute must be a bool, not %v`, privateAttr)
   249  	}
   250  	if plug.Attrs == nil {
   251  		plug.Attrs = make(map[string]interface{})
   252  	}
   253  	plug.Attrs["private"] = private
   254  
   255  	sharedMemoryAttr, isSet := plug.Attrs["shared-memory"]
   256  	sharedMemory, ok := sharedMemoryAttr.(string)
   257  	if isSet && !ok {
   258  		return fmt.Errorf(`shared-memory "shared-memory" attribute must be a string, not %v`,
   259  			plug.Attrs["shared-memory"])
   260  	}
   261  	if private {
   262  		if isSet {
   263  			return fmt.Errorf(`shared-memory "shared-memory" attribute must not be set together with "private: true"`)
   264  		}
   265  		// A private shared-memory plug cannot coexist with
   266  		// other shared-memory plugs/slots.
   267  		for _, other := range plug.Snap.Plugs {
   268  			if other != plug && other.Interface == "shared-memory" {
   269  				return fmt.Errorf(`shared-memory plug with "private: true" set cannot be used with other shared-memory plugs`)
   270  			}
   271  		}
   272  		for _, other := range plug.Snap.Slots {
   273  			if other.Interface == "shared-memory" {
   274  				return fmt.Errorf(`shared-memory plug with "private: true" set cannot be used with shared-memory slots`)
   275  			}
   276  		}
   277  	} else {
   278  		if sharedMemory == "" {
   279  			// shared-memory defaults to "plug" name if unspecified
   280  			plug.Attrs["shared-memory"] = plug.Name
   281  		}
   282  	}
   283  
   284  	return nil
   285  }
   286  
   287  func (iface *sharedMemoryInterface) isPrivate(plug *interfaces.ConnectedPlug) bool {
   288  	var private bool
   289  	if err := plug.Attr("private", &private); err == nil {
   290  		return private
   291  	}
   292  	panic("plug is not sanitized")
   293  }
   294  
   295  func (iface *sharedMemoryInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
   296  	if iface.isPrivate(plug) {
   297  		spec.AddSnippet(sharedMemoryPrivateConnectedPlugAppArmor)
   298  		spec.AddUpdateNSf(`  # Private /dev/shm
   299    /dev/ r,
   300    /dev/shm/{,**} rw,
   301    mount options=(bind, rw) /dev/shm/snap.%s/ -> /dev/shm/,
   302    umount /dev/shm/,`, plug.Snap().InstanceName())
   303  	} else {
   304  		sharedMemorySnippet := &bytes.Buffer{}
   305  		writeSharedMemoryPaths(sharedMemorySnippet, slot, snippetForPlug)
   306  		spec.AddSnippet(sharedMemorySnippet.String())
   307  	}
   308  	return nil
   309  }
   310  
   311  func (iface *sharedMemoryInterface) AppArmorConnectedSlot(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
   312  	if slot.Snap().Type() == snap.TypeOS || slot.Snap().Type() == snap.TypeSnapd {
   313  		return nil
   314  	}
   315  
   316  	sharedMemorySnippet := &bytes.Buffer{}
   317  	writeSharedMemoryPaths(sharedMemorySnippet, slot, snippetForSlot)
   318  	spec.AddSnippet(sharedMemorySnippet.String())
   319  	return nil
   320  }
   321  
   322  func (iface *sharedMemoryInterface) MountConnectedPlug(spec *mount.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
   323  	if !iface.isPrivate(plug) {
   324  		return nil
   325  	}
   326  
   327  	devShm := filepath.Join(dirs.GlobalRootDir, "/dev/shm")
   328  	if osutil.IsSymlink(devShm) {
   329  		return fmt.Errorf(`shared-memory plug with "private: true" cannot be connected if %q is a symlink`, devShm)
   330  	}
   331  
   332  	return spec.AddMountEntry(osutil.MountEntry{
   333  		Name:    filepath.Join(devShm, "snap."+plug.Snap().InstanceName()),
   334  		Dir:     "/dev/shm",
   335  		Options: []string{"bind", "rw"},
   336  	})
   337  }
   338  
   339  func (iface *sharedMemoryInterface) AutoConnect(plug *snap.PlugInfo, slot *snap.SlotInfo) bool {
   340  	// allow what declarations allowed
   341  	return true
   342  }
   343  
   344  func init() {
   345  	registerIface(&sharedMemoryInterface{})
   346  }