use core::sync::atomic::{
AtomicUsize,
Ordering::{Acquire, Relaxed, Release},
};
const ASSERT_BUF_CAP: usize = 1024;
static mut ASSERT_BUF: [u8; ASSERT_BUF_CAP] = [0; ASSERT_BUF_CAP];
static ASSERT_LEN: AtomicUsize = AtomicUsize::new(0);
unsafe fn buf_append(offset: usize, src: &[u8]) -> usize {
let remaining = ASSERT_BUF_CAP - offset;
let n = src.len().min(remaining);
unsafe {
let buf_ptr = core::ptr::addr_of_mut!(ASSERT_BUF);
core::ptr::copy_nonoverlapping(src.as_ptr(), (*buf_ptr).as_mut_ptr().add(offset), n);
}
offset + n
}
unsafe fn store_assert_message(assertion: &str, file: &str, line: u32, function: &str) {
let mut off = 0;
unsafe {
off = buf_append(off, b"Assertion failed: (");
off = buf_append(off, assertion.as_bytes());
off = buf_append(off, b"), ");
if !function.is_empty() {
off = buf_append(off, b"function ");
off = buf_append(off, function.as_bytes());
off = buf_append(off, b", ");
}
off = buf_append(off, b"file ");
off = buf_append(off, file.as_bytes());
off = buf_append(off, b", line ");
}
let mut line_buf = [0u8; 10];
let line_str = format_u32(line, &mut line_buf);
unsafe {
off = buf_append(off, line_str.as_bytes());
off = buf_append(off, b".");
}
ASSERT_LEN.store(off, Release);
}
fn format_u32(mut n: u32, buf: &mut [u8; 10]) -> &str {
if n == 0 {
buf[0] = b'0';
return unsafe { core::str::from_utf8_unchecked(&buf[..1]) };
}
let mut pos = buf.len();
while n > 0 {
pos -= 1;
buf[pos] = b'0' + (n % 10) as u8;
n /= 10;
}
unsafe { core::str::from_utf8_unchecked(&buf[pos..]) }
}
pub(crate) fn take_assert_message() -> Option<&'static str> {
let len = ASSERT_LEN.swap(0, Acquire);
if len == 0 {
return None;
}
let slice = unsafe {
let buf_ptr = core::ptr::addr_of!(ASSERT_BUF);
core::slice::from_raw_parts((*buf_ptr).as_ptr(), len)
};
Some(unsafe { core::str::from_utf8_unchecked(slice) })
}
unsafe fn cstr_to_str(ptr: *const libc::c_char, fallback: &str) -> &str {
if ptr.is_null() {
return fallback;
}
unsafe { core::ffi::CStr::from_ptr(ptr) }
.to_str()
.unwrap_or(fallback)
}
type AssertFailFn = unsafe extern "C" fn(
*const libc::c_char,
*const libc::c_char,
libc::c_uint,
*const libc::c_char,
) -> !;
static ORIG_ASSERT_FN: AtomicUsize = AtomicUsize::new(0);
unsafe extern "C" fn hook_assert_fail(
assertion: *const libc::c_char,
file: *const libc::c_char,
line: libc::c_uint,
function: *const libc::c_char,
) -> ! {
let assertion_str = unsafe { cstr_to_str(assertion, "<unknown>") };
let file_str = unsafe { cstr_to_str(file, "<unknown>") };
let function_str = unsafe { cstr_to_str(function, "") };
unsafe { store_assert_message(assertion_str, file_str, line, function_str) };
let orig = ORIG_ASSERT_FN.load(Acquire);
if orig != 0 {
let func: AssertFailFn = unsafe { core::mem::transmute::<usize, AssertFailFn>(orig) };
unsafe { func(assertion, file, line, function) }
} else {
unsafe { libc::abort() }
}
}
pub(crate) fn install_assert_hook() {
if ORIG_ASSERT_FN.load(Relaxed) != 0 {
return;
}
let result = unsafe {
libdd_gotter::hook_symbol(c"__assert_fail", hook_assert_fail as *const () as usize)
};
if let Ok(hook) = result {
let our_hook = hook_assert_fail as *const () as usize;
if hook.orig_addr != our_hook {
ORIG_ASSERT_FN.store(hook.orig_addr, Release);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_assert_message_with_function() {
let _ = take_assert_message();
unsafe { store_assert_message("x > 0", "foo.c", 42, "bar") };
let msg = take_assert_message().unwrap();
assert_eq!(
msg,
"Assertion failed: (x > 0), function bar, file foo.c, line 42."
);
}
#[test]
fn test_format_assert_message_without_function() {
let _ = take_assert_message();
unsafe { store_assert_message("ptr != NULL", "main.c", 100, "") };
let msg = take_assert_message().unwrap();
assert_eq!(
msg,
"Assertion failed: (ptr != NULL), file main.c, line 100."
);
}
#[test]
fn test_store_and_take_assert_message() {
let _ = take_assert_message();
unsafe { store_assert_message("test", "test.c", 1, "") };
let msg = take_assert_message();
assert!(msg.is_some());
assert_eq!(
msg.unwrap(),
"Assertion failed: (test), file test.c, line 1."
);
assert!(take_assert_message().is_none());
}
#[test]
fn test_take_assert_message_none_when_unset() {
let _ = take_assert_message();
assert!(take_assert_message().is_none());
}
#[test]
fn test_truncation() {
let _ = take_assert_message();
let long_expr = "x".repeat(ASSERT_BUF_CAP);
unsafe { store_assert_message(&long_expr, "f.c", 1, "") };
let msg = take_assert_message().unwrap();
assert_eq!(msg.len(), ASSERT_BUF_CAP);
assert!(msg.starts_with("Assertion failed: (xxxx"));
}
#[test]
fn test_format_u32() {
let mut buf = [0u8; 10];
assert_eq!(format_u32(0, &mut buf), "0");
assert_eq!(format_u32(1, &mut buf), "1");
assert_eq!(format_u32(42, &mut buf), "42");
assert_eq!(format_u32(4294967295, &mut buf), "4294967295");
}
#[cfg_attr(miri, ignore)]
#[test]
fn test_install_assert_hook() {
install_assert_hook();
let orig = ORIG_ASSERT_FN.load(Acquire);
if orig == 0 {
eprintln!(
"note: __assert_fail not found in dynamic symbol table \
(static libc?), GOT hook not installed"
);
}
}
}