github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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 }