Skip to main content

bootreason/
lib.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::{Context, Error};
6use async_lock::OnceCell;
7use fidl_fuchsia_boot as fboot;
8use fidl_fuchsia_feedback::{LastRebootInfoProviderMarker, RebootReason};
9use fidl_fuchsia_io as fio;
10use fuchsia_component::client::{connect_to_protocol, connect_to_protocol_sync};
11use fuchsia_fs::node::OpenError;
12use log::{debug, info};
13use zx_status::Status;
14
15/// Temp file for the Starnix lifecycle detection
16const STARTED_ONCE: &str = "component-started-once";
17/// Starnix session restart indicator.
18/// True if the current Starnix session was restarted without a full reboot.
19static HAS_STARNIX_SESSION_RESTARTED: OnceCell<bool> = OnceCell::new();
20/// The Android boot reason of the current session.
21static ANDROID_BOOTREASON: OnceCell<Result<String, Error>> = OnceCell::new();
22
23/// Timeout for FIDL calls to LastRebootInfoProvider
24const LRIP_FIDL_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::INFINITE;
25
26/// Determines whether the current session was restarted without a reboot.
27/// Returns true if the session was restarted, false if it is the initial session.
28async fn has_session_restarted(dir: Option<fio::DirectoryProxy>) -> bool {
29    match dir {
30        Some(dir) => {
31            match fuchsia_fs::directory::open_file(&dir, STARTED_ONCE, fio::Flags::FLAG_MUST_CREATE)
32                .await
33            {
34                Ok(_file) => false,
35                Err(OpenError::OpenError(Status::ALREADY_EXISTS)) => true,
36                Err(err) => {
37                    info!("Failed to generate the file with err {err:#?}.");
38                    false
39                }
40            }
41        }
42        None => false,
43    }
44}
45
46/// Get an Android-compatible boot reason suitable to add to the cmdline or bootconfig.
47pub async fn get_or_init_android_bootreason(
48    dir: Option<fio::DirectoryProxy>,
49    android_provided_bootreason: Option<String>,
50) -> &'static Result<String, Error> {
51    ANDROID_BOOTREASON
52        .get_or_init(async || update_android_bootreason(dir, android_provided_bootreason).await)
53        .await
54}
55
56/// Name of the `ZBI_TYPE_BOOTLOADER_FILE` boot item holding the bootloader's verbatim
57/// `androidboot.bootreason` string.
58///
59/// Android bootloaders report a much more specific reason than the coarse
60/// `ZBI_TYPE_HW_REBOOT_REASON` enum can express (e.g. `reboot,uvlo,pmic,sub` rather than just
61/// "brownout"), and that detail is needed to attribute a reboot to a particular power rail. Boot
62/// shims that can recover the string publish it under this name.
63const ANDROID_BOOTREASON_BOOTLOADER_FILE: &str = "androidboot.bootreason";
64
65/// Reads the bootloader's `androidboot.bootreason` from its `ZBI_TYPE_BOOTLOADER_FILE` boot item.
66///
67/// Returns `Ok(None)` if no such boot item was published.
68pub async fn get_bootloader_file_bootreason() -> Result<Option<String>, Error> {
69    let items =
70        connect_to_protocol::<fboot::ItemsMarker>().context("Failed to connect to boot items")?;
71
72    let Some(vmo) = items
73        .get_bootloader_file(ANDROID_BOOTREASON_BOOTLOADER_FILE)
74        .await
75        .context("FIDL: Failed to get bootloader file")?
76    else {
77        return Ok(None);
78    };
79
80    // `fuchsia.boot.Items` sets ZX_PROP_VMO_CONTENT_SIZE to the file's content size.
81    let size = vmo.get_content_size().context("Failed to get bootloader file content size")?;
82    let bytes = vmo.read_to_vec(0, size).context("Failed to read bootloader file vmo")?;
83
84    Ok(Some(String::from_utf8(bytes).context("Bootloader file is not valid UTF-8")?))
85}
86
87fn is_specific_bootloader_bootreason(reason: &str) -> bool {
88    const SPECIFIC_PREFIXES: &[&str] = &[
89        "reboot,uvlo",
90        "reboot,ocp",
91        "reboot,master_dc,reset",
92        "reboot,sys_ldo_ok",
93        "reboot,smpl_timeout",
94        "reboot,longkey",
95    ];
96    SPECIFIC_PREFIXES.iter().any(|prefix| reason.starts_with(prefix))
97}
98
99/// Update the Android bootreason.
100/// Use get_or_init_android_bootreason to get the cached Android boot reason instead of this.
101pub async fn update_android_bootreason(
102    dir: Option<fio::DirectoryProxy>,
103    android_provided_bootreason: Option<String>,
104) -> Result<String, Error> {
105    // Set the Android bootreason to kernel_panic if the current session was restarted.
106    if *HAS_STARNIX_SESSION_RESTARTED.get_or_init(async || has_session_restarted(dir).await).await {
107        info!("Session restart observed, set android bootreason to kernel_panic.");
108        return Ok("kernel_panic".to_string());
109    }
110
111    // There are certain values from the Android bootloader that are more specific than
112    // what the Fuchsia platform knows so use that when relevant.
113    if let Some(reason) = &android_provided_bootreason {
114        info!("Android bootloader provided bootreason: {reason}");
115        if is_specific_bootloader_bootreason(reason) {
116            return Ok(reason.clone());
117        }
118    }
119
120    info!("Converting LastRebootInfo to an android-friendly bootreason.");
121    let reboot_info_proxy = connect_to_protocol_sync::<LastRebootInfoProviderMarker>()?;
122    let deadline = zx::MonotonicInstant::after(LRIP_FIDL_TIMEOUT);
123    let reboot_info = reboot_info_proxy.get(deadline)?;
124
125    let bootreason = match reboot_info.reason {
126        Some(RebootReason::Unknown) => "reboot,unknown",
127        Some(RebootReason::Cold) => "reboot,cold",
128        Some(RebootReason::BriefPowerLoss) => "reboot,hard_reset",
129        Some(RebootReason::Brownout) => "reboot,undervoltage",
130        Some(RebootReason::KernelPanic) => "kernel_panic",
131        Some(RebootReason::SystemOutOfMemory) => "kernel_panic,oom",
132        Some(RebootReason::HardwareWatchdogTimeout) => "watchdog",
133        Some(RebootReason::SoftwareWatchdogTimeout) => "watchdog,sw",
134        Some(RebootReason::SuspensionFailure) => "kernel_panic",
135        Some(RebootReason::RootJobTermination) => "kernel_panic",
136        Some(RebootReason::UserRequest) => "reboot,userrequested",
137        Some(RebootReason::UserRequestDeviceStuck) => "reboot,userrequested",
138        Some(RebootReason::UserHardReset) => "reboot,longkey,s2",
139        Some(RebootReason::DeveloperRequest) => "reboot,shell",
140        Some(RebootReason::RetrySystemUpdate) => "reboot,ota",
141        Some(RebootReason::HighTemperature) => "shutdown,thermal",
142        Some(RebootReason::SessionFailure) => "kernel_panic",
143        Some(RebootReason::SysmgrFailure) => "kernel_panic",
144        Some(RebootReason::FactoryDataReset) => "reboot,factory_reset",
145        Some(RebootReason::CriticalComponentFailure) => "kernel_panic",
146        Some(RebootReason::CriticalDriverFailure) => "kernel_panic",
147        Some(RebootReason::ZbiSwap) => "reboot,normal",
148        Some(RebootReason::SystemUpdate) => "reboot,ota",
149        Some(RebootReason::NetstackMigration) => "reboot,normal",
150        Some(RebootReason::AndroidUnexpectedReason) => "reboot,normal",
151        Some(RebootReason::AndroidNoReason) => "reboot",
152        Some(RebootReason::AndroidRescueParty) => "reboot,rescueparty",
153        Some(RebootReason::AndroidCriticalProcessFailure) => "reboot,userspace_failed",
154        Some(RebootReason::BatteryDrained) => "shutdown,battery",
155        Some(RebootReason::__SourceBreaking { .. }) => "reboot,normal",
156        None => "reboot,unknown",
157    };
158    Ok(bootreason.to_string())
159}
160
161/// Get the last reboot reason code.
162fn get_reboot_reason() -> Option<RebootReason> {
163    let reboot_info_proxy = connect_to_protocol_sync::<LastRebootInfoProviderMarker>().ok();
164    let deadline = zx::MonotonicInstant::after(LRIP_FIDL_TIMEOUT);
165    let reboot_info = reboot_info_proxy?.get(deadline);
166    match reboot_info {
167        Ok(info) => match info.reason {
168            Some(r) => Some(r),
169            None => {
170                info!("Failed to get the reboot reason.");
171                Some(RebootReason::unknown())
172            }
173        },
174        Err(e) => {
175            info!("Failed to get the reboot info: {:?}", e);
176            Some(RebootReason::unknown())
177        }
178    }
179}
180
181/// Get contents for the pstore/console-ramoops* file.
182///
183/// In Linux it contains a limited amount of some of the previous boot's kernel logs.
184/// The ramoops won't be created after a normal reboot.
185pub fn get_console_ramoops() -> Option<Vec<u8>> {
186    debug!("Getting console-ramoops contents");
187    if HAS_STARNIX_SESSION_RESTARTED.get().copied().unwrap_or(false) {
188        return Some(format!("Last Reboot Reason: Starnix Crash\n").as_bytes().to_vec());
189    }
190    match ANDROID_BOOTREASON.get() {
191        Some(Ok(reason)) => match reason.as_str() {
192            "kernel_panic" | "watchdog" | "watchdog,sw" => Some(
193                format!("Last Reboot Reason: {:?}\n", get_reboot_reason()?).as_bytes().to_vec(),
194            ),
195            _ => None,
196        },
197        Some(Err(e)) => {
198            info!("Failed to get android bootreason for console_ramoops: {:?}", e);
199            None
200        }
201        None => {
202            info!("Android bootreason not initialized.");
203            None
204        }
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_is_specific_bootloader_bootreason() {
214        for specific in [
215            "reboot,uvlo",
216            "reboot,uvlo,pmic,main",
217            "reboot,uvlo,pmic,sub",
218            "reboot,ocp,pmic,if",
219            "reboot,ocp2,pmic,sub",
220            "reboot,ocp3,pmic,if,usb",
221            "reboot,master_dc,reset",
222            "reboot,sys_ldo_ok,pmic,main",
223            "reboot,smpl_timeout,pmic,main",
224            "reboot,longkey,s2",
225        ] {
226            assert!(
227                is_specific_bootloader_bootreason(specific),
228                "Expected {specific} to pass through"
229            );
230        }
231
232        for non_specific in ["warm", "cold", "reboot,cold", "reboot,warm", "reboot"] {
233            assert!(
234                !is_specific_bootloader_bootreason(non_specific),
235                "Did not expect {non_specific} to pass through"
236            );
237        }
238    }
239}