| ... | ... | @@ -6,15 +6,16 @@ |
| 6 | 6 | use crate::dialog::Ask; |
| 7 | 7 | use canvas::date::DateField; |
| 8 | 8 | use std::{ |
| 9 | cell::UnsafeCell, |
| 9 | 10 | collections::HashMap, |
| 10 | 11 | error::Error, |
| 11 | | ffi::CStr, |
| 12 | ffi::{CStr, CString, OsString, c_char, c_void}, |
| 12 | 13 | os::unix::ffi::OsStringExt, |
| 13 | 14 | path::PathBuf, |
| 14 | 15 | process::Command, |
| 15 | 16 | sync::{ |
| 16 | 17 | OnceLock, |
| 17 | | atomic::{AtomicU32, Ordering}, |
| 18 | atomic::{AtomicBool, AtomicU32, Ordering}, |
| 18 | 19 | }, |
| 19 | 20 | time::Duration, |
| 20 | 21 | }; |
| ... | ... | @@ -732,6 +733,11 @@ fn log_crashes() { |
| 732 | 733 | ); |
| 733 | 734 | crashed(|fd| write_all(fd, report.as_bytes())); |
| 734 | 735 | })); |
| 736 | if let Ok(path) = crate::loader::executable() |
| 737 | && let Ok(path) = CString::new(path.into_os_string().into_vec()) |
| 738 | { |
| 739 | let _ = EXECUTABLE.set((path, main_base())); |
| 740 | } |
| 735 | 741 | // glibc's backtrace loads its unwinder on first use, which a signal handler can't. |
| 736 | 742 | unsafe { libc::backtrace([std::ptr::null_mut(); 1].as_mut_ptr(), 1) }; |
| 737 | 743 | for signal in FATAL.map(|(signal, _)| signal) { |
| ... | ... | @@ -760,14 +766,10 @@ extern "C" fn fatal(signal: i32, info: *mut libc::siginfo_t, _: *mut libc::c_voi |
| 760 | 766 | let mut thread = [0u8; 16]; |
| 761 | 767 | unsafe { libc::prctl(libc::PR_GET_NAME, thread.as_mut_ptr()) }; |
| 762 | 768 | let thread = &thread[..thread.iter().position(|&byte| byte == 0).unwrap_or(15)]; |
| 763 | | let mut address = *b"0x0000000000000000"; |
| 764 | | let mut value = unsafe { (*info).si_addr() } as usize; |
| 765 | | for digit in address[2..].iter_mut().rev() { |
| 766 | | *digit = b"0123456789abcdef"[value & 15]; |
| 767 | | value >>= 4; |
| 768 | | } |
| 769 | | let mut frames = [std::ptr::null_mut(); 128]; |
| 769 | let address = hex(unsafe { (*info).si_addr() } as usize); |
| 770 | let mut frames = [std::ptr::null_mut(); FRAMES]; |
| 770 | 771 | let count = unsafe { libc::backtrace(frames.as_mut_ptr(), frames.len() as i32) }; |
| 772 | let functions = functions(&frames[..count.max(0) as usize]); |
| 771 | 773 | crashed(|fd| { |
| 772 | 774 | let parts: [&[u8]; 7] = [ |
| 773 | 775 | b"Thread \"", |
| ... | ... | @@ -775,18 +777,185 @@ extern "C" fn fatal(signal: i32, info: *mut libc::siginfo_t, _: *mut libc::c_voi |
| 775 | 777 | b"\" received ", |
| 776 | 778 | name.as_bytes(), |
| 777 | 779 | b" at ", |
| 778 | | &address, |
| 780 | &address[..18], |
| 779 | 781 | b"\n", |
| 780 | 782 | ]; |
| 781 | 783 | for part in parts { |
| 782 | 784 | write_all(fd, part); |
| 783 | 785 | } |
| 784 | 786 | unsafe { libc::backtrace_symbols_fd(frames.as_ptr(), count, fd) }; |
| 787 | write_all(fd, functions); |
| 785 | 788 | write_all(fd, b"\n"); |
| 786 | 789 | }); |
| 787 | 790 | unsafe { libc::raise(signal) }; |
| 788 | 791 | } |
| 789 | 792 | |
| 793 | const FRAMES: usize = 128; |
| 794 | |
| 795 | /// `0x` and 16 hex digits, NUL-terminated. |
| 796 | fn hex(mut value: usize) -> [u8; 19] { |
| 797 | let mut text = *b"0x0000000000000000\0"; |
| 798 | for digit in text[2..18].iter_mut().rev() { |
| 799 | *digit = b"0123456789abcdef"[value & 15]; |
| 800 | value >>= 4; |
| 801 | } |
| 802 | text |
| 803 | } |
| 804 | |
| 805 | /// The executable's path and where it is loaded, for `functions`. |
| 806 | static EXECUTABLE: OnceLock<(CString, usize)> = OnceLock::new(); |
| 807 | |
| 808 | /// Where the dynamic loader placed the executable: symbol values plus this are addresses. |
| 809 | fn main_base() -> usize { |
| 810 | unsafe extern "C" fn first(info: *mut libc::dl_phdr_info, _: usize, base: *mut c_void) -> i32 { |
| 811 | unsafe { *base.cast::<usize>() = (*info).dlpi_addr as usize }; |
| 812 | 1 |
| 813 | } |
| 814 | let mut base = 0usize; |
| 815 | unsafe { libc::dl_iterate_phdr(Some(first), (&raw mut base).cast()) }; |
| 816 | base |
| 817 | } |
| 818 | |
| 819 | /// The argument that runs Snowbound as `functions`' symbolizer, followed by frames' offsets. |
| 820 | pub const SYMBOLIZE: &CStr = c"--symbolize"; |
| 821 | |
| 822 | /// The symbolizer's arguments and output, which only the first crashing thread touches. |
| 823 | struct Scratch { |
| 824 | offsets: [[u8; 19]; FRAMES], |
| 825 | arguments: [*const c_char; FRAMES + 3], |
| 826 | text: [u8; 1 << 14], |
| 827 | } |
| 828 | struct Shared(UnsafeCell<Scratch>); |
| 829 | unsafe impl Sync for Shared {} |
| 830 | static SCRATCH: Shared = Shared(UnsafeCell::new(Scratch { |
| 831 | offsets: [[0; 19]; FRAMES], |
| 832 | arguments: [std::ptr::null(); FRAMES + 3], |
| 833 | text: [0; 1 << 14], |
| 834 | })); |
| 835 | |
| 836 | /// Names the frames in Snowbound's own code from its symbol table, which a crashed process |
| 837 | /// can't safely read: a new process started from the executable does. Empty where it can't. |
| 838 | fn functions(frames: &[*mut c_void]) -> &'static [u8] { |
| 839 | static TAKEN: AtomicBool = AtomicBool::new(false); |
| 840 | let Some((path, base)) = EXECUTABLE.get() else { |
| 841 | return b""; |
| 842 | }; |
| 843 | if TAKEN.swap(true, Ordering::Relaxed) { |
| 844 | return b""; |
| 845 | } |
| 846 | let scratch = unsafe { &mut *SCRATCH.0.get() }; |
| 847 | scratch.arguments[0] = path.as_ptr(); |
| 848 | scratch.arguments[1] = SYMBOLIZE.as_ptr(); |
| 849 | for (index, frame) in frames.iter().enumerate() { |
| 850 | scratch.offsets[index] = hex((*frame as usize).wrapping_sub(*base)); |
| 851 | scratch.arguments[index + 2] = scratch.offsets[index].as_ptr().cast(); |
| 852 | } |
| 853 | scratch.arguments[frames.len() + 2] = std::ptr::null(); |
| 854 | let mut pipe = [0; 2]; |
| 855 | if unsafe { libc::pipe2(pipe.as_mut_ptr(), libc::O_CLOEXEC) } != 0 { |
| 856 | return b""; |
| 857 | } |
| 858 | // A bare clone: glibc's fork takes malloc's locks, which the crashed thread may hold. |
| 859 | let child = unsafe { libc::syscall(libc::SYS_clone, libc::SIGCHLD, 0, 0, 0, 0) }; |
| 860 | if child == 0 { |
| 861 | unsafe { |
| 862 | libc::dup2(pipe[1], 1); |
| 863 | // One that hangs ends, and the crash with it. |
| 864 | libc::alarm(10); |
| 865 | libc::execv(path.as_ptr(), scratch.arguments.as_ptr()); |
| 866 | libc::_exit(127); |
| 867 | } |
| 868 | } |
| 869 | unsafe { libc::close(pipe[1]) }; |
| 870 | let mut length = 0; |
| 871 | if child > 0 { |
| 872 | while length < scratch.text.len() { |
| 873 | let read = unsafe { |
| 874 | libc::read( |
| 875 | pipe[0], |
| 876 | scratch.text[length..].as_mut_ptr().cast(), |
| 877 | scratch.text.len() - length, |
| 878 | ) |
| 879 | }; |
| 880 | if read <= 0 { |
| 881 | break; |
| 882 | } |
| 883 | length += read as usize; |
| 884 | } |
| 885 | unsafe { libc::waitpid(child as i32, std::ptr::null_mut(), 0) }; |
| 886 | } |
| 887 | unsafe { libc::close(pipe[0]) }; |
| 888 | &scratch.text[..length] |
| 889 | } |
| 890 | |
| 891 | /// The symbolizer `functions` starts: lists each frame offset that falls in a function of the |
| 892 | /// executable's symbol table, by its place in the backtrace. |
| 893 | pub fn symbolize(offsets: impl Iterator<Item = OsString>) -> Result<(), Box<dyn Error>> { |
| 894 | use std::{fmt::Write as _, io::Write as _, os::unix::fs::FileExt}; |
| 895 | let file = std::fs::File::open(crate::loader::executable()?)?; |
| 896 | let read = |offset: usize, length: usize| -> std::io::Result<Vec<u8>> { |
| 897 | let mut bytes = vec![0; length]; |
| 898 | file.read_exact_at(&mut bytes, offset as u64)?; |
| 899 | Ok(bytes) |
| 900 | }; |
| 901 | let field = |bytes: &[u8], at: usize, size: usize| { |
| 902 | let mut value = [0; 8]; |
| 903 | value[..size].copy_from_slice(&bytes[at..at + size]); |
| 904 | usize::from_le_bytes(value) |
| 905 | }; |
| 906 | // ELF64, little-endian, as both Linux builds are. |
| 907 | let header = read(0, 64)?; |
| 908 | let size = field(&header, 58, 2); |
| 909 | let sections = read(field(&header, 40, 8), size * field(&header, 60, 2))?; |
| 910 | let sections: Vec<&[u8]> = sections.chunks_exact(size).collect(); |
| 911 | let table = sections |
| 912 | .iter() |
| 913 | .find(|section| field(section, 4, 4) == 2) |
| 914 | .ok_or("no symbol table")?; |
| 915 | let symbols = read(field(table, 24, 8), field(table, 32, 8))?; |
| 916 | let strings = sections[field(table, 40, 4)]; |
| 917 | let strings = read(field(strings, 24, 8), field(strings, 32, 8))?; |
| 918 | let mut ranges: Vec<(usize, usize, &[u8])> = symbols |
| 919 | .chunks_exact(24) |
| 920 | .filter(|symbol| symbol[4] & 15 == 2 && field(symbol, 16, 8) > 0) |
| 921 | .map(|symbol| { |
| 922 | let name = &strings[field(symbol, 0, 4)..]; |
| 923 | let start = field(symbol, 8, 8); |
| 924 | let end = name |
| 925 | .iter() |
| 926 | .position(|&byte| byte == 0) |
| 927 | .unwrap_or(name.len()); |
| 928 | (start, start + field(symbol, 16, 8), &name[..end]) |
| 929 | }) |
| 930 | .collect(); |
| 931 | ranges.sort_unstable_by_key(|range| range.0); |
| 932 | let mut report = String::from("In Snowbound, by place in the backtrace:\n"); |
| 933 | let mut found = false; |
| 934 | for (place, offset) in offsets.enumerate() { |
| 935 | let offset = offset.to_str().and_then(|offset| offset.strip_prefix("0x")); |
| 936 | let Some(offset) = offset.and_then(|offset| usize::from_str_radix(offset, 16).ok()) else { |
| 937 | continue; |
| 938 | }; |
| 939 | let index = ranges.partition_point(|range| range.0 <= offset); |
| 940 | if let Some(&(start, end, name)) = index.checked_sub(1).map(|index| &ranges[index]) |
| 941 | && offset < end |
| 942 | { |
| 943 | let name = String::from_utf8_lossy(name); |
| 944 | writeln!( |
| 945 | report, |
| 946 | "{place:4} {:#} + {:#x}", |
| 947 | rustc_demangle::demangle(&name), |
| 948 | offset - start |
| 949 | )?; |
| 950 | found = true; |
| 951 | } |
| 952 | } |
| 953 | if found { |
| 954 | std::io::stdout().write_all(report.as_bytes())?; |
| 955 | } |
| 956 | Ok(()) |
| 957 | } |
| 958 | |
| 790 | 959 | /// Writes a report, through `write` on a descriptor, to stderr and the log, then says where the |
| 791 | 960 | /// log is. |
| 792 | 961 | fn crashed(write: impl Fn(i32)) { |