github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/boot/modeenv.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 boot 21 22 import ( 23 "bytes" 24 "encoding/json" 25 "fmt" 26 "io" 27 "os" 28 "path/filepath" 29 "reflect" 30 "sort" 31 "strings" 32 33 "github.com/mvo5/goconfigparser" 34 35 "github.com/snapcore/snapd/asserts" 36 "github.com/snapcore/snapd/dirs" 37 "github.com/snapcore/snapd/osutil" 38 "github.com/snapcore/snapd/release" 39 "github.com/snapcore/snapd/secboot" 40 ) 41 42 type bootAssetsMap map[string][]string 43 44 // bootCommandLines is a list of kernel command lines. The command lines are 45 // marshalled as JSON as a comma can be present in the module parameters. 46 type bootCommandLines []string 47 48 // Modeenv is a file on UC20 that provides additional information 49 // about the current mode (run,recover,install) 50 type Modeenv struct { 51 Mode string `key:"mode"` 52 RecoverySystem string `key:"recovery_system"` 53 // CurrentRecoverySystems is a list of labels corresponding to recovery 54 // systems that have been tested or are in the process of being tried, 55 // thus only the run key is resealed for these systems. 56 CurrentRecoverySystems []string `key:"current_recovery_systems"` 57 // GoodRecoverySystems is a list of labels corresponding to recovery 58 // systems that were tested and are prepared to use for recovering. 59 // The fallback keys are resealed for these systems. 60 GoodRecoverySystems []string `key:"good_recovery_systems"` 61 Base string `key:"base"` 62 TryBase string `key:"try_base"` 63 BaseStatus string `key:"base_status"` 64 CurrentKernels []string `key:"current_kernels"` 65 // Model, BrandID, Grade, SignKeyID describe the properties of current 66 // device model. 67 Model string `key:"model"` 68 BrandID string `key:"model,secondary"` 69 Grade string `key:"grade"` 70 ModelSignKeyID string `key:"model_sign_key_id"` 71 // TryModel, TryBrandID, TryGrade, TrySignKeyID describe the properties 72 // of the candidate model. 73 TryModel string `key:"try_model"` 74 TryBrandID string `key:"try_model,secondary"` 75 TryGrade string `key:"try_grade"` 76 TryModelSignKeyID string `key:"try_model_sign_key_id"` 77 // BootFlags is the set of boot flags. Whether this applies for the current 78 // or next boot is not indicated in the modeenv. When the modeenv is read in 79 // the initramfs these flags apply to the current boot and are copied into 80 // a file in /run that userspace should read instead of reading from this 81 // key. When setting boot flags for the next boot, then this key will be 82 // written to and used by the initramfs after rebooting. 83 BootFlags []string `key:"boot_flags"` 84 // CurrentTrustedBootAssets is a map of a run bootloader's asset names to 85 // a list of hashes of the asset contents. Typically the first entry in 86 // the list is a hash of an asset the system currently boots with (or is 87 // expected to have booted with). The second entry, if present, is the 88 // hash of an entry added when an update of the asset was being applied 89 // and will become the sole entry after a successful boot. 90 CurrentTrustedBootAssets bootAssetsMap `key:"current_trusted_boot_assets"` 91 // CurrentTrustedRecoveryBootAssetsMap is a map of a recovery bootloader's 92 // asset names to a list of hashes of the asset contents. Used similarly 93 // to CurrentTrustedBootAssets. 94 CurrentTrustedRecoveryBootAssets bootAssetsMap `key:"current_trusted_recovery_boot_assets"` 95 // CurrentKernelCommandLines is a list of the expected kernel command 96 // lines when booting into run mode. It will typically only be one 97 // element for normal operations, but may contain two elements during 98 // update scenarios. 99 CurrentKernelCommandLines bootCommandLines `key:"current_kernel_command_lines"` 100 // TODO:UC20 add a per recovery system list of kernel command lines 101 102 // read is set to true when a modenv was read successfully 103 read bool 104 105 // originRootdir is set to the root whence the modeenv was 106 // read from, and where it will be written back to 107 originRootdir string 108 109 // extrakeys is all the keys in the modeenv we read from the file but don't 110 // understand, we keep track of this so that if we read a new modeenv with 111 // extra keys and need to rewrite it, we will write those new keys as well 112 extrakeys map[string]string 113 } 114 115 var modeenvKnownKeys = make(map[string]bool) 116 117 func init() { 118 st := reflect.TypeOf(Modeenv{}) 119 num := st.NumField() 120 for i := 0; i < num; i++ { 121 f := st.Field(i) 122 if f.PkgPath != "" { 123 // unexported 124 continue 125 } 126 key := f.Tag.Get("key") 127 if key == "" { 128 panic(fmt.Sprintf("modeenv %s field has no key tag", f.Name)) 129 } 130 const secondaryModifier = ",secondary" 131 if strings.HasSuffix(key, secondaryModifier) { 132 // secondary field in a group fields 133 // corresponding to one file key 134 key := key[:len(key)-len(secondaryModifier)] 135 if !modeenvKnownKeys[key] { 136 panic(fmt.Sprintf("modeenv %s field marked as secondary for not yet defined key %q", f.Name, key)) 137 } 138 continue 139 } 140 if modeenvKnownKeys[key] { 141 panic(fmt.Sprintf("modeenv key %q repeated on %s", key, f.Name)) 142 } 143 modeenvKnownKeys[key] = true 144 } 145 } 146 147 func modeenvFile(rootdir string) string { 148 if rootdir == "" { 149 rootdir = dirs.GlobalRootDir 150 } 151 return dirs.SnapModeenvFileUnder(rootdir) 152 } 153 154 // ReadModeenv attempts to read the modeenv file at 155 // <rootdir>/var/iib/snapd/modeenv. 156 func ReadModeenv(rootdir string) (*Modeenv, error) { 157 modeenvPath := modeenvFile(rootdir) 158 cfg := goconfigparser.New() 159 cfg.AllowNoSectionHeader = true 160 if err := cfg.ReadFile(modeenvPath); err != nil { 161 return nil, err 162 } 163 164 // TODO:UC20: should we check these errors and try to do something? 165 m := Modeenv{ 166 read: true, 167 originRootdir: rootdir, 168 extrakeys: make(map[string]string), 169 } 170 unmarshalModeenvValueFromCfg(cfg, "recovery_system", &m.RecoverySystem) 171 unmarshalModeenvValueFromCfg(cfg, "current_recovery_systems", &m.CurrentRecoverySystems) 172 unmarshalModeenvValueFromCfg(cfg, "good_recovery_systems", &m.GoodRecoverySystems) 173 unmarshalModeenvValueFromCfg(cfg, "boot_flags", &m.BootFlags) 174 175 unmarshalModeenvValueFromCfg(cfg, "mode", &m.Mode) 176 if m.Mode == "" { 177 return nil, fmt.Errorf("internal error: mode is unset") 178 } 179 unmarshalModeenvValueFromCfg(cfg, "base", &m.Base) 180 unmarshalModeenvValueFromCfg(cfg, "base_status", &m.BaseStatus) 181 unmarshalModeenvValueFromCfg(cfg, "try_base", &m.TryBase) 182 183 // current_kernels is a comma-delimited list in a string 184 unmarshalModeenvValueFromCfg(cfg, "current_kernels", &m.CurrentKernels) 185 var bm modeenvModel 186 unmarshalModeenvValueFromCfg(cfg, "model", &bm) 187 m.BrandID = bm.brandID 188 m.Model = bm.model 189 // expect the caller to validate the grade 190 unmarshalModeenvValueFromCfg(cfg, "grade", &m.Grade) 191 unmarshalModeenvValueFromCfg(cfg, "model_sign_key_id", &m.ModelSignKeyID) 192 var tryBm modeenvModel 193 unmarshalModeenvValueFromCfg(cfg, "try_model", &tryBm) 194 m.TryBrandID = tryBm.brandID 195 m.TryModel = tryBm.model 196 unmarshalModeenvValueFromCfg(cfg, "try_grade", &m.TryGrade) 197 unmarshalModeenvValueFromCfg(cfg, "try_model_sign_key_id", &m.TryModelSignKeyID) 198 199 unmarshalModeenvValueFromCfg(cfg, "current_trusted_boot_assets", &m.CurrentTrustedBootAssets) 200 unmarshalModeenvValueFromCfg(cfg, "current_trusted_recovery_boot_assets", &m.CurrentTrustedRecoveryBootAssets) 201 unmarshalModeenvValueFromCfg(cfg, "current_kernel_command_lines", &m.CurrentKernelCommandLines) 202 203 // save all the rest of the keys we don't understand 204 keys, err := cfg.Options("") 205 if err != nil { 206 return nil, err 207 } 208 for _, k := range keys { 209 if !modeenvKnownKeys[k] { 210 val, err := cfg.Get("", k) 211 if err != nil { 212 return nil, err 213 } 214 m.extrakeys[k] = val 215 } 216 } 217 218 return &m, nil 219 } 220 221 // deepEqual compares two modeenvs to ensure they are textually the same. It 222 // does not consider whether the modeenvs were read from disk or created purely 223 // in memory. It also does not sort or otherwise mutate any sub-objects, 224 // performing simple strict verification of sub-objects. 225 func (m *Modeenv) deepEqual(m2 *Modeenv) bool { 226 b, err := json.Marshal(m) 227 if err != nil { 228 return false 229 } 230 b2, err := json.Marshal(m2) 231 if err != nil { 232 return false 233 } 234 return bytes.Equal(b, b2) 235 } 236 237 // Copy will make a deep copy of a Modeenv. 238 func (m *Modeenv) Copy() (*Modeenv, error) { 239 // to avoid hard-coding all fields here and manually copying everything, we 240 // take the easy way out and serialize to json then re-import into a 241 // empty Modeenv 242 b, err := json.Marshal(m) 243 if err != nil { 244 return nil, err 245 } 246 m2 := &Modeenv{} 247 err = json.Unmarshal(b, m2) 248 if err != nil { 249 return nil, err 250 } 251 252 // manually copy the unexported fields as they won't be in the JSON 253 m2.read = m.read 254 m2.originRootdir = m.originRootdir 255 return m2, nil 256 } 257 258 // Write outputs the modeenv to the file where it was read, only valid on 259 // modeenv that has been read. 260 func (m *Modeenv) Write() error { 261 if m.read { 262 return m.WriteTo(m.originRootdir) 263 } 264 return fmt.Errorf("internal error: must use WriteTo with modeenv not read from disk") 265 } 266 267 // WriteTo outputs the modeenv to the file at <rootdir>/var/lib/snapd/modeenv. 268 func (m *Modeenv) WriteTo(rootdir string) error { 269 modeenvPath := modeenvFile(rootdir) 270 271 if err := os.MkdirAll(filepath.Dir(modeenvPath), 0755); err != nil { 272 return err 273 } 274 buf := bytes.NewBuffer(nil) 275 if m.Mode == "" { 276 return fmt.Errorf("internal error: mode is unset") 277 } 278 marshalModeenvEntryTo(buf, "mode", m.Mode) 279 marshalModeenvEntryTo(buf, "recovery_system", m.RecoverySystem) 280 marshalModeenvEntryTo(buf, "current_recovery_systems", m.CurrentRecoverySystems) 281 marshalModeenvEntryTo(buf, "good_recovery_systems", m.GoodRecoverySystems) 282 marshalModeenvEntryTo(buf, "boot_flags", m.BootFlags) 283 marshalModeenvEntryTo(buf, "base", m.Base) 284 marshalModeenvEntryTo(buf, "try_base", m.TryBase) 285 marshalModeenvEntryTo(buf, "base_status", m.BaseStatus) 286 marshalModeenvEntryTo(buf, "current_kernels", strings.Join(m.CurrentKernels, ",")) 287 if m.Model != "" || m.Grade != "" { 288 if m.Model == "" { 289 return fmt.Errorf("internal error: model is unset") 290 } 291 if m.BrandID == "" { 292 return fmt.Errorf("internal error: brand is unset") 293 } 294 marshalModeenvEntryTo(buf, "model", &modeenvModel{brandID: m.BrandID, model: m.Model}) 295 } 296 // TODO: complain when grade or key are unset 297 marshalModeenvEntryTo(buf, "grade", m.Grade) 298 marshalModeenvEntryTo(buf, "model_sign_key_id", m.ModelSignKeyID) 299 if m.TryModel != "" || m.TryGrade != "" { 300 if m.TryModel == "" { 301 return fmt.Errorf("internal error: try model is unset") 302 } 303 if m.TryBrandID == "" { 304 return fmt.Errorf("internal error: try brand is unset") 305 } 306 marshalModeenvEntryTo(buf, "try_model", &modeenvModel{brandID: m.TryBrandID, model: m.TryModel}) 307 } 308 marshalModeenvEntryTo(buf, "try_grade", m.TryGrade) 309 marshalModeenvEntryTo(buf, "try_model_sign_key_id", m.TryModelSignKeyID) 310 marshalModeenvEntryTo(buf, "current_trusted_boot_assets", m.CurrentTrustedBootAssets) 311 marshalModeenvEntryTo(buf, "current_trusted_recovery_boot_assets", m.CurrentTrustedRecoveryBootAssets) 312 marshalModeenvEntryTo(buf, "current_kernel_command_lines", m.CurrentKernelCommandLines) 313 314 // write all the extra keys at the end 315 // sort them for test convenience 316 extraKeys := make([]string, 0, len(m.extrakeys)) 317 for k := range m.extrakeys { 318 extraKeys = append(extraKeys, k) 319 } 320 sort.Strings(extraKeys) 321 for _, k := range extraKeys { 322 marshalModeenvEntryTo(buf, k, m.extrakeys[k]) 323 } 324 325 if err := osutil.AtomicWriteFile(modeenvPath, buf.Bytes(), 0644, 0); err != nil { 326 return err 327 } 328 return nil 329 } 330 331 // modelForSealing is a helper type that implements 332 // github.com/snapcore/secboot.SnapModel interface. 333 type modelForSealing struct { 334 brandID string 335 model string 336 grade asserts.ModelGrade 337 modelSignKeyID string 338 } 339 340 // dummy to verify interface match 341 var _ secboot.ModelForSealing = (*modelForSealing)(nil) 342 343 func (m *modelForSealing) BrandID() string { return m.brandID } 344 func (m *modelForSealing) SignKeyID() string { return m.modelSignKeyID } 345 func (m *modelForSealing) Model() string { return m.model } 346 func (m *modelForSealing) Grade() asserts.ModelGrade { return m.grade } 347 func (m *modelForSealing) Series() string { return release.Series } 348 349 // modelUniqueID returns a unique ID which can be used as a map index of the 350 // provided model. 351 func modelUniqueID(m secboot.ModelForSealing) string { 352 return fmt.Sprintf("%s/%s,%s,%s", m.BrandID(), m.Model(), m.Grade(), m.SignKeyID()) 353 } 354 355 // ModelForSealing returns a wrapper implementing 356 // github.com/snapcore/secboot.SnapModel interface which describes the current 357 // model. 358 func (m *Modeenv) ModelForSealing() secboot.ModelForSealing { 359 return &modelForSealing{ 360 brandID: m.BrandID, 361 model: m.Model, 362 grade: asserts.ModelGrade(m.Grade), 363 modelSignKeyID: m.ModelSignKeyID, 364 } 365 } 366 367 // TryModelForSealing returns a wrapper implementing 368 // github.com/snapcore/secboot.SnapModel interface which describes the candidate 369 // or try model. 370 func (m *Modeenv) TryModelForSealing() secboot.ModelForSealing { 371 return &modelForSealing{ 372 brandID: m.TryBrandID, 373 model: m.TryModel, 374 grade: asserts.ModelGrade(m.TryGrade), 375 modelSignKeyID: m.TryModelSignKeyID, 376 } 377 } 378 379 func (m *Modeenv) setModel(model *asserts.Model) { 380 m.Model = model.Model() 381 m.BrandID = model.BrandID() 382 m.Grade = string(model.Grade()) 383 m.ModelSignKeyID = model.SignKeyID() 384 } 385 386 func (m *Modeenv) setTryModel(model *asserts.Model) { 387 m.TryModel = model.Model() 388 m.TryBrandID = model.BrandID() 389 m.TryGrade = string(model.Grade()) 390 m.TryModelSignKeyID = model.SignKeyID() 391 } 392 393 func (m *Modeenv) clearTryModel() { 394 m.TryModel = "" 395 m.TryBrandID = "" 396 m.TryGrade = "" 397 m.TryModelSignKeyID = "" 398 } 399 400 type modeenvValueMarshaller interface { 401 MarshalModeenvValue() (string, error) 402 } 403 404 type modeenvValueUnmarshaller interface { 405 UnmarshalModeenvValue(value string) error 406 } 407 408 // marshalModeenvEntryTo marshals to out what as value for an entry 409 // with the given key. If what is empty this is a no-op. 410 func marshalModeenvEntryTo(out io.Writer, key string, what interface{}) error { 411 var asString string 412 switch v := what.(type) { 413 case string: 414 if v == "" { 415 return nil 416 } 417 asString = v 418 case []string: 419 if len(v) == 0 { 420 return nil 421 } 422 asString = asModeenvStringList(v) 423 default: 424 if vm, ok := what.(modeenvValueMarshaller); ok { 425 marshalled, err := vm.MarshalModeenvValue() 426 if err != nil { 427 return fmt.Errorf("cannot marshal value for key %q: %v", key, err) 428 } 429 asString = marshalled 430 } else if jm, ok := what.(json.Marshaler); ok { 431 marshalled, err := jm.MarshalJSON() 432 if err != nil { 433 return fmt.Errorf("cannot marshal value for key %q as JSON: %v", key, err) 434 } 435 asString = string(marshalled) 436 if asString == "null" { 437 // no need to keep nulls in the modeenv 438 return nil 439 } 440 } else { 441 return fmt.Errorf("internal error: cannot marshal unsupported type %T value %v for key %q", what, what, key) 442 } 443 } 444 _, err := fmt.Fprintf(out, "%s=%s\n", key, asString) 445 return err 446 } 447 448 // unmarshalModeenvValueFromCfg unmarshals the value of the entry with 449 // th given key to dest. If there's no such entry dest might be left 450 // empty. 451 func unmarshalModeenvValueFromCfg(cfg *goconfigparser.ConfigParser, key string, dest interface{}) error { 452 if dest == nil { 453 return fmt.Errorf("internal error: cannot unmarshal to nil") 454 } 455 kv, _ := cfg.Get("", key) 456 457 switch v := dest.(type) { 458 case *string: 459 *v = kv 460 case *[]string: 461 *v = splitModeenvStringList(kv) 462 default: 463 if vm, ok := v.(modeenvValueUnmarshaller); ok { 464 if err := vm.UnmarshalModeenvValue(kv); err != nil { 465 return fmt.Errorf("cannot unmarshal modeenv value %q to %T: %v", kv, dest, err) 466 } 467 return nil 468 } else if jm, ok := v.(json.Unmarshaler); ok { 469 if len(kv) == 0 { 470 // leave jm empty 471 return nil 472 } 473 if err := jm.UnmarshalJSON([]byte(kv)); err != nil { 474 return fmt.Errorf("cannot unmarshal modeenv value %q as JSON to %T: %v", kv, dest, err) 475 } 476 return nil 477 } 478 return fmt.Errorf("internal error: cannot unmarshal value %q for unsupported type %T", kv, dest) 479 } 480 return nil 481 } 482 483 func splitModeenvStringList(v string) []string { 484 if v == "" { 485 return nil 486 } 487 split := strings.Split(v, ",") 488 // drop empty strings 489 nonEmpty := make([]string, 0, len(split)) 490 for _, one := range split { 491 if one != "" { 492 nonEmpty = append(nonEmpty, one) 493 } 494 } 495 if len(nonEmpty) == 0 { 496 return nil 497 } 498 return nonEmpty 499 } 500 501 func asModeenvStringList(v []string) string { 502 return strings.Join(v, ",") 503 } 504 505 type modeenvModel struct { 506 brandID, model string 507 } 508 509 func (m *modeenvModel) MarshalModeenvValue() (string, error) { 510 return fmt.Sprintf("%s/%s", m.brandID, m.model), nil 511 } 512 513 func (m *modeenvModel) UnmarshalModeenvValue(brandSlashModel string) error { 514 if bsmSplit := strings.SplitN(brandSlashModel, "/", 2); len(bsmSplit) == 2 { 515 if bsmSplit[0] != "" && bsmSplit[1] != "" { 516 m.brandID = bsmSplit[0] 517 m.model = bsmSplit[1] 518 } 519 } 520 return nil 521 } 522 523 func (b bootAssetsMap) MarshalJSON() ([]byte, error) { 524 asMap := map[string][]string(b) 525 return json.Marshal(asMap) 526 } 527 528 func (b *bootAssetsMap) UnmarshalJSON(data []byte) error { 529 var asMap map[string][]string 530 if err := json.Unmarshal(data, &asMap); err != nil { 531 return err 532 } 533 *b = bootAssetsMap(asMap) 534 return nil 535 } 536 537 func (s bootCommandLines) MarshalJSON() ([]byte, error) { 538 return json.Marshal([]string(s)) 539 } 540 541 func (s *bootCommandLines) UnmarshalJSON(data []byte) error { 542 var asList []string 543 if err := json.Unmarshal(data, &asList); err != nil { 544 return err 545 } 546 *s = bootCommandLines(asList) 547 return nil 548 }