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