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