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