1use anyhow::{Context, Result, ensure};
35use camino::{Utf8Path, Utf8PathBuf};
36use canon_json::CanonJsonSerialize;
37use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt};
38use composefs_ctl::composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
39use linux_kernel_cmdline::utf8::Cmdline;
40use rustix::fs::fsync;
41use serde::{Deserialize, Serialize};
42
43use crate::composefs_consts::{
44 ABOOT_ARTIFACT_DIR, ABOOT_ATTEMPTED_FNAME, ABOOT_IMAGE_FNAME, ABOOT_PENDING_FNAME,
45 ABOOT_ROLLBACK_FNAME, ABOOT_STATE_DIR, ABOOT_VBMETA_FNAME, STATE_DIR_RELATIVE,
46};
47use crate::spec::Bootloader;
48
49use super::{
50 boot::BootType,
51 state::{read_boot_type, remove_staged_deployment, write_staged_deployment},
52 status::StagedDeployment,
53};
54
55const SLOT_SUFFIX: &str = "androidboot.slot_suffix";
56const ABOOT_CONFIG: &str = "/etc/aboot.cfg";
57
58pub(crate) fn get_bootloader() -> Result<Bootloader> {
60 let config = match std::fs::read_to_string(ABOOT_CONFIG) {
61 Ok(config) => config,
62 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Bootloader::None),
63 Err(err) => return Err(err).with_context(|| format!("Reading {ABOOT_CONFIG}")),
64 };
65 Ok(bootloader_from_config(&config))
66}
67
68fn bootloader_from_config(config: &str) -> Bootloader {
69 let boot_type = config
72 .lines()
73 .filter(|line| !line.starts_with('#'))
74 .filter_map(|line| line.split_once('='))
75 .filter(|(key, _)| key.trim_ascii() == "BOOT_TYPE")
76 .map(|(_, value)| value.trim_ascii())
77 .next_back();
78 match boot_type {
79 Some("ukiboot") => Bootloader::Ukiboot,
80 _ => Bootloader::None,
81 }
82}
83
84pub(crate) fn require_aboot_deploy_in_image(root: &Dir) -> Result<()> {
86 ensure!(
87 crate::utils::have_executable_in_root(root, "aboot-deploy")?,
88 "Target image must contain aboot-deploy for aboot updates and rollbacks"
89 );
90 Ok(())
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub(crate) enum Slot {
96 A,
97 B,
98}
99
100impl Slot {
101 pub(crate) fn from_cmdline(cmdline: &Cmdline<'_>) -> Option<Self> {
102 let mut suffixes = cmdline
103 .iter()
104 .filter(|param| param.key() == SLOT_SUFFIX.into());
105 let suffix = suffixes.next()?;
106 if suffixes.next().is_some() {
107 return None;
108 }
109 match suffix.value()? {
110 "_a" => Some(Self::A),
111 "_b" => Some(Self::B),
112 _ => None,
113 }
114 }
115
116 fn name(self) -> &'static str {
117 match self {
118 Self::A => "a",
119 Self::B => "b",
120 }
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126pub(crate) struct PendingDeployment {
127 pub(crate) depl_id: String,
128 pub(crate) finalization_locked: bool,
129 boot_image_sha256: String,
130 vbmeta_image_sha256: Option<String>,
131}
132
133impl PendingDeployment {
134 pub(crate) fn staged(&self) -> StagedDeployment {
135 StagedDeployment {
136 depl_id: self.depl_id.clone(),
137 finalization_locked: self.finalization_locked,
138 }
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144pub(crate) struct AttemptedDeployment {
145 pub(crate) depl_id: String,
146 boot_id: String,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150struct QueuedRollback {
151 depl_id: String,
152 boot_id: String,
153}
154
155pub(crate) struct ArtifactPaths {
157 pub(crate) boot: Utf8PathBuf,
158 pub(crate) vbmeta: Option<Utf8PathBuf>,
159}
160
161fn boot_id() -> Result<String> {
162 Ok(std::fs::read_to_string("/proc/sys/kernel/random/boot_id")?
163 .trim()
164 .to_string())
165}
166
167enum ReconcileAction {
168 None,
169 RemoveTransient,
170 Publish(PendingDeployment),
171}
172
173pub(crate) struct AbootState<'a> {
175 sysroot: &'a Dir,
176 state: Option<Dir>,
177 slots: Option<Dir>,
178}
179
180impl<'a> AbootState<'a> {
181 pub(crate) fn open(sysroot: &'a Dir) -> Result<Self> {
182 let state = sysroot.open_dir_optional(ABOOT_STATE_DIR)?;
183 let slots = match state.as_ref() {
184 Some(state) => state.open_dir_optional("slots")?,
185 None => None,
186 };
187 Ok(Self {
188 sysroot,
189 state,
190 slots,
191 })
192 }
193
194 fn ensure_state(&mut self) -> Result<&Dir> {
195 if self.state.is_none() {
196 let mut dir = self.sysroot.try_clone()?;
197 for component in ABOOT_STATE_DIR.split('/') {
198 dir.create_dir_all(component)?;
199 fsync(dir.reopen_as_ownedfd()?).context("Syncing aboot state directory")?;
200 dir = dir.open_dir(component)?;
201 }
202 self.state = Some(dir);
203 }
204 Ok(self.state.as_ref().unwrap())
205 }
206
207 fn ensure_slots(&mut self) -> Result<&Dir> {
208 if self.slots.is_none() {
209 {
210 let state = self.ensure_state()?;
211 state.create_dir_all("slots")?;
212 fsync(state.reopen_as_ownedfd()?).context("Syncing aboot state directory")?;
213 }
214 self.slots = Some(self.state.as_ref().unwrap().open_dir("slots")?);
215 }
216 Ok(self.slots.as_ref().unwrap())
217 }
218
219 fn read_json<T: for<'de> Deserialize<'de>>(&self, name: &str) -> Result<Option<T>> {
220 let Some(state) = self.state.as_ref() else {
221 return Ok(None);
222 };
223 let Some(contents) = state.read_optional(name)? else {
224 return Ok(None);
225 };
226 serde_json::from_slice(&contents)
227 .with_context(|| format!("Parsing aboot {name}"))
228 .map(Some)
229 }
230
231 fn write_json(&mut self, name: &str, value: &impl CanonJsonSerialize) -> Result<()> {
232 let state = self.ensure_state()?;
233 state.atomic_write(name, value.to_canon_json_vec()?)?;
234 fsync(state.reopen_as_ownedfd()?).context("Syncing aboot state")?;
235 Ok(())
236 }
237
238 fn remove_state_file(&self, name: &str) -> Result<()> {
239 let Some(state) = self.state.as_ref() else {
240 return Ok(());
241 };
242 state.remove_file_optional(name)?;
243 fsync(state.reopen_as_ownedfd()?).context("Syncing aboot state")?;
244 Ok(())
245 }
246
247 pub(crate) fn read_pending(&self) -> Result<Option<PendingDeployment>> {
248 self.read_json(ABOOT_PENDING_FNAME)
249 }
250
251 pub(crate) fn write_pending(&mut self, pending: &PendingDeployment) -> Result<()> {
253 ensure!(
254 self.read_attempted()?.is_none(),
255 "Cannot replace an attempted aboot deployment"
256 );
257 self.write_json(ABOOT_PENDING_FNAME, pending)
258 }
259
260 pub(crate) fn read_attempted(&self) -> Result<Option<AttemptedDeployment>> {
261 self.read_json(ABOOT_ATTEMPTED_FNAME)
262 }
263
264 fn read_rollback(&self) -> Result<Option<QueuedRollback>> {
265 self.read_json(ABOOT_ROLLBACK_FNAME)
266 }
267
268 pub(crate) fn queued_rollback(&self) -> Result<Option<String>> {
269 let Some(rollback) = self.read_rollback()? else {
270 return Ok(None);
271 };
272 if rollback.boot_id != boot_id()? {
273 return Ok(None);
274 }
275 Ok(Some(rollback.depl_id))
276 }
277
278 pub(crate) fn queue_rollback(&mut self, deployment: &str) -> Result<()> {
279 let digest = Sha512HashValue::from_hex(deployment).context("Invalid rollback digest")?;
280 ensure!(
281 self.slot_deployments()?.contains(&digest),
282 "Rollback deployment is not mapped to an aboot slot"
283 );
284 self.write_json(
285 ABOOT_ROLLBACK_FNAME,
286 &QueuedRollback {
287 depl_id: deployment.to_string(),
288 boot_id: boot_id()?,
289 },
290 )
291 }
292
293 pub(crate) fn clear_rollback(&self) -> Result<()> {
294 self.remove_state_file(ABOOT_ROLLBACK_FNAME)
295 }
296
297 pub(crate) fn unlock_pending(&mut self, deployment: &str) -> Result<PendingDeployment> {
299 let mut pending = self
300 .read_pending()?
301 .ok_or_else(|| anyhow::anyhow!("No pending aboot deployment"))?;
302 ensure!(
303 pending.depl_id == deployment,
304 "Pending aboot deployment changed"
305 );
306 pending.finalization_locked = false;
307 self.write_pending(&pending)?;
308 Ok(pending)
309 }
310
311 pub(crate) fn stage_artifacts(
313 &self,
314 deployment: &str,
315 finalization_locked: bool,
316 boot: &[u8],
317 vbmeta: Option<&[u8]>,
318 ) -> Result<PendingDeployment> {
319 Sha512HashValue::from_hex(deployment).context("Invalid staged deployment digest")?;
320 ensure!(
321 read_boot_type(self.sysroot, deployment)? == Some(BootType::Aboot),
322 "Staged deployment is not aboot"
323 );
324 let deployment_dir = self
325 .sysroot
326 .open_dir(format!("{STATE_DIR_RELATIVE}/{deployment}"))
327 .context("Opening staged deployment")?;
328 deployment_dir.create_dir_all(ABOOT_ARTIFACT_DIR)?;
329 fsync(deployment_dir.reopen_as_ownedfd()?).context("Syncing staged deployment")?;
330 let artifacts = deployment_dir.open_dir(ABOOT_ARTIFACT_DIR)?;
331 artifacts.atomic_write(ABOOT_IMAGE_FNAME, boot)?;
332 match vbmeta {
333 Some(vbmeta) => artifacts.atomic_write(ABOOT_VBMETA_FNAME, vbmeta)?,
334 None => {
335 artifacts.remove_file_optional(ABOOT_VBMETA_FNAME)?;
336 }
337 }
338 fsync(artifacts.reopen_as_ownedfd()?).context("Syncing staged aboot artifacts")?;
339 Ok(PendingDeployment {
340 depl_id: deployment.to_string(),
341 finalization_locked,
342 boot_image_sha256: hex::encode(openssl::sha::sha256(boot)),
343 vbmeta_image_sha256: vbmeta.map(|data| hex::encode(openssl::sha::sha256(data))),
344 })
345 }
346
347 pub(crate) fn verify_artifacts(
349 &self,
350 root_path: &Utf8Path,
351 pending: &PendingDeployment,
352 ) -> Result<ArtifactPaths> {
353 let relative = Utf8PathBuf::from(STATE_DIR_RELATIVE)
354 .join(&pending.depl_id)
355 .join(ABOOT_ARTIFACT_DIR);
356 let artifacts = self.sysroot.open_dir(&relative)?;
357 let boot = artifacts.read(ABOOT_IMAGE_FNAME)?;
358 ensure!(
359 hex::encode(openssl::sha::sha256(&boot)) == pending.boot_image_sha256,
360 "Staged aboot image was modified"
361 );
362 let vbmeta = match pending.vbmeta_image_sha256.as_ref() {
363 Some(expected) => {
364 let data = artifacts.read(ABOOT_VBMETA_FNAME)?;
365 ensure!(
366 hex::encode(openssl::sha::sha256(&data)) == *expected,
367 "Staged vbmeta image was modified"
368 );
369 Some(root_path.join(&relative).join(ABOOT_VBMETA_FNAME))
370 }
371 None => None,
372 };
373 Ok(ArtifactPaths {
374 boot: root_path.join(relative).join(ABOOT_IMAGE_FNAME),
375 vbmeta,
376 })
377 }
378
379 fn remove_artifacts(&self, deployment: &str) -> Result<()> {
380 let deployment_dir = self
381 .sysroot
382 .open_dir(format!("{STATE_DIR_RELATIVE}/{deployment}"))
383 .context("Opening deployed state")?;
384 if deployment_dir
385 .open_dir_optional(ABOOT_ARTIFACT_DIR)?
386 .is_some()
387 {
388 deployment_dir
389 .remove_dir_all(ABOOT_ARTIFACT_DIR)
390 .context("Removing staged aboot artifacts")?;
391 }
392 fsync(deployment_dir.reopen_as_ownedfd()?).context("Syncing deployed state")?;
393 Ok(())
394 }
395
396 pub(crate) fn discard_pending(&self) -> Result<()> {
397 if let Some(attempted) = self.read_attempted()? {
398 ensure!(
399 attempted.boot_id != boot_id()?,
400 "Cannot discard an aboot update attempted during this boot"
401 );
402 }
403 if let Some(pending) = self.read_pending()? {
405 self.remove_state_file(ABOOT_PENDING_FNAME)?;
406 self.remove_artifacts(&pending.depl_id)?;
407 }
408 self.remove_state_file(ABOOT_ATTEMPTED_FNAME)?;
409 remove_staged_deployment()?;
410 Ok(())
411 }
412
413 pub(crate) fn discard_failed_attempt(&self, booted: &str) -> Result<bool> {
415 let Some(attempted) = self.read_attempted()? else {
416 return Ok(false);
417 };
418 if attempted.depl_id == booted {
419 return Ok(false);
420 }
421 self.discard_pending()?;
422 Ok(true)
423 }
424
425 pub(crate) fn record_attempt(&mut self, deployment: &str) -> Result<()> {
427 ensure!(
428 self.read_attempted()?.is_none(),
429 "Aboot deployment was already attempted"
430 );
431 let pending = self
432 .read_pending()?
433 .ok_or_else(|| anyhow::anyhow!("No pending aboot deployment"))?;
434 ensure!(
435 pending.depl_id == deployment,
436 "Pending aboot deployment changed"
437 );
438 self.write_json(
439 ABOOT_ATTEMPTED_FNAME,
440 &AttemptedDeployment {
441 depl_id: deployment.to_string(),
442 boot_id: boot_id()?,
443 },
444 )
445 }
446
447 fn reconcile_state(&mut self, booted: &str, current_boot_id: &str) -> Result<ReconcileAction> {
448 if self
449 .read_rollback()?
450 .is_some_and(|rollback| rollback.boot_id != current_boot_id)
451 {
452 self.remove_state_file(ABOOT_ROLLBACK_FNAME)?;
453 }
454 if let Some(attempted) = self.read_attempted()? {
455 if attempted.boot_id == current_boot_id {
456 return Ok(ReconcileAction::None);
458 }
459 if attempted.depl_id == booted {
460 self.remove_state_file(ABOOT_PENDING_FNAME)?;
462 self.remove_artifacts(&attempted.depl_id)?;
463 self.remove_state_file(ABOOT_ATTEMPTED_FNAME)?;
464 return Ok(ReconcileAction::RemoveTransient);
465 }
466 return Ok(ReconcileAction::None);
468 }
469
470 let Some(pending) = self.read_pending()? else {
471 return Ok(ReconcileAction::RemoveTransient);
472 };
473 Ok(ReconcileAction::Publish(pending))
474 }
475
476 pub(crate) fn reconcile(&mut self, booted: &str) -> Result<bool> {
478 match self.reconcile_state(booted, &boot_id()?)? {
479 ReconcileAction::None => Ok(false),
480 ReconcileAction::RemoveTransient => {
481 remove_staged_deployment()?;
482 Ok(false)
483 }
484 ReconcileAction::Publish(pending) => {
485 write_staged_deployment(&pending.staged())?;
486 Ok(!pending.finalization_locked)
487 }
488 }
489 }
490
491 pub(crate) fn slot_deployments(&self) -> Result<Vec<Sha512HashValue>> {
492 let mut deployments = Vec::new();
493 for slot in [Slot::A, Slot::B] {
494 let Some(deployment) = self.read_slot(slot)? else {
495 continue;
496 };
497 if !deployments.contains(&deployment) {
498 deployments.push(deployment);
499 }
500 }
501 Ok(deployments)
502 }
503
504 fn read_slot(&self, slot: Slot) -> Result<Option<Sha512HashValue>> {
505 let path = format!("{ABOOT_STATE_DIR}/slots/{}", slot.name());
506 let Some(slots) = self.slots.as_ref() else {
507 return Ok(None);
508 };
509 let Some(contents) = slots.read_optional(slot.name())? else {
510 return Ok(None);
511 };
512 let digest = match Sha512HashValue::from_hex(contents.trim_ascii()) {
513 Ok(digest) => digest,
514 Err(err) => {
515 tracing::warn!(%path, %err, "Invalid aboot slot mapping");
516 return Ok(None);
517 }
518 };
519 if read_boot_type(self.sysroot, &digest.to_hex())? != Some(BootType::Aboot) {
520 tracing::warn!(%path, "Aboot slot mapping has no corresponding aboot deployment");
521 return Ok(None);
522 }
523 Ok(Some(digest))
524 }
525
526 pub(crate) fn record_booted(&mut self, cmdline: &Cmdline<'_>, digest: &str) -> Result<()> {
528 let digest =
529 Sha512HashValue::from_hex(digest).context("Invalid booted deployment digest")?;
530 ensure!(
531 read_boot_type(self.sysroot, &digest.to_hex())? == Some(BootType::Aboot),
532 "Booted deployment is not aboot"
533 );
534 let Some(slot) = Slot::from_cmdline(cmdline) else {
535 tracing::warn!("No unambiguous aboot slot suffix; leaving slot mappings unchanged");
536 return Ok(());
537 };
538 if self.read_slot(slot)?.as_ref() == Some(&digest) {
539 return Ok(());
540 }
541
542 self.ensure_slots()?
543 .atomic_write(slot.name(), format!("{}\n", digest.to_hex()))
544 .context("Recording booted aboot slot")?;
545 Ok(())
546 }
547
548 pub(crate) fn invalidate_other(&self, booted_slot: Slot) -> Result<()> {
550 let other = match booted_slot {
551 Slot::A => Slot::B,
552 Slot::B => Slot::A,
553 };
554 let Some(slots) = self.slots.as_ref() else {
555 return Ok(());
556 };
557 slots.remove_file_optional(other.name())?;
558 fsync(slots.reopen_as_ownedfd()?).context("Syncing invalidated aboot slot")?;
559 Ok(())
560 }
561
562 pub(crate) fn delete_deployment(&self, deployment: &str) -> Result<()> {
563 let digest = Sha512HashValue::from_hex(deployment).context("Invalid deployment digest")?;
564 let current_boot_id = boot_id()?;
565
566 if let Some(rollback) = self.read_rollback()? {
567 if rollback.boot_id == current_boot_id {
568 ensure!(
569 rollback.depl_id != deployment,
570 "Cannot delete a queued aboot rollback deployment"
571 );
572 } else {
573 self.clear_rollback()?;
574 }
575 }
576
577 if let Some(attempted) = self
578 .read_attempted()?
579 .filter(|attempted| attempted.depl_id == deployment)
580 {
581 ensure!(
582 attempted.boot_id != current_boot_id,
583 "Cannot delete an aboot deployment attempted during this boot"
584 );
585 if self
586 .read_pending()?
587 .is_some_and(|pending| pending.depl_id == deployment)
588 {
589 self.remove_state_file(ABOOT_PENDING_FNAME)?;
590 remove_staged_deployment()?;
591 self.remove_artifacts(deployment)?;
592 }
593 self.remove_state_file(ABOOT_ATTEMPTED_FNAME)?;
594 } else if self
595 .read_pending()?
596 .is_some_and(|pending| pending.depl_id == deployment)
597 {
598 self.remove_state_file(ABOOT_PENDING_FNAME)?;
599 remove_staged_deployment()?;
600 self.remove_artifacts(deployment)?;
601 }
602
603 let mut removed = false;
604 for slot in [Slot::A, Slot::B] {
605 if self.read_slot(slot)?.as_ref() == Some(&digest) {
606 self.slots.as_ref().unwrap().remove_file(slot.name())?;
607 removed = true;
608 }
609 }
610 if removed {
611 fsync(self.slots.as_ref().unwrap().reopen_as_ownedfd()?)
612 .context("Syncing aboot slot mappings")?;
613 }
614 Ok(())
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621 use crate::composefs_consts::STATE_DIR_RELATIVE;
622 use cap_std_ext::{cap_std::ambient_authority, cap_tempfile::tempdir};
623
624 #[test]
625 fn bootloader_config() {
626 for (config, expected) in [
627 ("", Bootloader::None),
628 (
629 "PARTITION_A=/dev/disk/by-partlabel/boot_a\n",
630 Bootloader::None,
631 ),
632 ("BOOT_TYPE=aboot\n", Bootloader::None),
633 ("BOOT_TYPE=unknown\n", Bootloader::None),
634 ("BOOT_TYPE=\n", Bootloader::None),
635 ("BOOT_TYPE=ukiboot", Bootloader::Ukiboot),
636 (" \tBOOT_TYPE \t= \tukiboot \t\r\n", Bootloader::Ukiboot),
637 (
638 "#BOOT_TYPE=ukiboot\n #BOOT_TYPE=ukiboot\n",
639 Bootloader::None,
640 ),
641 ("BOOT_TYPE\nCMDLINE=a=b\n", Bootloader::None),
642 ("BOOT_TYPE=ukiboot\nBOOT_TYPE=aboot\n", Bootloader::None),
643 ("BOOT_TYPE=aboot\nBOOT_TYPE=ukiboot\n", Bootloader::Ukiboot),
644 ("BOOT_TYPE=\"ukiboot\"\n", Bootloader::None),
645 ("BOOT_TYPE='ukiboot'\n", Bootloader::None),
646 ("BOOT_TYPE=ukiboot # comment\n", Bootloader::None),
647 ] {
648 assert_eq!(bootloader_from_config(config), expected, "{config:?}");
649 }
650 }
651
652 fn deployment(root: &Dir, digest: &str, boot_type: &str) -> Result<()> {
653 let path = format!("{STATE_DIR_RELATIVE}/{digest}");
654 root.create_dir_all(&path)?;
655 root.atomic_write(
656 format!("{path}/{digest}.origin"),
657 format!("[boot]\nboot_type={boot_type}\n"),
658 )?;
659 Ok(())
660 }
661
662 #[test]
663 fn slot_suffix() {
664 for (cmdline, expected) in [
665 ("androidboot.slot_suffix=_a", Some(Slot::A)),
666 ("quiet androidboot.slot_suffix=_b rw", Some(Slot::B)),
667 ("", None),
668 ("androidboot.slot_suffix", None),
669 ("androidboot.slot_suffix=", None),
670 ("androidboot.slot_suffix=a", None),
671 ("androidboot.slot_suffix=_c", None),
672 (
673 "androidboot.slot_suffix=_a androidboot.slot_suffix=_b",
674 None,
675 ),
676 (
677 "androidboot.slot_suffix=_a androidboot.slot_suffix=_a",
678 None,
679 ),
680 ] {
681 assert_eq!(
682 Slot::from_cmdline(&Cmdline::from(cmdline)),
683 expected,
684 "{cmdline}"
685 );
686 }
687 }
688
689 #[test]
690 fn slot_lifecycle() -> Result<()> {
691 let root = tempdir(ambient_authority())?;
692 let a = "aa".repeat(64);
693 let b = "bb".repeat(64);
694 deployment(&root, &a, "aboot")?;
695 deployment(&root, &b, "aboot")?;
696 let cmdline_a = Cmdline::from("androidboot.slot_suffix=_a");
697 let cmdline_b = Cmdline::from("androidboot.slot_suffix=_b");
698 let mut state = AbootState::open(&root)?;
699
700 assert_eq!(state.read_slot(Slot::A)?, None);
701 state.invalidate_other(Slot::A)?;
702 state.record_booted(&cmdline_a, &a)?;
703 state.record_booted(&cmdline_a, &a)?;
704 assert_eq!(state.read_slot(Slot::B)?, None);
705 state.record_booted(&cmdline_b, &b)?;
706 assert_eq!(
707 state.read_slot(Slot::A)?,
708 Some(Sha512HashValue::from_hex(&a)?)
709 );
710 assert_eq!(
711 state.read_slot(Slot::B)?,
712 Some(Sha512HashValue::from_hex(&b)?)
713 );
714
715 for slot in [Slot::A, Slot::B] {
716 state.record_booted(&cmdline_a, &a)?;
717 state.record_booted(&cmdline_b, &b)?;
718 state.invalidate_other(slot)?;
719 state.invalidate_other(slot)?;
720 assert!(state.read_slot(slot)?.is_some());
721 let other = if slot == Slot::A { Slot::B } else { Slot::A };
722 assert_eq!(state.read_slot(other)?, None);
723 assert!(!root.try_exists(format!("{ABOOT_STATE_DIR}/slots/{}", other.name()))?);
724 }
725
726 state.record_booted(&cmdline_a, &a)?;
727 state.record_booted(&Cmdline::from(""), &b)?;
728 assert_eq!(
729 state.read_slot(Slot::A)?,
730 Some(Sha512HashValue::from_hex(&a)?)
731 );
732 state.record_booted(&cmdline_a, &b)?;
733 assert_eq!(
734 state.read_slot(Slot::A)?,
735 Some(Sha512HashValue::from_hex(&b)?)
736 );
737 Ok(())
738 }
739
740 #[test]
741 fn invalid_mappings() -> Result<()> {
742 let root = tempdir(ambient_authority())?;
743 let slots = format!("{ABOOT_STATE_DIR}/slots");
744 root.create_dir_all(&slots)?;
745 let mut state = AbootState::open(&root)?;
746 for contents in [String::new(), "invalid".into(), "aa".repeat(64)] {
747 root.atomic_write(format!("{slots}/a"), contents)?;
748 assert_eq!(state.read_slot(Slot::A)?, None);
749 }
750 root.atomic_write(format!("{slots}/a"), [0xff; 128])?;
751 assert_eq!(state.read_slot(Slot::A)?, None);
752 let digest = "aa".repeat(64);
753 root.atomic_write(format!("{slots}/a"), &digest)?;
754 deployment(&root, &digest, "uki")?;
755 assert_eq!(state.read_slot(Slot::A)?, None);
756 assert!(
757 state
758 .record_booted(&Cmdline::from("androidboot.slot_suffix=_a"), &digest)
759 .is_err()
760 );
761 assert!(
762 state
763 .record_booted(&Cmdline::from(""), "../invalid")
764 .is_err()
765 );
766 Ok(())
767 }
768
769 #[test]
770 fn deployments_deduplicates_slots() -> Result<()> {
771 let root = tempdir(ambient_authority())?;
772 let digest = "aa".repeat(64);
773 deployment(&root, &digest, "aboot")?;
774 let mut state = AbootState::open(&root)?;
775 state.record_booted(&Cmdline::from("androidboot.slot_suffix=_a"), &digest)?;
776 state.record_booted(&Cmdline::from("androidboot.slot_suffix=_b"), &digest)?;
777 assert_eq!(
778 state.slot_deployments()?,
779 vec![Sha512HashValue::from_hex(digest)?]
780 );
781 Ok(())
782 }
783
784 #[test]
785 fn pending_artifacts_and_attempt() -> Result<()> {
786 let root = tempdir(ambient_authority())?;
787 let digest = "aa".repeat(64);
788 deployment(&root, &digest, "aboot")?;
789 let mut state = AbootState::open(&root)?;
790 let pending = state.stage_artifacts(&digest, true, b"boot", Some(b"vbmeta"))?;
791 state.write_pending(&pending)?;
792 assert_eq!(state.read_pending()?, Some(pending.clone()));
793
794 let paths = state.verify_artifacts(Utf8Path::new("/sysroot"), &pending)?;
795 assert_eq!(
796 paths.boot,
797 Utf8PathBuf::from(format!(
798 "/sysroot/{STATE_DIR_RELATIVE}/{digest}/{ABOOT_ARTIFACT_DIR}/{ABOOT_IMAGE_FNAME}"
799 ))
800 );
801 assert!(paths.vbmeta.is_some());
802
803 let pending = state.unlock_pending(&digest)?;
804 assert!(!pending.finalization_locked);
805 state.record_attempt(&digest)?;
806 assert_eq!(state.read_attempted()?.unwrap().depl_id, digest);
807 assert!(state.write_pending(&pending).is_err());
808
809 let artifacts = root.open_dir(format!(
810 "{STATE_DIR_RELATIVE}/{digest}/{ABOOT_ARTIFACT_DIR}"
811 ))?;
812 artifacts.atomic_write(ABOOT_IMAGE_FNAME, b"modified")?;
813 assert!(
814 state
815 .verify_artifacts(Utf8Path::new("/sysroot"), &pending)
816 .is_err()
817 );
818 Ok(())
819 }
820
821 #[test]
822 fn reconcile_attempt() -> Result<()> {
823 let root = tempdir(ambient_authority())?;
824 let digest = "aa".repeat(64);
825 let artifact_dir = format!("{STATE_DIR_RELATIVE}/{digest}/{ABOOT_ARTIFACT_DIR}");
826 deployment(&root, &digest, "aboot")?;
827 let mut state = AbootState::open(&root)?;
828 let pending = state.stage_artifacts(&digest, false, b"boot", None)?;
829 state.write_pending(&pending)?;
830 assert!(root.try_exists(&artifact_dir)?);
831 assert!(matches!(
832 state.reconcile_state("booted", "new-boot")?,
833 ReconcileAction::Publish(_)
834 ));
835
836 state.write_json(
837 ABOOT_ATTEMPTED_FNAME,
838 &AttemptedDeployment {
839 depl_id: digest.clone(),
840 boot_id: "old-boot".into(),
841 },
842 )?;
843 for current_boot in ["old-boot", "new-boot", "another-boot"] {
844 assert!(matches!(
845 state.reconcile_state("booted", current_boot)?,
846 ReconcileAction::None
847 ));
848 assert_eq!(state.read_pending()?, Some(pending.clone()));
849 assert_eq!(state.read_attempted()?.unwrap().boot_id, "old-boot");
850 }
851 assert!(root.try_exists(&artifact_dir)?);
852 assert!(matches!(
853 state.reconcile_state(&digest, "new-boot")?,
854 ReconcileAction::RemoveTransient
855 ));
856 assert!(state.read_pending()?.is_none());
857 assert!(state.read_attempted()?.is_none());
858 assert!(!root.try_exists(&artifact_dir)?);
859 Ok(())
860 }
861
862 #[test]
863 fn replace_failed_attempt_after_reboot() -> Result<()> {
864 let root = tempdir(ambient_authority())?;
865 let digest = "aa".repeat(64);
866 let artifact_dir = format!("{STATE_DIR_RELATIVE}/{digest}/{ABOOT_ARTIFACT_DIR}");
867 deployment(&root, &digest, "aboot")?;
868 let mut state = AbootState::open(&root)?;
869 let pending = state.stage_artifacts(&digest, false, b"failed", None)?;
870 state.write_pending(&pending)?;
871 state.record_attempt(&digest)?;
872
873 assert!(state.discard_failed_attempt("booted").is_err());
874 assert!(state.discard_pending().is_err());
875 assert_eq!(state.read_pending()?, Some(pending));
876 assert!(root.try_exists(&artifact_dir)?);
877
878 state.write_json(
879 ABOOT_ATTEMPTED_FNAME,
880 &AttemptedDeployment {
881 depl_id: digest.clone(),
882 boot_id: "previous-boot".into(),
883 },
884 )?;
885 assert!(!state.discard_failed_attempt(&digest)?);
887 assert!(state.read_attempted()?.is_some());
888 assert!(state.discard_failed_attempt("booted")?);
889 assert!(state.read_pending()?.is_none());
890 assert!(state.read_attempted()?.is_none());
891 assert!(!root.try_exists(&artifact_dir)?);
892
893 let pending = state.stage_artifacts(&digest, false, b"retry", None)?;
895 state.write_pending(&pending)?;
896 state.record_attempt(&digest)?;
897 assert_eq!(state.read_attempted()?.unwrap().boot_id, boot_id()?);
898
899 state.write_json(
901 ABOOT_ATTEMPTED_FNAME,
902 &AttemptedDeployment {
903 depl_id: digest,
904 boot_id: "previous-boot".into(),
905 },
906 )?;
907 state.discard_pending()?;
908 assert!(state.read_pending()?.is_none());
909 assert!(state.read_attempted()?.is_none());
910 assert!(!root.try_exists(&artifact_dir)?);
911 Ok(())
912 }
913
914 #[test]
915 fn rollback_and_deletion() -> Result<()> {
916 let root = tempdir(ambient_authority())?;
917 let a = "aa".repeat(64);
918 let b = "bb".repeat(64);
919 let staged = "cc".repeat(64);
920 for digest in [&a, &b, &staged] {
921 deployment(&root, digest, "aboot")?;
922 }
923 let mut state = AbootState::open(&root)?;
924 state.record_booted(&Cmdline::from("androidboot.slot_suffix=_a"), &a)?;
925 state.record_booted(&Cmdline::from("androidboot.slot_suffix=_b"), &b)?;
926
927 state.queue_rollback(&b)?;
928 assert_eq!(state.queued_rollback()?.as_deref(), Some(b.as_str()));
929 assert!(state.delete_deployment(&b).is_err());
930 state.clear_rollback()?;
931
932 let pending = state.stage_artifacts(&staged, false, b"boot", None)?;
933 state.write_pending(&pending)?;
934 state.delete_deployment(&staged)?;
935 assert!(state.read_pending()?.is_none());
936 assert!(!root.try_exists(format!(
937 "{STATE_DIR_RELATIVE}/{staged}/{ABOOT_ARTIFACT_DIR}"
938 ))?);
939
940 state.delete_deployment(&b)?;
941 assert_eq!(state.read_slot(Slot::B)?, None);
942 assert_eq!(
943 state.read_slot(Slot::A)?,
944 Some(Sha512HashValue::from_hex(&a)?)
945 );
946
947 let pending = state.stage_artifacts(&staged, false, b"boot", None)?;
948 state.write_pending(&pending)?;
949 state.write_json(
950 ABOOT_ATTEMPTED_FNAME,
951 &AttemptedDeployment {
952 depl_id: staged.clone(),
953 boot_id: boot_id()?,
954 },
955 )?;
956 assert!(state.delete_deployment(&staged).is_err());
957 state.write_json(
958 ABOOT_ATTEMPTED_FNAME,
959 &AttemptedDeployment {
960 depl_id: staged.clone(),
961 boot_id: "previous-boot".into(),
962 },
963 )?;
964 state.delete_deployment(&staged)?;
965 assert!(state.read_pending()?.is_none());
966 assert!(state.read_attempted()?.is_none());
967 Ok(())
968 }
969
970 #[test]
971 fn rollback_marker_expires_after_boot() -> Result<()> {
972 let root = tempdir(ambient_authority())?;
973 let a = "aa".repeat(64);
974 let b = "bb".repeat(64);
975 deployment(&root, &a, "aboot")?;
976 deployment(&root, &b, "aboot")?;
977 let mut state = AbootState::open(&root)?;
978 state.record_booted(&Cmdline::from("androidboot.slot_suffix=_a"), &a)?;
979 state.record_booted(&Cmdline::from("androidboot.slot_suffix=_b"), &b)?;
980 state.queue_rollback(&b)?;
981
982 let current_boot_id = boot_id()?;
983 assert!(matches!(
984 state.reconcile_state(&a, ¤t_boot_id)?,
985 ReconcileAction::RemoveTransient
986 ));
987 assert_eq!(state.queued_rollback()?.as_deref(), Some(b.as_str()));
988 assert!(matches!(
989 state.reconcile_state(&b, "next-boot")?,
990 ReconcileAction::RemoveTransient
991 ));
992 assert!(state.read_rollback()?.is_none());
993 Ok(())
994 }
995}