github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/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(currentOrCandidate int, mode, system, gadgetDirOrSnapPath string) (string, error) { 138 if mode != ModeRun && mode != ModeRecover { 139 return "", fmt.Errorf("internal error: unsupported command line mode %q", mode) 140 } 141 // get the run mode bootloader under the native run partition layout 142 opts := &bootloader.Options{ 143 Role: bootloader.RoleRunMode, 144 NoSlashBoot: true, 145 } 146 bootloaderRootDir := InitramfsUbuntuBootDir 147 components := bootloader.CommandLineComponents{ 148 ModeArg: "snapd_recovery_mode=run", 149 } 150 if mode == ModeRecover { 151 if system == "" { 152 return "", fmt.Errorf("internal error: system is unset") 153 } 154 // dealing with recovery system bootloader 155 opts.Role = bootloader.RoleRecovery 156 bootloaderRootDir = InitramfsUbuntuSeedDir 157 // recovery mode & system command line arguments 158 components = bootloader.CommandLineComponents{ 159 ModeArg: "snapd_recovery_mode=recover", 160 SystemArg: fmt.Sprintf("snapd_recovery_system=%v", system), 161 } 162 } 163 mbl, err := getBootloaderManagingItsAssets(bootloaderRootDir, opts) 164 if err != nil { 165 if err == errBootConfigNotManaged { 166 return "", nil 167 } 168 return "", err 169 } 170 if gadgetDirOrSnapPath != "" { 171 extraOrFull, full, err := gadget.KernelCommandLineFromGadget(gadgetDirOrSnapPath) 172 if err != nil && err != gadget.ErrNoKernelCommandline { 173 return "", fmt.Errorf("cannot use kernel command line from gadget: %v", err) 174 } 175 if err == nil { 176 // gadget provides some part of the kernel command line 177 if full { 178 components.FullArgs = extraOrFull 179 } else { 180 components.ExtraArgs = extraOrFull 181 } 182 } 183 } 184 if currentOrCandidate == currentEdition { 185 return mbl.CommandLine(components) 186 } else { 187 return mbl.CandidateCommandLine(components) 188 } 189 } 190 191 // ComposeRecoveryCommandLine composes the kernel command line used when booting 192 // a given system in recover mode. 193 func ComposeRecoveryCommandLine(model *asserts.Model, system, gadgetDirOrSnapPath string) (string, error) { 194 if model.Grade() == asserts.ModelGradeUnset { 195 return "", nil 196 } 197 return composeCommandLine(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 if model.Grade() == asserts.ModelGradeUnset { 204 return "", nil 205 } 206 return composeCommandLine(currentEdition, ModeRun, "", gadgetDirOrSnapPath) 207 } 208 209 // ComposeCandidateCommandLine composes the kernel command line used when 210 // booting the system in run mode with the current built-in edition of managed 211 // boot assets. 212 func ComposeCandidateCommandLine(model *asserts.Model, gadgetDirOrSnapPath string) (string, error) { 213 if model.Grade() == asserts.ModelGradeUnset { 214 return "", nil 215 } 216 return composeCommandLine(candidateEdition, ModeRun, "", gadgetDirOrSnapPath) 217 } 218 219 // ComposeCandidateRecoveryCommandLine composes the kernel command line used 220 // when booting the given system in recover mode with the current built-in 221 // edition of managed boot assets. 222 func ComposeCandidateRecoveryCommandLine(model *asserts.Model, system, gadgetDirOrSnapPath string) (string, error) { 223 if model.Grade() == asserts.ModelGradeUnset { 224 return "", nil 225 } 226 return composeCommandLine(candidateEdition, ModeRecover, system, gadgetDirOrSnapPath) 227 } 228 229 // observeSuccessfulCommandLine observes a successful boot with a command line 230 // and takes an action based on the contents of the modeenv. The current kernel 231 // command lines in the modeenv can have up to 2 entries when the managed 232 // bootloader boot config gets updated. 233 func observeSuccessfulCommandLine(model *asserts.Model, m *Modeenv) (*Modeenv, error) { 234 // TODO:UC20 only care about run mode for now 235 if m.Mode != "run" { 236 return m, nil 237 } 238 239 switch len(m.CurrentKernelCommandLines) { 240 case 0: 241 // maybe a compatibility scenario, no command lines tracked in 242 // modeenv yet, this can happen when having booted with a newer 243 // snapd 244 return observeSuccessfulCommandLineCompatBoot(model, m) 245 case 1: 246 // no command line update 247 return m, nil 248 default: 249 return observeSuccessfulCommandLineUpdate(m) 250 } 251 } 252 253 // observeSuccessfulCommandLineUpdate observes a successful boot with a command 254 // line which is expected to be listed among the current kernel command line 255 // entries carried in the modeenv. One of those entries must match the current 256 // kernel command line of a running system and will be recorded alone as in use. 257 func observeSuccessfulCommandLineUpdate(m *Modeenv) (*Modeenv, error) { 258 newM, err := m.Copy() 259 if err != nil { 260 return nil, err 261 } 262 263 // get the current command line 264 cmdlineBootedWith, err := osutil.KernelCommandLine() 265 if err != nil { 266 return nil, err 267 } 268 if !strutil.ListContains([]string(m.CurrentKernelCommandLines), cmdlineBootedWith) { 269 return nil, fmt.Errorf("current command line content %q not matching any expected entry", 270 cmdlineBootedWith) 271 } 272 newM.CurrentKernelCommandLines = bootCommandLines{cmdlineBootedWith} 273 274 return newM, nil 275 } 276 277 // observeSuccessfulCommandLineCompatBoot observes a successful boot with a 278 // kernel command line, where the list of current kernel command lines in the 279 // modeenv is unpopulated. This handles a compatibility scenario with systems 280 // that were installed using a previous version of snapd. It verifies that the 281 // expected kernel command line matches the one the system booted with and 282 // populates modeenv kernel command line list accordingly. 283 func observeSuccessfulCommandLineCompatBoot(model *asserts.Model, m *Modeenv) (*Modeenv, error) { 284 // since this is a compatibility scenario, the kernel command line 285 // arguments would not have come from the gadget before either 286 cmdlineExpected, err := ComposeCommandLine(model, "") 287 if err != nil { 288 return nil, err 289 } 290 if cmdlineExpected == "" { 291 // there is no particular command line expected for this model 292 // and system bootloader, indicating that the command line is 293 // not being tracked 294 return m, nil 295 } 296 cmdlineBootedWith, err := osutil.KernelCommandLine() 297 if err != nil { 298 return nil, err 299 } 300 if cmdlineExpected != cmdlineBootedWith { 301 return nil, fmt.Errorf("unexpected current command line: %q", cmdlineBootedWith) 302 } 303 newM, err := m.Copy() 304 if err != nil { 305 return nil, err 306 } 307 newM.CurrentKernelCommandLines = bootCommandLines{cmdlineExpected} 308 return newM, nil 309 } 310 311 type commandLineUpdateReason int 312 313 const ( 314 commandLineUpdateReasonSnapd commandLineUpdateReason = iota 315 commandLineUpdateReasonGadget 316 ) 317 318 // observeCommandLineUpdate observes a pending kernel command line change caused 319 // by an update of boot config or the gadget snap. When needed, the modeenv is 320 // updated with a candidate command line and the encryption keys are resealed. 321 // This helper should be called right before updating the managed boot config. 322 func observeCommandLineUpdate(model *asserts.Model, reason commandLineUpdateReason, gadgetSnapOrDir string) (updated bool, err error) { 323 // TODO:UC20: consider updating a recovery system command line 324 325 m, err := loadModeenv() 326 if err != nil { 327 return false, err 328 } 329 330 if len(m.CurrentKernelCommandLines) == 0 { 331 return false, fmt.Errorf("internal error: current kernel command lines is unset") 332 } 333 // this is the current expected command line which was recorded by 334 // bootstate 335 cmdline := m.CurrentKernelCommandLines[0] 336 // this is the new expected command line 337 var candidateCmdline string 338 switch reason { 339 case commandLineUpdateReasonSnapd: 340 // pending boot config update 341 candidateCmdline, err = ComposeCandidateCommandLine(model, gadgetSnapOrDir) 342 case commandLineUpdateReasonGadget: 343 // pending gadget update 344 candidateCmdline, err = ComposeCommandLine(model, gadgetSnapOrDir) 345 } 346 if err != nil { 347 return false, err 348 } 349 if cmdline == candidateCmdline { 350 // command line is the same or no actual change in modeenv 351 return false, nil 352 } 353 // actual change of the command line content 354 m.CurrentKernelCommandLines = bootCommandLines{cmdline, candidateCmdline} 355 356 if err := m.Write(); err != nil { 357 return false, err 358 } 359 360 expectReseal := true 361 if err := resealKeyToModeenv(dirs.GlobalRootDir, m, expectReseal); err != nil { 362 return false, err 363 } 364 return true, nil 365 } 366 367 // kernelCommandLinesForResealWithFallback provides the list of kernel command 368 // lines for use during reseal. During normal operation, the command lines will 369 // be listed in the modeenv. 370 func kernelCommandLinesForResealWithFallback(modeenv *Modeenv) (cmdlines []string, err error) { 371 if len(modeenv.CurrentKernelCommandLines) > 0 { 372 return modeenv.CurrentKernelCommandLines, nil 373 } 374 // fallback for when reseal is called before mark boot successful set a 375 // default during snapd update, since this is a compatibility scenario 376 // there would be no kernel command lines arguments coming from the 377 // gadget either 378 gadgetDir := "" 379 cmdline, err := composeCommandLine(currentEdition, ModeRun, "", gadgetDir) 380 if err != nil { 381 return nil, err 382 } 383 return []string{cmdline}, nil 384 }