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

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-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 apparmor
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"os"
    27  	"os/exec"
    28  	"path/filepath"
    29  	"sort"
    30  	"strings"
    31  	"sync"
    32  
    33  	"github.com/snapcore/snapd/dirs"
    34  	"github.com/snapcore/snapd/osutil"
    35  	"github.com/snapcore/snapd/strutil"
    36  )
    37  
    38  // LevelType encodes the kind of support for apparmor
    39  // found on this system.
    40  type LevelType int
    41  
    42  const (
    43  	// Unknown indicates that apparmor was not probed yet.
    44  	Unknown LevelType = iota
    45  	// Unsupported indicates that apparmor is not enabled.
    46  	Unsupported
    47  	// Unusable indicates that apparmor is enabled but cannot be used.
    48  	Unusable
    49  	// Partial indicates that apparmor is enabled but some
    50  	// features are missing.
    51  	Partial
    52  	// Full indicates that all features are supported.
    53  	Full
    54  )
    55  
    56  func setupConfCacheDirs(newrootdir string) {
    57  	ConfDir = filepath.Join(newrootdir, "/etc/apparmor.d")
    58  	CacheDir = filepath.Join(newrootdir, "/var/cache/apparmor")
    59  
    60  	SystemCacheDir = filepath.Join(ConfDir, "cache")
    61  	exists, isDir, _ := osutil.DirExists(SystemCacheDir)
    62  	if !exists || !isDir {
    63  		// some systems use a single cache dir instead of splitting
    64  		// out the system cache
    65  		// TODO: it seems Solus has a different setup too, investigate this
    66  		SystemCacheDir = CacheDir
    67  	}
    68  }
    69  
    70  func init() {
    71  	dirs.AddRootDirCallback(setupConfCacheDirs)
    72  	setupConfCacheDirs(dirs.GlobalRootDir)
    73  }
    74  
    75  var (
    76  	ConfDir        string
    77  	CacheDir       string
    78  	SystemCacheDir string
    79  )
    80  
    81  func (level LevelType) String() string {
    82  	switch level {
    83  	case Unknown:
    84  		return "unknown"
    85  	case Unsupported:
    86  		return "none"
    87  	case Unusable:
    88  		return "unusable"
    89  	case Partial:
    90  		return "partial"
    91  	case Full:
    92  		return "full"
    93  	}
    94  	return fmt.Sprintf("AppArmorLevelType:%d", level)
    95  }
    96  
    97  // appArmorAssessment represents what is supported AppArmor-wise by the system.
    98  var appArmorAssessment = &appArmorAssess{appArmorProber: &appArmorProbe{}}
    99  
   100  // ProbedLevel quantifies how well apparmor is supported on the current
   101  // kernel. The computation is costly to perform. The result is cached internally.
   102  func ProbedLevel() LevelType {
   103  	appArmorAssessment.assess()
   104  	return appArmorAssessment.level
   105  }
   106  
   107  // Summary describes how well apparmor is supported on the current
   108  // kernel. The computation is costly to perform. The result is cached
   109  // internally.
   110  func Summary() string {
   111  	appArmorAssessment.assess()
   112  	return appArmorAssessment.summary
   113  }
   114  
   115  // KernelFeatures returns a sorted list of apparmor features like
   116  // []string{"dbus", "network"}. The result is cached internally.
   117  func KernelFeatures() ([]string, error) {
   118  	return appArmorAssessment.KernelFeatures()
   119  }
   120  
   121  // ParserFeatures returns a sorted list of apparmor parser features
   122  // like []string{"unsafe", ...}. The computation is costly to perform. The
   123  // result is cached internally.
   124  func ParserFeatures() ([]string, error) {
   125  	return appArmorAssessment.ParserFeatures()
   126  }
   127  
   128  // ParserMtime returns the mtime of the AppArmor parser, else 0.
   129  func ParserMtime() int64 {
   130  	var mtime int64
   131  	mtime = 0
   132  
   133  	if path, err := findAppArmorParser(); err == nil {
   134  		if fi, err := os.Stat(path); err == nil {
   135  			mtime = fi.ModTime().Unix()
   136  		}
   137  	}
   138  	return mtime
   139  }
   140  
   141  // probe related code
   142  
   143  var (
   144  	// requiredParserFeatures denotes the features that must be present in the parser.
   145  	// Absence of any of those features results in the effective level be at most UnusableAppArmor.
   146  	requiredParserFeatures = []string{
   147  		"unsafe",
   148  	}
   149  	// preferredParserFeatures denotes the features that should be present in the parser.
   150  	// Absence of any of those features results in the effective level be at most PartialAppArmor.
   151  	preferredParserFeatures = []string{
   152  		"unsafe",
   153  	}
   154  	// requiredKernelFeatures denotes the features that must be present in the kernel.
   155  	// Absence of any of those features results in the effective level be at most UnusableAppArmor.
   156  	requiredKernelFeatures = []string{
   157  		// For now, require at least file and simply prefer the rest.
   158  		"file",
   159  	}
   160  	// preferredKernelFeatures denotes the features that should be present in the kernel.
   161  	// Absence of any of those features results in the effective level be at most PartialAppArmor.
   162  	preferredKernelFeatures = []string{
   163  		"caps",
   164  		"dbus",
   165  		"domain",
   166  		"file",
   167  		"mount",
   168  		"namespaces",
   169  		"network",
   170  		"ptrace",
   171  		"signal",
   172  	}
   173  	// Since AppArmorParserMtime() will be called by generateKey() in
   174  	// system-key and that could be called by different users on the
   175  	// system, use a predictable search path for finding the parser.
   176  	parserSearchPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
   177  
   178  	// Filesystem root defined locally to avoid dependency on the
   179  	// 'dirs' package
   180  	rootPath = "/"
   181  )
   182  
   183  // Each apparmor feature is manifested as a directory entry.
   184  const featuresSysPath = "sys/kernel/security/apparmor/features"
   185  
   186  type appArmorProber interface {
   187  	KernelFeatures() ([]string, error)
   188  	ParserFeatures() ([]string, error)
   189  }
   190  
   191  type appArmorAssess struct {
   192  	appArmorProber
   193  	// level contains the assessment of the "level" of apparmor support.
   194  	level LevelType
   195  	// summary contains a human readable description of the assessment.
   196  	summary string
   197  
   198  	once sync.Once
   199  }
   200  
   201  func (aaa *appArmorAssess) assess() {
   202  	aaa.once.Do(func() {
   203  		aaa.level, aaa.summary = aaa.doAssess()
   204  	})
   205  }
   206  
   207  func (aaa *appArmorAssess) doAssess() (level LevelType, summary string) {
   208  	// First, quickly check if apparmor is available in the kernel at all.
   209  	kernelFeatures, err := aaa.KernelFeatures()
   210  	if os.IsNotExist(err) {
   211  		return Unsupported, "apparmor not enabled"
   212  	}
   213  	// Then check that the parser supports the required parser features.
   214  	// If we have any missing required features then apparmor is unusable.
   215  	parserFeatures, err := aaa.ParserFeatures()
   216  	if os.IsNotExist(err) {
   217  		return Unsupported, "apparmor_parser not found"
   218  	}
   219  	var missingParserFeatures []string
   220  	for _, feature := range requiredParserFeatures {
   221  		if !strutil.SortedListContains(parserFeatures, feature) {
   222  			missingParserFeatures = append(missingParserFeatures, feature)
   223  		}
   224  	}
   225  	if len(missingParserFeatures) > 0 {
   226  		summary := fmt.Sprintf("apparmor_parser is available but required parser features are missing: %s",
   227  			strings.Join(missingParserFeatures, ", "))
   228  		return Unusable, summary
   229  	}
   230  
   231  	// Next, check that the kernel supports the required kernel features.
   232  	var missingKernelFeatures []string
   233  	for _, feature := range requiredKernelFeatures {
   234  		if !strutil.SortedListContains(kernelFeatures, feature) {
   235  			missingKernelFeatures = append(missingKernelFeatures, feature)
   236  		}
   237  	}
   238  	if len(missingKernelFeatures) > 0 {
   239  		summary := fmt.Sprintf("apparmor is enabled but required kernel features are missing: %s",
   240  			strings.Join(missingKernelFeatures, ", "))
   241  		return Unusable, summary
   242  	}
   243  
   244  	// Next check that the parser supports preferred parser features.
   245  	// If we have any missing preferred features then apparmor is partially enabled.
   246  	for _, feature := range preferredParserFeatures {
   247  		if !strutil.SortedListContains(parserFeatures, feature) {
   248  			missingParserFeatures = append(missingParserFeatures, feature)
   249  		}
   250  	}
   251  	if len(missingParserFeatures) > 0 {
   252  		summary := fmt.Sprintf("apparmor_parser is available but some features are missing: %s",
   253  			strings.Join(missingParserFeatures, ", "))
   254  		return Partial, summary
   255  	}
   256  
   257  	// Lastly check that the kernel supports preferred kernel features.
   258  	for _, feature := range preferredKernelFeatures {
   259  		if !strutil.SortedListContains(kernelFeatures, feature) {
   260  			missingKernelFeatures = append(missingKernelFeatures, feature)
   261  		}
   262  	}
   263  	if len(missingKernelFeatures) > 0 {
   264  		summary := fmt.Sprintf("apparmor is enabled but some kernel features are missing: %s",
   265  			strings.Join(missingKernelFeatures, ", "))
   266  		return Partial, summary
   267  	}
   268  
   269  	// If we got here then all features are available and supported.
   270  	return Full, "apparmor is enabled and all features are available"
   271  }
   272  
   273  type appArmorProbe struct {
   274  	// kernelFeatures contains a list of kernel features that are supported.
   275  	kernelFeatures []string
   276  	// kernelError contains an error, if any, encountered when
   277  	// discovering available kernel features.
   278  	kernelError error
   279  	// parserFeatures contains a list of parser features that are supported.
   280  	parserFeatures []string
   281  	// parserError contains an error, if any, encountered when
   282  	// discovering available parser features.
   283  	parserError error
   284  
   285  	probeKernelOnce sync.Once
   286  	probeParserOnce sync.Once
   287  }
   288  
   289  func (aap *appArmorProbe) KernelFeatures() ([]string, error) {
   290  	aap.probeKernelOnce.Do(func() {
   291  		aap.kernelFeatures, aap.kernelError = probeKernelFeatures()
   292  	})
   293  	return aap.kernelFeatures, aap.kernelError
   294  }
   295  
   296  func (aap *appArmorProbe) ParserFeatures() ([]string, error) {
   297  	aap.probeParserOnce.Do(func() {
   298  		aap.parserFeatures, aap.parserError = probeParserFeatures()
   299  	})
   300  	return aap.parserFeatures, aap.parserError
   301  }
   302  
   303  func probeKernelFeatures() ([]string, error) {
   304  	// note that ioutil.ReadDir() is already sorted
   305  	dentries, err := ioutil.ReadDir(filepath.Join(rootPath, featuresSysPath))
   306  	if err != nil {
   307  		return []string{}, err
   308  	}
   309  	features := make([]string, 0, len(dentries))
   310  	for _, fi := range dentries {
   311  		if fi.IsDir() {
   312  			features = append(features, fi.Name())
   313  		}
   314  	}
   315  	return features, nil
   316  }
   317  
   318  func probeParserFeatures() ([]string, error) {
   319  	parser, err := findAppArmorParser()
   320  	if err != nil {
   321  		return []string{}, err
   322  	}
   323  	features := make([]string, 0, 1)
   324  	if tryAppArmorParserFeature(parser, "change_profile unsafe /**,") {
   325  		features = append(features, "unsafe")
   326  	}
   327  	sort.Strings(features)
   328  	return features, nil
   329  }
   330  
   331  // findAppArmorParser returns the path of the apparmor_parser binary if one is found.
   332  func findAppArmorParser() (string, error) {
   333  	for _, dir := range filepath.SplitList(parserSearchPath) {
   334  		path := filepath.Join(dir, "apparmor_parser")
   335  		if _, err := os.Stat(path); err == nil {
   336  			return path, nil
   337  		}
   338  	}
   339  	return "", os.ErrNotExist
   340  }
   341  
   342  // tryAppArmorParserFeature attempts to pre-process a bit of apparmor syntax with a given parser.
   343  func tryAppArmorParserFeature(parser, rule string) bool {
   344  	cmd := exec.Command(parser, "--preprocess")
   345  	cmd.Stdin = bytes.NewBufferString(fmt.Sprintf("profile snap-test {\n %s\n}", rule))
   346  	if err := cmd.Run(); err != nil {
   347  		return false
   348  	}
   349  	return true
   350  }
   351  
   352  // mocking
   353  
   354  type mockAppArmorProbe struct {
   355  	kernelFeatures []string
   356  	kernelError    error
   357  	parserFeatures []string
   358  	parserError    error
   359  }
   360  
   361  func (m *mockAppArmorProbe) KernelFeatures() ([]string, error) {
   362  	return m.kernelFeatures, m.kernelError
   363  }
   364  
   365  func (m *mockAppArmorProbe) ParserFeatures() ([]string, error) {
   366  	return m.parserFeatures, m.parserError
   367  }
   368  
   369  // MockAppArmorLevel makes the system believe it has certain level of apparmor
   370  // support.
   371  //
   372  // AppArmor kernel and parser features are set to unrealistic values that do
   373  // not match the requested level. Use this function to observe behavior that
   374  // relies solely on the apparmor level value.
   375  func MockLevel(level LevelType) (restore func()) {
   376  	oldAppArmorAssessment := appArmorAssessment
   377  	mockProbe := &mockAppArmorProbe{
   378  		kernelFeatures: []string{"mocked-kernel-feature"},
   379  		parserFeatures: []string{"mocked-parser-feature"},
   380  	}
   381  	appArmorAssessment = &appArmorAssess{
   382  		appArmorProber: mockProbe,
   383  		level:          level,
   384  		summary:        fmt.Sprintf("mocked apparmor level: %s", level),
   385  	}
   386  	appArmorAssessment.once.Do(func() {})
   387  	return func() {
   388  		appArmorAssessment = oldAppArmorAssessment
   389  	}
   390  }
   391  
   392  // MockAppArmorFeatures makes the system believe it has certain kernel and
   393  // parser features.
   394  //
   395  // AppArmor level and summary are automatically re-assessed as needed
   396  // on both the change and the restore process. Use this function to
   397  // observe real assessment of arbitrary features.
   398  func MockFeatures(kernelFeatures []string, kernelError error, parserFeatures []string, parserError error) (restore func()) {
   399  	oldAppArmorAssessment := appArmorAssessment
   400  	mockProbe := &mockAppArmorProbe{
   401  		kernelFeatures: kernelFeatures,
   402  		kernelError:    kernelError,
   403  		parserFeatures: parserFeatures,
   404  		parserError:    parserError,
   405  	}
   406  	appArmorAssessment = &appArmorAssess{
   407  		appArmorProber: mockProbe,
   408  	}
   409  	appArmorAssessment.assess()
   410  	return func() {
   411  		appArmorAssessment = oldAppArmorAssessment
   412  	}
   413  
   414  }