gitee.com/mysnapcore/mysnapd@v0.1.0/sandbox/apparmor/profile.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-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 apparmor
    21  
    22  import (
    23  	"fmt"
    24  	"io"
    25  	"os"
    26  	"os/exec"
    27  	"path"
    28  	"path/filepath"
    29  	"runtime"
    30  	"strings"
    31  
    32  	"gitee.com/mysnapcore/mysnapd/dirs"
    33  	"gitee.com/mysnapcore/mysnapd/osutil"
    34  )
    35  
    36  type AaParserFlags int
    37  
    38  const (
    39  	// SkipReadCache causes apparmor_parser to be invoked with --skip-read-cache.
    40  	// This allows us to essentially overwrite a cache that we know is stale regardless
    41  	// of the time and date settings (apparmor_parser caching is based on mtime).
    42  	// Note that writing of the cache relies on --write-cache but we pass that
    43  	// command-line option unconditionally.
    44  	SkipReadCache AaParserFlags = 1 << iota
    45  
    46  	// ConserveCPU tells apparmor_parser to spare up to two CPUs on multi-core systems to
    47  	// reduce load when processing many profiles at once.
    48  	ConserveCPU AaParserFlags = 1 << iota
    49  
    50  	// SkipKernelLoad tells apparmor_parser not to load profiles into the kernel. The use
    51  	// case of this is when in pre-seeding mode.
    52  	SkipKernelLoad AaParserFlags = 1 << iota
    53  )
    54  
    55  var runtimeNumCPU = runtime.NumCPU
    56  
    57  func numberOfJobsParam() string {
    58  	cpus := runtimeNumCPU()
    59  	// Do not use all CPUs as this may have negative impact when booting.
    60  	if cpus > 2 {
    61  		// otherwise spare 2
    62  		cpus = cpus - 2
    63  	} else {
    64  		// Systems with only two CPUs, spare 1.
    65  		//
    66  		// When there is a a single CPU, pass -j1 to allow a single
    67  		// compilation job only. Note, we could pass -j0 in such case
    68  		// for further improvement, but that has incompatible meaning
    69  		// between apparmor 2.x (automatic job count, equivalent to
    70  		// -jauto) and 3.x (compile everything in the main process).
    71  		cpus = 1
    72  	}
    73  
    74  	return fmt.Sprintf("-j%d", cpus)
    75  }
    76  
    77  // LoadProfiles loads apparmor profiles from the given files.
    78  //
    79  // If no such profiles were previously loaded then they are simply added to the kernel.
    80  // If there were some profiles with the same name before, those profiles are replaced.
    81  var LoadProfiles = func(fnames []string, cacheDir string, flags AaParserFlags) error {
    82  	if len(fnames) == 0 {
    83  		return nil
    84  	}
    85  
    86  	// Use no-expr-simplify since expr-simplify is actually slower on armhf (LP: #1383858)
    87  	args := []string{"--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s", cacheDir)}
    88  	if flags&ConserveCPU != 0 {
    89  		args = append(args, numberOfJobsParam())
    90  	}
    91  
    92  	if flags&SkipKernelLoad != 0 {
    93  		args = append(args, "--skip-kernel-load")
    94  	}
    95  
    96  	if flags&SkipReadCache != 0 {
    97  		args = append(args, "--skip-read-cache")
    98  	}
    99  	if !osutil.GetenvBool("SNAPD_DEBUG") {
   100  		args = append(args, "--quiet")
   101  	}
   102  	args = append(args, fnames...)
   103  
   104  	output, err := exec.Command("apparmor_parser", args...).CombinedOutput()
   105  	if err != nil {
   106  		return fmt.Errorf("cannot load apparmor profiles: %s\napparmor_parser output:\n%s", err, string(output))
   107  	}
   108  	return nil
   109  }
   110  
   111  // UnloadProfiles is meant to remove the named profiles from the running
   112  // kernel and then remove any cache files. Importantly, we can only unload
   113  // profiles when we are sure there are no lingering processes from the snap
   114  // (ie, forcibly stop all running processes from the snap). Otherwise, any
   115  // running processes will become unconfined. Since we don't have this guarantee
   116  // yet, leave the profiles loaded in the kernel but remove the cache files from
   117  // the system so the policy is gone on the next reboot. LP: #1818241
   118  func UnloadProfiles(names []string, cacheDir string) error {
   119  	if len(names) == 0 {
   120  		return nil
   121  	}
   122  
   123  	/* TODO: uncomment when no lingering snap processes is guaranteed
   124  	// By the time this function is called, all the profiles (names) have
   125  	// been removed from dirs.SnapAppArmorDir, so to unload the profiles
   126  	// from the running kernel we must instead use sysfs and write the
   127  	// profile names one at a time to
   128  	// /sys/kernel/security/apparmor/.remove (with no trailing \n).
   129  	apparmorSysFsRemove := "/sys/kernel/security/apparmor/.remove"
   130  	if !osutil.IsWritable(appArmorSysFsRemove) {
   131  	        return fmt.Errorf("cannot unload apparmor profile: %s does not exist\n", appArmorSysFsRemove)
   132  	}
   133  	for _, n := range names {
   134  	        // ignore errors since it is ok if the profile isn't removed
   135  	        // from the kernel
   136  	        ioutil.WriteFile(appArmorSysFsRemove, []byte(n), 0666)
   137  	}
   138  	*/
   139  
   140  	// AppArmor 2.13 and higher has a cache forest while 2.12 and lower has
   141  	// a flat directory (on 2.12 and earlier, .features and the snap
   142  	// profiles are in the top-level directory instead of a subdirectory).
   143  	// With 2.13+, snap profiles are not expected to be in every
   144  	// subdirectory, so don't error on ENOENT but otherwise if we get an
   145  	// error, something weird happened so stop processing.
   146  	if li, err := filepath.Glob(filepath.Join(cacheDir, "*/.features")); err == nil && len(li) > 0 { // 2.13+
   147  		for _, p := range li {
   148  			dir := path.Dir(p)
   149  			if err := osutil.UnlinkMany(dir, names); err != nil && !os.IsNotExist(err) {
   150  				return fmt.Errorf("cannot remove apparmor profile cache in %s: %s", dir, err)
   151  			}
   152  		}
   153  	} else if err := osutil.UnlinkMany(cacheDir, names); err != nil && !os.IsNotExist(err) { // 2.12-
   154  		return fmt.Errorf("cannot remove apparmor profile cache: %s", err)
   155  	}
   156  	return nil
   157  }
   158  
   159  // ReloadAllSnapProfiles reload the AppArmor profiles of all installed snaps,
   160  // as well as that of snap-confine.
   161  // This method is meant to be called when some rules have been changed in
   162  // AppArmor include files (like in the tunable files for HOMEDIRS or other
   163  // variables) which are bound to affect most snaps.
   164  func ReloadAllSnapProfiles() error {
   165  	profiles, err := filepath.Glob(filepath.Join(dirs.SnapAppArmorDir, "*"))
   166  	if err != nil {
   167  		// This only happens if the pattern is malformed
   168  		return err
   169  	}
   170  
   171  	// We also need to reload the profile of snap-confine; it could come from
   172  	// the core snap, in which case the glob above will already include it, or
   173  	// from the distribution package, in which case it's under
   174  	// /etc/apparmor.d/.
   175  	if snapConfineProfile := SnapConfineDistroProfilePath(); snapConfineProfile != "" {
   176  		profiles = append(profiles, snapConfineProfile)
   177  	}
   178  
   179  	// We want to reload the profiles no matter what, so don't even bother
   180  	// checking if the cached profile is newer
   181  	aaFlags := SkipReadCache
   182  	if err := LoadProfiles(profiles, SystemCacheDir, aaFlags); err != nil {
   183  		return err
   184  	}
   185  
   186  	return nil
   187  }
   188  
   189  // profilesPath contains information about the currently loaded apparmor profiles.
   190  const realProfilesPath = "/sys/kernel/security/apparmor/profiles"
   191  
   192  var profilesPath = realProfilesPath
   193  
   194  // LoadedProfiles interrogates the kernel and returns a list of loaded apparmor profiles.
   195  //
   196  // Snappy manages apparmor profiles named "snap.*". Other profiles might exist on
   197  // the system (via snappy dimension) and those are filtered-out.
   198  func LoadedProfiles() ([]string, error) {
   199  	file, err := os.Open(profilesPath)
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  	defer file.Close()
   204  	var profiles []string
   205  	for {
   206  		var name, mode string
   207  		n, err := fmt.Fscanf(file, "%s %s\n", &name, &mode)
   208  		if n > 0 && n != 2 {
   209  			return nil, fmt.Errorf("syntax error, expected: name (mode)")
   210  		}
   211  		if err == io.EOF {
   212  			break
   213  		}
   214  		if err != nil {
   215  			return nil, err
   216  		}
   217  		if strings.HasPrefix(name, "snap.") {
   218  			profiles = append(profiles, name)
   219  		}
   220  	}
   221  	return profiles, nil
   222  }
   223  
   224  // SnapConfineDistroProfilePath returns the path to the AppArmor profile of the
   225  // snap-confine binary shipped by the distribution package.
   226  // If such a profile is not found (for instance, because we are running Ubuntu
   227  // Core) return an empty string
   228  var SnapConfineDistroProfilePath = func() string {
   229  	// For historical reasons we may have a filename that ends with .real or
   230  	// not.  If we do then we prefer the file ending with the name .real as
   231  	// that is the more recent name we use.
   232  	for _, profileName := range []string{
   233  		"usr.lib.snapd.snap-confine.real",
   234  		"usr.lib.snapd.snap-confine",
   235  		"usr.libexec.snapd.snap-confine",
   236  	} {
   237  		maybeProfilePath := filepath.Join(ConfDir, profileName)
   238  		if osutil.FileExists(maybeProfilePath) {
   239  			return maybeProfilePath
   240  		}
   241  	}
   242  
   243  	return ""
   244  }