gitee.com/mysnapcore/mysnapd@v0.1.0/interfaces/builtin/custom_device.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  	"regexp"
    27  	"strings"
    28  
    29  	"gitee.com/mysnapcore/mysnapd/interfaces"
    30  	"gitee.com/mysnapcore/mysnapd/interfaces/apparmor"
    31  	"gitee.com/mysnapcore/mysnapd/interfaces/udev"
    32  	"gitee.com/mysnapcore/mysnapd/interfaces/utils"
    33  	"gitee.com/mysnapcore/mysnapd/snap"
    34  	"gitee.com/mysnapcore/mysnapd/strutil"
    35  )
    36  
    37  const customDeviceSummary = `provides access to custom devices specified via the gadget snap`
    38  
    39  const customDeviceBaseDeclarationSlots = `
    40    custom-device:
    41      allow-installation: false
    42      allow-connection:
    43        plug-attributes:
    44          content: $SLOT(custom-device)
    45      deny-auto-connection: true
    46  `
    47  
    48  var (
    49  	// A cryptic, uninformative error message that we use only on impossible code paths
    50  	customDeviceInternalError = errors.New(`custom-device interface internal error`)
    51  
    52  	// Validating regexp for filesystem paths
    53  	customDevicePathRegexp = regexp.MustCompile(`^/[^"@]*$`)
    54  
    55  	// Validating regexp for udev device names.
    56  	// We forbid:
    57  	// - `|`: it's valid for udev, but more work for us
    58  	// - `{}`: have a special meaning for AppArmor
    59  	// - `"`: it's just dangerous (both for udev and AppArmor)
    60  	// - `\`: also dangerous
    61  	customDeviceUDevDeviceRegexp = regexp.MustCompile(`^/dev/[^"|{}\\]+$`)
    62  
    63  	// Validating regexp for udev tag values (all but kernel devices)
    64  	customDeviceUDevValueRegexp = regexp.MustCompile(`^[^"{}\\]+$`)
    65  )
    66  
    67  // customDeviceInterface allows sharing customDevice between snaps
    68  type customDeviceInterface struct{}
    69  
    70  func (iface *customDeviceInterface) validateFilePath(path string, attrName string) error {
    71  	if !customDevicePathRegexp.MatchString(path) {
    72  		return fmt.Errorf(`custom-device %q path must start with / and cannot contain special characters: %q`, attrName, path)
    73  	}
    74  
    75  	if !cleanSubPath(path) {
    76  		return fmt.Errorf(`custom-device %q path is not clean: %q`, attrName, path)
    77  	}
    78  
    79  	if _, err := utils.NewPathPattern(path); err != nil {
    80  		return fmt.Errorf(`custom-device %q path cannot be used: %v`, attrName, err)
    81  	}
    82  
    83  	// We don't allow "**" because that's an AppArmor specific globbing pattern
    84  	// which we don't want to expose in our API contract.
    85  	if strings.Contains(path, "**") {
    86  		return fmt.Errorf(`custom-device %q path contains invalid glob pattern "**"`, attrName)
    87  	}
    88  
    89  	return nil
    90  }
    91  
    92  func (iface *customDeviceInterface) validateDevice(path string, attrName string) error {
    93  	// The device must satisfy udev's device name rules and generic path rules
    94  	if !customDeviceUDevDeviceRegexp.MatchString(path) {
    95  		return fmt.Errorf(`custom-device %q path must start with /dev/ and cannot contain special characters: %q`, attrName, path)
    96  	}
    97  
    98  	if err := iface.validateFilePath(path, attrName); err != nil {
    99  		return err
   100  	}
   101  
   102  	return nil
   103  }
   104  
   105  func (iface *customDeviceInterface) validatePaths(attrName string, paths []string) error {
   106  	for _, path := range paths {
   107  		if err := iface.validateFilePath(path, attrName); err != nil {
   108  			return err
   109  		}
   110  	}
   111  
   112  	return nil
   113  }
   114  
   115  func (iface *customDeviceInterface) validateUDevValue(value interface{}) error {
   116  	stringValue, ok := value.(string)
   117  	if !ok {
   118  		return fmt.Errorf(`value "%v" is not a string`, value)
   119  	}
   120  
   121  	if !customDeviceUDevValueRegexp.MatchString(stringValue) {
   122  		return fmt.Errorf(`value "%v" contains invalid characters`, stringValue)
   123  	}
   124  
   125  	return nil
   126  }
   127  
   128  func (iface *customDeviceInterface) validateUDevValueMap(value interface{}) error {
   129  	valueMap, ok := value.(map[string]interface{})
   130  	if !ok {
   131  		return fmt.Errorf(`value "%v" is not a map`, value)
   132  	}
   133  
   134  	for key, val := range valueMap {
   135  		if !customDeviceUDevValueRegexp.MatchString(key) {
   136  			return fmt.Errorf(`key "%v" contains invalid characters`, key)
   137  		}
   138  		if err := iface.validateUDevValue(val); err != nil {
   139  			return err
   140  		}
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  func (iface *customDeviceInterface) validateUDevTaggingRule(rule map[string]interface{}, devices []string) error {
   147  	hasKernelTag := false
   148  	for key, value := range rule {
   149  		var err error
   150  		switch key {
   151  		case "subsystem":
   152  			err = iface.validateUDevValue(value)
   153  		case "kernel":
   154  			hasKernelTag = true
   155  			err = iface.validateUDevValue(value)
   156  			if err == nil {
   157  				deviceName := value.(string)
   158  				// furthermore, the kernel name must match the name of one of
   159  				// the given devices
   160  				if !strutil.ListContains(devices, "/dev/"+deviceName) {
   161  					err = fmt.Errorf(`%q does not match a specified device`, deviceName)
   162  				}
   163  			}
   164  		case "attributes", "environment":
   165  			err = iface.validateUDevValueMap(value)
   166  		default:
   167  			err = errors.New(`unknown tag`)
   168  		}
   169  
   170  		if err != nil {
   171  			return fmt.Errorf(`custom-device "udev-tagging" invalid %q tag: %v`, key, err)
   172  		}
   173  	}
   174  
   175  	if !hasKernelTag {
   176  		return errors.New(`custom-device udev tagging rule missing mandatory "kernel" key`)
   177  	}
   178  
   179  	return nil
   180  }
   181  
   182  func (iface *customDeviceInterface) Name() string {
   183  	return "custom-device"
   184  }
   185  
   186  func (iface *customDeviceInterface) StaticInfo() interfaces.StaticInfo {
   187  	return interfaces.StaticInfo{
   188  		Summary:              customDeviceSummary,
   189  		BaseDeclarationSlots: customDeviceBaseDeclarationSlots,
   190  	}
   191  }
   192  
   193  func (iface *customDeviceInterface) BeforePrepareSlot(slot *snap.SlotInfo) error {
   194  	if slot.Attrs == nil {
   195  		slot.Attrs = make(map[string]interface{})
   196  	}
   197  	customDeviceAttr, isSet := slot.Attrs["custom-device"]
   198  	customDevice, ok := customDeviceAttr.(string)
   199  	if isSet && !ok {
   200  		return fmt.Errorf(`custom-device "custom-device" attribute must be a string, not %v`,
   201  			customDeviceAttr)
   202  	}
   203  	if customDevice == "" {
   204  		// custom-device defaults to "slot" name if unspecified
   205  		slot.Attrs["custom-device"] = slot.Name
   206  	}
   207  
   208  	var devices []string
   209  	err := slot.Attr("devices", &devices)
   210  	if err != nil && !errors.Is(err, snap.AttributeNotFoundError{}) {
   211  		return err
   212  	}
   213  	for _, device := range devices {
   214  		if err := iface.validateDevice(device, "devices"); err != nil {
   215  			return err
   216  		}
   217  	}
   218  
   219  	var readDevices []string
   220  	err = slot.Attr("read-devices", &readDevices)
   221  	if err != nil && !errors.Is(err, snap.AttributeNotFoundError{}) {
   222  		return err
   223  	}
   224  	for _, device := range readDevices {
   225  		if err := iface.validateDevice(device, "read-devices"); err != nil {
   226  			return err
   227  		}
   228  		if strutil.ListContains(devices, device) {
   229  			return fmt.Errorf(`cannot specify path %q both in "devices" and "read-devices" attributes`, device)
   230  		}
   231  	}
   232  
   233  	allDevices := devices
   234  	allDevices = append(allDevices, readDevices...)
   235  
   236  	// validate files
   237  	var filesMap map[string][]string
   238  	err = slot.Attr("files", &filesMap)
   239  	if err != nil && !errors.Is(err, snap.AttributeNotFoundError{}) {
   240  		return err
   241  	}
   242  	for key, val := range filesMap {
   243  		switch key {
   244  		case "read":
   245  			if err := iface.validatePaths("read", val); err != nil {
   246  				return err
   247  			}
   248  		case "write":
   249  			if err := iface.validatePaths("write", val); err != nil {
   250  				return err
   251  			}
   252  		default:
   253  			return fmt.Errorf(`cannot specify %q in "files" section, only "read" and "write" allowed`, key)
   254  		}
   255  	}
   256  
   257  	if len(allDevices) == 0 && len(filesMap) == 0 {
   258  		return fmt.Errorf("cannot use custom-device slot without any files or devices")
   259  	}
   260  
   261  	var udevTaggingRules []map[string]interface{}
   262  	err = slot.Attr("udev-tagging", &udevTaggingRules)
   263  	if err != nil && !errors.Is(err, snap.AttributeNotFoundError{}) {
   264  		return err
   265  	}
   266  	for _, udevTaggingRule := range udevTaggingRules {
   267  		if err := iface.validateUDevTaggingRule(udevTaggingRule, allDevices); err != nil {
   268  			return err
   269  		}
   270  	}
   271  
   272  	return nil
   273  }
   274  
   275  func (iface *customDeviceInterface) BeforePreparePlug(plug *snap.PlugInfo) error {
   276  	customDeviceAttr, isSet := plug.Attrs["custom-device"]
   277  	customDevice, ok := customDeviceAttr.(string)
   278  	if isSet && !ok {
   279  		return fmt.Errorf(`custom-device "custom-device" attribute must be a string, not %v`,
   280  			plug.Attrs["custom-device"])
   281  	}
   282  	if customDevice == "" {
   283  		if plug.Attrs == nil {
   284  			plug.Attrs = make(map[string]interface{})
   285  		}
   286  		// custom-device defaults to "plug" name if unspecified
   287  		plug.Attrs["custom-device"] = plug.Name
   288  	}
   289  
   290  	return nil
   291  }
   292  
   293  func (iface *customDeviceInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
   294  	snippet := &bytes.Buffer{}
   295  	emitRule := func(paths []string, permissions string) {
   296  		for _, path := range paths {
   297  			fmt.Fprintf(snippet, "\"%s\" %s,\n", path, permissions)
   298  		}
   299  	}
   300  
   301  	// get all attributes without validation, since that was done before;
   302  	// should an error occur, we'll simply not write any rule.
   303  
   304  	var devicePaths []string
   305  	_ = slot.Attr("devices", &devicePaths)
   306  	emitRule(devicePaths, "rw")
   307  
   308  	var readDevicePaths []string
   309  	_ = slot.Attr("read-devices", &readDevicePaths)
   310  	emitRule(readDevicePaths, "r")
   311  
   312  	var filesMap map[string][]string
   313  	err := slot.Attr("files", &filesMap)
   314  	if err != nil && !errors.Is(err, snap.AttributeNotFoundError{}) {
   315  		return err
   316  	}
   317  	for key, val := range filesMap {
   318  		perm := ""
   319  		switch key {
   320  		case "read":
   321  			perm = "r"
   322  		case "write":
   323  			perm = "rw"
   324  		default:
   325  			return fmt.Errorf(`cannot specify %q in "files" section, only "read" and "write" allowed`, key)
   326  		}
   327  
   328  		emitRule(val, perm)
   329  	}
   330  
   331  	spec.AddSnippet(snippet.String())
   332  	return nil
   333  }
   334  
   335  // extractStringMapAttribute looks up the given key in the container, and
   336  // returns its value as a map[string]string.
   337  // No validation is performed, since it already occurred before connecting the
   338  // interface.
   339  func (iface *customDeviceInterface) extractStringMapAttribute(container map[string]interface{}, key string) map[string]string {
   340  	valueMap, ok := container[key].(map[string]interface{})
   341  	if !ok {
   342  		return nil
   343  	}
   344  
   345  	stringMap := make(map[string]string, len(valueMap))
   346  	for key, value := range valueMap {
   347  		stringMap[key] = value.(string)
   348  	}
   349  
   350  	return stringMap
   351  }
   352  
   353  func (iface *customDeviceInterface) UDevConnectedPlug(spec *udev.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
   354  	// Collect all the device paths specified in either the "devices" or
   355  	// "read-devices" attributes.
   356  	var devicePaths []string
   357  	_ = slot.Attr("devices", &devicePaths)
   358  	var readDevicePaths []string
   359  	_ = slot.Attr("read-devices", &readDevicePaths)
   360  	allDevicePaths := devicePaths
   361  	allDevicePaths = append(allDevicePaths, readDevicePaths...)
   362  
   363  	// Generate a basic udev rule for each device; we put them into a map
   364  	// indexed by the device name, so that we can overwrite the entry later
   365  	// with a more specific rule.
   366  	deviceRules := make(map[string]string, len(allDevicePaths))
   367  	for _, devicePath := range allDevicePaths {
   368  		if strings.HasPrefix(devicePath, "/dev/") {
   369  			deviceName := devicePath[5:]
   370  			deviceRules[deviceName] = fmt.Sprintf(`KERNEL=="%s"`, deviceName)
   371  		}
   372  	}
   373  
   374  	// Generate udev rules from the "udev-tagging" attribute; note that these
   375  	// rules might override the simpler KERNEL=="<device>" rules we computed
   376  	// above -- that's fine.
   377  	var udevTaggingRules []map[string]interface{}
   378  	_ = slot.Attr("udev-tagging", &udevTaggingRules)
   379  	for _, udevTaggingRule := range udevTaggingRules {
   380  		rule := &bytes.Buffer{}
   381  
   382  		deviceName, ok := udevTaggingRule["kernel"].(string)
   383  		if !ok {
   384  			return customDeviceInternalError
   385  		}
   386  
   387  		fmt.Fprintf(rule, `KERNEL=="%s"`, deviceName)
   388  
   389  		if subsystem, ok := udevTaggingRule["subsystem"].(string); ok {
   390  			fmt.Fprintf(rule, `, SUBSYSTEM=="%s"`, subsystem)
   391  		}
   392  
   393  		environment := iface.extractStringMapAttribute(udevTaggingRule, "environment")
   394  		for variable, value := range environment {
   395  			fmt.Fprintf(rule, `, ENV{%s}=="%s"`, variable, value)
   396  		}
   397  
   398  		attributes := iface.extractStringMapAttribute(udevTaggingRule, "attributes")
   399  		for variable, value := range attributes {
   400  			fmt.Fprintf(rule, `, ATTR{%s}=="%s"`, variable, value)
   401  		}
   402  
   403  		deviceRules[deviceName] = rule.String()
   404  	}
   405  
   406  	// Now write all the rules
   407  	for _, rule := range deviceRules {
   408  		spec.TagDevice(rule)
   409  	}
   410  
   411  	return nil
   412  }
   413  
   414  func (iface *customDeviceInterface) AutoConnect(plug *snap.PlugInfo, slot *snap.SlotInfo) bool {
   415  	// allow what declarations allowed
   416  	return true
   417  }
   418  
   419  func init() {
   420  	registerIface(&customDeviceInterface{})
   421  }