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, °radedJSON) 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 }