| ... | @@ -6,15 +6,16 @@ | ... | @@ -6,15 +6,16 @@ |
| 6 | use crate::dialog::Ask; | 6 | use crate::dialog::Ask; |
| 7 | use canvas::date::DateField; | 7 | use canvas::date::DateField; |
| 8 | use std::{ | 8 | use std::{ |
| | 9 | cell::UnsafeCell, |
| 9 | collections::HashMap, | 10 | collections::HashMap, |
| 10 | error::Error, | 11 | error::Error, |
| 11 | ffi::CStr, | 12 | ffi::{CStr, CString, OsString, c_char, c_void}, |
| 12 | os::unix::ffi::OsStringExt, | 13 | os::unix::ffi::OsStringExt, |
| 13 | path::PathBuf, | 14 | path::PathBuf, |
| 14 | process::Command, | 15 | process::Command, |
| 15 | sync::{ | 16 | sync::{ |
| 16 | OnceLock, | 17 | OnceLock, |
| 17 | atomic::{AtomicU32, Ordering}, | 18 | atomic::{AtomicBool, AtomicU32, Ordering}, |
| 18 | }, | 19 | }, |
| 19 | time::Duration, | 20 | time::Duration, |
| 20 | }; | 21 | }; |
| ... | @@ -732,6 +733,11 @@ fn log_crashes() { | ... | @@ -732,6 +733,11 @@ fn log_crashes() { |
| 732 | ); | 733 | ); |
| 733 | crashed(|fd| write_all(fd, report.as_bytes())); | 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 | // glibc's backtrace loads its unwinder on first use, which a signal handler can't. | 741 | // glibc's backtrace loads its unwinder on first use, which a signal handler can't. |
| 736 | unsafe { libc::backtrace([std::ptr::null_mut(); 1].as_mut_ptr(), 1) }; | 742 | unsafe { libc::backtrace([std::ptr::null_mut(); 1].as_mut_ptr(), 1) }; |
| 737 | for signal in FATAL.map(|(signal, _)| signal) { | 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,14 +766,10 @@ extern "C" fn fatal(signal: i32, info: *mut libc::siginfo_t, _: *mut libc::c_voi |
| 760 | let mut thread = [0u8; 16]; | 766 | let mut thread = [0u8; 16]; |
| 761 | unsafe { libc::prctl(libc::PR_GET_NAME, thread.as_mut_ptr()) }; | 767 | unsafe { libc::prctl(libc::PR_GET_NAME, thread.as_mut_ptr()) }; |
| 762 | let thread = &thread[..thread.iter().position(|&byte| byte == 0).unwrap_or(15)]; | 768 | let thread = &thread[..thread.iter().position(|&byte| byte == 0).unwrap_or(15)]; |
| 763 | let mut address = *b"0x0000000000000000"; | 769 | let address = hex(unsafe { (*info).si_addr() } as usize); |
| 764 | let mut value = unsafe { (*info).si_addr() } as usize; | 770 | let mut frames = [std::ptr::null_mut(); FRAMES]; |
| 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]; | | |
| 770 | let count = unsafe { libc::backtrace(frames.as_mut_ptr(), frames.len() as i32) }; | 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 | crashed(|fd| { | 773 | crashed(|fd| { |
| 772 | let parts: [&[u8]; 7] = [ | 774 | let parts: [&[u8]; 7] = [ |
| 773 | b"Thread \"", | 775 | b"Thread \"", |
| ... | @@ -775,18 +777,185 @@ extern "C" fn fatal(signal: i32, info: *mut libc::siginfo_t, _: *mut libc::c_voi | ... | @@ -775,18 +777,185 @@ extern "C" fn fatal(signal: i32, info: *mut libc::siginfo_t, _: *mut libc::c_voi |
| 775 | b"\" received ", | 777 | b"\" received ", |
| 776 | name.as_bytes(), | 778 | name.as_bytes(), |
| 777 | b" at ", | 779 | b" at ", |
| 778 | &address, | 780 | &address[..18], |
| 779 | b"\n", | 781 | b"\n", |
| 780 | ]; | 782 | ]; |
| 781 | for part in parts { | 783 | for part in parts { |
| 782 | write_all(fd, part); | 784 | write_all(fd, part); |
| 783 | } | 785 | } |
| 784 | unsafe { libc::backtrace_symbols_fd(frames.as_ptr(), count, fd) }; | 786 | unsafe { libc::backtrace_symbols_fd(frames.as_ptr(), count, fd) }; |
| | 787 | write_all(fd, functions); |
| 785 | write_all(fd, b"\n"); | 788 | write_all(fd, b"\n"); |
| 786 | }); | 789 | }); |
| 787 | unsafe { libc::raise(signal) }; | 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 | /// Writes a report, through `write` on a descriptor, to stderr and the log, then says where the | 959 | /// Writes a report, through `write` on a descriptor, to stderr and the log, then says where the |
| 791 | /// log is. | 960 | /// log is. |
| 792 | fn crashed(write: impl Fn(i32)) { | 961 | fn crashed(write: impl Fn(i32)) { |