github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/boot/flags.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 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 boot
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  
    30  	"github.com/snapcore/snapd/bootloader"
    31  	"github.com/snapcore/snapd/dirs"
    32  	"github.com/snapcore/snapd/osutil"
    33  	"github.com/snapcore/snapd/strutil"
    34  )
    35  
    36  var (
    37  	errNotUC20 = fmt.Errorf("cannot get boot flags on non-UC20 device")
    38  
    39  	understoodBootFlags = []string{
    40  		// the factory boot flag is set to indicate that this is a
    41  		// boot inside a factory environment
    42  		"factory",
    43  	}
    44  )
    45  
    46  type unknownFlagError string
    47  
    48  func (e unknownFlagError) Error() string {
    49  	return string(e)
    50  }
    51  
    52  func IsUnknownBootFlagError(e error) bool {
    53  	_, ok := e.(unknownFlagError)
    54  	return ok
    55  }
    56  
    57  // splitBootFlagString splits the given comma delimited list of boot flags, removing
    58  // empty strings.
    59  // Note that this explicitly does not filter out unsupported boot flags in the
    60  // off chance that an old version of the initramfs is reading new boot flags
    61  // written by a new version of snapd in userspace on a previous boot.
    62  func splitBootFlagString(s string) []string {
    63  	flags := []string{}
    64  	for _, flag := range strings.Split(s, ",") {
    65  		if flag != "" {
    66  			flags = append(flags, flag)
    67  		}
    68  	}
    69  
    70  	return flags
    71  }
    72  
    73  func checkBootFlagList(flags []string, allowList []string) ([]string, error) {
    74  	allowedFlags := make([]string, 0, len(flags))
    75  	disallowedFlags := make([]string, 0, len(flags))
    76  	if len(allowList) != 0 {
    77  		// then we need to enforce the allow list
    78  		for _, flag := range flags {
    79  			if strutil.ListContains(allowList, flag) {
    80  				allowedFlags = append(allowedFlags, flag)
    81  			} else {
    82  				if flag == "" {
    83  					// this is to make it more obvious
    84  					disallowedFlags = append(disallowedFlags, `""`)
    85  				} else {
    86  					disallowedFlags = append(disallowedFlags, flag)
    87  				}
    88  			}
    89  		}
    90  	}
    91  	if len(allowedFlags) != len(flags) {
    92  		return allowedFlags, unknownFlagError(fmt.Sprintf("unknown boot flags %v not allowed", disallowedFlags))
    93  	}
    94  	return flags, nil
    95  }
    96  
    97  func serializeBootFlags(flags []string) string {
    98  	// drop empty strings before serializing
    99  	nonEmptyFlags := make([]string, 0, len(flags))
   100  	for _, flag := range flags {
   101  		if strings.TrimSpace(flag) != "" {
   102  			nonEmptyFlags = append(nonEmptyFlags, flag)
   103  		}
   104  	}
   105  
   106  	return strings.Join(nonEmptyFlags, ",")
   107  }
   108  
   109  // setImageBootFlags sets the provided flags in the provided
   110  // bootenv-representing map. It first checks them.
   111  func setImageBootFlags(flags []string, blVars map[string]string) error {
   112  	// check that the flagList is supported
   113  	if _, err := checkBootFlagList(flags, understoodBootFlags); err != nil {
   114  		return err
   115  	}
   116  
   117  	// also ensure that the serialized value of the boot flags fits inside the
   118  	// bootenv value, on lk systems the max size of a bootenv value is 255 chars
   119  	s := serializeBootFlags(flags)
   120  	if len(s) > 254 {
   121  		return fmt.Errorf("internal error: boot flags too large to fit inside bootenv value")
   122  	}
   123  
   124  	blVars["snapd_boot_flags"] = s
   125  	return nil
   126  }
   127  
   128  // InitramfsActiveBootFlags returns the set of boot flags that are currently set
   129  // for the current boot, by querying them directly from the source. This method
   130  // is only meant to be used from the initramfs, since it may query the bootenv
   131  // or query the modeenv depending on the current mode of the system.
   132  // For detecting the current set of boot flags outside of the initramfs, use
   133  // BootFlags(), which will query for the runtime version of the flags in /run
   134  // that the initramfs will have setup for userspace.
   135  // Note that no filtering is done on the flags in order to allow new flags to be
   136  // used by a userspace that is newer than the initramfs, but empty flags will be
   137  // dropped automatically.
   138  // Only to be used on UC20+ systems with recovery systems.
   139  func InitramfsActiveBootFlags(mode string) ([]string, error) {
   140  	switch mode {
   141  	case ModeRecover:
   142  		// no boot flags are consumed / used on recover mode, so return nothing
   143  		return nil, nil
   144  
   145  	case ModeRun:
   146  		// boot flags come from the modeenv
   147  		modeenv, err := ReadModeenv(InitramfsWritableDir)
   148  		if err != nil {
   149  			return nil, err
   150  		}
   151  
   152  		// TODO: consider passing in the modeenv or returning the modeenv here
   153  		// to reduce the number of times we read the modeenv ?
   154  		return modeenv.BootFlags, nil
   155  
   156  	case ModeInstall:
   157  		// boot flags always come from the bootenv of the recovery bootloader
   158  		// in install mode
   159  
   160  		opts := &bootloader.Options{
   161  			Role: bootloader.RoleRecovery,
   162  		}
   163  		bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts)
   164  		if err != nil {
   165  			return nil, err
   166  		}
   167  
   168  		m, err := bl.GetBootVars("snapd_boot_flags")
   169  		if err != nil {
   170  			return nil, err
   171  		}
   172  
   173  		return splitBootFlagString(m["snapd_boot_flags"]), nil
   174  
   175  	default:
   176  		return nil, fmt.Errorf("internal error: unsupported mode %q", mode)
   177  	}
   178  }
   179  
   180  // InitramfsExposeBootFlagsForSystem sets the boot flags for the current boot in
   181  // the /run file that will be consulted in userspace by BootFlags() below. It is
   182  // meant to be used only from the initramfs.
   183  // Note that no filtering is done on the flags in order to allow new flags to be
   184  // used by a userspace that is newer than the initramfs, but empty flags will be
   185  // dropped automatically.
   186  // Only to be used on UC20+ systems with recovery systems.
   187  func InitramfsExposeBootFlagsForSystem(flags []string) error {
   188  	s := serializeBootFlags(flags)
   189  
   190  	if err := os.MkdirAll(filepath.Dir(snapBootFlagsFile), 0755); err != nil {
   191  		return err
   192  	}
   193  
   194  	return ioutil.WriteFile(snapBootFlagsFile, []byte(s), 0644)
   195  }
   196  
   197  // BootFlags returns the current set of boot flags active for this boot. It uses
   198  // the initramfs-capture values in /run. The flags from the initramfs are
   199  // checked against the currently understood set of flags, so that if there are
   200  // unrecognized flags, they are removed from the returned list and the returned
   201  // error will have IsUnknownFlagErroror() return true. This is to allow gracefully
   202  // ignoring unknown boot flags while still processing supported flags.
   203  // Only to be used on UC20+ systems with recovery systems.
   204  func BootFlags(dev Device) ([]string, error) {
   205  	if !dev.HasModeenv() {
   206  		return nil, errNotUC20
   207  	}
   208  
   209  	// read the file that the initramfs wrote in /run, we don't use the modeenv
   210  	// or bootenv to avoid ambiguity about whether the flags in the modeenv or
   211  	// bootenv are for this boot or the next one, but the initramfs will always
   212  	// copy the flags that were set into /run, so we always know the current
   213  	// boot's flags are written in /run
   214  	b, err := ioutil.ReadFile(snapBootFlagsFile)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  
   219  	flags := splitBootFlagString(string(b))
   220  	if allowFlags, err := checkBootFlagList(flags, understoodBootFlags); err != nil {
   221  		if e, ok := err.(unknownFlagError); ok {
   222  			return allowFlags, e
   223  		}
   224  		return nil, err
   225  	}
   226  	return flags, nil
   227  }
   228  
   229  // nextBootFlags returns the set of boot flags that are applicable for the next
   230  // boot. This information always comes from the modeenv, since the only
   231  // situation where boot flags are set for the next boot and we query their state
   232  // is during run mode. The next boot flags for install mode are not queried
   233  // during prepare-image time, since they are only written to the bootenv at
   234  // prepare-image time.
   235  // Only to be used on UC20+ systems with recovery systems.
   236  // TODO: should this accept a modeenv that was previously read from i.e.
   237  // devicestate manager?
   238  func nextBootFlags(dev Device) ([]string, error) {
   239  	if !dev.HasModeenv() {
   240  		return nil, errNotUC20
   241  	}
   242  
   243  	m, err := ReadModeenv("")
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  
   248  	return m.BootFlags, nil
   249  }
   250  
   251  // setNextBootFlags sets the boot flags for the next boot to take effect after
   252  // rebooting. This information always gets saved to the modeenv.
   253  // Only to be used on UC20+ systems with recovery systems.
   254  func setNextBootFlags(dev Device, rootDir string, flags []string) error {
   255  	if !dev.HasModeenv() {
   256  		return errNotUC20
   257  	}
   258  
   259  	m, err := ReadModeenv(rootDir)
   260  	if err != nil {
   261  		return err
   262  	}
   263  
   264  	// for run time, enforce the allow list so we don't write unsupported boot
   265  	// flags
   266  	if _, err := checkBootFlagList(flags, understoodBootFlags); err != nil {
   267  		return err
   268  	}
   269  
   270  	m.BootFlags = flags
   271  
   272  	return m.Write()
   273  }
   274  
   275  // HostUbuntuDataForMode returns a list of locations where the run
   276  // mode root filesystem is mounted for the given mode.
   277  // For run mode, it's "/run/mnt/data" and "/".
   278  // For install mode it's "/run/mnt/ubuntu-data".
   279  // For recover mode it's either "/host/ubuntu-data" or nil if that is not
   280  // mounted. Note that, for recover mode, this function only returns a non-empty
   281  // return value if the partition is mounted and trusted, there are certain
   282  // corner-cases where snap-bootstrap in the initramfs may have mounted
   283  // ubuntu-data in an untrusted manner, but for the purposes of this function
   284  // that is ignored.
   285  // This is primarily meant to be consumed by "snap{,ctl} system-mode".
   286  func HostUbuntuDataForMode(mode string) ([]string, error) {
   287  	var runDataRootfsMountLocations []string
   288  	switch mode {
   289  	case ModeRun:
   290  		// in run mode we have both /run/mnt/data and "/"
   291  		runDataRootfsMountLocations = []string{InitramfsDataDir, dirs.GlobalRootDir}
   292  	case ModeRecover:
   293  		// TODO: should this be it's own dedicated helper to read degraded.json?
   294  
   295  		// for recover mode, the source of truth to determine if we have the
   296  		// host mount is snap-bootstrap's /run/snapd/snap-bootstrap/degraded.json, so
   297  		// we have to go parse that
   298  		degradedJSONFile := filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json")
   299  		b, err := ioutil.ReadFile(degradedJSONFile)
   300  		if err != nil {
   301  			return nil, err
   302  		}
   303  
   304  		degradedJSON := struct {
   305  			UbuntuData struct {
   306  				MountState    string `json:"mount-state"`
   307  				MountLocation string `json:"mount-location"`
   308  			} `json:"ubuntu-data"`
   309  		}{}
   310  
   311  		err = json.Unmarshal(b, &degradedJSON)
   312  		if err != nil {
   313  			return nil, err
   314  		}
   315  
   316  		// don't permit mounted-untrusted state, only mounted state is allowed
   317  		if degradedJSON.UbuntuData.MountState == "mounted" {
   318  			runDataRootfsMountLocations = []string{degradedJSON.UbuntuData.MountLocation}
   319  		}
   320  		// otherwise leave it empty
   321  
   322  	case ModeInstall:
   323  		// the var we have is for /run/mnt/ubuntu-data/writable, but the caller
   324  		// probably wants /run/mnt/ubuntu-data
   325  
   326  		// note that we may be running in install mode before this directory is
   327  		// actually created so check if it exists first
   328  		installModeLocation := filepath.Dir(InstallHostWritableDir)
   329  		if exists, _, _ := osutil.DirExists(installModeLocation); exists {
   330  			runDataRootfsMountLocations = []string{installModeLocation}
   331  		}
   332  	default:
   333  		return nil, ErrUnsupportedSystemMode
   334  	}
   335  
   336  	return runDataRootfsMountLocations, nil
   337  }