github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/interfaces/apparmor/apparmor.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016 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 contains primitives for working with apparmor. 21 // 22 // References: 23 // - http://wiki.apparmor.net/index.php/Kernel_interfaces 24 // - http://apparmor.wiki.kernel.org/ 25 // - http://manpages.ubuntu.com/manpages/xenial/en/man7/apparmor.7.html 26 package apparmor 27 28 import ( 29 "fmt" 30 "io" 31 "os" 32 "os/exec" 33 "path" 34 "path/filepath" 35 "runtime" 36 "strings" 37 38 "github.com/snapcore/snapd/osutil" 39 ) 40 41 // ValidateNoAppArmorRegexp will check that the given string does not 42 // contain AppArmor regular expressions (AARE), double quotes or \0. 43 func ValidateNoAppArmorRegexp(s string) error { 44 const AARE = `?*[]{}^"` + "\x00" 45 46 if strings.ContainsAny(s, AARE) { 47 return fmt.Errorf("%q contains a reserved apparmor char from %s", s, AARE) 48 } 49 return nil 50 } 51 52 type aaParserFlags int 53 54 const ( 55 // skipReadCache causes apparmor_parser to be invoked with --skip-read-cache. 56 // This allows us to essentially overwrite a cache that we know is stale regardless 57 // of the time and date settings (apparmor_parser caching is based on mtime). 58 // Note that writing of the cache relies on --write-cache but we pass that 59 // command-line option unconditionally. 60 skipReadCache aaParserFlags = 1 << iota 61 62 // conserveCPU tells apparmor_parser to spare up to two CPUs on multi-core systems to 63 // reduce load when processing many profiles at once. 64 conserveCPU aaParserFlags = 1 << iota 65 66 // skipKernelLoad tells apparmor_parser not to load profiles into the kernel. The use 67 // case of this is when in pre-seeding mode. 68 skipKernelLoad aaParserFlags = 1 << iota 69 ) 70 71 var runtimeNumCPU = runtime.NumCPU 72 73 func maybeSetNumberOfJobs() string { 74 cpus := runtimeNumCPU() 75 // Do not use all CPUs as this may have negative impact when booting. Note, -j0 has special meaning 76 // so we don't want to pass it to apparmor parser. 77 if cpus > 1 { 78 if cpus == 2 { 79 // systems with only two CPUs, spare 1 80 return fmt.Sprintf("-j%d", cpus-1) 81 } else { 82 // otherwise spare 2 83 return fmt.Sprintf("-j%d", cpus-2) 84 } 85 } 86 return "" 87 } 88 89 // loadProfiles loads apparmor profiles from the given files. 90 // 91 // If no such profiles were previously loaded then they are simply added to the kernel. 92 // If there were some profiles with the same name before, those profiles are replaced. 93 func loadProfiles(fnames []string, cacheDir string, flags aaParserFlags) error { 94 if len(fnames) == 0 { 95 return nil 96 } 97 98 // Use no-expr-simplify since expr-simplify is actually slower on armhf (LP: #1383858) 99 args := []string{"--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s", cacheDir)} 100 if flags&conserveCPU != 0 { 101 if jobArg := maybeSetNumberOfJobs(); jobArg != "" { 102 args = append(args, jobArg) 103 } 104 } 105 106 if flags&skipKernelLoad != 0 { 107 args = append(args, "--skip-kernel-load") 108 } 109 110 if flags&skipReadCache != 0 { 111 args = append(args, "--skip-read-cache") 112 } 113 if !osutil.GetenvBool("SNAPD_DEBUG") { 114 args = append(args, "--quiet") 115 } 116 args = append(args, fnames...) 117 118 output, err := exec.Command("apparmor_parser", args...).CombinedOutput() 119 if err != nil { 120 return fmt.Errorf("cannot load apparmor profiles: %s\napparmor_parser output:\n%s", err, string(output)) 121 } 122 return nil 123 } 124 125 // unloadProfiles is meant to remove the named profiles from the running 126 // kernel and then remove any cache files. Importantly, we can only unload 127 // profiles when we are sure there are no lingering processes from the snap 128 // (ie, forcibly stop all running processes from the snap). Otherwise, any 129 // running processes will become unconfined. Since we don't have this guarantee 130 // yet, leave the profiles loaded in the kernel but remove the cache files from 131 // the system so the policy is gone on the next reboot. LP: #1818241 132 func unloadProfiles(names []string, cacheDir string) error { 133 if len(names) == 0 { 134 return nil 135 } 136 137 /* TODO: uncomment when no lingering snap processes is guaranteed 138 // By the time this function is called, all the profiles (names) have 139 // been removed from dirs.SnapAppArmorDir, so to unload the profiles 140 // from the running kernel we must instead use sysfs and write the 141 // profile names one at a time to 142 // /sys/kernel/security/apparmor/.remove (with no trailing \n). 143 apparmorSysFsRemove := "/sys/kernel/security/apparmor/.remove" 144 if !osutil.IsWritable(appArmorSysFsRemove) { 145 return fmt.Errorf("cannot unload apparmor profile: %s does not exist\n", appArmorSysFsRemove) 146 } 147 for _, n := range names { 148 // ignore errors since it is ok if the profile isn't removed 149 // from the kernel 150 ioutil.WriteFile(appArmorSysFsRemove, []byte(n), 0666) 151 } 152 */ 153 154 // AppArmor 2.13 and higher has a cache forest while 2.12 and lower has 155 // a flat directory (on 2.12 and earlier, .features and the snap 156 // profiles are in the top-level directory instead of a subdirectory). 157 // With 2.13+, snap profiles are not expected to be in every 158 // subdirectory, so don't error on ENOENT but otherwise if we get an 159 // error, something weird happened so stop processing. 160 if li, err := filepath.Glob(filepath.Join(cacheDir, "*/.features")); err == nil && len(li) > 0 { // 2.13+ 161 for _, p := range li { 162 dir := path.Dir(p) 163 if err := osutil.UnlinkMany(dir, names); err != nil && !os.IsNotExist(err) { 164 return fmt.Errorf("cannot remove apparmor profile cache in %s: %s", dir, err) 165 } 166 } 167 } else if err := osutil.UnlinkMany(cacheDir, names); err != nil && !os.IsNotExist(err) { // 2.12- 168 return fmt.Errorf("cannot remove apparmor profile cache: %s", err) 169 } 170 return nil 171 } 172 173 // profilesPath contains information about the currently loaded apparmor profiles. 174 const realProfilesPath = "/sys/kernel/security/apparmor/profiles" 175 176 var profilesPath = realProfilesPath 177 178 // LoadedProfiles interrogates the kernel and returns a list of loaded apparmor profiles. 179 // 180 // Snappy manages apparmor profiles named "snap.*". Other profiles might exist on 181 // the system (via snappy dimension) and those are filtered-out. 182 func LoadedProfiles() ([]string, error) { 183 file, err := os.Open(profilesPath) 184 if err != nil { 185 return nil, err 186 } 187 defer file.Close() 188 var profiles []string 189 for { 190 var name, mode string 191 n, err := fmt.Fscanf(file, "%s %s\n", &name, &mode) 192 if n > 0 && n != 2 { 193 return nil, fmt.Errorf("syntax error, expected: name (mode)") 194 } 195 if err == io.EOF { 196 break 197 } 198 if err != nil { 199 return nil, err 200 } 201 if strings.HasPrefix(name, "snap.") { 202 profiles = append(profiles, name) 203 } 204 } 205 return profiles, nil 206 }