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  }