github.com/rigado/snapd@v2.42.5-go-mod+incompatible/snap/validate.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 snap 21 22 import ( 23 "errors" 24 "fmt" 25 "os" 26 "path/filepath" 27 "regexp" 28 "sort" 29 "strconv" 30 "strings" 31 "unicode/utf8" 32 33 "github.com/snapcore/snapd/osutil" 34 "github.com/snapcore/snapd/snap/naming" 35 "github.com/snapcore/snapd/spdx" 36 "github.com/snapcore/snapd/strutil" 37 "github.com/snapcore/snapd/timeout" 38 "github.com/snapcore/snapd/timeutil" 39 ) 40 41 // The fixed length of valid snap IDs. 42 const validSnapIDLength = 32 43 44 // ValidateInstanceName checks if a string can be used as a snap instance name. 45 func ValidateInstanceName(instanceName string) error { 46 return naming.ValidateInstance(instanceName) 47 } 48 49 // ValidateName checks if a string can be used as a snap name. 50 func ValidateName(name string) error { 51 return naming.ValidateSnap(name) 52 } 53 54 // ValidatePlugName checks if a string can be used as a slot name. 55 // 56 // Slot names and plug names within one snap must have unique names. 57 // This is not enforced by this function but is enforced by snap-level 58 // validation. 59 func ValidatePlugName(name string) error { 60 return naming.ValidatePlug(name) 61 } 62 63 // ValidateSlotName checks if a string can be used as a slot name. 64 // 65 // Slot names and plug names within one snap must have unique names. 66 // This is not enforced by this function but is enforced by snap-level 67 // validation. 68 func ValidateSlotName(name string) error { 69 return naming.ValidateSlot(name) 70 } 71 72 // ValidateInterfaceName checks if a string can be used as an interface name. 73 func ValidateInterfaceName(name string) error { 74 return naming.ValidateInterface(name) 75 } 76 77 // NB keep this in sync with snapcraft and the review tools :-) 78 var isValidVersion = regexp.MustCompile("^[a-zA-Z0-9](?:[a-zA-Z0-9:.+~-]{0,30}[a-zA-Z0-9+~])?$").MatchString 79 80 var isNonGraphicalASCII = regexp.MustCompile("[^[:graph:]]").MatchString 81 var isInvalidFirstVersionChar = regexp.MustCompile("^[^a-zA-Z0-9]").MatchString 82 var isInvalidLastVersionChar = regexp.MustCompile("[^a-zA-Z0-9+~]$").MatchString 83 var invalidMiddleVersionChars = regexp.MustCompile("[^a-zA-Z0-9:.+~-]+").FindAllString 84 85 // ValidateVersion checks if a string is a valid snap version. 86 func ValidateVersion(version string) error { 87 if !isValidVersion(version) { 88 // maybe it was too short? 89 if len(version) == 0 { 90 return errors.New("invalid snap version: cannot be empty") 91 } 92 if isNonGraphicalASCII(version) { 93 // note that while this way of quoting the version can produce ugly 94 // output in some cases (e.g. if you're trying to set a version to 95 // "hello😁", seeing “invalid version "hello😁"” could be clearer than 96 // “invalid snap version "hello\U0001f601"”), in a lot of more 97 // interesting cases you _need_ to have the thing that's not ASCII 98 // pointed out: homoglyphs and near-homoglyphs are too hard to spot 99 // otherwise. Take for example a version of "аерс". Or "v1.0‑x". 100 return fmt.Errorf("invalid snap version %s: must be printable, non-whitespace ASCII", 101 strconv.QuoteToASCII(version)) 102 } 103 // now we know it's a non-empty ASCII string, we can get serious 104 var reasons []string 105 // ... too long? 106 if len(version) > 32 { 107 reasons = append(reasons, fmt.Sprintf("cannot be longer than 32 characters (got: %d)", len(version))) 108 } 109 // started with a symbol? 110 if isInvalidFirstVersionChar(version) { 111 // note that we can only say version[0] because we know it's ASCII :-) 112 reasons = append(reasons, fmt.Sprintf("must start with an ASCII alphanumeric (and not %q)", version[0])) 113 } 114 if len(version) > 1 { 115 if isInvalidLastVersionChar(version) { 116 tpl := "must end with an ASCII alphanumeric or one of '+' or '~' (and not %q)" 117 reasons = append(reasons, fmt.Sprintf(tpl, version[len(version)-1])) 118 } 119 if len(version) > 2 { 120 if all := invalidMiddleVersionChars(version[1:len(version)-1], -1); len(all) > 0 { 121 reasons = append(reasons, fmt.Sprintf("contains invalid characters: %s", strutil.Quoted(all))) 122 } 123 } 124 } 125 switch len(reasons) { 126 case 0: 127 // huh 128 return fmt.Errorf("invalid snap version %q", version) 129 case 1: 130 return fmt.Errorf("invalid snap version %q: %s", version, reasons[0]) 131 default: 132 reasons, last := reasons[:len(reasons)-1], reasons[len(reasons)-1] 133 return fmt.Errorf("invalid snap version %q: %s, and %s", version, strings.Join(reasons, ", "), last) 134 } 135 } 136 return nil 137 } 138 139 // ValidateLicense checks if a string is a valid SPDX expression. 140 func ValidateLicense(license string) error { 141 if err := spdx.ValidateLicense(license); err != nil { 142 return fmt.Errorf("cannot validate license %q: %s", license, err) 143 } 144 return nil 145 } 146 147 // ValidateHook validates the content of the given HookInfo 148 func ValidateHook(hook *HookInfo) error { 149 if err := naming.ValidateHook(hook.Name); err != nil { 150 return err 151 } 152 153 // Also validate the command chain 154 for _, value := range hook.CommandChain { 155 if !commandChainContentWhitelist.MatchString(value) { 156 return fmt.Errorf("hook command-chain contains illegal %q (legal: '%s')", value, commandChainContentWhitelist) 157 } 158 } 159 160 return nil 161 } 162 163 // ValidateAlias checks if a string can be used as an alias name. 164 func ValidateAlias(alias string) error { 165 return naming.ValidateAlias(alias) 166 } 167 168 // validateSocketName checks if a string ca be used as a name for a socket (for 169 // socket activation). 170 func validateSocketName(name string) error { 171 return naming.ValidateSocket(name) 172 } 173 174 // validateSocketmode checks that the socket mode is a valid file mode. 175 func validateSocketMode(mode os.FileMode) error { 176 if mode > 0777 { 177 return fmt.Errorf("cannot use mode: %04o", mode) 178 } 179 180 return nil 181 } 182 183 // validateSocketAddr checks that the value of socket addresses. 184 func validateSocketAddr(socket *SocketInfo, fieldName string, address string) error { 185 if address == "" { 186 return fmt.Errorf("%q is not defined", fieldName) 187 } 188 189 switch address[0] { 190 case '/', '$': 191 return validateSocketAddrPath(socket, fieldName, address) 192 case '@': 193 return validateSocketAddrAbstract(socket, fieldName, address) 194 default: 195 return validateSocketAddrNet(socket, fieldName, address) 196 } 197 } 198 199 func validateSocketAddrPath(socket *SocketInfo, fieldName string, path string) error { 200 if clean := filepath.Clean(path); clean != path { 201 return fmt.Errorf("invalid %q: %q should be written as %q", fieldName, path, clean) 202 } 203 204 if !(strings.HasPrefix(path, "$SNAP_DATA/") || strings.HasPrefix(path, "$SNAP_COMMON/") || strings.HasPrefix(path, "$XDG_RUNTIME_DIR/")) { 205 return fmt.Errorf( 206 "invalid %q: must have a prefix of $SNAP_DATA, $SNAP_COMMON or $XDG_RUNTIME_DIR", fieldName) 207 } 208 209 return nil 210 } 211 212 func validateSocketAddrAbstract(socket *SocketInfo, fieldName string, path string) error { 213 // this comes from snap declaration, so the prefix can only be the snap 214 // name at this point 215 prefix := fmt.Sprintf("@snap.%s.", socket.App.Snap.SnapName()) 216 if !strings.HasPrefix(path, prefix) { 217 return fmt.Errorf("path for %q must be prefixed with %q", fieldName, prefix) 218 } 219 return nil 220 } 221 222 func validateSocketAddrNet(socket *SocketInfo, fieldName string, address string) error { 223 lastIndex := strings.LastIndex(address, ":") 224 if lastIndex >= 0 { 225 if err := validateSocketAddrNetHost(socket, fieldName, address[:lastIndex]); err != nil { 226 return err 227 } 228 return validateSocketAddrNetPort(socket, fieldName, address[lastIndex+1:]) 229 } 230 231 // Address only contains a port 232 return validateSocketAddrNetPort(socket, fieldName, address) 233 } 234 235 func validateSocketAddrNetHost(socket *SocketInfo, fieldName string, address string) error { 236 validAddresses := []string{"127.0.0.1", "[::1]", "[::]"} 237 for _, valid := range validAddresses { 238 if address == valid { 239 return nil 240 } 241 } 242 243 return fmt.Errorf("invalid %q address %q, must be one of: %s", fieldName, address, strings.Join(validAddresses, ", ")) 244 } 245 246 func validateSocketAddrNetPort(socket *SocketInfo, fieldName string, port string) error { 247 var val uint64 248 var err error 249 retErr := fmt.Errorf("invalid %q port number %q", fieldName, port) 250 if val, err = strconv.ParseUint(port, 10, 16); err != nil { 251 return retErr 252 } 253 if val < 1 || val > 65535 { 254 return retErr 255 } 256 return nil 257 } 258 259 func validateDescription(descr string) error { 260 if count := utf8.RuneCountInString(descr); count > 4096 { 261 return fmt.Errorf("description can have up to 4096 codepoints, got %d", count) 262 } 263 return nil 264 } 265 266 func validateTitle(title string) error { 267 if count := utf8.RuneCountInString(title); count > 40 { 268 return fmt.Errorf("title can have up to 40 codepoints, got %d", count) 269 } 270 return nil 271 } 272 273 // Validate verifies the content in the info. 274 func Validate(info *Info) error { 275 name := info.InstanceName() 276 if name == "" { 277 return errors.New("snap name cannot be empty") 278 } 279 280 if err := ValidateName(info.SnapName()); err != nil { 281 return err 282 } 283 if err := ValidateInstanceName(name); err != nil { 284 return err 285 } 286 287 if err := validateTitle(info.Title()); err != nil { 288 return err 289 } 290 291 if err := validateDescription(info.Description()); err != nil { 292 return err 293 } 294 295 if err := ValidateVersion(info.Version); err != nil { 296 return err 297 } 298 299 if err := info.Epoch.Validate(); err != nil { 300 return err 301 } 302 303 if license := info.License; license != "" { 304 if err := ValidateLicense(license); err != nil { 305 return err 306 } 307 } 308 309 // validate app entries 310 for _, app := range info.Apps { 311 if err := ValidateApp(app); err != nil { 312 return fmt.Errorf("invalid definition of application %q: %v", app.Name, err) 313 } 314 } 315 316 // validate apps ordering according to after/before 317 if err := validateAppOrderCycles(info.Services()); err != nil { 318 return err 319 } 320 321 // validate aliases 322 for alias, app := range info.LegacyAliases { 323 if err := naming.ValidateAlias(alias); err != nil { 324 return fmt.Errorf("cannot have %q as alias name for app %q - use only letters, digits, dash, underscore and dot characters", alias, app.Name) 325 } 326 } 327 328 // validate hook entries 329 for _, hook := range info.Hooks { 330 if err := ValidateHook(hook); err != nil { 331 return err 332 } 333 } 334 335 // Ensure that plugs and slots have appropriate names and interface names. 336 if err := plugsSlotsInterfacesNames(info); err != nil { 337 return err 338 } 339 340 // Ensure that plug and slot have unique names. 341 if err := plugsSlotsUniqueNames(info); err != nil { 342 return err 343 } 344 345 // Ensure that base field is valid 346 if err := ValidateBase(info); err != nil { 347 return err 348 } 349 350 // Ensure system usernames are valid 351 if err := ValidateSystemUsernames(info); err != nil { 352 return err 353 } 354 355 // ensure that common-id(s) are unique 356 if err := ValidateCommonIDs(info); err != nil { 357 return err 358 } 359 360 return ValidateLayoutAll(info) 361 } 362 363 // ValidateBase validates the base field. 364 func ValidateBase(info *Info) error { 365 // validate that bases do not have base fields 366 if info.GetType() == TypeOS || info.GetType() == TypeBase { 367 if info.Base != "" && info.Base != "none" { 368 return fmt.Errorf(`cannot have "base" field on %q snap %q`, info.GetType(), info.InstanceName()) 369 } 370 } 371 372 if info.Base == "none" && (len(info.Hooks) > 0 || len(info.Apps) > 0) { 373 return fmt.Errorf(`cannot have apps or hooks with base "none"`) 374 } 375 376 if info.Base != "" { 377 baseSnapName, instanceKey := SplitInstanceName(info.Base) 378 if instanceKey != "" { 379 return fmt.Errorf("base cannot specify a snap instance name: %q", info.Base) 380 } 381 if err := ValidateName(baseSnapName); err != nil { 382 return fmt.Errorf("invalid base name: %s", err) 383 } 384 } 385 return nil 386 } 387 388 // ValidateLayoutAll validates the consistency of all the layout elements in a snap. 389 func ValidateLayoutAll(info *Info) error { 390 paths := make([]string, 0, len(info.Layout)) 391 for _, layout := range info.Layout { 392 paths = append(paths, layout.Path) 393 } 394 sort.Strings(paths) 395 396 // Validate that each source path is used consistently as a file or as a directory. 397 sourceKindMap := make(map[string]string) 398 for _, path := range paths { 399 layout := info.Layout[path] 400 if layout.Bind != "" { 401 // Layout refers to a directory. 402 sourcePath := info.ExpandSnapVariables(layout.Bind) 403 if kind, ok := sourceKindMap[sourcePath]; ok { 404 if kind != "dir" { 405 return fmt.Errorf("layout %q refers to directory %q but another layout treats it as file", layout.Path, layout.Bind) 406 } 407 } 408 sourceKindMap[sourcePath] = "dir" 409 } 410 if layout.BindFile != "" { 411 // Layout refers to a file. 412 sourcePath := info.ExpandSnapVariables(layout.BindFile) 413 if kind, ok := sourceKindMap[sourcePath]; ok { 414 if kind != "file" { 415 return fmt.Errorf("layout %q refers to file %q but another layout treats it as a directory", layout.Path, layout.BindFile) 416 } 417 } 418 sourceKindMap[sourcePath] = "file" 419 } 420 } 421 422 // Validate that layout are not attempting to define elements that normally 423 // come from other snaps. This is separate from the ValidateLayout below to 424 // simplify argument passing. 425 thisSnapMntDir := filepath.Join("/snap/", info.SnapName()) 426 for _, path := range paths { 427 if strings.HasPrefix(path, "/snap/") && !strings.HasPrefix(path, thisSnapMntDir) { 428 return fmt.Errorf("layout %q defines a layout in space belonging to another snap", path) 429 } 430 } 431 432 // Validate each layout item and collect resulting constraints. 433 constraints := make([]LayoutConstraint, 0, len(info.Layout)) 434 for _, path := range paths { 435 layout := info.Layout[path] 436 if err := ValidateLayout(layout, constraints); err != nil { 437 return err 438 } 439 constraints = append(constraints, layout.constraint()) 440 } 441 return nil 442 } 443 444 func plugsSlotsInterfacesNames(info *Info) error { 445 for plugName, plug := range info.Plugs { 446 if err := ValidatePlugName(plugName); err != nil { 447 return err 448 } 449 if err := ValidateInterfaceName(plug.Interface); err != nil { 450 return fmt.Errorf("invalid interface name %q for plug %q", plug.Interface, plugName) 451 } 452 } 453 for slotName, slot := range info.Slots { 454 if err := ValidateSlotName(slotName); err != nil { 455 return err 456 } 457 if err := ValidateInterfaceName(slot.Interface); err != nil { 458 return fmt.Errorf("invalid interface name %q for slot %q", slot.Interface, slotName) 459 } 460 } 461 return nil 462 } 463 func plugsSlotsUniqueNames(info *Info) error { 464 // we could choose the smaller collection if we wanted to optimize this check 465 for plugName := range info.Plugs { 466 if info.Slots[plugName] != nil { 467 return fmt.Errorf("cannot have plug and slot with the same name: %q", plugName) 468 } 469 } 470 return nil 471 } 472 473 func validateField(name, cont string, whitelist *regexp.Regexp) error { 474 if !whitelist.MatchString(cont) { 475 return fmt.Errorf("app description field '%s' contains illegal %q (legal: '%s')", name, cont, whitelist) 476 477 } 478 return nil 479 } 480 481 func validateAppSocket(socket *SocketInfo) error { 482 if err := validateSocketName(socket.Name); err != nil { 483 return err 484 } 485 486 if err := validateSocketMode(socket.SocketMode); err != nil { 487 return err 488 } 489 return validateSocketAddr(socket, "listen-stream", socket.ListenStream) 490 } 491 492 // validateAppOrderCycles checks for cycles in app ordering dependencies 493 func validateAppOrderCycles(apps []*AppInfo) error { 494 if _, err := SortServices(apps); err != nil { 495 return err 496 } 497 return nil 498 } 499 500 func validateAppOrderNames(app *AppInfo, dependencies []string) error { 501 // we must be a service to request ordering 502 if len(dependencies) > 0 && !app.IsService() { 503 return errors.New("must be a service to define before/after ordering") 504 } 505 506 for _, dep := range dependencies { 507 // dependency is not defined 508 other, ok := app.Snap.Apps[dep] 509 if !ok { 510 return fmt.Errorf("before/after references a missing application %q", dep) 511 } 512 513 if !other.IsService() { 514 return fmt.Errorf("before/after references a non-service application %q", dep) 515 } 516 } 517 return nil 518 } 519 520 func validateAppTimeouts(app *AppInfo) error { 521 type T struct { 522 desc string 523 timeout timeout.Timeout 524 } 525 for _, t := range []T{ 526 {"start-timeout", app.StartTimeout}, 527 {"stop-timeout", app.StopTimeout}, 528 {"watchdog-timeout", app.WatchdogTimeout}, 529 } { 530 if t.timeout == 0 { 531 continue 532 } 533 if !app.IsService() { 534 return fmt.Errorf("%s is only applicable to services", t.desc) 535 } 536 if t.timeout < 0 { 537 return fmt.Errorf("%s cannot be negative", t.desc) 538 } 539 } 540 return nil 541 } 542 543 func validateAppTimer(app *AppInfo) error { 544 if app.Timer == nil { 545 return nil 546 } 547 548 if !app.IsService() { 549 return errors.New("timer is only applicable to services") 550 } 551 552 if _, err := timeutil.ParseSchedule(app.Timer.Timer); err != nil { 553 return fmt.Errorf("timer has invalid format: %v", err) 554 } 555 556 return nil 557 } 558 559 func validateAppRestart(app *AppInfo) error { 560 // app.RestartCond value is validated when unmarshalling 561 562 if app.RestartDelay == 0 && app.RestartCond == "" { 563 return nil 564 } 565 566 if app.RestartDelay != 0 { 567 if !app.IsService() { 568 return errors.New("restart-delay is only applicable to services") 569 } 570 571 if app.RestartDelay < 0 { 572 return errors.New("restart-delay cannot be negative") 573 } 574 } 575 576 if app.RestartCond != "" { 577 if !app.IsService() { 578 return errors.New("restart-condition is only applicable to services") 579 } 580 } 581 return nil 582 } 583 584 // appContentWhitelist is the whitelist of legal chars in the "apps" 585 // section of snap.yaml. Do not allow any of [',",`] here or snap-exec 586 // will get confused. chainContentWhitelist is the same, but for the 587 // command-chain, which also doesn't allow whitespace. 588 var appContentWhitelist = regexp.MustCompile(`^[A-Za-z0-9/. _#:$-]*$`) 589 var commandChainContentWhitelist = regexp.MustCompile(`^[A-Za-z0-9/._#:$-]*$`) 590 591 // ValidAppName tells whether a string is a valid application name. 592 func ValidAppName(n string) bool { 593 return naming.ValidateApp(n) == nil 594 } 595 596 // ValidateApp verifies the content in the app info. 597 func ValidateApp(app *AppInfo) error { 598 switch app.Daemon { 599 case "", "simple", "forking", "oneshot", "dbus", "notify": 600 // valid 601 default: 602 return fmt.Errorf(`"daemon" field contains invalid value %q`, app.Daemon) 603 } 604 605 // Validate app name 606 if !ValidAppName(app.Name) { 607 return fmt.Errorf("cannot have %q as app name - use letters, digits, and dash as separator", app.Name) 608 } 609 610 // Validate the rest of the app info 611 checks := map[string]string{ 612 "command": app.Command, 613 "stop-command": app.StopCommand, 614 "reload-command": app.ReloadCommand, 615 "post-stop-command": app.PostStopCommand, 616 "bus-name": app.BusName, 617 } 618 619 for name, value := range checks { 620 if err := validateField(name, value, appContentWhitelist); err != nil { 621 return err 622 } 623 } 624 625 // Also validate the command chain 626 for _, value := range app.CommandChain { 627 if err := validateField("command-chain", value, commandChainContentWhitelist); err != nil { 628 return err 629 } 630 } 631 632 // Socket activation requires the "network-bind" plug 633 if len(app.Sockets) > 0 { 634 if _, ok := app.Plugs["network-bind"]; !ok { 635 return fmt.Errorf(`"network-bind" interface plug is required when sockets are used`) 636 } 637 } 638 639 for _, socket := range app.Sockets { 640 if err := validateAppSocket(socket); err != nil { 641 return fmt.Errorf("invalid definition of socket %q: %v", socket.Name, err) 642 } 643 } 644 645 if err := validateAppRestart(app); err != nil { 646 return err 647 } 648 if err := validateAppOrderNames(app, app.Before); err != nil { 649 return err 650 } 651 if err := validateAppOrderNames(app, app.After); err != nil { 652 return err 653 } 654 655 if err := validateAppTimeouts(app); err != nil { 656 return err 657 } 658 659 // validate stop-mode 660 if err := app.StopMode.Validate(); err != nil { 661 return err 662 } 663 // validate refresh-mode 664 switch app.RefreshMode { 665 case "", "endure", "restart": 666 // valid 667 default: 668 return fmt.Errorf(`"refresh-mode" field contains invalid value %q`, app.RefreshMode) 669 } 670 if app.StopMode != "" && app.Daemon == "" { 671 return fmt.Errorf(`"stop-mode" cannot be used for %q, only for services`, app.Name) 672 } 673 if app.RefreshMode != "" && app.Daemon == "" { 674 return fmt.Errorf(`"refresh-mode" cannot be used for %q, only for services`, app.Name) 675 } 676 677 return validateAppTimer(app) 678 } 679 680 // ValidatePathVariables ensures that given path contains only $SNAP, $SNAP_DATA or $SNAP_COMMON. 681 func ValidatePathVariables(path string) error { 682 for path != "" { 683 start := strings.IndexRune(path, '$') 684 if start < 0 { 685 break 686 } 687 path = path[start+1:] 688 end := strings.IndexFunc(path, func(c rune) bool { 689 return (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && c != '_' 690 }) 691 if end < 0 { 692 end = len(path) 693 } 694 v := path[:end] 695 if v != "SNAP" && v != "SNAP_DATA" && v != "SNAP_COMMON" { 696 return fmt.Errorf("reference to unknown variable %q", "$"+v) 697 } 698 path = path[end:] 699 } 700 return nil 701 } 702 703 func isAbsAndClean(path string) bool { 704 return (filepath.IsAbs(path) || strings.HasPrefix(path, "$")) && filepath.Clean(path) == path 705 } 706 707 // LayoutConstraint abstracts validation of conflicting layout elements. 708 type LayoutConstraint interface { 709 IsOffLimits(path string) bool 710 } 711 712 // mountedTree represents a mounted file-system tree or a bind-mounted directory. 713 type mountedTree string 714 715 // IsOffLimits returns true if the mount point is (perhaps non-proper) prefix of a given path. 716 func (mountPoint mountedTree) IsOffLimits(path string) bool { 717 return strings.HasPrefix(path, string(mountPoint)+"/") || path == string(mountPoint) 718 } 719 720 // mountedFile represents a bind-mounted file. 721 type mountedFile string 722 723 // IsOffLimits returns true if the mount point is (perhaps non-proper) prefix of a given path. 724 func (mountPoint mountedFile) IsOffLimits(path string) bool { 725 return strings.HasPrefix(path, string(mountPoint)+"/") || path == string(mountPoint) 726 } 727 728 // symlinkFile represents a layout using symbolic link. 729 type symlinkFile string 730 731 // IsOffLimits returns true for mounted files if a path is identical to the path of the mount point. 732 func (mountPoint symlinkFile) IsOffLimits(path string) bool { 733 return strings.HasPrefix(path, string(mountPoint)+"/") || path == string(mountPoint) 734 } 735 736 func (layout *Layout) constraint() LayoutConstraint { 737 path := layout.Snap.ExpandSnapVariables(layout.Path) 738 if layout.Symlink != "" { 739 return symlinkFile(path) 740 } else if layout.BindFile != "" { 741 return mountedFile(path) 742 } 743 return mountedTree(path) 744 } 745 746 // layoutRejectionList contains directories that cannot be used as layout 747 // targets. Nothing there, or underneath can be replaced with $SNAP or 748 // $SNAP_DATA, or $SNAP_COMMON content, even from the point of view of a single 749 // snap. 750 var layoutRejectionList = []string{ 751 // Special locations that need to retain their properties: 752 753 // The /dev directory contains essential device nodes and there's no valid 754 // reason to allow snaps to replace it. 755 "/dev", 756 // The /proc directory contains essential process meta-data and 757 // miscellaneous kernel configuration parameters and there is no valid 758 // reason to allow snaps to replace it. 759 "/proc", 760 // The /sys directory exposes many kernel internals, similar to /proc and 761 // there is no known reason to allow snaps to replace it. 762 "/sys", 763 // The media directory is mounted with bi-directional mount event sharing. 764 // Any mount operations there are reflected in the host's view of /media, 765 // which may be either itself or /run/media. 766 "/media", 767 // The /run directory contains various ephemeral information files or 768 // sockets used by various programs. Providing view of the true /run allows 769 // snap applications to be integrated with the rest of the system and 770 // therefore snaps should not be allowed to replace it. 771 "/run", 772 // The /tmp directory contains a private, per-snap, view of /tmp and 773 // there's no valid reason to allow snaps to replace it. 774 "/tmp", 775 // The /var/lib/snapd directory contains essential snapd state and is 776 // sometimes consulted from inside the mount namespace. 777 "/var/lib/snapd", 778 779 // Locations that may be used to attack the host: 780 781 // The firmware is sometimes loaded on demand by the kernel, in response to 782 // a process performing generic I/O to a specific device. In that case the 783 // mount namespace of the process is searched, by the kernel, for the 784 // firmware. Therefore firmware must not be replaceable to prevent 785 // malicious firmware from attacking the host. 786 "/lib/firmware", 787 // Similarly the kernel will load modules and the modules should not be 788 // something that snaps can tamper with. 789 "/lib/modules", 790 791 // Locations that store essential data: 792 793 // The /var/snap directory contains system-wide state of particular snaps 794 // and should not be replaced as it would break content interface 795 // connections that use $SNAP_DATA or $SNAP_COMMON. 796 "/var/snap", 797 // The /home directory contains user data, including $SNAP_USER_DATA, 798 // $SNAP_USER_COMMON and should be disallowed for the same reasons as 799 // /var/snap. 800 "/home", 801 802 // Locations that should be pristine to avoid confusion. 803 804 // There's no known reason to allow snaps to replace things there. 805 "/boot", 806 // The lost+found directory is used by fsck tools to link lost blocks back 807 // into the filesystem tree. Using layouts for this element is just 808 // confusing and there is no valid reason to allow it. 809 "/lost+found", 810 } 811 812 // ValidateLayout ensures that the given layout contains only valid subset of constructs. 813 func ValidateLayout(layout *Layout, constraints []LayoutConstraint) error { 814 si := layout.Snap 815 // Rules for validating layouts: 816 // 817 // * source of mount --bind must be in on of $SNAP, $SNAP_DATA or $SNAP_COMMON 818 // * target of symlink must in in one of $SNAP, $SNAP_DATA, or $SNAP_COMMON 819 // * may not mount on top of an existing layout mountpoint 820 821 mountPoint := layout.Path 822 823 if mountPoint == "" { 824 return errors.New("layout cannot use an empty path") 825 } 826 827 if err := ValidatePathVariables(mountPoint); err != nil { 828 return fmt.Errorf("layout %q uses invalid mount point: %s", layout.Path, err) 829 } 830 mountPoint = si.ExpandSnapVariables(mountPoint) 831 if !isAbsAndClean(mountPoint) { 832 return fmt.Errorf("layout %q uses invalid mount point: must be absolute and clean", layout.Path) 833 } 834 835 for _, path := range layoutRejectionList { 836 // We use the mountedTree constraint as this has the right semantics. 837 if mountedTree(path).IsOffLimits(mountPoint) { 838 return fmt.Errorf("layout %q in an off-limits area", layout.Path) 839 } 840 } 841 842 for _, constraint := range constraints { 843 if constraint.IsOffLimits(mountPoint) { 844 return fmt.Errorf("layout %q underneath prior layout item %q", layout.Path, constraint) 845 } 846 } 847 848 var nused int 849 if layout.Bind != "" { 850 nused++ 851 } 852 if layout.BindFile != "" { 853 nused++ 854 } 855 if layout.Type != "" { 856 nused++ 857 } 858 if layout.Symlink != "" { 859 nused++ 860 } 861 if nused != 1 { 862 return fmt.Errorf("layout %q must define a bind mount, a filesystem mount or a symlink", layout.Path) 863 } 864 865 if layout.Bind != "" || layout.BindFile != "" { 866 mountSource := layout.Bind + layout.BindFile 867 if err := ValidatePathVariables(mountSource); err != nil { 868 return fmt.Errorf("layout %q uses invalid bind mount source %q: %s", layout.Path, mountSource, err) 869 } 870 mountSource = si.ExpandSnapVariables(mountSource) 871 if !isAbsAndClean(mountSource) { 872 return fmt.Errorf("layout %q uses invalid bind mount source %q: must be absolute and clean", layout.Path, mountSource) 873 } 874 // Bind mounts *must* use $SNAP, $SNAP_DATA or $SNAP_COMMON as bind 875 // mount source. This is done so that snaps cannot bypass restrictions 876 // by mounting something outside into their own space. 877 if !strings.HasPrefix(mountSource, si.ExpandSnapVariables("$SNAP")) && 878 !strings.HasPrefix(mountSource, si.ExpandSnapVariables("$SNAP_DATA")) && 879 !strings.HasPrefix(mountSource, si.ExpandSnapVariables("$SNAP_COMMON")) { 880 return fmt.Errorf("layout %q uses invalid bind mount source %q: must start with $SNAP, $SNAP_DATA or $SNAP_COMMON", layout.Path, mountSource) 881 } 882 } 883 884 switch layout.Type { 885 case "tmpfs": 886 case "": 887 // nothing to do 888 default: 889 return fmt.Errorf("layout %q uses invalid filesystem %q", layout.Path, layout.Type) 890 } 891 892 if layout.Symlink != "" { 893 oldname := layout.Symlink 894 if err := ValidatePathVariables(oldname); err != nil { 895 return fmt.Errorf("layout %q uses invalid symlink old name %q: %s", layout.Path, oldname, err) 896 } 897 oldname = si.ExpandSnapVariables(oldname) 898 if !isAbsAndClean(oldname) { 899 return fmt.Errorf("layout %q uses invalid symlink old name %q: must be absolute and clean", layout.Path, oldname) 900 } 901 // Symlinks *must* use $SNAP, $SNAP_DATA or $SNAP_COMMON as oldname. 902 // This is done so that snaps cannot attempt to bypass restrictions 903 // by mounting something outside into their own space. 904 if !strings.HasPrefix(oldname, si.ExpandSnapVariables("$SNAP")) && 905 !strings.HasPrefix(oldname, si.ExpandSnapVariables("$SNAP_DATA")) && 906 !strings.HasPrefix(oldname, si.ExpandSnapVariables("$SNAP_COMMON")) { 907 return fmt.Errorf("layout %q uses invalid symlink old name %q: must start with $SNAP, $SNAP_DATA or $SNAP_COMMON", layout.Path, oldname) 908 } 909 } 910 911 // When new users and groups are supported those must be added to interfaces/mount/spec.go as well. 912 // For now only "root" is allowed (and default). 913 914 switch layout.User { 915 case "root", "": 916 // TODO: allow declared snap user and group names. 917 default: 918 return fmt.Errorf("layout %q uses invalid user %q", layout.Path, layout.User) 919 } 920 switch layout.Group { 921 case "root", "": 922 default: 923 return fmt.Errorf("layout %q uses invalid group %q", layout.Path, layout.Group) 924 } 925 926 if layout.Mode&01777 != layout.Mode { 927 return fmt.Errorf("layout %q uses invalid mode %#o", layout.Path, layout.Mode) 928 } 929 return nil 930 } 931 932 func ValidateCommonIDs(info *Info) error { 933 seen := make(map[string]string, len(info.Apps)) 934 for _, app := range info.Apps { 935 if app.CommonID != "" { 936 if other, was := seen[app.CommonID]; was { 937 return fmt.Errorf("application %q common-id %q must be unique, already used by application %q", 938 app.Name, app.CommonID, other) 939 } 940 seen[app.CommonID] = app.Name 941 } 942 } 943 return nil 944 } 945 946 func ValidateSystemUsernames(info *Info) error { 947 for username := range info.SystemUsernames { 948 if !osutil.IsValidUsername(username) { 949 return fmt.Errorf("invalid system username %q", username) 950 } 951 } 952 return nil 953 } 954 955 // neededDefaultProviders returns the names of all default-providers for 956 // the content plugs that the given snap.Info needs. 957 func NeededDefaultProviders(info *Info) (cps []string) { 958 // XXX: unify with the other places that parse default-providers 959 for _, plug := range info.Plugs { 960 if plug.Interface == "content" { 961 var dprovider string 962 if err := plug.Attr("default-provider", &dprovider); err == nil && dprovider != "" { 963 // usage can be "snap:slot" but we only check 964 // the snap here 965 name := strings.Split(dprovider, ":")[0] 966 cps = append(cps, name) 967 } 968 } 969 } 970 return cps 971 } 972 973 // ValidateBasesAndProviders checks that all bases/default-providers are part of the seed 974 func ValidateBasesAndProviders(snapInfos map[string]*Info) []error { 975 var errs []error 976 for _, info := range snapInfos { 977 // ensure base is available 978 if info.Base != "" && info.Base != "none" { 979 if _, ok := snapInfos[info.Base]; !ok { 980 errs = append(errs, fmt.Errorf("cannot use snap %q: base %q is missing", info.InstanceName(), info.Base)) 981 } 982 } 983 // ensure core is available 984 if info.Base == "" && info.SnapType == TypeApp && info.InstanceName() != "snapd" { 985 if _, ok := snapInfos["core"]; !ok { 986 errs = append(errs, fmt.Errorf(`cannot use snap %q: required snap "core" missing`, info.InstanceName())) 987 } 988 } 989 // ensure default-providers are available 990 for _, dp := range NeededDefaultProviders(info) { 991 if _, ok := snapInfos[dp]; !ok { 992 errs = append(errs, fmt.Errorf("cannot use snap %q: default provider %q is missing", info.InstanceName(), dp)) 993 } 994 } 995 } 996 return errs 997 }