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