gitee.com/mysnapcore/mysnapd@v0.1.0/interfaces/builtin/mount_control.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2021 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 builtin 21 22 import ( 23 "bytes" 24 "errors" 25 "fmt" 26 "regexp" 27 "strings" 28 29 "gitee.com/mysnapcore/mysnapd/interfaces" 30 "gitee.com/mysnapcore/mysnapd/interfaces/apparmor" 31 "gitee.com/mysnapcore/mysnapd/interfaces/utils" 32 "gitee.com/mysnapcore/mysnapd/snap" 33 "gitee.com/mysnapcore/mysnapd/strutil" 34 "gitee.com/mysnapcore/mysnapd/systemd" 35 ) 36 37 const mountControlSummary = `allows creating transient and persistent mounts` 38 39 const mountControlBaseDeclarationPlugs = ` 40 mount-control: 41 allow-installation: false 42 deny-auto-connection: true 43 ` 44 45 const mountControlBaseDeclarationSlots = ` 46 mount-control: 47 allow-installation: 48 slot-snap-type: 49 - core 50 deny-connection: true 51 ` 52 53 var mountAttrTypeError = errors.New(`mount-control "mount" attribute must be a list of dictionaries`) 54 55 const mountControlConnectedPlugSecComp = ` 56 # Description: Allow mount and umount syscall access. No filtering here, as we 57 # rely on AppArmor to filter the mount operations. 58 mount 59 umount 60 umount2 61 ` 62 63 // The reason why this list is not shared with osutil.MountOptsToCommonFlags or 64 // other parts of the codebase is that this one only contains the options which 65 // have been deemed safe and have been vetted by the security team. 66 var allowedMountOptions = []string{ 67 "async", 68 "atime", 69 "bind", 70 "diratime", 71 "dirsync", 72 "iversion", 73 "lazytime", 74 "nofail", 75 "noiversion", 76 "nomand", 77 "noatime", 78 "nodev", 79 "nodiratime", 80 "noexec", 81 "nolazytime", 82 "norelatime", 83 "nosuid", 84 "nostrictatime", 85 "nouser", 86 "relatime", 87 "strictatime", 88 "sync", 89 "ro", 90 "rw", 91 } 92 93 // A few mount flags are special in that if they are specified, the filesystem 94 // type is ignored. We list them here, and we will ensure that the plug 95 // declaration does not specify a type, if any of them is present among the 96 // options. 97 var optionsWithoutFsType = []string{ 98 "bind", 99 // Note: the following flags should also fall into this list, but we are 100 // not currently allowing them (and don't plan to): 101 // - "make-private" 102 // - "make-shared" 103 // - "make-slave" 104 // - "make-unbindable" 105 // - "move" 106 // - "remount" 107 } 108 109 // List of filesystem types to allow if the plug declaration does not 110 // explicitly specify a filesystem type. 111 var defaultFSTypes = []string{ 112 "aufs", 113 "autofs", 114 "btrfs", 115 "ext2", 116 "ext3", 117 "ext4", 118 "hfs", 119 "iso9660", 120 "jfs", 121 "msdos", 122 "ntfs", 123 "ramfs", 124 "reiserfs", 125 "squashfs", 126 "tmpfs", 127 "ubifs", 128 "udf", 129 "ufs", 130 "vfat", 131 "zfs", 132 "xfs", 133 } 134 135 // The filesystems in the following list were considered either dangerous or 136 // not relevant for this interface: 137 var disallowedFSTypes = []string{ 138 "bpf", 139 "cgroup", 140 "cgroup2", 141 "debugfs", 142 "devpts", 143 "ecryptfs", 144 "hugetlbfs", 145 "overlayfs", 146 "proc", 147 "securityfs", 148 "sysfs", 149 "tracefs", 150 } 151 152 // mountControlInterface allows creating transient and persistent mounts 153 type mountControlInterface struct { 154 commonInterface 155 } 156 157 // The "what" and "where" attributes end up in the AppArmor profile, surrounded 158 // by double quotes; to ensure that a malicious snap cannot inject arbitrary 159 // rules by specifying something like 160 // where: $SNAP_DATA/foo", /** rw, # 161 // which would generate a profile line like: 162 // mount options=() "$SNAP_DATA/foo", /** rw, #" 163 // (which would grant read-write access to the whole filesystem), it's enough 164 // to exclude the `"` character: without it, whatever is written in the 165 // attribute will not be able to escape being treated like a pattern. 166 // 167 // To be safe, there's more to be done: the pattern also needs to be valid, as 168 // a malformed one (for example, a pattern having an unmatched `}`) would cause 169 // apparmor_parser to fail loading the profile. For this situation, we use the 170 // PathPattern interface to validate the pattern. 171 // 172 // Besides that, we are also excluding the `@` character, which is used to mark 173 // AppArmor variables (tunables): when generating the profile we lack the 174 // knowledge of which variables have been defined, so it's safer to exclude 175 // them. 176 // The what attribute regular expression here is intentionally permissive of 177 // nearly any path, and due to the super-privileged nature of this interface it 178 // is expected that sensible values of what are enforced by the store manual 179 // review queue and security teams. 180 var ( 181 whatRegexp = regexp.MustCompile(`^(none|/[^"@]*)$`) 182 whereRegexp = regexp.MustCompile(`^(\$SNAP_COMMON|\$SNAP_DATA)?/[^\$"@]+$`) 183 ) 184 185 // Excluding spaces and other characters which might allow constructing a 186 // malicious string like 187 // auto) options=() /malicious/content /var/lib/snapd/hostfs/...,\n mount fstype=( 188 var typeRegexp = regexp.MustCompile(`^[a-z0-9]+$`) 189 190 type MountInfo struct { 191 what string 192 where string 193 persistent bool 194 types []string 195 options []string 196 } 197 198 func parseStringList(mountEntry map[string]interface{}, fieldName string) ([]string, error) { 199 var list []string 200 value, ok := mountEntry[fieldName] 201 if ok { 202 interfaceList, ok := value.([]interface{}) 203 if !ok { 204 return nil, fmt.Errorf(`mount-control "%s" must be an array of strings (got %q)`, fieldName, value) 205 } 206 for i, iface := range interfaceList { 207 valueString, ok := iface.(string) 208 if !ok { 209 return nil, fmt.Errorf(`mount-control "%s" element %d not a string (%q)`, fieldName, i+1, iface) 210 } 211 list = append(list, valueString) 212 } 213 } 214 return list, nil 215 } 216 217 func enumerateMounts(plug interfaces.Attrer, fn func(mountInfo *MountInfo) error) error { 218 var mounts []map[string]interface{} 219 err := plug.Attr("mount", &mounts) 220 if err != nil && !errors.Is(err, snap.AttributeNotFoundError{}) { 221 return mountAttrTypeError 222 } 223 224 for _, mount := range mounts { 225 what, ok := mount["what"].(string) 226 if !ok { 227 return fmt.Errorf(`mount-control "what" must be a string`) 228 } 229 230 where, ok := mount["where"].(string) 231 if !ok { 232 return fmt.Errorf(`mount-control "where" must be a string`) 233 } 234 235 persistent := false 236 persistentValue, ok := mount["persistent"] 237 if ok { 238 if persistent, ok = persistentValue.(bool); !ok { 239 return fmt.Errorf(`mount-control "persistent" must be a boolean`) 240 } 241 } 242 243 types, err := parseStringList(mount, "type") 244 if err != nil { 245 return err 246 } 247 248 options, err := parseStringList(mount, "options") 249 if err != nil { 250 return err 251 } 252 253 mountInfo := &MountInfo{ 254 what: what, 255 where: where, 256 persistent: persistent, 257 types: types, 258 options: options, 259 } 260 261 if err := fn(mountInfo); err != nil { 262 return err 263 } 264 } 265 266 return nil 267 } 268 269 func validateWhatAttr(what string) error { 270 if !whatRegexp.MatchString(what) { 271 return fmt.Errorf(`mount-control "what" attribute is invalid: must start with / and not contain special characters`) 272 } 273 274 if !cleanSubPath(what) { 275 return fmt.Errorf(`mount-control "what" pattern is not clean: %q`, what) 276 } 277 278 if _, err := utils.NewPathPattern(what); err != nil { 279 return fmt.Errorf(`mount-control "what" setting cannot be used: %v`, err) 280 } 281 282 return nil 283 } 284 285 func validateWhereAttr(where string) error { 286 if !whereRegexp.MatchString(where) { 287 return fmt.Errorf(`mount-control "where" attribute must start with $SNAP_COMMON, $SNAP_DATA or / and not contain special characters`) 288 } 289 290 if !cleanSubPath(where) { 291 return fmt.Errorf(`mount-control "where" pattern is not clean: %q`, where) 292 } 293 294 if _, err := utils.NewPathPattern(where); err != nil { 295 return fmt.Errorf(`mount-control "where" setting cannot be used: %v`, err) 296 } 297 298 return nil 299 } 300 301 func validateMountTypes(types []string) error { 302 includesTmpfs := false 303 for _, t := range types { 304 if !typeRegexp.MatchString(t) { 305 return fmt.Errorf(`mount-control filesystem type invalid: %q`, t) 306 } 307 if strutil.ListContains(disallowedFSTypes, t) { 308 return fmt.Errorf(`mount-control forbidden filesystem type: %q`, t) 309 } 310 if t == "tmpfs" { 311 includesTmpfs = true 312 } 313 } 314 315 if includesTmpfs && len(types) > 1 { 316 return errors.New(`mount-control filesystem type "tmpfs" cannot be listed with other types`) 317 } 318 return nil 319 } 320 321 func validateMountOptions(options []string) error { 322 if len(options) == 0 { 323 return errors.New(`mount-control "options" cannot be empty`) 324 } 325 for _, o := range options { 326 if !strutil.ListContains(allowedMountOptions, o) { 327 return fmt.Errorf(`mount-control option unrecognized or forbidden: %q`, o) 328 } 329 } 330 return nil 331 } 332 333 // Find the first option which is incompatible with a FS type declaration 334 func optionIncompatibleWithFsType(options []string) string { 335 for _, o := range options { 336 if strutil.ListContains(optionsWithoutFsType, o) { 337 return o 338 } 339 } 340 return "" 341 } 342 343 func validateMountInfo(mountInfo *MountInfo) error { 344 if err := validateWhatAttr(mountInfo.what); err != nil { 345 return err 346 } 347 348 if err := validateWhereAttr(mountInfo.where); err != nil { 349 return err 350 } 351 352 if err := validateMountTypes(mountInfo.types); err != nil { 353 return err 354 } 355 356 if err := validateMountOptions(mountInfo.options); err != nil { 357 return err 358 } 359 360 // Check if any options are incompatible with specifying a FS type 361 fsExclusiveOption := optionIncompatibleWithFsType(mountInfo.options) 362 if fsExclusiveOption != "" && len(mountInfo.types) > 0 { 363 return fmt.Errorf(`mount-control option %q is incompatible with specifying filesystem type`, fsExclusiveOption) 364 } 365 366 // "what" must be set to "none" iff the type is "tmpfs" 367 isTmpfs := len(mountInfo.types) == 1 && mountInfo.types[0] == "tmpfs" 368 if mountInfo.what == "none" { 369 if !isTmpfs { 370 return errors.New(`mount-control "what" attribute can be "none" only with "tmpfs"`) 371 } 372 } else if isTmpfs { 373 return fmt.Errorf(`mount-control "what" attribute must be "none" with "tmpfs"; found %q instead`, mountInfo.what) 374 } 375 376 // Until we have a clear picture of how this should work, disallow creating 377 // persistent mounts into $SNAP_DATA 378 if mountInfo.persistent && strings.HasPrefix(mountInfo.where, "$SNAP_DATA") { 379 return errors.New(`mount-control "persistent" attribute cannot be used to mount onto $SNAP_DATA`) 380 } 381 382 return nil 383 } 384 385 func (iface *mountControlInterface) BeforeConnectPlug(plug *interfaces.ConnectedPlug) error { 386 // The systemd.ListMountUnits() method works by issuing the command 387 // "systemctl show *.mount", but globbing was only added in systemd v209. 388 if err := systemd.EnsureAtLeast(209); err != nil { 389 return err 390 } 391 392 hasMountEntries := false 393 err := enumerateMounts(plug, func(mountInfo *MountInfo) error { 394 hasMountEntries = true 395 return validateMountInfo(mountInfo) 396 }) 397 if err != nil { 398 return err 399 } 400 401 if !hasMountEntries { 402 return mountAttrTypeError 403 } 404 405 return nil 406 } 407 408 func (iface *mountControlInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { 409 mountControlSnippet := bytes.NewBuffer(nil) 410 emit := func(f string, args ...interface{}) { 411 fmt.Fprintf(mountControlSnippet, f, args...) 412 } 413 snapInfo := plug.Snap() 414 415 emit(` 416 # Rules added by the mount-control interface 417 capability sys_admin, # for mount 418 419 owner @{PROC}/@{pid}/mounts r, 420 owner @{PROC}/@{pid}/mountinfo r, 421 owner @{PROC}/self/mountinfo r, 422 423 /{,usr/}bin/mount ixr, 424 /{,usr/}bin/umount ixr, 425 # mount/umount (via libmount) track some mount info in these files 426 /run/mount/utab* wrlk, 427 `) 428 429 // No validation is occurring here, as it was already performed in 430 // BeforeConnectPlug() 431 enumerateMounts(plug, func(mountInfo *MountInfo) error { 432 433 source := mountInfo.what 434 target := mountInfo.where 435 if target[0] == '$' { 436 matches := whereRegexp.FindStringSubmatchIndex(target) 437 if matches == nil || len(matches) < 4 { 438 // This cannot really happen, as the string wouldn't pass the validation 439 return fmt.Errorf(`internal error: "where" fails to match regexp: %q`, mountInfo.where) 440 } 441 // the first two elements in "matches" are the boundaries of the whole 442 // string; the next two are the boundaries of the first match, which is 443 // what we care about as it contains the environment variable we want 444 // to expand: 445 variableStart, variableEnd := matches[2], matches[3] 446 variable := target[variableStart:variableEnd] 447 expanded := snapInfo.ExpandSnapVariables(variable) 448 target = expanded + target[variableEnd:] 449 } 450 451 var typeRule string 452 if optionIncompatibleWithFsType(mountInfo.options) != "" { 453 // In this rule the FS type will not match unless it's empty 454 typeRule = "" 455 } else { 456 var types []string 457 if len(mountInfo.types) > 0 { 458 types = mountInfo.types 459 } else { 460 types = defaultFSTypes 461 } 462 typeRule = "fstype=(" + strings.Join(types, ",") + ")" 463 } 464 465 options := strings.Join(mountInfo.options, ",") 466 467 emit(" mount %s options=(%s) \"%s\" -> \"%s{,/}\",\n", typeRule, options, source, target) 468 emit(" umount \"%s{,/}\",\n", target) 469 return nil 470 }) 471 472 spec.AddSnippet(mountControlSnippet.String()) 473 return nil 474 } 475 476 func (iface *mountControlInterface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool { 477 return true 478 } 479 480 func init() { 481 registerIface(&mountControlInterface{ 482 commonInterface: commonInterface{ 483 name: "mount-control", 484 summary: mountControlSummary, 485 baseDeclarationPlugs: mountControlBaseDeclarationPlugs, 486 baseDeclarationSlots: mountControlBaseDeclarationSlots, 487 implicitOnCore: true, 488 implicitOnClassic: true, 489 connectedPlugSecComp: mountControlConnectedPlugSecComp, 490 }, 491 }) 492 }