blob: 35f2240d9bd60d2d8da7e012a40b5708be2f4ffa [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Lorenz Brun35fcf032023-06-29 04:15:58 +02004package update
5
6import (
Lorenz Brun35fcf032023-06-29 04:15:58 +02007 "bytes"
8 "context"
Lorenz Brund79881d2023-11-30 19:02:06 +01009 "crypto/sha256"
Lorenz Brund14be0e2023-07-31 16:46:14 +020010 "debug/pe"
Lorenz Brund79881d2023-11-30 19:02:06 +010011 _ "embed"
Lorenz Brun35fcf032023-06-29 04:15:58 +020012 "errors"
13 "fmt"
14 "io"
Lorenz Brun35fcf032023-06-29 04:15:58 +020015 "os"
16 "path/filepath"
17 "regexp"
Jan Schär1a7e1fe2025-07-25 16:50:12 +020018 "runtime"
Lorenz Brun35fcf032023-06-29 04:15:58 +020019 "strconv"
Lorenz Brund14be0e2023-07-31 16:46:14 +020020 "strings"
Jan Schär62cecde2025-04-16 15:24:04 +000021 "time"
Lorenz Brun35fcf032023-06-29 04:15:58 +020022
23 "github.com/cenkalti/backoff/v4"
Jan Schär1a7e1fe2025-07-25 16:50:12 +020024 ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1"
Lorenz Brund14be0e2023-07-31 16:46:14 +020025 "golang.org/x/sys/unix"
Lorenz Brun35fcf032023-06-29 04:15:58 +020026 "google.golang.org/grpc/codes"
27 "google.golang.org/grpc/status"
Lorenz Brun54a5a052023-10-02 16:40:11 +020028 "google.golang.org/protobuf/proto"
Lorenz Brun35fcf032023-06-29 04:15:58 +020029
Serge Bazanski3c5d0632024-09-12 10:49:12 +000030 "source.monogon.dev/go/logging"
Jan Schäre19d2792025-06-23 12:37:58 +000031 "source.monogon.dev/metropolis/installer/install"
Jan Schärb86917b2025-05-14 16:31:08 +000032 "source.monogon.dev/metropolis/node/core/productinfo"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020033 "source.monogon.dev/osbase/blockdev"
34 "source.monogon.dev/osbase/efivarfs"
35 "source.monogon.dev/osbase/gpt"
36 "source.monogon.dev/osbase/kexec"
Jan Schär2963b682025-07-17 17:03:44 +020037 "source.monogon.dev/osbase/oci"
Jan Schäre19d2792025-06-23 12:37:58 +000038 "source.monogon.dev/osbase/oci/osimage"
Jan Schär62cecde2025-04-16 15:24:04 +000039 "source.monogon.dev/osbase/oci/registry"
Jan Schär62cecde2025-04-16 15:24:04 +000040
Jan Schär69b76872025-05-14 16:39:47 +000041 abloaderpb "source.monogon.dev/metropolis/node/abloader/spec"
Jan Schär62cecde2025-04-16 15:24:04 +000042 apb "source.monogon.dev/metropolis/proto/api"
Lorenz Brun35fcf032023-06-29 04:15:58 +020043)
44
45// Service contains data and functionality to perform A/B updates on a
46// Metropolis node.
47type Service struct {
48 // Path to the mount point of the EFI System Partition (ESP).
49 ESPPath string
Tim Windelschmidt8e87a062023-07-31 01:33:10 +000050 // gpt.Partition of the ESP System Partition.
51 ESPPart *gpt.Partition
Lorenz Brun35fcf032023-06-29 04:15:58 +020052 // Partition number (1-based) of the ESP in the GPT partitions array.
53 ESPPartNumber uint32
Tim Windelschmidt8e87a062023-07-31 01:33:10 +000054
Lorenz Brun35fcf032023-06-29 04:15:58 +020055 // Logger service for the update service.
Serge Bazanski3c5d0632024-09-12 10:49:12 +000056 Logger logging.Leveled
Lorenz Brun35fcf032023-06-29 04:15:58 +020057}
58
59type Slot int
60
61const (
62 SlotInvalid Slot = 0
63 SlotA Slot = 1
64 SlotB Slot = 2
65)
66
67// Other returns the "other" slot, i.e. returns slot A for B and B for A.
68// It returns SlotInvalid for any s which is not SlotA or SlotB.
69func (s Slot) Other() Slot {
70 switch s {
71 case SlotA:
72 return SlotB
73 case SlotB:
74 return SlotA
75 default:
76 return SlotInvalid
77 }
78}
79
80func (s Slot) String() string {
81 switch s {
82 case SlotA:
83 return "A"
84 case SlotB:
85 return "B"
86 default:
87 return "<invalid slot>"
88 }
89}
90
91func (s Slot) EFIBootPath() string {
92 switch s {
93 case SlotA:
Jan Schäre19d2792025-06-23 12:37:58 +000094 return install.EFIBootAPath
Lorenz Brun35fcf032023-06-29 04:15:58 +020095 case SlotB:
Jan Schäre19d2792025-06-23 12:37:58 +000096 return install.EFIBootBPath
Lorenz Brun35fcf032023-06-29 04:15:58 +020097 default:
98 return ""
99 }
100}
101
102var slotRegexp = regexp.MustCompile(`PARTLABEL=METROPOLIS-SYSTEM-([AB])`)
103
104// ProvideESP is a convenience function for providing information about the
105// ESP after the update service has been instantiated.
Tim Windelschmidt8e87a062023-07-31 01:33:10 +0000106func (s *Service) ProvideESP(path string, partNum uint32, part *gpt.Partition) {
Lorenz Brun35fcf032023-06-29 04:15:58 +0200107 s.ESPPath = path
108 s.ESPPartNumber = partNum
Tim Windelschmidt8e87a062023-07-31 01:33:10 +0000109 s.ESPPart = part
Lorenz Brun35fcf032023-06-29 04:15:58 +0200110}
111
112// CurrentlyRunningSlot returns the slot the current system is booted from.
113func (s *Service) CurrentlyRunningSlot() Slot {
114 cmdline, err := os.ReadFile("/proc/cmdline")
115 if err != nil {
116 return SlotInvalid
117 }
118 slotMatches := slotRegexp.FindStringSubmatch(string(cmdline))
119 if len(slotMatches) != 2 {
120 return SlotInvalid
121 }
122 switch slotMatches[1] {
123 case "A":
124 return SlotA
125 case "B":
126 return SlotB
127 default:
128 panic("unreachable")
129 }
130}
131
132var bootVarRegexp = regexp.MustCompile(`^Boot([0-9A-Fa-f]{4})$`)
133
Lorenz Brun35fcf032023-06-29 04:15:58 +0200134// MarkBootSuccessful must be called after each boot if some implementation-
135// defined criteria for a successful boot are met. If an update has been
136// installed and booted and this function is called, the updated version is
137// marked as default. If an issue occurs during boot and so this function is
138// not called the old version will be started again on next boot.
139func (s *Service) MarkBootSuccessful() error {
140 if s.ESPPath == "" {
141 return errors.New("no ESP information provided to update service, cannot continue")
142 }
Lorenz Brund79881d2023-11-30 19:02:06 +0100143 if err := s.fixupEFI(); err != nil {
144 s.Logger.Errorf("Error when checking boot entry configuration: %v", err)
145 }
146 if err := s.fixupPreloader(); err != nil {
147 s.Logger.Errorf("Error when fixing A/B preloader: %v", err)
148 }
Lorenz Brun35fcf032023-06-29 04:15:58 +0200149 activeSlot := s.CurrentlyRunningSlot()
Lorenz Brun54a5a052023-10-02 16:40:11 +0200150 abState, err := s.getABState()
Lorenz Brun35fcf032023-06-29 04:15:58 +0200151 if err != nil {
Lorenz Brun54a5a052023-10-02 16:40:11 +0200152 s.Logger.Warningf("Error while getting A/B loader state, recreating: %v", err)
153 abState = &abloaderpb.ABLoaderData{
154 ActiveSlot: abloaderpb.Slot(activeSlot),
Lorenz Brun35fcf032023-06-29 04:15:58 +0200155 }
Lorenz Brun54a5a052023-10-02 16:40:11 +0200156 err := s.setABState(abState)
157 if err != nil {
158 return fmt.Errorf("while recreating A/B loader state: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200159 }
160 }
Lorenz Brun54a5a052023-10-02 16:40:11 +0200161 if Slot(abState.ActiveSlot) != activeSlot {
162 err := s.setABState(&abloaderpb.ABLoaderData{
163 ActiveSlot: abloaderpb.Slot(activeSlot),
164 })
165 if err != nil {
166 return fmt.Errorf("while setting next A/B slot: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200167 }
168 s.Logger.Infof("Permanently activated slot %v", activeSlot)
169 } else {
170 s.Logger.Infof("Normal boot from slot %v", activeSlot)
171 }
172
173 return nil
174}
175
Lorenz Brunca6da6a2024-09-09 17:55:15 +0200176// Rollback sets the currently-inactive slot as the next boot slot. This is
177// intended to recover from scenarios where roll-forward fixing is difficult.
178// Only the next boot slot is set to make sure that the node is not
179// made unbootable accidentally. On successful bootup that code can switch the
180// active slot to itself.
181func (s *Service) Rollback() error {
182 if s.ESPPath == "" {
183 return errors.New("no ESP information provided to update service, cannot continue")
184 }
185 activeSlot := s.CurrentlyRunningSlot()
186 abState, err := s.getABState()
187 if err != nil {
188 return fmt.Errorf("no valid A/B loader state, cannot rollback: %w", err)
189 }
190 nextSlot := activeSlot.Other()
191 err = s.setABState(&abloaderpb.ABLoaderData{
192 ActiveSlot: abState.ActiveSlot,
193 NextSlot: abloaderpb.Slot(nextSlot),
194 })
195 if err != nil {
196 return fmt.Errorf("while setting next A/B slot: %w", err)
197 }
198 s.Logger.Warningf("Rollback requested, NextSlot set to %v", nextSlot)
199 return nil
200}
201
Lorenz Brun1640c282024-09-09 17:50:48 +0200202// KexecLoadNext loads the slot to be booted next into the kexec staging area.
203// The next slot can then be launched by executing kexec via the reboot
204// syscall. Calling this function counts as a next boot for the purposes of
205// A/B state tracking, so it should not be called without kexecing afterwards.
206func (s *Service) KexecLoadNext() error {
207 state, err := s.getABState()
208 if err != nil {
209 return fmt.Errorf("bad A/B state: %w", err)
210 }
211 slotToLoad := Slot(state.ActiveSlot)
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100212 if state.NextSlot != abloaderpb.Slot_SLOT_NONE {
Lorenz Brun1640c282024-09-09 17:50:48 +0200213 slotToLoad = Slot(state.NextSlot)
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100214 state.NextSlot = abloaderpb.Slot_SLOT_NONE
Lorenz Brun1640c282024-09-09 17:50:48 +0200215 err = s.setABState(state)
216 if err != nil {
217 return fmt.Errorf("while updating A/B state: %w", err)
218 }
219 }
220 boot, err := os.Open(filepath.Join(s.ESPPath, slotToLoad.EFIBootPath()))
221 if err != nil {
222 return fmt.Errorf("failed to open boot file for slot %v: %w", slotToLoad, err)
223 }
224 defer boot.Close()
225 if err := s.stageKexec(boot, slotToLoad); err != nil {
226 return fmt.Errorf("failed to stage next slot for kexec: %w", err)
227 }
228 return nil
229}
230
Lorenz Brun35fcf032023-06-29 04:15:58 +0200231func openSystemSlot(slot Slot) (*blockdev.Device, error) {
232 switch slot {
233 case SlotA:
234 return blockdev.Open("/dev/system-a")
235 case SlotB:
236 return blockdev.Open("/dev/system-b")
237 default:
238 return nil, errors.New("invalid slot identifier given")
239 }
240}
241
Lorenz Brun54a5a052023-10-02 16:40:11 +0200242func (s *Service) getABState() (*abloaderpb.ABLoaderData, error) {
243 abDataRaw, err := os.ReadFile(filepath.Join(s.ESPPath, "EFI/metropolis/loader_state.pb"))
244 if err != nil {
245 return nil, err
246 }
247 var abData abloaderpb.ABLoaderData
248 if err := proto.Unmarshal(abDataRaw, &abData); err != nil {
249 return nil, err
250 }
251 return &abData, nil
252}
253
254func (s *Service) setABState(d *abloaderpb.ABLoaderData) error {
255 abDataRaw, err := proto.Marshal(d)
256 if err != nil {
257 return fmt.Errorf("while marshaling: %w", err)
258 }
259 if err := os.WriteFile(filepath.Join(s.ESPPath, "EFI/metropolis/loader_state.pb"), abDataRaw, 0666); err != nil {
260 return err
261 }
262 return nil
263}
264
Jan Schär1a7e1fe2025-07-25 16:50:12 +0200265func selectArchitecture(ref oci.Ref, architecture string) (*oci.Image, error) {
266 switch ref := ref.(type) {
267 case *oci.Image:
268 return ref, nil
269 case *oci.Index:
270 var found *ocispecv1.Descriptor
271 for i := range ref.Manifest.Manifests {
272 descriptor := &ref.Manifest.Manifests[i]
273 if descriptor.Platform != nil && descriptor.Platform.Architecture == architecture {
274 if found != nil {
275 return nil, fmt.Errorf("invalid index, found multiple matching entries")
276 }
277 found = descriptor
278 }
279 }
280 if found == nil {
281 return nil, fmt.Errorf("no matching entry found in index for architecture %s", architecture)
282 }
283 return oci.AsImage(ref.Ref(found))
284 default:
285 return nil, fmt.Errorf("unknown manifest media type %q", ref.MediaType())
286 }
287}
288
Jan Schär62cecde2025-04-16 15:24:04 +0000289// InstallImage fetches the given image, installs it into the currently inactive
290// slot and sets that slot to boot next. If it doesn't return an error, a reboot
291// boots into the new slot.
292func (s *Service) InstallImage(ctx context.Context, imageRef *apb.OSImageRef, withKexec bool) error {
293 if imageRef == nil {
294 return fmt.Errorf("missing OS image in OS installation request")
295 }
296 if imageRef.Digest == "" {
297 return fmt.Errorf("missing digest in OS installation request")
298 }
Lorenz Brun35fcf032023-06-29 04:15:58 +0200299 if s.ESPPath == "" {
300 return errors.New("no ESP information provided to update service, cannot continue")
301 }
Jan Schär62cecde2025-04-16 15:24:04 +0000302
303 downloadCtx, cancel := context.WithTimeout(ctx, 15*time.Minute)
304 defer cancel()
305
306 client := &registry.Client{
307 GetBackOff: func() backoff.BackOff {
308 return backoff.NewExponentialBackOff()
309 },
310 RetryNotify: func(err error, d time.Duration) {
311 s.Logger.Warningf("Error while fetching OS image, retrying in %v: %v", d, err)
312 },
Jan Schärb86917b2025-05-14 16:31:08 +0000313 UserAgent: "MonogonOS/" + productinfo.Get().VersionString,
Jan Schär62cecde2025-04-16 15:24:04 +0000314 Scheme: imageRef.Scheme,
315 Host: imageRef.Host,
316 Repository: imageRef.Repository,
Lorenz Brun35fcf032023-06-29 04:15:58 +0200317 }
Jan Schär62cecde2025-04-16 15:24:04 +0000318
Jan Schär1a7e1fe2025-07-25 16:50:12 +0200319 ref, err := client.Read(downloadCtx, imageRef.Tag, imageRef.Digest)
320 if err != nil {
321 return fmt.Errorf("failed to fetch OS image: %w", err)
322 }
323 image, err := selectArchitecture(ref, runtime.GOARCH)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200324 if err != nil {
Jan Schär62cecde2025-04-16 15:24:04 +0000325 return fmt.Errorf("failed to fetch OS image: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200326 }
Jan Schär62cecde2025-04-16 15:24:04 +0000327
Jan Schäre19d2792025-06-23 12:37:58 +0000328 osImage, err := osimage.Read(image)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200329 if err != nil {
Jan Schär62cecde2025-04-16 15:24:04 +0000330 return fmt.Errorf("failed to fetch OS image: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200331 }
Jan Schär62cecde2025-04-16 15:24:04 +0000332
333 efiPayload, err := osImage.Payload("kernel.efi")
Lorenz Brun35fcf032023-06-29 04:15:58 +0200334 if err != nil {
Jan Schär62cecde2025-04-16 15:24:04 +0000335 return fmt.Errorf("cannot open EFI payload in OS image: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200336 }
Jan Schär62cecde2025-04-16 15:24:04 +0000337 systemImage, err := osImage.Payload("system")
338 if err != nil {
339 return fmt.Errorf("cannot open system image in OS image: %w", err)
340 }
341
Lorenz Brun35fcf032023-06-29 04:15:58 +0200342 activeSlot := s.CurrentlyRunningSlot()
343 if activeSlot == SlotInvalid {
344 return errors.New("unable to determine active slot, cannot continue")
345 }
346 targetSlot := activeSlot.Other()
347
Lorenz Brun35fcf032023-06-29 04:15:58 +0200348 systemPart, err := openSystemSlot(targetSlot)
349 if err != nil {
350 return status.Errorf(codes.Internal, "Inactive system slot unavailable: %v", err)
351 }
Jan Schär62cecde2025-04-16 15:24:04 +0000352 systemImageContent, err := systemImage.Open()
353 if err != nil {
354 systemPart.Close()
355 return fmt.Errorf("failed to open system image: %w", err)
356 }
357 _, err = io.Copy(blockdev.NewRWS(systemPart), systemImageContent)
358 systemImageContent.Close()
359 closeErr := systemPart.Close()
360 if err == nil {
361 err = closeErr
362 }
363 if err != nil {
Lorenz Brun35fcf032023-06-29 04:15:58 +0200364 return status.Errorf(codes.Unavailable, "Failed to copy system image: %v", err)
365 }
366
367 bootFile, err := os.Create(filepath.Join(s.ESPPath, targetSlot.EFIBootPath()))
368 if err != nil {
369 return fmt.Errorf("failed to open boot file: %w", err)
370 }
371 defer bootFile.Close()
Jan Schär62cecde2025-04-16 15:24:04 +0000372 efiPayloadContent, err := efiPayload.Open()
373 if err != nil {
374 return fmt.Errorf("failed to open EFI payload: %w", err)
375 }
376 _, err = io.Copy(bootFile, efiPayloadContent)
377 efiPayloadContent.Close()
378 if err != nil {
Lorenz Brun35fcf032023-06-29 04:15:58 +0200379 return fmt.Errorf("failed to write boot file: %w", err)
380 }
381
Lorenz Brund14be0e2023-07-31 16:46:14 +0200382 if withKexec {
383 if err := s.stageKexec(bootFile, targetSlot); err != nil {
384 return fmt.Errorf("while kexec staging: %w", err)
385 }
386 } else {
Lorenz Brun54a5a052023-10-02 16:40:11 +0200387 err := s.setABState(&abloaderpb.ABLoaderData{
388 ActiveSlot: abloaderpb.Slot(activeSlot),
389 NextSlot: abloaderpb.Slot(targetSlot),
390 })
391 if err != nil {
392 return fmt.Errorf("while setting next A/B slot: %w", err)
Lorenz Brund14be0e2023-07-31 16:46:14 +0200393 }
Lorenz Brun35fcf032023-06-29 04:15:58 +0200394 }
395
396 return nil
397}
398
Lorenz Brund14be0e2023-07-31 16:46:14 +0200399// newMemfile creates a new file which is not located on a specific filesystem,
400// but is instead backed by anonymous memory.
401func newMemfile(name string, flags int) (*os.File, error) {
402 fd, err := unix.MemfdCreate(name, flags)
403 if err != nil {
404 return nil, fmt.Errorf("memfd_create: %w", err)
405 }
406 return os.NewFile(uintptr(fd), name), nil
407}
408
409// stageKexec stages the kernel, command line and initramfs if available for
410// a future kexec. It extracts the relevant data from the EFI boot executable.
411func (s *Service) stageKexec(bootFile io.ReaderAt, targetSlot Slot) error {
412 bootPE, err := pe.NewFile(bootFile)
413 if err != nil {
414 return fmt.Errorf("unable to open bootFile as PE: %w", err)
415 }
416 var cmdlineRaw []byte
417 cmdlineSection := bootPE.Section(".cmdline")
418 if cmdlineSection == nil {
419 return fmt.Errorf("no .cmdline section in boot PE")
420 }
421 cmdlineRaw, err = cmdlineSection.Data()
422 if err != nil {
423 return fmt.Errorf("while reading .cmdline PE section: %w", err)
424 }
425 cmdline := string(bytes.TrimRight(cmdlineRaw, "\x00"))
426 cmdline = strings.ReplaceAll(cmdline, "METROPOLIS-SYSTEM-X", fmt.Sprintf("METROPOLIS-SYSTEM-%s", targetSlot))
427 kernelFile, err := newMemfile("kernel", 0)
428 if err != nil {
429 return fmt.Errorf("failed to create kernel memfile: %w", err)
430 }
431 defer kernelFile.Close()
432 kernelSection := bootPE.Section(".linux")
433 if kernelSection == nil {
434 return fmt.Errorf("no .linux section in boot PE")
435 }
436 if _, err := io.Copy(kernelFile, kernelSection.Open()); err != nil {
437 return fmt.Errorf("while copying .linux PE section: %w", err)
438 }
439
440 initramfsSection := bootPE.Section(".initrd")
441 var initramfsFile *os.File
442 if initramfsSection != nil && initramfsSection.Size > 0 {
443 initramfsFile, err = newMemfile("initramfs", 0)
444 if err != nil {
445 return fmt.Errorf("failed to create initramfs memfile: %w", err)
446 }
447 defer initramfsFile.Close()
448 if _, err := io.Copy(initramfsFile, initramfsSection.Open()); err != nil {
449 return fmt.Errorf("while copying .initrd PE section: %w", err)
450 }
451 }
452 if err := kexec.FileLoad(kernelFile, initramfsFile, cmdline); err != nil {
453 return fmt.Errorf("while staging new kexec kernel: %w", err)
454 }
455 return nil
456}
Lorenz Brund79881d2023-11-30 19:02:06 +0100457
Jan Schär2b9a0a02025-07-09 07:54:12 +0000458//go:embed metropolis/node/abloader/abloader.efi
Lorenz Brund79881d2023-11-30 19:02:06 +0100459var abloader []byte
460
461func (s *Service) fixupPreloader() error {
Jan Schäre19d2792025-06-23 12:37:58 +0000462 efiBootPath, err := install.EFIBootPath(productinfo.Get().Info.Architecture())
Jan Schär4b888262025-05-13 09:12:03 +0000463 if err != nil {
464 return err
465 }
466 efiBootFilePath := filepath.Join(s.ESPPath, efiBootPath)
467 abLoaderFile, err := os.Open(efiBootFilePath)
Lorenz Brund79881d2023-11-30 19:02:06 +0100468 if err != nil {
469 s.Logger.Warningf("A/B preloader not available, attempting to restore: %v", err)
470 } else {
471 expectedSum := sha256.Sum256(abloader)
472 h := sha256.New()
473 _, err := io.Copy(h, abLoaderFile)
474 abLoaderFile.Close()
475 if err == nil {
476 if bytes.Equal(h.Sum(nil), expectedSum[:]) {
477 // A/B Preloader is present and has correct hash
478 return nil
479 } else {
480 s.Logger.Infof("Replacing A/B preloader with current version: %x %x", h.Sum(nil), expectedSum[:])
481 }
482 } else {
483 s.Logger.Warningf("Error while reading A/B preloader, restoring: %v", err)
484 }
485 }
486 preloader, err := os.Create(filepath.Join(s.ESPPath, "preloader.swp"))
487 if err != nil {
488 return fmt.Errorf("while creating preloader swap file: %w", err)
489 }
490 if _, err := preloader.Write(abloader); err != nil {
491 return fmt.Errorf("while writing preloader swap file: %w", err)
492 }
493 if err := preloader.Sync(); err != nil {
494 return fmt.Errorf("while sync'ing preloader swap file: %w", err)
495 }
496 preloader.Close()
Jan Schär4b888262025-05-13 09:12:03 +0000497 if err := os.Rename(filepath.Join(s.ESPPath, "preloader.swp"), efiBootFilePath); err != nil {
Lorenz Brund79881d2023-11-30 19:02:06 +0100498 return fmt.Errorf("while swapping preloader: %w", err)
499 }
500 s.Logger.Info("Successfully wrote current preloader")
501 return nil
502}
503
504// fixupEFI checks for the existence and correctness of the EFI boot entry
505// repairs/recreates it if needed.
506func (s *Service) fixupEFI() error {
Jan Schäre19d2792025-06-23 12:37:58 +0000507 efiBootPath, err := install.EFIBootPath(productinfo.Get().Info.Architecture())
Jan Schär4b888262025-05-13 09:12:03 +0000508 if err != nil {
509 return err
510 }
511 efiBootVarPath := "/" + efiBootPath
Lorenz Brund79881d2023-11-30 19:02:06 +0100512 varNames, err := efivarfs.List(efivarfs.ScopeGlobal)
513 if err != nil {
514 return fmt.Errorf("failed to list EFI variables: %w", err)
515 }
Tim Windelschmidt5e460a92024-04-11 01:33:09 +0200516 var validBootEntryIdx = -1
Lorenz Brund79881d2023-11-30 19:02:06 +0100517 for _, varName := range varNames {
518 m := bootVarRegexp.FindStringSubmatch(varName)
519 if m == nil {
520 continue
521 }
522 idx, err := strconv.ParseUint(m[1], 16, 16)
523 if err != nil {
524 // This cannot be hit as all regexp matches are parseable.
525 panic(err)
526 }
527 e, err := efivarfs.GetBootEntry(int(idx))
528 if err != nil {
529 s.Logger.Warningf("Unable to get boot entry %d, skipping: %v", idx, err)
530 continue
531 }
532 if len(e.FilePath) != 2 {
533 // Not our entry, ours always have two parts
534 continue
535 }
536 switch p := e.FilePath[0].(type) {
537 case *efivarfs.HardDrivePath:
538 gptMatch, ok := p.PartitionMatch.(*efivarfs.PartitionGPT)
539 if ok && gptMatch.PartitionUUID != s.ESPPart.ID {
540 // Not related to our ESP
541 continue
542 }
543 default:
544 continue
545 }
546 switch p := e.FilePath[1].(type) {
547 case efivarfs.FilePath:
Jan Schär4b888262025-05-13 09:12:03 +0000548 if string(p) == efiBootVarPath {
Lorenz Brund79881d2023-11-30 19:02:06 +0100549 if validBootEntryIdx == -1 {
550 validBootEntryIdx = int(idx)
551 } else {
552 // Another valid boot entry already exists, delete this one
553 err := efivarfs.DeleteBootEntry(int(idx))
554 if err == nil {
555 s.Logger.Infof("Deleted duplicate boot entry %q", e.Description)
556 } else {
557 s.Logger.Warningf("Error while deleting duplicate boot entry %q: %v", e.Description, err)
558 }
559 }
560 } else if strings.Contains(e.Description, "Metropolis") {
561 err := efivarfs.DeleteBootEntry(int(idx))
562 if err == nil {
563 s.Logger.Infof("Deleted orphaned boot entry %q", e.Description)
564 } else {
565 s.Logger.Warningf("Error while deleting orphaned boot entry %q: %v", e.Description, err)
566 }
567 }
568 default:
569 continue
570 }
571 }
572 if validBootEntryIdx == -1 {
573 validBootEntryIdx, err = efivarfs.AddBootEntry(&efivarfs.LoadOption{
574 Description: "Metropolis",
575 FilePath: efivarfs.DevicePath{
576 &efivarfs.HardDrivePath{
577 PartitionNumber: 1,
578 PartitionStartBlock: s.ESPPart.FirstBlock,
579 PartitionSizeBlocks: s.ESPPart.SizeBlocks(),
580 PartitionMatch: efivarfs.PartitionGPT{
581 PartitionUUID: s.ESPPart.ID,
582 },
583 },
Jan Schär4b888262025-05-13 09:12:03 +0000584 efivarfs.FilePath(efiBootVarPath),
Lorenz Brund79881d2023-11-30 19:02:06 +0100585 },
586 })
587 if err == nil {
588 s.Logger.Infof("Restored missing EFI boot entry for Metropolis")
589 } else {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200590 return fmt.Errorf("while restoring missing EFI boot entry for Metropolis: %w", err)
Lorenz Brund79881d2023-11-30 19:02:06 +0100591 }
592 }
593 bootOrder, err := efivarfs.GetBootOrder()
594 if err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200595 return fmt.Errorf("failed to get EFI boot order: %w", err)
Lorenz Brund79881d2023-11-30 19:02:06 +0100596 }
597 for _, bentry := range bootOrder {
598 if bentry == uint16(validBootEntryIdx) {
599 // Our boot entry is in the boot order, everything's ok
600 return nil
601 }
602 }
603 newBootOrder := append(efivarfs.BootOrder{uint16(validBootEntryIdx)}, bootOrder...)
604 if err := efivarfs.SetBootOrder(newBootOrder); err != nil {
605 return fmt.Errorf("while setting EFI boot order: %w", err)
606 }
607 return nil
608}