github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/gadget/validate.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2019-2020 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 gadget 21 22 import ( 23 "fmt" 24 "path/filepath" 25 "sort" 26 "strings" 27 28 "github.com/snapcore/snapd/kernel" 29 "github.com/snapcore/snapd/osutil" 30 "github.com/snapcore/snapd/strutil" 31 ) 32 33 // ValidationConstraints carries extra constraints on top of those 34 // implied by the model to use for gadget validation. 35 // They might be constraints that are determined only at runtime. 36 type ValidationConstraints struct { 37 // EncryptedData when true indicates that the gadget will be used on a 38 // device where the data partition will be encrypted. 39 EncryptedData bool 40 } 41 42 // Validate checks that the given gadget metadata matches the 43 // consistency rules for role usage, labels etc as implied by the 44 // model and extra constraints that might be known only at runtime. 45 func Validate(info *Info, model Model, extra *ValidationConstraints) error { 46 if err := ruleValidateVolumes(info.Volumes, model); err != nil { 47 return err 48 } 49 if extra != nil { 50 if extra.EncryptedData { 51 if err := validateEncryptionSupport(info); err != nil { 52 return fmt.Errorf("gadget does not support encrypted data: %v", err) 53 } 54 } 55 } 56 return nil 57 } 58 59 func validateEncryptionSupport(info *Info) error { 60 for name, vol := range info.Volumes { 61 var haveSave bool 62 for _, s := range vol.Structure { 63 if s.Role == SystemSave { 64 haveSave = true 65 } 66 } 67 if !haveSave { 68 return fmt.Errorf("volume %q has no structure with system-save role", name) 69 } 70 // TODO:UC20: shall we make sure that size of ubuntu-save is reasonable? 71 } 72 return nil 73 } 74 75 type roleInstance struct { 76 volName string 77 s *VolumeStructure 78 } 79 80 func ruleValidateVolumes(vols map[string]*Volume, model Model) error { 81 roles := map[string]*roleInstance{ 82 SystemSeed: nil, 83 SystemBoot: nil, 84 SystemData: nil, 85 SystemSave: nil, 86 } 87 88 xvols := "" 89 if len(vols) != 1 { 90 xvols = " across volumes" 91 } 92 93 // TODO: is this too strict for old gadgets? 94 for name, v := range vols { 95 for i := range v.Structure { 96 s := &v.Structure[i] 97 if inst, ok := roles[s.Role]; ok { 98 if inst != nil { 99 return fmt.Errorf("cannot have more than one partition with %s role%s", s.Role, xvols) 100 } 101 roles[s.Role] = &roleInstance{ 102 volName: name, 103 s: s, 104 } 105 } 106 } 107 } 108 109 expectedSeed := false 110 if model != nil { 111 expectedSeed = wantsSystemSeed(model) 112 } else { 113 // if system-seed role is mentioned assume the uc20 114 // consistency rules 115 expectedSeed = roles[SystemSeed] != nil 116 } 117 118 for name, v := range vols { 119 if err := ruleValidateVolume(name, v, expectedSeed); err != nil { 120 return fmt.Errorf("invalid volume %q: %v", name, err) 121 } 122 } 123 124 if err := ensureRolesConsistency(roles, expectedSeed); err != nil { 125 return err 126 } 127 128 return nil 129 } 130 131 func ruleValidateVolume(name string, vol *Volume, expectedSeed bool) error { 132 for idx, s := range vol.Structure { 133 if err := ruleValidateVolumeStructure(&s, expectedSeed); err != nil { 134 return fmt.Errorf("invalid structure %v: %v", fmtIndexAndName(idx, s.Name), err) 135 } 136 } 137 138 return nil 139 } 140 141 func ruleValidateVolumeStructure(vs *VolumeStructure, expectedSeed bool) error { 142 var reservedLabels []string 143 if expectedSeed { 144 reservedLabels = reservedLabelsWithSeed 145 } else { 146 reservedLabels = reservedLabelsWithoutSeed 147 } 148 if err := validateReservedLabels(vs, reservedLabels); err != nil { 149 return err 150 } 151 return nil 152 } 153 154 var ( 155 reservedLabelsWithSeed = []string{ 156 ubuntuBootLabel, 157 ubuntuSeedLabel, 158 ubuntuDataLabel, 159 ubuntuSaveLabel, 160 } 161 162 // labels that we don't expect to be used on a UC16/18 system: 163 // * seed needs to be the ESP so there's a conflict 164 // * ubuntu-data is the main data partition which on UC16/18 165 // is expected to be named writable instead 166 reservedLabelsWithoutSeed = []string{ 167 ubuntuSeedLabel, 168 ubuntuDataLabel, 169 } 170 ) 171 172 func validateReservedLabels(vs *VolumeStructure, reservedLabels []string) error { 173 if vs.Role != "" { 174 // structure specifies a role, its labels will be checked later 175 return nil 176 } 177 if vs.Label == "" { 178 return nil 179 } 180 if strutil.ListContains(reservedLabels, vs.Label) { 181 // a structure without a role uses one of reserved labels 182 return fmt.Errorf("label %q is reserved", vs.Label) 183 } 184 return nil 185 } 186 187 func ensureRolesConsistency(roles map[string]*roleInstance, expectedSeed bool) error { 188 // TODO: should we validate usage of uc20 specific system-recovery-{image,select} 189 // roles too? they should only be used on uc20 systems, so models that 190 // have a grade set and are not classic 191 192 switch { 193 case roles[SystemSeed] == nil && roles[SystemData] == nil: 194 if expectedSeed { 195 return fmt.Errorf("model requires system-seed partition, but no system-seed or system-data partition found") 196 } 197 case roles[SystemSeed] != nil && roles[SystemData] == nil: 198 return fmt.Errorf("the system-seed role requires system-data to be defined") 199 case roles[SystemSeed] == nil && roles[SystemData] != nil: 200 // error if we have the SystemSeed constraint but no actual system-seed structure 201 if expectedSeed { 202 return fmt.Errorf("model requires system-seed structure, but none was found") 203 } 204 // without SystemSeed, system-data label must be implicit or writable 205 if err := checkImplicitLabel(SystemData, roles[SystemData].s, implicitSystemDataLabel); err != nil { 206 return err 207 } 208 case roles[SystemSeed] != nil && roles[SystemData] != nil: 209 // error if we don't have the SystemSeed constraint but we have a system-seed structure 210 if !expectedSeed { 211 return fmt.Errorf("model does not support the system-seed role") 212 } 213 if err := checkSeedDataImplicitLabels(roles); err != nil { 214 return err 215 } 216 } 217 if roles[SystemSave] != nil { 218 if !expectedSeed { 219 return fmt.Errorf("model does not support the system-save role") 220 } 221 if err := ensureSystemSaveRuleConsistency(roles); err != nil { 222 return err 223 } 224 } 225 226 if expectedSeed { 227 // make sure that all roles come from the same volume 228 // TODO:UC20: there is more to do in order to support multi-volume situations 229 230 // if SystemSeed is unset we must have failed earlier 231 seedVolName := roles[SystemSeed].volName 232 233 for _, otherRole := range []string{SystemBoot, SystemData, SystemSave} { 234 ri := roles[otherRole] 235 if ri != nil && ri.volName != seedVolName { 236 return fmt.Errorf("system-boot, system-data, and system-save are expected to share the same volume as system-seed") 237 } 238 } 239 } 240 241 return nil 242 } 243 244 func ensureSystemSaveRuleConsistency(roles map[string]*roleInstance) error { 245 if roles[SystemData] == nil || roles[SystemSeed] == nil { 246 // previous checks should stop reaching here 247 return fmt.Errorf("internal error: system-save requires system-seed and system-data structures") 248 } 249 if err := checkImplicitLabel(SystemSave, roles[SystemSave].s, ubuntuSaveLabel); err != nil { 250 return err 251 } 252 return nil 253 } 254 255 func checkSeedDataImplicitLabels(roles map[string]*roleInstance) error { 256 if err := checkImplicitLabel(SystemData, roles[SystemData].s, ubuntuDataLabel); err != nil { 257 return err 258 } 259 if err := checkImplicitLabel(SystemSeed, roles[SystemSeed].s, ubuntuSeedLabel); err != nil { 260 return err 261 } 262 return nil 263 } 264 265 func checkImplicitLabel(role string, vs *VolumeStructure, implicitLabel string) error { 266 if vs.Label != "" && vs.Label != implicitLabel { 267 return fmt.Errorf("%s structure must have an implicit label or %q, not %q", role, implicitLabel, vs.Label) 268 269 } 270 return nil 271 } 272 273 // content validation 274 275 func splitKernelRef(kernelRef string) (asset, content string, err error) { 276 // kernel ref has format: $kernel:<asset-name>/<content-path> where 277 // asset name and content is listed in kernel.yaml, content looks like a 278 // sane path 279 if !strings.HasPrefix(kernelRef, "$kernel:") { 280 return "", "", fmt.Errorf("internal error: splitKernelRef called for non kernel ref %q", kernelRef) 281 } 282 assetAndContent := kernelRef[len("$kernel:"):] 283 l := strings.SplitN(assetAndContent, "/", 2) 284 if len(l) < 2 { 285 return "", "", fmt.Errorf("invalid asset and content in kernel ref %q", kernelRef) 286 } 287 asset = l[0] 288 content = l[1] 289 nonDirContent := content 290 if strings.HasSuffix(nonDirContent, "/") { 291 // a single trailing / is allowed to indicate all content under directory 292 nonDirContent = strings.TrimSuffix(nonDirContent, "/") 293 } 294 if len(asset) == 0 || len(content) == 0 { 295 return "", "", fmt.Errorf("missing asset name or content in kernel ref %q", kernelRef) 296 } 297 if filepath.Clean(nonDirContent) != nonDirContent || strings.Contains(content, "..") || nonDirContent == "/" { 298 return "", "", fmt.Errorf("invalid content in kernel ref %q", kernelRef) 299 } 300 if !kernel.ValidAssetName.MatchString(asset) { 301 return "", "", fmt.Errorf("invalid asset name in kernel ref %q", kernelRef) 302 } 303 return asset, content, nil 304 } 305 306 func validateVolumeContentsPresence(gadgetSnapRootDir string, vol *LaidOutVolume) error { 307 // bare structure content is checked to exist during layout 308 // make sure that filesystem content source paths exist as well 309 for _, s := range vol.LaidOutStructure { 310 if !s.HasFilesystem() { 311 continue 312 } 313 for _, c := range s.Content { 314 // TODO: detect and skip Content with "$kernel:" style 315 // refs if there is no kernelSnapRootDir passed in as 316 // well 317 if strings.HasPrefix(c.UnresolvedSource, "$kernel:") { 318 // This only validates that the ref is valid. 319 // Resolving happens with ResolveContentPaths() 320 if _, _, err := splitKernelRef(c.UnresolvedSource); err != nil { 321 return fmt.Errorf("cannot use kernel reference %q: %v", c.UnresolvedSource, err) 322 } 323 continue 324 } 325 realSource := filepath.Join(gadgetSnapRootDir, c.UnresolvedSource) 326 if !osutil.FileExists(realSource) { 327 return fmt.Errorf("structure %v, content %v: source path does not exist", s, c) 328 } 329 if strings.HasSuffix(c.UnresolvedSource, "/") { 330 // expecting a directory 331 if err := checkSourceIsDir(realSource + "/"); err != nil { 332 return fmt.Errorf("structure %v, content %v: %v", s, c, err) 333 } 334 } 335 } 336 } 337 return nil 338 } 339 340 // ValidateContent checks whether the given directory contains valid matching content with respect to the given pre-validated gadget metadata. 341 func ValidateContent(info *Info, gadgetSnapRootDir, kernelSnapRootDir string) error { 342 // TODO: also validate that only one "<bl-name>.conf" file is 343 // in the root directory of the gadget snap, because the 344 // "<bl-name>.conf" file indicates precisely which bootloader 345 // the gadget uses and as such there cannot be more than one 346 // such bootloader 347 for name, vol := range info.Volumes { 348 constraints := DefaultConstraints 349 // At this point we may not know what kernel will be used 350 // with the gadget yet. Skip this check in this case. 351 if kernelSnapRootDir == "" { 352 constraints.SkipResolveContent = true 353 } 354 lv, err := LayoutVolume(gadgetSnapRootDir, kernelSnapRootDir, vol, constraints) 355 if err != nil { 356 return fmt.Errorf("invalid layout of volume %q: %v", name, err) 357 } 358 if err := validateVolumeContentsPresence(gadgetSnapRootDir, lv); err != nil { 359 return fmt.Errorf("invalid volume %q: %v", name, err) 360 } 361 } 362 363 // Ensure that at least one kernel.yaml reference can be resolved 364 // by the gadget 365 if kernelSnapRootDir != "" { 366 kinfo, err := kernel.ReadInfo(kernelSnapRootDir) 367 if err != nil { 368 return err 369 } 370 resolvedOnce := false 371 for _, vol := range info.Volumes { 372 err := gadgetVolumeConsumesOneKernelUpdateAsset(vol, kinfo) 373 if err == nil { 374 resolvedOnce = true 375 } 376 } 377 if !resolvedOnce { 378 return fmt.Errorf("no asset from the kernel.yaml needing synced update is consumed by the gadget at %q", gadgetSnapRootDir) 379 } 380 } 381 382 return nil 383 } 384 385 // gadgetVolumeConsumesOneKernelUpdateAsset ensures that at least one kernel 386 // assets from the kernel.yaml has a reference in the given 387 // LaidOutVolume. 388 func gadgetVolumeConsumesOneKernelUpdateAsset(pNew *Volume, kernelInfo *kernel.Info) error { 389 notFoundAssets := make([]string, 0, len(kernelInfo.Assets)) 390 for assetName, asset := range kernelInfo.Assets { 391 if !asset.Update { 392 continue 393 } 394 for _, ps := range pNew.Structure { 395 for _, rc := range ps.Content { 396 pathOrRef := rc.UnresolvedSource 397 if !strings.HasPrefix(pathOrRef, "$kernel:") { 398 // regular asset from the gadget snap 399 continue 400 } 401 wantedAsset, _, err := splitKernelRef(pathOrRef) 402 if err != nil { 403 return err 404 } 405 if assetName == wantedAsset { 406 // found a valid kernel asset, 407 // that is enough 408 return nil 409 } 410 } 411 } 412 notFoundAssets = append(notFoundAssets, assetName) 413 } 414 if len(notFoundAssets) > 0 { 415 sort.Strings(notFoundAssets) 416 return fmt.Errorf("gadget does not consume any of the kernel assets needing synced update %s", strutil.Quoted(notFoundAssets)) 417 } 418 return nil 419 }