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