github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/boot/cmdline.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 "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/gadget" 30 "github.com/snapcore/snapd/logger" 31 "github.com/snapcore/snapd/osutil" 32 "github.com/snapcore/snapd/strutil" 33 ) 34 35 const ( 36 // ModeRun indicates the regular operating system mode of the device. 37 ModeRun = "run" 38 // ModeInstall is a mode in which a new system is installed on the 39 // device. 40 ModeInstall = "install" 41 // ModeRecover is a mode in which the device boots into the recovery 42 // system. 43 ModeRecover = "recover" 44 ) 45 46 var ( 47 validModes = []string{ModeInstall, ModeRecover, ModeRun} 48 ) 49 50 // ModeAndRecoverySystemFromKernelCommandLine returns the current system mode 51 // and the recovery system label as passed in the kernel command line by the 52 // bootloader. 53 func ModeAndRecoverySystemFromKernelCommandLine() (mode, sysLabel string, err error) { 54 m, err := osutil.KernelCommandLineKeyValues("snapd_recovery_mode", "snapd_recovery_system") 55 if err != nil { 56 return "", "", err 57 } 58 var modeOk bool 59 mode, modeOk = m["snapd_recovery_mode"] 60 61 // no mode specified gets interpreted as install 62 if modeOk { 63 if mode == "" { 64 mode = ModeInstall 65 } else if !strutil.ListContains(validModes, mode) { 66 return "", "", fmt.Errorf("cannot use unknown mode %q", mode) 67 } 68 } 69 70 sysLabel = m["snapd_recovery_system"] 71 72 switch { 73 case mode == "" && sysLabel == "": 74 return "", "", fmt.Errorf("cannot detect mode nor recovery system to use") 75 case mode == "" && sysLabel != "": 76 return "", "", fmt.Errorf("cannot specify system label without a mode") 77 case mode == ModeInstall && sysLabel == "": 78 return "", "", fmt.Errorf("cannot specify install mode without system label") 79 case mode == ModeRun && sysLabel != "": 80 // XXX: should we silently ignore the label? at least log for now 81 logger.Noticef(`ignoring recovery system label %q in "run" mode`, sysLabel) 82 sysLabel = "" 83 } 84 return mode, sysLabel, nil 85 } 86 87 var errBootConfigNotManaged = errors.New("boot config is not managed") 88 89 func getBootloaderManagingItsAssets(where string, opts *bootloader.Options) (bootloader.TrustedAssetsBootloader, error) { 90 bl, err := bootloader.Find(where, opts) 91 if err != nil { 92 return nil, fmt.Errorf("internal error: cannot find trusted assets bootloader under %q: %v", where, err) 93 } 94 mbl, ok := bl.(bootloader.TrustedAssetsBootloader) 95 if !ok { 96 // the bootloader cannot manage its scripts 97 return nil, errBootConfigNotManaged 98 } 99 return mbl, nil 100 } 101 102 // bootVarsForTrustedCommandLineFromGadget returns a set of boot variables that 103 // carry the command line arguments requested by the gadget. This is only useful 104 // if snapd is managing the boot config. 105 func bootVarsForTrustedCommandLineFromGadget(gadgetDirOrSnapPath string) (map[string]string, error) { 106 extraOrFull, full, err := gadget.KernelCommandLineFromGadget(gadgetDirOrSnapPath) 107 if err != nil { 108 if err == gadget.ErrNoKernelCommandline { 109 // nothing set by the gadget, but we could have had 110 // arguments before, so make sure those are cleared now 111 clear := map[string]string{ 112 "snapd_extra_cmdline_args": "", 113 "snapd_full_cmdline_args": "", 114 } 115 return clear, nil 116 } 117 return nil, fmt.Errorf("cannot use kernel command line from gadget: %v", err) 118 } 119 // gadget has the kernel command line 120 args := map[string]string{ 121 "snapd_extra_cmdline_args": "", 122 "snapd_full_cmdline_args": "", 123 } 124 if full { 125 args["snapd_full_cmdline_args"] = extraOrFull 126 } else { 127 args["snapd_extra_cmdline_args"] = extraOrFull 128 } 129 return args, nil 130 } 131 132 const ( 133 currentEdition = iota 134 candidateEdition 135 ) 136 137 func composeCommandLine(model *asserts.Model, currentOrCandidate int, mode, system, gadgetDirOrSnapPath string) (string, error) { 138 if model.Grade() == asserts.ModelGradeUnset { 139 return "", nil 140 } 141 if mode != ModeRun && mode != ModeRecover { 142 return "", fmt.Errorf("internal error: unsupported command line mode %q", mode) 143 } 144 // get the run mode bootloader under the native run partition layout 145 opts := &bootloader.Options{ 146 Role: bootloader.RoleRunMode, 147 NoSlashBoot: true, 148 } 149 bootloaderRootDir := InitramfsUbuntuBootDir 150 components := bootloader.CommandLineComponents{ 151 ModeArg: "snapd_recovery_mode=run", 152 } 153 if mode == ModeRecover { 154 if system == "" { 155 return "", fmt.Errorf("internal error: system is unset") 156 } 157 // dealing with recovery system bootloader 158 opts.Role = bootloader.RoleRecovery 159 bootloaderRootDir = InitramfsUbuntuSeedDir 160 // recovery mode & system command line arguments 161 components = bootloader.CommandLineComponents{ 162 ModeArg: "snapd_recovery_mode=recover", 163 SystemArg: fmt.Sprintf("snapd_recovery_system=%v", system), 164 } 165 } 166 mbl, err := getBootloaderManagingItsAssets(bootloaderRootDir, opts) 167 if err != nil { 168 if err == errBootConfigNotManaged { 169 return "", nil 170 } 171 return "", err 172 } 173 if gadgetDirOrSnapPath != "" { 174 extraOrFull, full, err := gadget.KernelCommandLineFromGadget(gadgetDirOrSnapPath) 175 if err != nil && err != gadget.ErrNoKernelCommandline { 176 return "", fmt.Errorf("cannot use kernel command line from gadget: %v", err) 177 } 178 if err == nil { 179 // gadget provides some part of the kernel command line 180 if full { 181 components.FullArgs = extraOrFull 182 } else { 183 components.ExtraArgs = extraOrFull 184 } 185 } 186 } 187 if currentOrCandidate == currentEdition { 188 return mbl.CommandLine(components) 189 } else { 190 return mbl.CandidateCommandLine(components) 191 } 192 } 193 194 // ComposeRecoveryCommandLine composes the kernel command line used when booting 195 // a given system in recover mode. 196 func ComposeRecoveryCommandLine(model *asserts.Model, system, gadgetDirOrSnapPath string) (string, error) { 197 return composeCommandLine(model, currentEdition, ModeRecover, system, gadgetDirOrSnapPath) 198 } 199 200 // ComposeCommandLine composes the kernel command line used when booting the 201 // system in run mode. 202 func ComposeCommandLine(model *asserts.Model, gadgetDirOrSnapPath string) (string, error) { 203 return composeCommandLine(model, currentEdition, ModeRun, "", gadgetDirOrSnapPath) 204 } 205 206 // ComposeCandidateCommandLine composes the kernel command line used when 207 // booting the system in run mode with the current built-in edition of managed 208 // boot assets. 209 func ComposeCandidateCommandLine(model *asserts.Model, gadgetDirOrSnapPath string) (string, error) { 210 return composeCommandLine(model, candidateEdition, ModeRun, "", gadgetDirOrSnapPath) 211 } 212 213 // ComposeCandidateRecoveryCommandLine composes the kernel command line used 214 // when booting the given system in recover mode with the current built-in 215 // edition of managed boot assets. 216 func ComposeCandidateRecoveryCommandLine(model *asserts.Model, system, gadgetDirOrSnapPath string) (string, error) { 217 return composeCommandLine(model, candidateEdition, ModeRecover, system, gadgetDirOrSnapPath) 218 } 219 220 // observeSuccessfulCommandLine observes a successful boot with a command line 221 // and takes an action based on the contents of the modeenv. The current kernel 222 // command lines in the modeenv can have up to 2 entries when the managed 223 // bootloader boot config gets updated. 224 func observeSuccessfulCommandLine(model *asserts.Model, m *Modeenv) (*Modeenv, error) { 225 // TODO:UC20 only care about run mode for now 226 if m.Mode != "run" { 227 return m, nil 228 } 229 230 switch len(m.CurrentKernelCommandLines) { 231 case 0: 232 // maybe a compatibility scenario, no command lines tracked in 233 // modeenv yet, this can happen when having booted with a newer 234 // snapd 235 return observeSuccessfulCommandLineCompatBoot(model, m) 236 case 1: 237 // no command line update 238 return m, nil 239 default: 240 return observeSuccessfulCommandLineUpdate(m) 241 } 242 } 243 244 // observeSuccessfulCommandLineUpdate observes a successful boot with a command 245 // line which is expected to be listed among the current kernel command line 246 // entries carried in the modeenv. One of those entries must match the current 247 // kernel command line of a running system and will be recorded alone as in use. 248 func observeSuccessfulCommandLineUpdate(m *Modeenv) (*Modeenv, error) { 249 newM, err := m.Copy() 250 if err != nil { 251 return nil, err 252 } 253 254 // get the current command line 255 cmdlineBootedWith, err := osutil.KernelCommandLine() 256 if err != nil { 257 return nil, err 258 } 259 if !strutil.ListContains([]string(m.CurrentKernelCommandLines), cmdlineBootedWith) { 260 return nil, fmt.Errorf("current command line content %q not matching any expected entry", 261 cmdlineBootedWith) 262 } 263 newM.CurrentKernelCommandLines = bootCommandLines{cmdlineBootedWith} 264 265 return newM, nil 266 } 267 268 // observeSuccessfulCommandLineCompatBoot observes a successful boot with a 269 // kernel command line, where the list of current kernel command lines in the 270 // modeenv is unpopulated. This handles a compatibility scenario with systems 271 // that were installed using a previous version of snapd. It verifies that the 272 // expected kernel command line matches the one the system booted with and 273 // populates modeenv kernel command line list accordingly. 274 func observeSuccessfulCommandLineCompatBoot(model *asserts.Model, m *Modeenv) (*Modeenv, error) { 275 // since this is a compatibility scenario, the kernel command line 276 // arguments would not have come from the gadget before either 277 cmdlineExpected, err := ComposeCommandLine(model, "") 278 if err != nil { 279 return nil, err 280 } 281 if cmdlineExpected == "" { 282 // there is no particular command line expected for this model 283 // and system bootloader, indicating that the command line is 284 // not being tracked 285 return m, nil 286 } 287 cmdlineBootedWith, err := osutil.KernelCommandLine() 288 if err != nil { 289 return nil, err 290 } 291 if cmdlineExpected != cmdlineBootedWith { 292 return nil, fmt.Errorf("unexpected current command line: %q", cmdlineBootedWith) 293 } 294 newM, err := m.Copy() 295 if err != nil { 296 return nil, err 297 } 298 newM.CurrentKernelCommandLines = bootCommandLines{cmdlineExpected} 299 return newM, nil 300 } 301 302 type commandLineUpdateReason int 303 304 const ( 305 commandLineUpdateReasonSnapd commandLineUpdateReason = iota 306 commandLineUpdateReasonGadget 307 ) 308 309 // observeCommandLineUpdate observes a pending kernel command line change caused 310 // by an update of boot config or the gadget snap. When needed, the modeenv is 311 // updated with a candidate command line and the encryption keys are resealed. 312 // This helper should be called right before updating the managed boot config. 313 func observeCommandLineUpdate(model *asserts.Model, reason commandLineUpdateReason, gadgetSnapOrDir string) (updated bool, err error) { 314 // TODO:UC20: consider updating a recovery system command line 315 316 m, err := loadModeenv() 317 if err != nil { 318 return false, err 319 } 320 321 if len(m.CurrentKernelCommandLines) == 0 { 322 return false, fmt.Errorf("internal error: current kernel command lines is unset") 323 } 324 // this is the current expected command line which was recorded by 325 // bootstate 326 cmdline := m.CurrentKernelCommandLines[0] 327 // this is the new expected command line 328 var candidateCmdline string 329 switch reason { 330 case commandLineUpdateReasonSnapd: 331 // pending boot config update 332 candidateCmdline, err = ComposeCandidateCommandLine(model, gadgetSnapOrDir) 333 case commandLineUpdateReasonGadget: 334 // pending gadget update 335 candidateCmdline, err = ComposeCommandLine(model, gadgetSnapOrDir) 336 } 337 if err != nil { 338 return false, err 339 } 340 if cmdline == candidateCmdline { 341 // command line is the same or no actual change in modeenv 342 return false, nil 343 } 344 // actual change of the command line content 345 m.CurrentKernelCommandLines = bootCommandLines{cmdline, candidateCmdline} 346 347 if err := m.Write(); err != nil { 348 return false, err 349 } 350 351 expectReseal := true 352 if err := resealKeyToModeenv(dirs.GlobalRootDir, model, m, expectReseal); err != nil { 353 return false, err 354 } 355 return true, nil 356 } 357 358 // kernelCommandLinesForResealWithFallback provides the list of kernel command 359 // lines for use during reseal. During normal operation, the command lines will 360 // be listed in the modeenv. 361 func kernelCommandLinesForResealWithFallback(model *asserts.Model, modeenv *Modeenv) (cmdlines []string, err error) { 362 if len(modeenv.CurrentKernelCommandLines) > 0 { 363 return modeenv.CurrentKernelCommandLines, nil 364 } 365 // fallback for when reseal is called before mark boot successful set a 366 // default during snapd update, since this is a compatibility scenario 367 // there would be no kernel command lines arguments coming from the 368 // gadget either 369 cmdline, err := ComposeCommandLine(model, "") 370 if err != nil { 371 return nil, err 372 } 373 return []string{cmdline}, nil 374 }