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