github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/boot/cmdline.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 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 "errors" 24 "fmt" 25 26 "github.com/snapcore/snapd/asserts" 27 "github.com/snapcore/snapd/bootloader" 28 "github.com/snapcore/snapd/dirs" 29 "github.com/snapcore/snapd/logger" 30 "github.com/snapcore/snapd/osutil" 31 "github.com/snapcore/snapd/strutil" 32 ) 33 34 const ( 35 // ModeRun indicates the regular operating system mode of the device. 36 ModeRun = "run" 37 // ModeInstall is a mode in which a new system is installed on the 38 // device. 39 ModeInstall = "install" 40 // ModeRecover is a mode in which the device boots into the recovery 41 // system. 42 ModeRecover = "recover" 43 ) 44 45 var ( 46 validModes = []string{ModeInstall, ModeRecover, ModeRun} 47 ) 48 49 // ModeAndRecoverySystemFromKernelCommandLine returns the current system mode 50 // and the recovery system label as passed in the kernel command line by the 51 // bootloader. 52 func ModeAndRecoverySystemFromKernelCommandLine() (mode, sysLabel string, err error) { 53 m, err := osutil.KernelCommandLineKeyValues("snapd_recovery_mode", "snapd_recovery_system") 54 if err != nil { 55 return "", "", err 56 } 57 var modeOk bool 58 mode, modeOk = m["snapd_recovery_mode"] 59 60 // no mode specified gets interpreted as install 61 if modeOk { 62 if mode == "" { 63 mode = ModeInstall 64 } else if !strutil.ListContains(validModes, mode) { 65 return "", "", fmt.Errorf("cannot use unknown mode %q", mode) 66 } 67 } 68 69 sysLabel = m["snapd_recovery_system"] 70 71 switch { 72 case mode == "" && sysLabel == "": 73 return "", "", fmt.Errorf("cannot detect mode nor recovery system to use") 74 case mode == "" && sysLabel != "": 75 return "", "", fmt.Errorf("cannot specify system label without a mode") 76 case mode == ModeInstall && sysLabel == "": 77 return "", "", fmt.Errorf("cannot specify install mode without system label") 78 case mode == ModeRun && sysLabel != "": 79 // XXX: should we silently ignore the label? at least log for now 80 logger.Noticef(`ignoring recovery system label %q in "run" mode`, sysLabel) 81 sysLabel = "" 82 } 83 return mode, sysLabel, nil 84 } 85 86 var errBootConfigNotManaged = errors.New("boot config is not managed") 87 88 func getBootloaderManagingItsAssets(where string, opts *bootloader.Options) (bootloader.TrustedAssetsBootloader, error) { 89 bl, err := bootloader.Find(where, opts) 90 if err != nil { 91 return nil, fmt.Errorf("internal error: cannot find trusted assets bootloader under %q: %v", where, err) 92 } 93 mbl, ok := bl.(bootloader.TrustedAssetsBootloader) 94 if !ok { 95 // the bootloader cannot manage its scripts 96 return nil, errBootConfigNotManaged 97 } 98 return mbl, nil 99 } 100 101 const ( 102 currentEdition = iota 103 candidateEdition 104 ) 105 106 func composeCommandLine(model *asserts.Model, currentOrCandidate int, mode, system string) (string, error) { 107 if model.Grade() == asserts.ModelGradeUnset { 108 return "", nil 109 } 110 if mode != ModeRun && mode != ModeRecover { 111 return "", fmt.Errorf("internal error: unsupported command line mode %q", mode) 112 } 113 // get the run mode bootloader under the native run partition layout 114 opts := &bootloader.Options{ 115 Role: bootloader.RoleRunMode, 116 NoSlashBoot: true, 117 } 118 bootloaderRootDir := InitramfsUbuntuBootDir 119 modeArg := "snapd_recovery_mode=run" 120 systemArg := "" 121 if mode == ModeRecover { 122 if system == "" { 123 return "", fmt.Errorf("internal error: system is unset") 124 } 125 // dealing with recovery system bootloader 126 opts.Role = bootloader.RoleRecovery 127 bootloaderRootDir = InitramfsUbuntuSeedDir 128 // recovery mode & system command line arguments 129 modeArg = "snapd_recovery_mode=recover" 130 systemArg = fmt.Sprintf("snapd_recovery_system=%v", system) 131 } 132 mbl, err := getBootloaderManagingItsAssets(bootloaderRootDir, opts) 133 if err != nil { 134 if err == errBootConfigNotManaged { 135 return "", nil 136 } 137 return "", err 138 } 139 // TODO:UC20: fetch extra args from gadget 140 extraArgs := "" 141 if currentOrCandidate == currentEdition { 142 return mbl.CommandLine(modeArg, systemArg, extraArgs) 143 } else { 144 return mbl.CandidateCommandLine(modeArg, systemArg, extraArgs) 145 } 146 } 147 148 // ComposeRecoveryCommandLine composes the kernel command line used when booting 149 // a given system in recover mode. 150 func ComposeRecoveryCommandLine(model *asserts.Model, system string) (string, error) { 151 return composeCommandLine(model, currentEdition, ModeRecover, system) 152 } 153 154 // ComposeCommandLine composes the kernel command line used when booting the 155 // system in run mode. 156 func ComposeCommandLine(model *asserts.Model) (string, error) { 157 return composeCommandLine(model, currentEdition, ModeRun, "") 158 } 159 160 // ComposeCandidateCommandLine composes the kernel command line used when 161 // booting the system in run mode with the current built-in edition of managed 162 // boot assets. 163 func ComposeCandidateCommandLine(model *asserts.Model) (string, error) { 164 return composeCommandLine(model, candidateEdition, ModeRun, "") 165 } 166 167 // ComposeCandidateRecoveryCommandLine composes the kernel command line used 168 // when booting the given system in recover mode with the current built-in 169 // edition of managed boot assets. 170 func ComposeCandidateRecoveryCommandLine(model *asserts.Model, system string) (string, error) { 171 return composeCommandLine(model, candidateEdition, ModeRecover, system) 172 } 173 174 // observeSuccessfulCommandLine observes a successful boot with a command line 175 // and takes an action based on the contents of the modeenv. The current kernel 176 // command lines in the modeenv can have up to 2 entries when the managed 177 // bootloader boot config gets updated. 178 func observeSuccessfulCommandLine(model *asserts.Model, m *Modeenv) (*Modeenv, error) { 179 // TODO:UC20 only care about run mode for now 180 if m.Mode != "run" { 181 return m, nil 182 } 183 184 switch len(m.CurrentKernelCommandLines) { 185 case 0: 186 // compatibility scenario, no command lines tracked in modeenv 187 // yet, this can happen when having booted with a newer snapd 188 return observeSuccessfulCommandLineCompatBoot(model, m) 189 case 1: 190 // no command line update 191 return m, nil 192 default: 193 return observeSuccessfulCommandLineUpdate(m) 194 } 195 } 196 197 // observeSuccessfulCommandLineUpdate observes a successful boot with a command 198 // line which is expected to be listed among the current kernel command line 199 // entries carried in the modeenv. One of those entries must match the current 200 // kernel command line of a running system and will be recorded alone as in use. 201 func observeSuccessfulCommandLineUpdate(m *Modeenv) (*Modeenv, error) { 202 newM, err := m.Copy() 203 if err != nil { 204 return nil, err 205 } 206 207 // get the current command line 208 cmdlineBootedWith, err := osutil.KernelCommandLine() 209 if err != nil { 210 return nil, err 211 } 212 if !strutil.ListContains([]string(m.CurrentKernelCommandLines), cmdlineBootedWith) { 213 return nil, fmt.Errorf("current command line content %q not matching any expected entry", 214 cmdlineBootedWith) 215 } 216 newM.CurrentKernelCommandLines = bootCommandLines{cmdlineBootedWith} 217 218 return newM, nil 219 } 220 221 // observeSuccessfulCommandLineCompatBoot observes a successful boot with a 222 // kernel command line, where the list of current kernel command lines in the 223 // modeenv is unpopulated. This handles a compatibility scenario with systems 224 // that were installed using a previous version of snapd. It verifies that the 225 // expected kernel command line matches the one the system booted with and 226 // populates modeenv kernel command line list accordingly. 227 func observeSuccessfulCommandLineCompatBoot(model *asserts.Model, m *Modeenv) (*Modeenv, error) { 228 cmdlineExpected, err := ComposeCommandLine(model) 229 if err != nil { 230 return nil, err 231 } 232 cmdlineBootedWith, err := osutil.KernelCommandLine() 233 if err != nil { 234 return nil, err 235 } 236 if cmdlineExpected != cmdlineBootedWith { 237 return nil, fmt.Errorf("unexpected current command line: %q", cmdlineBootedWith) 238 } 239 newM, err := m.Copy() 240 if err != nil { 241 return nil, err 242 } 243 newM.CurrentKernelCommandLines = bootCommandLines{cmdlineExpected} 244 return newM, nil 245 } 246 247 // observeCommandLineUpdate observes a pending kernel command line change caused 248 // by an update of boot config. When needed, the modeenv is updated with a 249 // candidate command line and the encryption keys are resealed. This helper 250 // should be called right before updating the managed boot config. 251 func observeCommandLineUpdate(model *asserts.Model) error { 252 // TODO:UC20: consider updating a recovery system command line 253 254 m, err := loadModeenv() 255 if err != nil { 256 return err 257 } 258 259 if len(m.CurrentKernelCommandLines) == 0 { 260 return fmt.Errorf("internal error: current kernel command lines is unset") 261 } 262 // this is the current expected command line which was recorded by 263 // bootstate 264 cmdline := m.CurrentKernelCommandLines[0] 265 // this is the new expected command line 266 candidateCmdline, err := ComposeCandidateCommandLine(model) 267 if err != nil { 268 return err 269 } 270 if cmdline == candidateCmdline { 271 // no change in command line contents, nothing to do 272 return nil 273 } 274 m.CurrentKernelCommandLines = bootCommandLines{cmdline, candidateCmdline} 275 276 if err := m.Write(); err != nil { 277 return err 278 } 279 280 expectReseal := true 281 if err := resealKeyToModeenv(dirs.GlobalRootDir, model, m, expectReseal); err != nil { 282 return err 283 } 284 return nil 285 } 286 287 // kernelCommandLinesForResealWithFallback provides the list of kernel command 288 // lines for use during reseal. During normal operation, the command lines will 289 // be listed in the modeenv. 290 func kernelCommandLinesForResealWithFallback(model *asserts.Model, modeenv *Modeenv) (cmdlines []string, err error) { 291 if len(modeenv.CurrentKernelCommandLines) > 0 { 292 return modeenv.CurrentKernelCommandLines, nil 293 } 294 // fallback for when reseal is called before mark boot successful set a 295 // default during snapd update 296 cmdline, err := ComposeCommandLine(model) 297 if err != nil { 298 return nil, err 299 } 300 return []string{cmdline}, nil 301 }