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

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2018-2019 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 interfaces
    21  
    22  import (
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"io/ioutil"
    28  	"os"
    29  	"path/filepath"
    30  	"reflect"
    31  	"strconv"
    32  	"strings"
    33  
    34  	"github.com/snapcore/snapd/dirs"
    35  	"github.com/snapcore/snapd/logger"
    36  	"github.com/snapcore/snapd/osutil"
    37  	"github.com/snapcore/snapd/sandbox/apparmor"
    38  	"github.com/snapcore/snapd/sandbox/cgroup"
    39  	"github.com/snapcore/snapd/sandbox/seccomp"
    40  	"github.com/snapcore/snapd/snapdtool"
    41  )
    42  
    43  // ErrSystemKeyIncomparableVersions indicates that the system-key
    44  // on disk and the system-key calculated from generateSystemKey
    45  // have different inputs and are therefore incomparable.
    46  //
    47  // This means:
    48  // - "snapd" needs to re-generate security profiles
    49  // - "snap run" cannot wait for those security profiles
    50  var (
    51  	ErrSystemKeyVersion = errors.New("system-key versions not comparable")
    52  	ErrSystemKeyMissing = errors.New("system-key missing on disk")
    53  )
    54  
    55  // systemKey describes the environment for which security profiles
    56  // have been generated. It is useful to compare if the current
    57  // running system is similar enough to the generated profiles or
    58  // if the profiles need to be re-generated to match the new system.
    59  //
    60  // Note that this key gets generated on *each* `snap run` - so it
    61  // *must* be cheap to calculate it (no hashes of big binaries etc).
    62  type systemKey struct {
    63  	// IMPORTANT: when adding/removing/changing inputs bump this version (see below)
    64  	Version int `json:"version"`
    65  
    66  	// This is the build-id of the snapd that generated the profiles.
    67  	BuildID string `json:"build-id"`
    68  
    69  	// These inputs come from the host environment via e.g.
    70  	// kernel version or similar settings. If those change we may
    71  	// need to change the generated profiles (e.g. when the user
    72  	// boots into a more featureful seccomp).
    73  	AppArmorFeatures       []string `json:"apparmor-features"`
    74  	AppArmorParserMtime    int64    `json:"apparmor-parser-mtime"`
    75  	AppArmorParserFeatures []string `json:"apparmor-parser-features"`
    76  	NFSHome                bool     `json:"nfs-home"`
    77  	OverlayRoot            string   `json:"overlay-root"`
    78  	SecCompActions         []string `json:"seccomp-features"`
    79  	SeccompCompilerVersion string   `json:"seccomp-compiler-version"`
    80  	CgroupVersion          string   `json:"cgroup-version"`
    81  }
    82  
    83  // IMPORTANT: when adding/removing/changing inputs bump this
    84  const systemKeyVersion = 10
    85  
    86  var (
    87  	isHomeUsingNFS        = osutil.IsHomeUsingNFS
    88  	isRootWritableOverlay = osutil.IsRootWritableOverlay
    89  	mockedSystemKey       *systemKey
    90  
    91  	readBuildID = osutil.ReadBuildID
    92  )
    93  
    94  func seccompCompilerVersionInfo(path string) (seccomp.VersionInfo, error) {
    95  	return seccomp.CompilerVersionInfo(func(name string) (string, error) { return filepath.Join(path, name), nil })
    96  }
    97  
    98  func generateSystemKey() (*systemKey, error) {
    99  	// for testing only
   100  	if mockedSystemKey != nil {
   101  		return mockedSystemKey, nil
   102  	}
   103  
   104  	sk := &systemKey{
   105  		Version: systemKeyVersion,
   106  	}
   107  	snapdPath, err := snapdtool.InternalToolPath("snapd")
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	buildID, err := readBuildID(snapdPath)
   112  	if err != nil && !os.IsNotExist(err) {
   113  		return nil, err
   114  	}
   115  	sk.BuildID = buildID
   116  
   117  	// Add apparmor-features (which is already sorted)
   118  	sk.AppArmorFeatures, _ = apparmor.KernelFeatures()
   119  
   120  	// Add apparmor-parser-mtime
   121  	sk.AppArmorParserMtime = apparmor.ParserMtime()
   122  
   123  	// Add if home is using NFS, if so we need to have a different
   124  	// security profile and if this changes we need to change our
   125  	// profile.
   126  	sk.NFSHome, err = isHomeUsingNFS()
   127  	if err != nil {
   128  		// just log the error here
   129  		logger.Noticef("cannot determine nfs usage in generateSystemKey: %v", err)
   130  		return nil, err
   131  	}
   132  
   133  	// Add if '/' is on overlayfs so we can add AppArmor rules for
   134  	// upperdir such that if this changes, we change our profile.
   135  	sk.OverlayRoot, err = isRootWritableOverlay()
   136  	if err != nil {
   137  		// just log the error here
   138  		logger.Noticef("cannot determine root filesystem on overlay in generateSystemKey: %v", err)
   139  		return nil, err
   140  	}
   141  
   142  	// Add seccomp-features
   143  	sk.SecCompActions = seccomp.Actions()
   144  
   145  	versionInfo, err := seccompCompilerVersionInfo(filepath.Dir(snapdPath))
   146  	if err != nil {
   147  		logger.Noticef("cannot determine seccomp compiler version in generateSystemKey: %v", err)
   148  		return nil, err
   149  	}
   150  	sk.SeccompCompilerVersion = string(versionInfo)
   151  
   152  	cgv, err := cgroup.Version()
   153  	if err != nil {
   154  		logger.Noticef("cannot determine cgroup version: %v", err)
   155  		return nil, err
   156  	}
   157  	sk.CgroupVersion = strconv.FormatInt(int64(cgv), 10)
   158  
   159  	return sk, nil
   160  }
   161  
   162  // UnmarshalJSONSystemKey unmarshalls the data from the reader as JSON into a
   163  // system key usable with SystemKeysMatch.
   164  func UnmarshalJSONSystemKey(r io.Reader) (interface{}, error) {
   165  	sk := &systemKey{}
   166  	err := json.NewDecoder(r).Decode(sk)
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  	return sk, nil
   171  }
   172  
   173  // WriteSystemKey will write the current system-key to disk
   174  func WriteSystemKey() error {
   175  	sk, err := generateSystemKey()
   176  	if err != nil {
   177  		return err
   178  	}
   179  
   180  	// only fix AppArmorParserFeatures if we didn't already mock a system-key
   181  	// if we mocked a system-key we are running a test and don't want to use
   182  	// the real host system's parser features
   183  	if mockedSystemKey == nil {
   184  		// We only want to calculate this when the mtime of the parser changes.
   185  		// Since we calculate the mtime() as part of generateSystemKey, we can
   186  		// simply unconditionally write this out here.
   187  		sk.AppArmorParserFeatures, _ = apparmor.ParserFeatures()
   188  	}
   189  
   190  	sks, err := json.Marshal(sk)
   191  	if err != nil {
   192  		return err
   193  	}
   194  	return osutil.AtomicWriteFile(dirs.SnapSystemKeyFile, sks, 0644, 0)
   195  }
   196  
   197  // SystemKeyMismatch checks if the running binary expects a different
   198  // system-key than what is on disk.
   199  //
   200  // This is used in two places:
   201  // - snap run: when there is a mismatch it will wait for snapd
   202  //             to re-generate the security profiles
   203  // - snapd: on startup it checks if the system-key has changed and
   204  //          if so re-generate the security profiles
   205  //
   206  // This ensures that "snap run" and "snapd" have a consistent set
   207  // of security profiles. Without it we may have the following
   208  // scenario:
   209  // 1. snapd gets refreshed and snaps need updated security profiles
   210  //    to work (e.g. because snap-exec needs a new permission)
   211  // 2. The system reboots to start the new snapd. At this point
   212  //    the old security profiles are on disk (because the new
   213  //    snapd did not run yet)
   214  // 3. Snaps that run as daemon get started during boot by systemd
   215  //    (e.g. network-manager). This may happen before snapd had a
   216  //    chance to refresh the security profiles.
   217  // 4. Because the security profiles are for the old version of
   218  //    the snaps that run before snapd fail to start. For e.g.
   219  //    network-manager this is of course catastrophic.
   220  // To prevent this, in step(4) we have this wait-for-snapd
   221  // step to ensure the expected profiles are on disk.
   222  //
   223  // The apparmor-parser-features system-key is handled specially
   224  // and not included in this comparison because it is written out
   225  // to disk whenever apparmor-parser-mtime changes (in this manner
   226  // snap run only has to obtain the mtime of apparmor_parser and
   227  // doesn't have to invoke it)
   228  func SystemKeyMismatch() (bool, error) {
   229  	mySystemKey, err := generateSystemKey()
   230  	if err != nil {
   231  		return false, err
   232  	}
   233  
   234  	diskSystemKey, err := readSystemKey()
   235  	if err != nil {
   236  		return false, err
   237  	}
   238  
   239  	// deal with the race that "snap run" may start, then snapd
   240  	// is upgraded and generates a new system-key with different
   241  	// inputs than the "snap run" in memory. In this case we
   242  	// should be fine because new security profiles will also
   243  	// have been written to disk.
   244  	if mySystemKey.Version != diskSystemKey.Version {
   245  		return false, ErrSystemKeyVersion
   246  	}
   247  
   248  	// special case to detect local runs
   249  	if mockedSystemKey == nil {
   250  		if exe, err := os.Readlink("/proc/self/exe"); err == nil {
   251  			// detect running local local builds
   252  			if !strings.HasPrefix(exe, "/usr") && !strings.HasPrefix(exe, "/snap") {
   253  				logger.Noticef("running from non-installed location %s: ignoring system-key", exe)
   254  				return false, ErrSystemKeyVersion
   255  			}
   256  		}
   257  	}
   258  
   259  	// since we always write out apparmor-parser-feature when
   260  	// apparmor-parser-mtime changes, we don't need to compare it here
   261  	// (allowing snap run to only need to check the mtime of the parser)
   262  	// so just set both to nil to make the DeepEqual happy
   263  	diskSystemKey.AppArmorParserFeatures = nil
   264  	mySystemKey.AppArmorParserFeatures = nil
   265  
   266  	ok, err := SystemKeysMatch(mySystemKey, diskSystemKey)
   267  	return !ok, err
   268  }
   269  
   270  func readSystemKey() (*systemKey, error) {
   271  	raw, err := ioutil.ReadFile(dirs.SnapSystemKeyFile)
   272  	if err != nil && os.IsNotExist(err) {
   273  		return nil, ErrSystemKeyMissing
   274  	}
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  	var diskSystemKey systemKey
   279  	if err := json.Unmarshal(raw, &diskSystemKey); err != nil {
   280  		return nil, err
   281  	}
   282  	return &diskSystemKey, nil
   283  }
   284  
   285  // RecordedSystemKey returns the system key read from the disk as opaque interface{}.
   286  func RecordedSystemKey() (interface{}, error) {
   287  	diskSystemKey, err := readSystemKey()
   288  	if err != nil {
   289  		return nil, err
   290  	}
   291  	return diskSystemKey, nil
   292  }
   293  
   294  // CurrentSystemKey calculates and returns the current system key as opaque interface{}.
   295  func CurrentSystemKey() (interface{}, error) {
   296  	currentSystemKey, err := generateSystemKey()
   297  	return currentSystemKey, err
   298  }
   299  
   300  // SystemKeysMatch returns whether the given system keys match.
   301  func SystemKeysMatch(systemKey1, systemKey2 interface{}) (bool, error) {
   302  	// sanity check
   303  	_, ok1 := systemKey1.(*systemKey)
   304  	_, ok2 := systemKey2.(*systemKey)
   305  	if !(ok1 && ok2) {
   306  		return false, fmt.Errorf("SystemKeysMatch: arguments are not system keys")
   307  	}
   308  
   309  	// TODO: write custom struct compare
   310  	return reflect.DeepEqual(systemKey1, systemKey2), nil
   311  }
   312  
   313  func MockSystemKey(s string) func() {
   314  	var sk systemKey
   315  	err := json.Unmarshal([]byte(s), &sk)
   316  	if err != nil {
   317  		panic(err)
   318  	}
   319  	mockedSystemKey = &sk
   320  	return func() { mockedSystemKey = nil }
   321  }