Skip to main content

starnix_core/execution/
crash_reporter.rs

1// Copyright 2024 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 crate::signals::SignalInfo;
6use crate::task::CurrentTask;
7use crash_throttling::{CrashThrottler, PendingCrashReport};
8use fidl_fuchsia_feedback::{
9    Annotation, CrashReport, CrashReporterProxy, MAX_ANNOTATION_VALUE_LENGTH,
10    MAX_CRASH_SIGNATURE_LENGTH, NativeCrashReport, SpecificCrashReport,
11};
12use fuchsia_inspect::Node;
13use starnix_logging::{
14    CATEGORY_STARNIX, CoreDumpInfo, CoreDumpList, TraceScope, log_error, log_info, log_warn,
15    trace_instant,
16};
17
18pub struct CrashReporter {
19    /// Diagnostics information about crashed tasks.
20    core_dumps: CoreDumpList,
21
22    /// Throttles crash reports to avoid spamming the system.
23    throttler: CrashThrottler,
24
25    /// Connection to the feedback stack for reporting crashes.
26    proxy: Option<CrashReporterProxy>,
27}
28
29impl CrashReporter {
30    pub fn new(
31        inspect_node: &Node,
32        proxy: Option<CrashReporterProxy>,
33        crash_loop_age_out: zx::MonotonicDuration,
34        enable_throttling: bool,
35    ) -> Self {
36        Self {
37            core_dumps: CoreDumpList::new(inspect_node.create_child("coredumps")),
38            throttler: CrashThrottler::new(inspect_node, crash_loop_age_out, enable_throttling),
39            proxy,
40        }
41    }
42
43    /// Returns a PendingCrashReport if the crash report should be reported. Otherwise, returns
44    /// None.
45    pub fn begin_crash_report(&self, current_task: &CurrentTask) -> Option<PendingCrashReport> {
46        let argv = current_task
47            .read_argv(MAX_ANNOTATION_VALUE_LENGTH as usize)
48            .unwrap_or_else(|_| vec!["<unknown>".into()])
49            .into_iter()
50            .map(|a| a.to_string())
51            .collect::<Vec<_>>();
52        let argv0 = argv.get(0).map(AsRef::as_ref).unwrap_or_else(|| "<unknown>");
53
54        // Get the filename.
55        let argv0 = argv0.rsplit_once("/").unwrap_or(("", &argv0)).1.to_string();
56
57        self.throttler.should_report(argv, argv0, zx::MonotonicInstant::get())
58    }
59
60    /// Callers should first check whether the crash should be reported via begin_crash_report.
61    pub fn handle_core_dump(
62        &self,
63        current_task: &CurrentTask,
64        signal_info: &SignalInfo,
65        pending_crash_report: PendingCrashReport,
66    ) {
67        trace_instant!(CATEGORY_STARNIX, "RecordCoreDump", TraceScope::Process);
68
69        let argv = pending_crash_report.argv;
70        let argv0 = pending_crash_report.argv0;
71        let process_koid = current_task
72            .thread_group()
73            .process
74            .koid()
75            .expect("handles for processes with crashing threads are still valid");
76        let thread_koid = current_task
77            .live()
78            .thread
79            .read()
80            .as_ref()
81            .expect("coredumps occur in tasks with associated threads")
82            .koid()
83            .expect("handles for crashing threads are still valid");
84        let linux_pid = current_task.thread_group().leader as i64;
85        let thread_name = current_task.command().to_string();
86
87        // TODO(https://fxbug.dev/356912301) use boot time
88        let uptime = zx::MonotonicInstant::get() - current_task.thread_group().start_time;
89
90        let dump_info = CoreDumpInfo {
91            process_koid,
92            thread_koid,
93            linux_pid,
94            uptime: uptime.into_nanos(),
95            argv: argv.clone(),
96            thread_name: thread_name.clone(),
97            signal: signal_info.signal.to_string(),
98        };
99        self.core_dumps.record_core_dump(dump_info);
100
101        let mut argv_joined = argv.join(" ");
102        truncate_with_ellipsis(&mut argv_joined, MAX_ANNOTATION_VALUE_LENGTH as usize);
103
104        let mut env_joined = current_task
105            .read_env(MAX_ANNOTATION_VALUE_LENGTH as usize)
106            .unwrap_or_else(|_| vec![])
107            .into_iter()
108            .map(|a| a.to_string())
109            .collect::<Vec<_>>()
110            .join(" ");
111        truncate_with_ellipsis(&mut env_joined, MAX_ANNOTATION_VALUE_LENGTH as usize);
112
113        let signal_str = signal_info.signal.to_string();
114
115        // Truncate program name to fit in crash signature with a space and signal string added.
116        let max_signature_prefix_len = MAX_CRASH_SIGNATURE_LENGTH as usize - (signal_str.len() + 1);
117        let mut crash_signature = argv0.clone();
118        truncate_with_ellipsis(&mut crash_signature, max_signature_prefix_len);
119        crash_signature.push(' ');
120        crash_signature.push_str(&signal_str);
121
122        let crash_report = CrashReport {
123            crash_signature: Some(crash_signature),
124            program_name: Some(argv0.clone()),
125            program_uptime: Some(uptime.into_nanos()),
126            specific_report: Some(SpecificCrashReport::Native(NativeCrashReport {
127                process_koid: Some(process_koid.raw_koid()),
128                process_name: Some(argv0),
129                thread_koid: Some(thread_koid.raw_koid()),
130                thread_name: Some(thread_name),
131                ..Default::default()
132            })),
133            annotations: Some(vec![
134                // Note that this pid will be different from the Zircon process koid that's visible
135                // to the rest of Fuchsia. We want to include both so that this can be correlated
136                // against debugging artifacts produced by Android code.
137                Annotation { key: "linux.pid".to_string(), value: linux_pid.to_string() },
138                Annotation { key: "linux.argv".to_string(), value: argv_joined },
139                Annotation { key: "linux.env".to_string(), value: env_joined },
140                Annotation { key: "linux.signal".to_string(), value: signal_str },
141            ]),
142            is_fatal: Some(true),
143            weight: Some(pending_crash_report.weight),
144            ..Default::default()
145        };
146
147        if let Some(reporter) = &self.proxy {
148            let reporter = reporter.clone();
149            // Do the actual report in the background since they can take a while to file.
150            current_task.kernel().kthreads.spawn_future(
151                move || async move {
152                    match reporter.file_report(crash_report).await {
153                        Ok(Ok(_)) => (),
154                        Ok(Err(filing_error)) => {
155                            log_error!(filing_error:?; "Couldn't file crash report.");
156                        }
157                        Err(fidl_error) => log_warn!(
158                            fidl_error:?;
159                            "Couldn't file crash report due to error on underlying channel."
160                        ),
161                    };
162                },
163                "crash-filing",
164            );
165        } else {
166            log_info!(crash_report:?; "no crash reporter available for crash");
167        }
168    }
169}
170
171fn truncate_with_ellipsis(s: &mut String, max_len: usize) {
172    if s.len() <= max_len {
173        return;
174    }
175
176    // 3 bytes for ellipsis.
177    let max_content_len = max_len - 3;
178
179    // String::truncate panics if the new max length is in the middle of a character, so we need to
180    // find an appropriate byte boundary.
181    let mut new_len = 0;
182    let mut iter = s.char_indices();
183    while let Some((offset, _)) = iter.next() {
184        if offset > max_content_len {
185            break;
186        }
187        new_len = offset;
188    }
189
190    s.truncate(new_len);
191    s.push_str("...");
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn truncate_noop_on_max_length_string() {
200        let mut s = String::from("1234567890");
201        let before = s.clone();
202        truncate_with_ellipsis(&mut s, 10);
203        assert_eq!(s, before);
204    }
205
206    #[test]
207    fn truncate_adds_ellipsis() {
208        let mut s = String::from("1234567890");
209        truncate_with_ellipsis(&mut s, 9);
210        assert_eq!(s.len(), 9);
211        assert_eq!(s, "123456...", "truncate must add ellipsis and still fit under max len");
212    }
213
214    #[test]
215    fn truncate_is_sensible_in_middle_of_multibyte_chars() {
216        let mut s = String::from("æææææææææ");
217        // æ is 2 bytes, so any odd byte length should be in the middle of a character. Truncate
218        // adds 3 bytes for the ellipsis so we actually need an even max length to hit the middle
219        // of a character.
220        truncate_with_ellipsis(&mut s, 8);
221        assert_eq!(s.len(), 7, "may end up shorter than provided max length w/ multi-byte chars");
222        assert_eq!(s, "ææ...", "truncate must remove whole characters and add ellipsis");
223    }
224}