// SPDX-License-Identifier: MIT //! Rust wrapper library for the [user-space debugfs](https://git.sr.ht/~nabijaczleweli/febug) ABI. //! //! Full documentation at //! [libfebug.rs(3)](https://srhtcdn.githack.com/~nabijaczleweli/febug/blob/man/Debian/libfebug.rs.3.html), //! and of the rest of febug at [index(0)](https://srhtcdn.githack.com/~nabijaczleweli/febug/blob/man/Debian/index.0.html). //! //! Set `$FEBUG_DONT` at build-time to turn everything into a no-op, //! set `$FEBUG_SIGNUM` at build-time to override the default signal (`SIGUSR2`), //! set `$FEBUG_SOCKET` at build-time to override the default location (`[/var]/run/febug.sock`). //! //! Set `$FEBUG_DONT` at run-time to not connect (and, hence, make everything a no-op), //! set `$FEBUG_SOCKET` at run-time to set an alternate location. //! //! See [`examples/string-sorts.rs`](https://git.sr.ht/~nabijaczleweli/febug/tree/trunk/item/examples/string-sorts.rs) //! for a usage example. extern crate libc; extern crate once_cell; use std::hash::{BuildHasher, Hasher, Hash}; #[cfg(not(febug_dont))] use libc::{getenv, close, c_char, c_uint}; #[cfg(all(not(febug_dont), target_os="netbsd"))] use libc::{recv, ssize_t}; use libc::{strerror, c_void, c_int}; #[cfg(not(febug_dont))] use std::os::unix::io::FromRawFd; use std::sync::atomic::AtomicI32; #[cfg(not(febug_dont))] use std::sync::atomic::Ordering; use std::collections::BTreeMap; use std::marker::PhantomData; #[cfg(not(febug_dont))] use std::io::{Cursor, Write}; #[cfg(not(febug_dont))] use std::{slice, cmp, ptr}; use std::convert::TryInto; #[cfg(not(febug_dont))] use std::mem::MaybeUninit; use once_cell::sync::Lazy; use std::sync::Mutex; use std::any::TypeId; use std::{fmt, mem}; use std::ffi::CStr; use std::fs::File; // Borrowed from https://github.com/rust-random/getrandom/blob/2b03b0e0b8a65ec5272b867311d3004cea73f381/src/util_libc.rs #[cfg(any(target_os = "netbsd", target_os = "openbsd"))] use libc::__errno as errno_location; #[cfg(any(target_os = "linux"))] use libc::__errno_location as errno_location; #[cfg(any(target_os = "freebsd", target_os = "macos"))] use libc::__error as errno_location; // GUESS WHAT?? DARWIN DEFINES SOCK_SEQPACKET BUT DOESN'T ACTUALLY FUCKING SUPPORT IT (i.e. socket(2) returns EPROTONOSUPPORT). WHY? BECAUSE FUCK YOU. #[cfg(all(not(febug_dont), target_os = "macos"))] use libc::SOCK_STREAM as SOCK_SEQPACKET; #[cfg(all(not(febug_dont), not(target_os = "macos")))] use libc::SOCK_SEQPACKET as SOCK_SEQPACKET; /// [`febug-abi(5)`](https://srhtcdn.githack.com/~nabijaczleweli/febug/blob/man/Debian/febug-abi.5.html) pub mod abi { use libc::c_char; use std::mem; /// `febug_message` #[repr(packed)] pub struct FebugMessage { pub variable_id: u64, pub variable_type: u64, pub signal: u8, pub name: [c_char; 4096 - 8 - 8 - 1], } const _FEBUG_MESSAGE_ASSERT: [(); mem::size_of::() - 4096] = []; /// `stop_febug_message` #[repr(packed)] pub struct StopFebugMessage { pub variable_id: u64, } const _STOP_FEBUG_MESSAGE_ASSERT: [(); mem::size_of::() - 8] = []; /// `attn_febug_message` #[repr(packed)] pub struct AttnFebugMessage { pub variable_id: u64, pub variable_type: u64, } const _ATTN_FEBUG_MESSAGE_ASSERT: [(); mem::size_of::() - 16] = []; } /// FD for the connection to [`febug(8)`](https://srhtcdn.githack.com/~nabijaczleweli/febug/blob/man/Debian/febug.8.html), or /// -1 if none. /// /// c_int is universally i32 (on platforms that we support, anyway) pub static GLOBAL_CONTROLLED_SOCKET: AtomicI32 = AtomicI32::new(-1); const _ILP32_ASSERT: [(); mem::size_of::() - mem::size_of::()] = []; struct Strerror; impl fmt::Display for Strerror { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(unsafe { CStr::from_ptr(strerror(*errno_location())) }.to_str().unwrap_or("")) } } /// Call `start_raw()` with the default socket location for this platform, overridable with `FEBUG_SOCKET` pub fn start() { start_raw(include!(concat!(env!("OUT_DIR"), "/febug_socket.rs"))) } /// Disabled #[cfg(febug_dont)] pub fn start_raw(_: &[u8]) {} /// If `$FEBUG_DONT` isn't set, dial /// [`febug(8)`](https://srhtcdn.githack.com/~nabijaczleweli/febug/blob/man/Debian/febug.8.html) at the specified path, /// or `$FEBUG_SOCKET` #[cfg(not(febug_dont))] pub fn start_raw(mut path: &[u8]) { if unsafe { getenv(b"FEBUG_DONT\0".as_ptr() as *const c_char) } != ptr::null_mut() { return; } let sock = unsafe { libc::socket(libc::AF_UNIX, SOCK_SEQPACKET | libc::SOCK_CLOEXEC, 0) }; if sock == -1 { eprintln!("febug::start_raw: socket: {}", Strerror); return; } let fs = unsafe { getenv(b"FEBUG_SOCKET\0".as_ptr() as *const _) }; if fs != ptr::null_mut() { path = unsafe { CStr::from_ptr(fs) }.to_bytes(); } let path = unsafe { slice::from_raw_parts(path.as_ptr() as *const c_char, path.len()) }; let mut addr: libc::sockaddr_un = unsafe { MaybeUninit::zeroed().assume_init() }; addr.sun_family = libc::AF_UNIX as libc::sa_family_t; let path_strlen = cmp::min(addr.sun_path.len() - 1, path.len()); addr.sun_path[0..path_strlen].copy_from_slice(&path[0..path_strlen]); if unsafe { libc::connect(sock, &addr as *const libc::sockaddr_un as *const libc::sockaddr, mem::size_of_val(&addr) as u32) } == -1 { eprintln!("febug::start_raw: connect: {}", Strerror); unsafe { close(sock) }; return; } #[cfg(any(target_os="linux", target_os="openbsd", target_os="macos"))] { // Handled automatically with SO_PASSCRED, also the manual variant didn't work for some reason // Only way is getsockopt(SO_PEERCRED) // Only way is getsockopt(LOCAL_PEERCRED)+getsockopt(LOCAL_GETPEEREPID) } #[cfg(target_os="netbsd")] { // Correct way is automatically via LOCAL_CREDS // However, the message /must/ be sent after the peer sets it; use a sync message from the server for this, // otherwise we sent the first febug_message too quickly sometimes let mut sync_msg = abi::AttnFebugMessage { variable_id: 0, variable_type: 0, }; if unsafe { recv(sock, &mut sync_msg as *mut _ as *mut c_void, mem::size_of_val(&sync_msg), 0) } != mem::size_of_val(&sync_msg) as ssize_t { eprintln!("febug::start_raw: recv: {}", Strerror); unsafe { close(sock) }; return; } } #[cfg(not(any(target_os="linux", target_os="openbsd", target_os="macos", target_os="netbsd")))] { // From FreeBSD 12.1-RELEASE-p7 /usr/include/socket.h: // // Credentials structure, used to verify the identity of a peer // process that has sent us a message. This is allocated by the // peer process but filled in by the kernel. This prevents the // peer from lying about its identity. (Note that cmcred_groups[0] // is the effective GID.) // let mut cmsgbuf = [0u8; mem::align_of::() * 2 + mem::size_of::() * 2]; let cmsgbuf_len = unsafe { libc::CMSG_SPACE(mem::size_of::() as c_uint) as usize }; // This is a macro in C. Not so here. Alas! assert!(cmsgbuf.len() >= cmsgbuf_len, "{} >= {}", cmsgbuf.len(), cmsgbuf_len); let mut msg = libc::msghdr { msg_name: ptr::null_mut(), msg_namelen: 0, msg_iov: ptr::null_mut(), msg_iovlen: 0, msg_control: cmsgbuf.as_mut_ptr() as *mut _, msg_controllen: cmsgbuf.len() as c_uint, msg_flags: 0, }; let cmsg = unsafe { libc::CMSG_FIRSTHDR(&msg) }; unsafe { (*cmsg).cmsg_level = libc::SOL_SOCKET }; unsafe { (*cmsg).cmsg_type = libc::SCM_CREDS }; unsafe { (*cmsg).cmsg_len = libc::CMSG_LEN(mem::size_of::() as c_uint) }; msg.msg_controllen = unsafe { (*cmsg).cmsg_len }; // total size of all control blocks if unsafe { libc::sendmsg(sock, &msg, 0) } == -1 { eprintln!("febug::start_raw: sendmsg: {}", Strerror); unsafe { close(sock) }; return; } } GLOBAL_CONTROLLED_SOCKET.store(sock, Ordering::Relaxed); } /// Call `debug_handler()` with `FEBUG_SIGNUM` (`SIGUSR2` by default) pub fn install_handler() -> bool { install_handler_signal(include!(concat!(env!("OUT_DIR"), "/febug_signum.rs")) as u8) } /// Disabled #[cfg(febug_dont)] pub fn install_handler_signal(_: u8) -> bool { false } /// Install `debug_handler()` as a handler for the specified signal /// /// Returns `true` if became installed #[cfg(not(febug_dont))] pub fn install_handler_signal(signal: u8) -> bool { if GLOBAL_CONTROLLED_SOCKET.load(Ordering::Relaxed) != -1 { let mut act: libc::sigaction = unsafe { MaybeUninit::zeroed().assume_init() }; act.sa_sigaction = debug_handler as usize; if unsafe { libc::sigaction(signal as c_int, &act, ptr::null_mut()) } == -1 { eprintln!("febug::install_handler: sigaction: {}", Strerror); false } else { true } } else { false } } /// Disabled #[cfg(febug_dont)] pub fn end() {} /// Hang up on [`febug(8)`](https://srhtcdn.githack.com/~nabijaczleweli/febug/blob/man/Debian/febug.8.html) #[cfg(not(febug_dont))] pub fn end() { let sock = GLOBAL_CONTROLLED_SOCKET.swap(-1, Ordering::Relaxed); if sock != -1 { unsafe { close(sock) }; } } /// Helper trait for constructing `Wrapper`s pub trait Wrappable { fn wrap_type(&self, tp: u64, name: fmt::Arguments) -> Wrapper; fn wrap_type_signal(&self, tp: u64, signal: u8, name: fmt::Arguments) -> Wrapper; } impl Wrappable for T { fn wrap_type(&self, tp: u64, name: fmt::Arguments) -> Wrapper { Wrapper::new(tp, self, name) } fn wrap_type_signal(&self, tp: u64, signal: u8, name: fmt::Arguments) -> Wrapper { Wrapper::new_signal(tp, self, signal, name) } } /// Helper trait for constructing `Wrapper`s with type equal to `TypeId::of::()` pub trait StaticWrappable: 'static { fn wrap(&self, name: fmt::Arguments) -> Wrapper; fn wrap_signal(&self, signal: u8, name: fmt::Arguments) -> Wrapper; } impl StaticWrappable for T { fn wrap(&self, name: fmt::Arguments) -> Wrapper { Wrapper::new(Type::from(TypeId::of::()).0, self, name) } fn wrap_signal(&self, signal: u8, name: fmt::Arguments) -> Wrapper { Wrapper::new_signal(Type::from(TypeId::of::()).0, self, signal, name) } } struct NullHasher(u64); impl Hasher for NullHasher { fn finish(&self) -> u64 { self.0 } fn write(&mut self, bytes: &[u8]) { match bytes.try_into() { Ok(eight) => self.0 = u64::from_ne_bytes(eight), Err(_) => unreachable!() } } } impl BuildHasher for NullHasher { type Hasher = NullHasher; fn build_hasher(&self) -> Self::Hasher { NullHasher(0) } } /// Create this to register a variable to be debugged pub struct Wrapper { #[cfg_attr(febug_dont, allow(dead_code))] id: u64, tee: PhantomData, } impl Wrapper { /// Call `new_signal()` with `FEBUG_SIGNUM` (`SIGUSR2` by default) pub fn new(tp: u64, data: &T, name: fmt::Arguments) -> Wrapper { Wrapper::new_signal(tp, data, include!(concat!(env!("OUT_DIR"), "/febug_signum.rs")) as u8, name) } /// Register the specified variable of the specified type with the specified name to be notified (if not `SIGKILL`) on the /// specified signal to format it pub fn new_signal(tp: u64, data: &T, signal: u8, name: fmt::Arguments) -> Wrapper { let id = data as *const T as *const c_void as usize as u64; #[cfg(febug_dont)] let _ = (tp, signal, name); #[cfg(not(febug_dont))] { let s = GLOBAL_CONTROLLED_SOCKET.load(Ordering::Relaxed); if s != -1 { let mut msg = abi::FebugMessage { variable_id: id, variable_type: tp, signal: signal, name: unsafe { MaybeUninit::zeroed().assume_init() }, }; let _ = Cursor::new(unsafe { slice::from_raw_parts_mut(msg.name.as_mut_ptr() as *mut u8, msg.name.len()) }).write_fmt(name); unsafe { libc::send(s, &msg as *const _ as *const c_void, mem::size_of_val(&msg), 0) }; } } Wrapper { id: id, tee: PhantomData, } } } impl Drop for Wrapper { /// Disabled #[cfg(febug_dont)] fn drop(&mut self) {} /// Deregister the variable #[cfg(not(febug_dont))] fn drop(&mut self) { let s = GLOBAL_CONTROLLED_SOCKET.load(Ordering::Relaxed); if s != -1 { let msg = abi::StopFebugMessage { variable_id: self.id }; unsafe { libc::send(s, &msg as *const _ as *const c_void, mem::size_of_val(&msg), 0) }; } } } /// Desugars a `TypeId` to its hash, or an explicit type name /// /// Required since `TypeId`s are non-reconstructible since Rust 1.72, cf. #[derive(Clone, Hash, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Type(pub u64); impl From for Type { fn from(t: TypeId) -> Type { let mut hash = NullHasher(0); t.hash(&mut hash); Type(hash.0) } } impl From for Type { fn from(u: u64) -> Type { Type(u) } } /// Register formatters for your variable types here; /// /// type -> fn(return pipe, ID) pub static FORMATTERS: Lazy>> = Lazy::new(|| Mutex::new(BTreeMap::new())); /// Disabled #[cfg(febug_dont)] pub extern "C" fn debug_handler(_: c_int) {} /// Register this as a signal handler or call this from an event loop to format variables to be inspected #[cfg(not(febug_dont))] pub extern "C" fn debug_handler(_: c_int) { let s = GLOBAL_CONTROLLED_SOCKET.load(Ordering::Relaxed); if s == -1 { return; } let mut afmsg = abi::AttnFebugMessage { variable_id: 0, variable_type: 0, }; let mut buf_i = libc::iovec { iov_base: &mut afmsg as *mut _ as *mut c_void, iov_len: mem::size_of_val(&afmsg), }; let mut retpipe: c_int = -1; let mut cmsgbuf = [0u8; mem::align_of::() * 2 + mem::size_of::() * 8]; let cmsgbuf_len = unsafe { libc::CMSG_SPACE(mem::size_of_val(&retpipe) as c_uint) as usize }; // This is a macro in C. Not so here. Alas! assert!(cmsgbuf.len() >= cmsgbuf_len, "{} >= {}", cmsgbuf.len(), cmsgbuf_len); let mut msg = libc::msghdr { msg_name: ptr::null_mut(), msg_namelen: 0, msg_iov: &mut buf_i, msg_iovlen: 1, msg_control: cmsgbuf.as_mut_ptr() as *mut _, #[cfg(target_os="linux")] msg_controllen: cmsgbuf_len, #[cfg(not(target_os="linux"))] msg_controllen: cmsgbuf_len as c_uint, msg_flags: 0, }; if unsafe { libc::recvmsg(s, &mut msg, 0) } == -1 { eprintln!("febug::debug_handler: recvmsg: {}", Strerror); return; } let cmsg = unsafe { libc::CMSG_FIRSTHDR(&msg) }; if cmsg != ptr::null_mut() && unsafe { (*cmsg).cmsg_type } == libc::SCM_RIGHTS { unsafe { ptr::copy_nonoverlapping(libc::CMSG_DATA(cmsg), &mut retpipe as *mut _ as *mut u8, mem::size_of_val(&retpipe)) }; let mut retpipe = unsafe { File::from_raw_fd(retpipe) }; let tp = afmsg.variable_type; let id = afmsg.variable_id; match FORMATTERS.lock() { Ok(fmts) => { match fmts.get(&Type(tp)) { Some(fmt) => fmt(&mut retpipe, afmsg.variable_id as usize), None => { let _ = writeln!(retpipe, "Unknown variable type {} with ID {}", tp, id); } } } Err(e) => { let _ = writeln!(retpipe, "Can't see variable {} with ID {}: poisoned: {}!", tp, id, e); } }; // closed by drop } else { eprintln!("febug::debug_handler: cmsg: no fd"); } }