#![deny(missing_docs)]
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use std::collections::BTreeMap;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
thread_local! {
static FORBID: Cell<u32> = const { Cell::new(0) };
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Mode {
#[default]
Off,
Report,
Abort,
}
static MODE: AtomicU8 = AtomicU8::new(0);
static SEEN_TOTAL: AtomicU64 = AtomicU64::new(0);
static SITES: Mutex<BTreeMap<u64, Site>> = Mutex::new(BTreeMap::new());
#[derive(Debug)]
struct Site {
count: u64,
largest: usize,
}
#[must_use]
pub fn mode() -> Mode {
match MODE.load(Ordering::Relaxed) {
1 => Mode::Report,
2 => Mode::Abort,
_ => Mode::Off,
}
}
pub fn set_mode(m: Mode) {
MODE.store(
match m {
Mode::Off => 0,
Mode::Report => 1,
Mode::Abort => 2,
},
Ordering::Relaxed,
);
}
#[must_use]
pub fn set_mode_from_env() -> Option<Mode> {
let m = parse_mode(std::env::var("YO_ALLOC").ok().as_deref())?;
set_mode(m);
Some(m)
}
fn parse_mode(v: Option<&str>) -> Option<Mode> {
match v {
None | Some("" | "off") => Some(Mode::Off),
Some("report") => Some(Mode::Report),
Some("abort") => Some(Mode::Abort),
Some(_) => None,
}
}
#[must_use = "the mark lasts as long as the guard, so dropping it here does nothing"]
pub fn guard() -> Guard {
let on = mode() != Mode::Off;
if on {
enter_no_alloc();
}
Guard(on)
}
#[derive(Debug)]
pub struct Guard(bool);
impl Drop for Guard {
#[inline]
fn drop(&mut self) {
if self.0 {
exit_no_alloc();
}
}
}
#[must_use]
pub fn seen() -> (usize, u64) {
let n = SITES.lock().map_or(0, |s| s.len());
(n, SEEN_TOTAL.load(Ordering::Relaxed))
}
#[inline]
pub fn enter_no_alloc() {
FORBID.with(|f| f.set(f.get().saturating_add(1)));
}
#[inline]
pub fn exit_no_alloc() {
FORBID.with(|f| f.set(f.get().saturating_sub(1)));
}
#[inline]
pub fn is_forbidden() -> bool {
FORBID.with(|f| f.get()) > 0
}
#[inline]
pub fn allow<T>(f: impl FnOnce() -> T) -> T {
let saved = FORBID.with(|c| c.replace(0));
let guard = Restore(saved);
let out = f();
drop(guard);
out
}
#[inline]
pub fn first_touch<T>(f: impl FnOnce() -> T) -> T {
allow(f)
}
struct Restore(u32);
impl Drop for Restore {
#[inline]
fn drop(&mut self) {
FORBID.with(|c| c.set(self.0));
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct YoAlloc;
impl YoAlloc {
pub const fn new() -> YoAlloc {
YoAlloc
}
}
#[cold]
#[inline(never)]
fn violation(layout: Layout, what: &str) {
if mode() == Mode::Report {
report(layout, what);
return;
}
abort_now(layout, what)
}
fn report(layout: Layout, what: &str) {
SEEN_TOTAL.fetch_add(1, Ordering::Relaxed);
allow(|| {
let trace = std::backtrace::Backtrace::force_capture().to_string();
let key = fnv1a(trace.as_bytes());
let Ok(mut sites) = SITES.lock() else {
return;
};
let size = layout.size();
match sites.entry(key) {
std::collections::btree_map::Entry::Occupied(mut e) => {
let s = e.get_mut();
s.count += 1;
s.largest = s.largest.max(size);
}
std::collections::btree_map::Entry::Vacant(e) => {
e.insert(Site {
count: 1,
largest: size,
});
eprintln!("yo: allocation on a marked thread: {what} of {size} bytes\n{trace}");
}
}
});
}
fn fnv1a(bytes: &[u8]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &b in bytes {
h ^= u64::from(b);
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
#[cold]
#[inline(never)]
fn abort_now(layout: Layout, what: &str) -> ! {
use std::io::Write as _;
let mut buf = [0u8; 32];
let n = write_usize(&mut buf, layout.size());
let mut err = std::io::stderr().lock();
let _ = err.write_all(b"yo: allocation on a shard thread: ");
let _ = err.write_all(what.as_bytes());
let _ = err.write_all(b" of ");
let _ = err.write_all(&buf[..n]);
let _ = err.write_all(
b" bytes.\nThis is Y7: no global allocator call on a command path.\n\
Move the allocation to setup, or wrap it in yo_alloc::allow if it is\n\
genuinely off the command path.\n",
);
let _ = err.flush();
std::process::abort()
}
fn write_usize(buf: &mut [u8; 32], mut v: usize) -> usize {
if v == 0 {
buf[0] = b'0';
return 1;
}
let mut tmp = [0u8; 32];
let mut n = 0;
while v > 0 {
tmp[n] = b'0' + (v % 10) as u8;
v /= 10;
n += 1;
}
for i in 0..n {
buf[i] = tmp[n - 1 - i];
}
n
}
unsafe impl GlobalAlloc for YoAlloc {
#[inline]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
if is_forbidden() {
violation(layout, "alloc");
}
unsafe { System.alloc(layout) }
}
#[inline]
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
if is_forbidden() {
violation(layout, "alloc_zeroed");
}
unsafe { System.alloc_zeroed(layout) }
}
#[inline]
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
if is_forbidden() {
violation(layout, "realloc");
}
unsafe { System.realloc(ptr, layout, new_size) }
}
#[inline]
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn starts_permitted() {
assert!(!is_forbidden());
}
#[test]
fn enter_and_exit_are_balanced() {
assert!(!is_forbidden());
enter_no_alloc();
assert!(is_forbidden());
enter_no_alloc();
assert!(is_forbidden());
exit_no_alloc();
assert!(is_forbidden(), "one exit must not undo two enters");
exit_no_alloc();
assert!(!is_forbidden());
}
#[test]
fn allow_permits_and_restores() {
enter_no_alloc();
assert!(is_forbidden());
let v = allow(|| {
assert!(!is_forbidden());
vec![1u8, 2, 3]
});
assert_eq!(v.len(), 3);
assert!(is_forbidden(), "allow must restore the previous state");
exit_no_alloc();
}
#[test]
fn allow_nests() {
enter_no_alloc();
allow(|| {
allow(|| assert!(!is_forbidden()));
assert!(!is_forbidden());
});
assert!(is_forbidden());
exit_no_alloc();
}
#[test]
fn allow_restores_when_the_body_panics() {
enter_no_alloc();
let r = std::panic::catch_unwind(|| {
allow(|| panic!("boom"));
});
assert!(r.is_err());
assert!(
is_forbidden(),
"a panic inside allow must not leave the thread permitted"
);
exit_no_alloc();
}
#[test]
fn the_flag_does_not_cross_threads() {
enter_no_alloc();
let other = std::thread::spawn(is_forbidden).join().unwrap();
assert!(!other, "another thread saw this thread's flag");
exit_no_alloc();
}
static MODE_TESTS: Mutex<()> = Mutex::new(());
fn one_at_a_time() -> std::sync::MutexGuard<'static, ()> {
MODE_TESTS.lock().unwrap_or_else(|e| e.into_inner())
}
#[test]
fn the_mode_starts_off_and_survives_a_round_trip() {
let _turn = one_at_a_time();
assert_eq!(Mode::default(), Mode::Off, "off is the default");
for m in [Mode::Report, Mode::Abort, Mode::Off] {
set_mode(m);
assert_eq!(mode(), m);
}
}
#[test]
fn the_env_variable_reads_three_words_and_refuses_the_rest() {
assert_eq!(parse_mode(None), Some(Mode::Off));
assert_eq!(parse_mode(Some("")), Some(Mode::Off));
assert_eq!(parse_mode(Some("off")), Some(Mode::Off));
assert_eq!(parse_mode(Some("report")), Some(Mode::Report));
assert_eq!(parse_mode(Some("abort")), Some(Mode::Abort));
assert_eq!(parse_mode(Some("abrot")), None);
assert_eq!(parse_mode(Some("Report")), None);
assert_eq!(parse_mode(Some("1")), None);
}
#[test]
fn the_guard_marks_only_when_the_mode_asks() {
let _turn = one_at_a_time();
std::thread::spawn(|| {
set_mode(Mode::Off);
{
let _g = guard();
assert!(!is_forbidden(), "off must not mark the thread at all");
}
for m in [Mode::Report, Mode::Abort] {
set_mode(m);
{
let _g = guard();
assert!(is_forbidden(), "{m:?} must mark it");
}
assert!(!is_forbidden(), "and the guard must undo it");
}
set_mode(Mode::Off);
})
.join()
.unwrap();
}
#[test]
fn the_guard_unmarks_when_the_body_panics() {
let _turn = one_at_a_time();
std::thread::spawn(|| {
set_mode(Mode::Report);
let r = std::panic::catch_unwind(|| {
let _g = guard();
assert!(is_forbidden());
panic!("boom");
});
assert!(r.is_err());
assert!(
!is_forbidden(),
"a panic inside the guard must not leave the thread marked forever"
);
set_mode(Mode::Off);
})
.join()
.unwrap();
}
#[test]
fn report_mode_records_instead_of_aborting() {
let _turn = one_at_a_time();
std::thread::spawn(|| {
let (sites_before, total_before) = seen();
set_mode(Mode::Report);
{
let _g = guard();
assert!(is_forbidden());
for size in [8usize, 64, 4096] {
let layout = Layout::from_size_align(size, 8).unwrap();
violation(layout, "alloc");
}
assert!(is_forbidden(), "reporting must put the mark back");
}
set_mode(Mode::Off);
let (sites, total) = seen();
assert_eq!(total - total_before, 3, "every violation is counted");
assert_eq!(sites - sites_before, 1, "one line is one site");
})
.join()
.unwrap();
}
#[test]
fn distinct_traces_hash_apart() {
assert_ne!(fnv1a(b"one"), fnv1a(b"two"));
assert_eq!(fnv1a(b"same"), fnv1a(b"same"));
}
#[test]
fn integers_render_without_allocating() {
let mut buf = [0u8; 32];
for (v, want) in [
(0usize, "0"),
(7, "7"),
(1024, "1024"),
(2097152, "2097152"),
] {
let n = write_usize(&mut buf, v);
assert_eq!(std::str::from_utf8(&buf[..n]).unwrap(), want);
}
}
}