• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (C) 2024  Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 use super::partition::{MetadataBytes, SlotBlock};
16 use super::{
17     BootTarget, BootToken, Bootability, Manager, OneShot, RecoveryTarget, Slot, SlotIterator,
18     Suffix, UnbootableReason,
19 };
20 
21 use core::convert::TryInto;
22 use core::iter::zip;
23 use core::mem::size_of;
24 use core::ops::{BitAnd, BitOr, Not, Shl, Shr};
25 use crc32fast::Hasher;
26 use liberror::Error;
27 use zerocopy::byteorder::little_endian::U32 as LittleEndianU32;
28 use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Ref, SplitByteSlice};
29 
30 extern crate static_assertions;
31 
32 const MAX_SLOTS: u8 = 4;
33 
34 // TODO(b/332338968): remove the manual field definitions and use bindgen definitions.
35 
36 // Helper function to extract values from bitfields.
37 // Preconditions:
38 // 1) All bits in a bitfield are consecutive.
39 // 1a) No fields interleave their bits.
40 // 2) `offset` defines the position of the least significant bit in the field.
41 // 3) If a bit is set in `mask`, all bits of lower significance are set.
42 // 4) If a bit is NOT set in `mask`, all bits of greater significanec are NOT set.
get_field<N, R>(base: N, offset: N, mask: N) -> R where N: Shr<Output = N> + BitAnd<Output = N>, R: Default + TryFrom<N>,43 fn get_field<N, R>(base: N, offset: N, mask: N) -> R
44 where
45     N: Shr<Output = N> + BitAnd<Output = N>,
46     R: Default + TryFrom<N>,
47 {
48     ((base >> offset) & mask).try_into().unwrap_or_default()
49 }
50 
51 // Helper function to set values in bit fields.
52 // All the preconditions for `get_field` apply.
53 // Returns the modified field. It is the caller's responsibility
54 // to assign the result appropriately.
set_field<N, R>(base: N, val: R, offset: N, mask: N) -> N where N: Copy + Shl<Output = N> + BitAnd<Output = N> + BitOr<Output = N> + Not<Output = N>, R: Into<N>,55 fn set_field<N, R>(base: N, val: R, offset: N, mask: N) -> N
56 where
57     N: Copy + Shl<Output = N> + BitAnd<Output = N> + BitOr<Output = N> + Not<Output = N>,
58     R: Into<N>,
59 {
60     (base & !(mask << offset)) | ((val.into() & mask) << offset)
61 }
62 
63 const DEFAULT_PRIORITY: u8 = 7;
64 const DEFAULT_RETRIES: u8 = 7;
65 
66 /// Android reference implementation for slot-specific metadata.
67 /// See `BootloaderControl` for more background information.
68 ///
69 /// Does NOT contain unbootable reason information.
70 #[repr(C, packed)]
71 #[derive(Copy, Clone, Debug, PartialEq, Eq, Immutable, IntoBytes, FromBytes, KnownLayout)]
72 struct SlotMetaData(u16);
73 
74 #[allow(dead_code)]
75 #[allow(missing_docs)]
76 impl SlotMetaData {
77     const PRIORITY_MASK: u16 = 0b1111;
78     const PRIORITY_OFFSET: u16 = 0;
79 
80     const TRIES_MASK: u16 = 0b111;
81     const TRIES_OFFSET: u16 = 4;
82 
83     const SUCCESSFUL_MASK: u16 = 0b1;
84     const SUCCESSFUL_OFFSET: u16 = 7;
85 
86     const VERITY_CORRUPTED_MASK: u16 = 0b1;
87     const VERITY_CORRUPTED_OFFSET: u16 = 8;
88 
priority(&self) -> u889     fn priority(&self) -> u8 {
90         get_field(self.0, Self::PRIORITY_OFFSET, Self::PRIORITY_MASK)
91     }
set_priority(&mut self, priority: u8)92     fn set_priority(&mut self, priority: u8) {
93         self.0 = set_field(self.0, priority, Self::PRIORITY_OFFSET, Self::PRIORITY_MASK)
94     }
95 
tries(&self) -> u896     fn tries(&self) -> u8 {
97         get_field(self.0, Self::TRIES_OFFSET, Self::TRIES_MASK)
98     }
set_tries(&mut self, tries: u8)99     fn set_tries(&mut self, tries: u8) {
100         self.0 = set_field(self.0, tries, Self::TRIES_OFFSET, Self::TRIES_MASK)
101     }
102 
successful(&self) -> bool103     fn successful(&self) -> bool {
104         get_field::<_, u8>(self.0, Self::SUCCESSFUL_OFFSET, Self::SUCCESSFUL_MASK) != 0
105     }
set_successful(&mut self, successful: bool)106     fn set_successful(&mut self, successful: bool) {
107         self.0 = set_field(self.0, successful, Self::SUCCESSFUL_OFFSET, Self::SUCCESSFUL_MASK);
108     }
109 
verity_corrupted(&self) -> bool110     fn verity_corrupted(&self) -> bool {
111         get_field::<_, u8>(self.0, Self::VERITY_CORRUPTED_OFFSET, Self::VERITY_CORRUPTED_MASK) != 0
112     }
set_verity_corrupted(&mut self, verity_corrupted: bool)113     fn set_verity_corrupted(&mut self, verity_corrupted: bool) {
114         self.0 = set_field(
115             self.0,
116             verity_corrupted,
117             Self::VERITY_CORRUPTED_OFFSET,
118             Self::VERITY_CORRUPTED_MASK,
119         );
120     }
121 }
122 static_assertions::const_assert_eq!(
123     core::mem::size_of::<SlotMetaData>(),
124     core::mem::size_of::<u16>()
125 );
126 
127 impl Default for SlotMetaData {
default() -> Self128     fn default() -> Self {
129         let mut val = Self(0);
130         val.set_priority(DEFAULT_PRIORITY);
131         val.set_tries(DEFAULT_RETRIES);
132 
133         val
134     }
135 }
136 
137 #[derive(
138     Copy, Clone, Debug, Default, PartialEq, Eq, Immutable, IntoBytes, FromBytes, KnownLayout,
139 )]
140 #[repr(C, packed)]
141 struct ControlBits(u16);
142 
143 #[allow(dead_code)]
144 #[allow(missing_docs)]
145 impl ControlBits {
146     const NB_SLOT_MASK: u16 = 0b111;
147     const NB_SLOT_OFFSET: u16 = 0;
148 
149     const RECOVERY_TRIES_MASK: u16 = 0b111;
150     const RECOVERY_TRIES_OFFSET: u16 = 3;
151 
152     const MERGE_STATUS_MASK: u16 = 0b111;
153     const MERGE_STATUS_OFFSET: u16 = 6;
154 
nb_slots(&self) -> u8155     fn nb_slots(&self) -> u8 {
156         core::cmp::min(get_field(self.0, Self::NB_SLOT_OFFSET, Self::NB_SLOT_MASK), MAX_SLOTS)
157     }
set_nb_slots(&mut self, nb_slots: u8)158     fn set_nb_slots(&mut self, nb_slots: u8) {
159         self.0 = set_field(
160             self.0,
161             core::cmp::min(nb_slots, MAX_SLOTS),
162             Self::NB_SLOT_OFFSET,
163             Self::NB_SLOT_MASK,
164         );
165     }
166 
recovery_tries(&self) -> u8167     fn recovery_tries(&self) -> u8 {
168         get_field(self.0, Self::RECOVERY_TRIES_OFFSET, Self::RECOVERY_TRIES_MASK)
169     }
set_recovery_tries(&mut self, recovery_tries: u8)170     fn set_recovery_tries(&mut self, recovery_tries: u8) {
171         self.0 = set_field(
172             self.0,
173             recovery_tries,
174             Self::RECOVERY_TRIES_OFFSET,
175             Self::RECOVERY_TRIES_MASK,
176         );
177     }
178 
merge_status(&self) -> u8179     fn merge_status(&self) -> u8 {
180         get_field(self.0, Self::MERGE_STATUS_OFFSET, Self::MERGE_STATUS_MASK)
181     }
set_merge_status(&mut self, merge_status: u8)182     fn set_merge_status(&mut self, merge_status: u8) {
183         self.0 =
184             set_field(self.0, merge_status, Self::MERGE_STATUS_OFFSET, Self::MERGE_STATUS_MASK);
185     }
186 }
187 
188 const BOOT_CTRL_MAGIC: u32 = 0x42414342;
189 const BOOT_CTRL_VERSION: u8 = 1;
190 
191 /// The reference implementation for Android A/B bootloader message structures.
192 /// It is designed to be put in the `slot_suffix` field of the `bootloader_message`
193 /// structure described bootloader_message.h.
194 ///
195 /// See //hardware/interfaces/boot/1.1/default/boot_control/libboot_control.cpp
196 /// and //hardware/interfaces/boot/1.1/default/boot_control/include/private/boot_control_definition.h
197 /// for structure definition and semantics.
198 ///
199 /// Does NOT support oneshots
200 #[repr(C, packed)]
201 #[derive(Copy, Clone, Debug, PartialEq, Eq, Immutable, IntoBytes, FromBytes, KnownLayout)]
202 struct BootloaderControl {
203     slot_suffix: [u8; 4],
204     magic: u32,
205     version: u8,
206     control_bits: ControlBits,
207     reserved0: [u8; 1],
208     slot_metadata: [SlotMetaData; MAX_SLOTS as usize],
209     reserved1: [u8; 8],
210     crc32: LittleEndianU32,
211 }
212 static_assertions::const_assert_eq!(core::mem::size_of::<BootloaderControl>(), 32);
213 
214 impl BootloaderControl {
calculate_crc32(&self) -> u32215     fn calculate_crc32(&self) -> u32 {
216         let mut hasher = Hasher::new();
217         hasher.update(&self.as_bytes()[..(size_of::<Self>() - size_of::<LittleEndianU32>())]);
218         hasher.finalize()
219     }
220 }
221 
222 impl Default for BootloaderControl {
default() -> Self223     fn default() -> Self {
224         let mut data = Self {
225             slot_suffix: Default::default(),
226             magic: BOOT_CTRL_MAGIC,
227             version: BOOT_CTRL_VERSION,
228             control_bits: Default::default(),
229             reserved0: Default::default(),
230             slot_metadata: Default::default(),
231             reserved1: Default::default(),
232             crc32: LittleEndianU32::ZERO,
233         };
234         // The slot suffix field stores the current active slot,
235         // which starts as the first one.
236         // Notice that it stores the entire suffix,
237         // including the leading underscore.
238         '_'.encode_utf8(&mut data.slot_suffix[0..]);
239         'a'.encode_utf8(&mut data.slot_suffix[1..]);
240         data.control_bits.set_nb_slots(4);
241         data.crc32.set(data.calculate_crc32());
242         data
243     }
244 }
245 
246 impl MetadataBytes for BootloaderControl {
validate<B: SplitByteSlice>(buffer: B) -> Result<Ref<B, Self>, Error>247     fn validate<B: SplitByteSlice>(buffer: B) -> Result<Ref<B, Self>, Error> {
248         let boot_control_data = Ref::<B, Self>::new_from_prefix(buffer)
249             .ok_or(Error::BufferTooSmall(Some(size_of::<BootloaderControl>())))?
250             .0;
251 
252         if boot_control_data.magic != BOOT_CTRL_MAGIC {
253             return Err(Error::BadMagic);
254         }
255         if boot_control_data.version > BOOT_CTRL_VERSION {
256             return Err(Error::UnsupportedVersion);
257         }
258         if boot_control_data.crc32.get() != boot_control_data.calculate_crc32() {
259             return Err(Error::BadChecksum);
260         }
261 
262         Ok(boot_control_data)
263     }
264 
prepare_for_sync(&mut self)265     fn prepare_for_sync(&mut self) {
266         self.crc32 = self.calculate_crc32().into();
267     }
268 }
269 
270 impl super::private::SlotGet for SlotBlock<BootloaderControl> {
get_slot_by_number(&self, number: usize) -> Result<Slot, Error>271     fn get_slot_by_number(&self, number: usize) -> Result<Slot, Error> {
272         let lower_ascii_suffixes = ('a'..='z').map(Suffix);
273         let control = self.get_data();
274         let (suffix, &slot_data) = zip(lower_ascii_suffixes, control.slot_metadata.iter())
275             // Note: there may be fewer slots than the maximum possible
276             .take(control.control_bits.nb_slots().into())
277             .nth(number)
278             .ok_or(Error::BadIndex(number))?;
279 
280         let bootability = match (slot_data.successful(), slot_data.tries()) {
281             (true, _) => Bootability::Successful,
282             (false, t) if t > 0 => Bootability::Retriable(t.into()),
283             (_, _) => Bootability::Unbootable(UnbootableReason::Unknown),
284         };
285 
286         Ok(Slot { suffix, priority: slot_data.priority().into(), bootability })
287     }
288 }
289 
290 impl Manager for SlotBlock<BootloaderControl> {
slots_iter(&self) -> SlotIterator291     fn slots_iter(&self) -> SlotIterator {
292         SlotIterator::new(self)
293     }
294 
get_boot_target(&self) -> Result<BootTarget, Error>295     fn get_boot_target(&self) -> Result<BootTarget, Error> {
296         Ok(self
297             .slots_iter()
298             .filter(Slot::is_bootable)
299             .max_by_key(|slot| (slot.priority, slot.suffix.rank()))
300             .map_or(
301                 // TODO(b/326253270): how is the recovery slot actually determined?
302                 BootTarget::Recovery(RecoveryTarget::Slotted(self.get_slot_last_set_active()?)),
303                 BootTarget::NormalBoot,
304             ))
305     }
306 
set_slot_unbootable( &mut self, slot_suffix: Suffix, reason: UnbootableReason, ) -> Result<(), Error>307     fn set_slot_unbootable(
308         &mut self,
309         slot_suffix: Suffix,
310         reason: UnbootableReason,
311     ) -> Result<(), Error> {
312         let (idx, slot) = self
313             .slots_iter()
314             .enumerate()
315             .find(|(_, slot)| slot.suffix == slot_suffix)
316             .ok_or(Error::InvalidInput)?;
317         if slot.bootability == Bootability::Unbootable(reason) {
318             return Ok(());
319         }
320 
321         let slot_data = &mut self.get_mut_data().slot_metadata[idx];
322         slot_data.set_tries(0);
323         slot_data.set_successful(false);
324 
325         Ok(())
326     }
327 
mark_boot_attempt(&mut self) -> Result<BootToken, Error>328     fn mark_boot_attempt(&mut self) -> Result<BootToken, Error> {
329         let target_slot = match self.get_boot_target()? {
330             BootTarget::NormalBoot(slot) => slot,
331             BootTarget::Recovery(RecoveryTarget::Dedicated) => Err(Error::OperationProhibited)?,
332             BootTarget::Recovery(RecoveryTarget::Slotted(slot)) => {
333                 self.slots_iter().find(|s| s.suffix == slot.suffix).ok_or(Error::InvalidInput)?;
334                 return self.take_boot_token().ok_or(Error::OperationProhibited);
335             }
336         };
337 
338         let (idx, slot) = self
339             .slots_iter()
340             .enumerate()
341             .find(|(_, slot)| slot.suffix == target_slot.suffix)
342             .ok_or(Error::InvalidInput)?;
343         match slot.bootability {
344             Bootability::Unbootable(_) => Err(Error::OperationProhibited),
345             Bootability::Retriable(_) => {
346                 let metadata = &mut self.get_mut_data().slot_metadata[idx];
347                 metadata.set_tries(metadata.tries() - 1);
348                 let token = self.take_boot_token().ok_or(Error::OperationProhibited)?;
349                 Ok(token)
350             }
351             Bootability::Successful => {
352                 let token = self.take_boot_token().ok_or(Error::OperationProhibited)?;
353                 Ok(token)
354             }
355         }
356     }
357 
set_active_slot(&mut self, slot_suffix: Suffix) -> Result<(), Error>358     fn set_active_slot(&mut self, slot_suffix: Suffix) -> Result<(), Error> {
359         let idx =
360             self.slots_iter().position(|s| s.suffix == slot_suffix).ok_or(Error::InvalidInput)?;
361 
362         let data = self.get_mut_data();
363         for (i, slot) in data.slot_metadata.iter_mut().enumerate() {
364             if i == idx {
365                 *slot = Default::default();
366             } else {
367                 slot.set_priority(DEFAULT_PRIORITY - 1);
368             }
369         }
370 
371         // Note: we know this is safe because the slot suffix is an ASCII char,
372         // which is only 1 byte long in utf8.
373         // The 0th element of self.data.slot_suffix is an underscore character.
374         slot_suffix.0.encode_utf8(&mut self.get_mut_data().slot_suffix[1..]);
375 
376         Ok(())
377     }
378 
set_oneshot_status(&mut self, _: OneShot) -> Result<(), Error>379     fn set_oneshot_status(&mut self, _: OneShot) -> Result<(), Error> {
380         Err(Error::OperationProhibited)
381     }
382 
clear_oneshot_status(&mut self)383     fn clear_oneshot_status(&mut self) {}
384 
write_back(&mut self, persist: &mut dyn FnMut(&mut [u8]) -> Result<(), Error>)385     fn write_back(&mut self, persist: &mut dyn FnMut(&mut [u8]) -> Result<(), Error>) {
386         self.sync_to_disk(persist)
387     }
388 }
389 
390 #[cfg(test)]
391 mod test {
392     use super::*;
393     use crate::slots::{android::BootloaderControl, partition::MetadataBytes};
394 
395     #[test]
test_slot_block_defaults()396     fn test_slot_block_defaults() {
397         let sb: SlotBlock<BootloaderControl> = Default::default();
398         let expected: Vec<Slot> = ('a'..='d')
399             .map(|c| Slot {
400                 suffix: c.into(),
401                 priority: DEFAULT_PRIORITY.into(),
402                 bootability: Bootability::Retriable(sb.get_max_retries().unwrap()),
403             })
404             .collect();
405         let actual: Vec<Slot> = sb.slots_iter().collect();
406         assert_eq!(actual, expected);
407         assert_eq!(sb.get_oneshot_status(), None);
408         assert_eq!(sb.get_boot_target().unwrap(), BootTarget::NormalBoot(expected[0]));
409         // Include the explicit null bytes for safety.
410         assert_eq!(sb.get_data().slot_suffix.as_slice(), "_a\0\0".as_bytes());
411     }
412 
413     #[test]
test_slot_block_fewer_slots()414     fn test_slot_block_fewer_slots() {
415         let mut sb: SlotBlock<BootloaderControl> = Default::default();
416         sb.get_mut_data().control_bits.set_nb_slots(2);
417 
418         let expected: Vec<Slot> = ('a'..='b')
419             .map(|c| Slot {
420                 suffix: c.into(),
421                 priority: DEFAULT_PRIORITY.into(),
422                 bootability: Bootability::Retriable(sb.get_max_retries().unwrap()),
423             })
424             .collect();
425         let actual: Vec<Slot> = sb.slots_iter().collect();
426         assert_eq!(actual, expected);
427     }
428 
429     #[test]
test_slot_block_slot_count_saturates()430     fn test_slot_block_slot_count_saturates() {
431         let mut ctrl: BootloaderControl = Default::default();
432         ctrl.control_bits.set_nb_slots(255);
433         assert_eq!(ctrl.control_bits.nb_slots(), MAX_SLOTS);
434 
435         let mut sb: SlotBlock<BootloaderControl> = Default::default();
436         sb.get_mut_data().control_bits.set_nb_slots(255);
437         assert_eq!(sb.slots_iter().count(), MAX_SLOTS.into());
438     }
439 
440     #[test]
test_slot_block_parse()441     fn test_slot_block_parse() {
442         let boot_ctrl: BootloaderControl = Default::default();
443         assert_eq!(
444             BootloaderControl::validate(boot_ctrl.as_bytes()),
445             Ok(Ref::new(boot_ctrl.as_bytes()).unwrap())
446         );
447     }
448 
449     #[test]
test_slot_block_parse_buffer_too_small()450     fn test_slot_block_parse_buffer_too_small() {
451         let buffer: [u8; 0] = Default::default();
452         assert_eq!(
453             BootloaderControl::validate(buffer.as_slice()),
454             Err(Error::BufferTooSmall(Some(size_of::<BootloaderControl>())))
455         );
456     }
457 
458     #[test]
test_slot_block_parse_bad_magic()459     fn test_slot_block_parse_bad_magic() {
460         let mut boot_ctrl: BootloaderControl = Default::default();
461         boot_ctrl.magic += 1;
462         assert_eq!(BootloaderControl::validate(boot_ctrl.as_bytes()), Err(Error::BadMagic));
463     }
464 
465     #[test]
test_slot_block_parse_bad_version()466     fn test_slot_block_parse_bad_version() {
467         let mut boot_ctrl: BootloaderControl = Default::default();
468         boot_ctrl.version = 15;
469         assert_eq!(
470             BootloaderControl::validate(boot_ctrl.as_bytes()),
471             Err(Error::UnsupportedVersion)
472         );
473     }
474 
475     #[test]
test_slot_block_parse_bad_crc()476     fn test_slot_block_parse_bad_crc() {
477         let mut boot_ctrl: BootloaderControl = Default::default();
478         let bad_crc = boot_ctrl.crc32.get() ^ LittleEndianU32::MAX_VALUE.get();
479         boot_ctrl.crc32 = bad_crc.into();
480         assert_eq!(BootloaderControl::validate(boot_ctrl.as_bytes()), Err(Error::BadChecksum));
481     }
482 
483     #[test]
test_get_boot_target_recovery()484     fn test_get_boot_target_recovery() {
485         let mut sb: SlotBlock<BootloaderControl> = Default::default();
486         sb.get_mut_data().slot_metadata.iter_mut().for_each(|bits| bits.set_tries(0));
487         let a_slot = sb.slots_iter().next().unwrap();
488 
489         assert_eq!(
490             sb.get_boot_target().unwrap(),
491             BootTarget::Recovery(RecoveryTarget::Slotted(a_slot))
492         );
493     }
494 
495     #[test]
test_get_boot_target_recovery_nondefault_recovery_slot()496     fn test_get_boot_target_recovery_nondefault_recovery_slot() {
497         let mut sb: SlotBlock<BootloaderControl> = Default::default();
498         let b_suffix: Suffix = 'b'.into();
499         assert!(sb.set_active_slot(b_suffix).is_ok());
500         sb.get_mut_data().slot_metadata.iter_mut().for_each(|bits| bits.set_tries(0));
501         let b_slot = sb.slots_iter().find(|s| s.suffix == b_suffix).unwrap();
502 
503         assert_eq!(
504             sb.get_boot_target().unwrap(),
505             BootTarget::Recovery(RecoveryTarget::Slotted(b_slot))
506         );
507     }
508 
509     #[test]
test_get_slot_last_set_active()510     fn test_get_slot_last_set_active() {
511         let mut sb: SlotBlock<BootloaderControl> = Default::default();
512         let v: Vec<Slot> = sb.slots_iter().collect();
513         assert_eq!(sb.set_active_slot(v[1].suffix), Ok(()));
514         assert_eq!(sb.get_slot_last_set_active().unwrap(), v[1]);
515         for slot in v.iter() {
516             assert_eq!(sb.set_slot_unbootable(slot.suffix, UnbootableReason::NoMoreTries), Ok(()));
517         }
518 
519         assert_eq!(sb.get_slot_last_set_active().unwrap(), sb.slots_iter().nth(1).unwrap());
520         assert_eq!(sb.get_data().slot_suffix.as_slice(), "_b\0\0".as_bytes());
521     }
522 
523     #[test]
test_slot_mark_boot_attempt()524     fn test_slot_mark_boot_attempt() {
525         let mut sb: SlotBlock<BootloaderControl> = Default::default();
526         let slot = Slot { suffix: 'a'.into(), ..Default::default() };
527         assert_eq!(sb.mark_boot_attempt(), Ok(BootToken(())));
528         assert_eq!(
529             sb.slots_iter().next().unwrap(),
530             Slot {
531                 suffix: slot.suffix,
532                 priority: DEFAULT_PRIORITY.into(),
533                 bootability: Bootability::Retriable((DEFAULT_RETRIES - 1).into())
534             }
535         );
536 
537         // Make sure we can call exactly once
538         assert_eq!(sb.mark_boot_attempt(), Err(Error::OperationProhibited));
539     }
540 
541     #[test]
test_slot_mark_boot_attempt_no_more_tries()542     fn test_slot_mark_boot_attempt_no_more_tries() {
543         let mut sb: SlotBlock<BootloaderControl> = Default::default();
544         sb.get_mut_data().slot_metadata[0].set_tries(1);
545         let slot = Slot { suffix: 'a'.into(), ..Default::default() };
546         assert_eq!(sb.mark_boot_attempt(), Ok(BootToken(())));
547         assert_eq!(
548             sb.slots_iter().next().unwrap(),
549             Slot {
550                 suffix: slot.suffix,
551                 priority: DEFAULT_PRIORITY.into(),
552                 // Default implementation does not track unbootable reasons
553                 bootability: Bootability::Unbootable(UnbootableReason::Unknown)
554             }
555         );
556         assert_eq!(sb.get_data().slot_metadata[0].tries(), 0);
557     }
558 
559     #[test]
test_slot_mark_boot_attempt_successful()560     fn test_slot_mark_boot_attempt_successful() {
561         let mut sb: SlotBlock<BootloaderControl> = Default::default();
562         let initial_tries;
563         {
564             let metadata = &mut sb.get_mut_data().slot_metadata[0];
565             initial_tries = metadata.tries();
566             metadata.set_successful(true);
567         }
568         let target = BootTarget::NormalBoot(Slot {
569             suffix: 'a'.into(),
570             priority: DEFAULT_PRIORITY.into(),
571             bootability: Bootability::Successful,
572         });
573         assert_eq!(sb.mark_boot_attempt(), Ok(BootToken(())));
574         assert_eq!(BootTarget::NormalBoot(sb.slots_iter().next().unwrap()), target);
575         assert_eq!(sb.get_data().slot_metadata[0].tries(), initial_tries);
576     }
577 
578     #[test]
test_mark_slot_tried_slotted_recovery()579     fn test_mark_slot_tried_slotted_recovery() {
580         let mut sb: SlotBlock<BootloaderControl> = Default::default();
581         assert!(sb.set_slot_unbootable('a'.into(), UnbootableReason::UserRequested).is_ok());
582         assert!(sb.set_slot_unbootable('b'.into(), UnbootableReason::UserRequested).is_ok());
583         assert_eq!(sb.mark_boot_attempt(), Ok(BootToken(())));
584     }
585 
586     #[test]
test_set_oneshot_status_unsupported()587     fn test_set_oneshot_status_unsupported() {
588         let mut sb: SlotBlock<BootloaderControl> = Default::default();
589         let oneshots = [
590             OneShot::Bootloader,
591             OneShot::Continue(RecoveryTarget::Dedicated),
592             OneShot::Continue(RecoveryTarget::Slotted(sb.get_slot_last_set_active().unwrap())),
593         ];
594 
595         for oneshot in oneshots {
596             assert_eq!(sb.set_oneshot_status(oneshot), Err(Error::OperationProhibited));
597         }
598     }
599 }
600