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

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2018 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 udev implements integration between snapd, udev and
    21  // snap-confine around tagging character and block devices so that they
    22  // can be accessed by applications.
    23  //
    24  // TODO: Document this better
    25  package udev
    26  
    27  import (
    28  	"bytes"
    29  	"fmt"
    30  	"os"
    31  	"path/filepath"
    32  	"strings"
    33  
    34  	"github.com/snapcore/snapd/dirs"
    35  	"github.com/snapcore/snapd/interfaces"
    36  	"github.com/snapcore/snapd/osutil"
    37  	"github.com/snapcore/snapd/sandbox/cgroup"
    38  	"github.com/snapcore/snapd/snap"
    39  	"github.com/snapcore/snapd/timings"
    40  )
    41  
    42  // Backend is responsible for maintaining udev rules.
    43  type Backend struct {
    44  	preseed bool
    45  }
    46  
    47  // Initialize does nothing.
    48  func (b *Backend) Initialize(opts *interfaces.SecurityBackendOptions) error {
    49  	if opts != nil && opts.Preseed {
    50  		b.preseed = true
    51  	}
    52  	return nil
    53  }
    54  
    55  // Name returns the name of the backend.
    56  func (b *Backend) Name() interfaces.SecuritySystem {
    57  	return interfaces.SecurityUDev
    58  }
    59  
    60  // snapRulesFileName returns the path of the snap udev rules file.
    61  func snapRulesFilePath(snapName string) string {
    62  	rulesFileName := fmt.Sprintf("70-%s.rules", snap.SecurityTag(snapName))
    63  	return filepath.Join(dirs.SnapUdevRulesDir, rulesFileName)
    64  }
    65  
    66  // Setup creates udev rules specific to a given snap.
    67  // If any of the rules are changed or removed then udev database is reloaded.
    68  //
    69  // UDev has no concept of a complain mode so confinement options are ignored.
    70  //
    71  // If the method fails it should be re-tried (with a sensible strategy) by the caller.
    72  func (b *Backend) Setup(snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository, tm timings.Measurer) error {
    73  	snapName := snapInfo.InstanceName()
    74  	spec, err := repo.SnapSpecification(b.Name(), snapName)
    75  	if err != nil {
    76  		return fmt.Errorf("cannot obtain udev specification for snap %q: %s", snapName, err)
    77  	}
    78  	content := b.deriveContent(spec.(*Specification), snapInfo)
    79  	subsystemTriggers := spec.(*Specification).TriggeredSubsystems()
    80  
    81  	dir := dirs.SnapUdevRulesDir
    82  	if err := os.MkdirAll(dir, 0755); err != nil {
    83  		return fmt.Errorf("cannot create directory for udev rules %q: %s", dir, err)
    84  	}
    85  
    86  	rulesFilePath := snapRulesFilePath(snapInfo.InstanceName())
    87  
    88  	if len(content) == 0 {
    89  		// Make sure that the rules file gets removed when we don't have any
    90  		// content and exists.
    91  		err = os.Remove(rulesFilePath)
    92  		if err != nil && !os.IsNotExist(err) {
    93  			return err
    94  		} else if err == nil {
    95  			// FIXME: somehow detect the interfaces that were
    96  			// disconnected and set subsystemTriggers appropriately.
    97  			// ATM, it is always going to be empty on disconnect.
    98  			return b.reloadRules(subsystemTriggers)
    99  		}
   100  		return nil
   101  	}
   102  
   103  	var buffer bytes.Buffer
   104  	buffer.WriteString("# This file is automatically generated.\n")
   105  	if (opts.DevMode || opts.Classic) && !opts.JailMode {
   106  		buffer.WriteString("# udev tagging/device cgroups disabled with non-strict mode snaps\n")
   107  	}
   108  	for _, snippet := range content {
   109  		if (opts.DevMode || opts.Classic) && !opts.JailMode {
   110  			buffer.WriteRune('#')
   111  			snippet = strings.Replace(snippet, "\n", "\n#", -1)
   112  		}
   113  		buffer.WriteString(snippet)
   114  		buffer.WriteByte('\n')
   115  	}
   116  
   117  	rulesFileState := &osutil.MemoryFileState{
   118  		Content: buffer.Bytes(),
   119  		Mode:    0644,
   120  	}
   121  
   122  	// EnsureFileState will make sure the file will be only updated when its content
   123  	// has changed and will otherwise return an error which prevents us from reloading
   124  	// udev rules when not needed.
   125  	err = osutil.EnsureFileState(rulesFilePath, rulesFileState)
   126  	if err == osutil.ErrSameState {
   127  		return nil
   128  	} else if err != nil {
   129  		return err
   130  	}
   131  
   132  	// FIXME: somehow detect the interfaces that were disconnected and set
   133  	// subsystemTriggers appropriately. ATM, it is always going to be empty
   134  	// on disconnect.
   135  	return b.reloadRules(subsystemTriggers)
   136  }
   137  
   138  // Remove removes udev rules specific to a given snap.
   139  // If any of the rules are removed then udev database is reloaded.
   140  //
   141  // This method should be called after removing a snap.
   142  //
   143  // If the method fails it should be re-tried (with a sensible strategy) by the caller.
   144  func (b *Backend) Remove(snapName string) error {
   145  	rulesFilePath := snapRulesFilePath(snapName)
   146  	err := os.Remove(rulesFilePath)
   147  	if os.IsNotExist(err) {
   148  		// If file doesn't exist we avoid reloading the udev rules when we return here
   149  		return nil
   150  	} else if err != nil {
   151  		return err
   152  	}
   153  
   154  	// FIXME: somehow detect the interfaces that were disconnected and set
   155  	// subsystemTriggers appropriately. ATM, it is always going to be empty
   156  	// on disconnect.
   157  	return b.reloadRules(nil)
   158  }
   159  
   160  func (b *Backend) deriveContent(spec *Specification, snapInfo *snap.Info) (content []string) {
   161  	content = append(content, spec.Snippets()...)
   162  	return content
   163  }
   164  
   165  func (b *Backend) NewSpecification() interfaces.Specification {
   166  	return &Specification{}
   167  }
   168  
   169  // SandboxFeatures returns the list of features supported by snapd for mediating access to kernel devices.
   170  func (b *Backend) SandboxFeatures() []string {
   171  	commonFeatures := []string{
   172  		"tagging", /* Tagging dynamically associates new devices with specific snaps */
   173  	}
   174  	cgroupv1Features := []string{
   175  		"device-filtering", /* Snapd can limit device access for each snap */
   176  		"device-cgroup-v1", /* Snapd creates a device group (v1) for each snap */
   177  	}
   178  
   179  	if cgroup.IsUnified() {
   180  		// TODO: update v2 device cgroup is supported
   181  		return commonFeatures
   182  	}
   183  
   184  	features := append(cgroupv1Features, commonFeatures...)
   185  	return features
   186  }