From 7de61587b0537129ee8563f6224f5cef2ddf8b11 Mon Sep 17 00:00:00 2001 From: Roman Lebedev Date: Fri, 6 Mar 2026 05:07:44 +0300 Subject: [PATCH] Automatic ASLR disablement While ASLR is a useful security hardening feature, it introduces unreproducible noise into benchmarks, and we really really really don't want any noise, especially easily avoidable one. Unless prevented by some other security hardening, we can disable ASLR for the current process, and restart it, thus eliminating this noise. This implementation matches the implementation that has been contributed by me to google benchmark, it is battle-tested and is known to be correct. X-Ref: https://github.com/google/benchmark/pull/1978 X-Ref: https://github.com/google/benchmark/issues/461 --- CHANGELOG.md | 8 ++++ Cargo.toml | 9 ++++- bencher_compat/src/lib.rs | 9 +++-- book/src/SUMMARY.md | 2 + book/src/tips/aslr.md | 38 +++++++++++++++++++ book/src/tips/tips.md | 4 ++ src/deaslr.rs | 78 +++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/macros.rs | 2 + 9 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 book/src/tips/aslr.md create mode 100644 book/src/tips/tips.md create mode 100644 src/deaslr.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 58269a9d..4d45a3a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [0.8.2](https://github.com/criterion-rs/criterion.rs/compare/criterion-v0.8.1...criterion-v0.8.2) - 2026-02-04 +### Added + +- Add support for automatic disablement of ASLR security hardening feature + for the benchmark process, thus eliminating some execution-to-execution + performance variance. + This is as an optional feature, labelled `deaslr`. + Requires `nix` crate. + ### Fixed - don't build alloca on unsupported targets diff --git a/Cargo.toml b/Cargo.toml index e3919846..d32d919e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,9 @@ tokio = { version = "1.0", default-features = false, features = [ [target.'cfg(any(windows, unix))'.dependencies] alloca = "0.4" +[target.'cfg(unix)'.dependencies] +nix = { version = "0.31.2", default-features = false, features = [], optional = true } + [dependencies.plotters] version = "^0.3.2" optional = true @@ -85,7 +88,7 @@ stable = [ "async_smol", "async_tokio", ] -default = ["rayon", "plotters", "cargo_bench_support"] +default = ["rayon", "plotters", "cargo_bench_support", "deaslr"] # This is a legacy feature that no longer does anything, but removing it would be a semver break. real_blackbox = [] @@ -115,6 +118,10 @@ cargo_bench_support = [] # cargo-criterion's --message-format=json option. csv_output = ["dep:csv"] +# Support disabling Address-Space-Layout Randomization at runtime, +# thus eliminating some of exec-to-exec noise. +deaslr = ["dep:nix", "nix/personality", "nix/process"] + [[bench]] name = "bench_main" harness = false diff --git a/bencher_compat/src/lib.rs b/bencher_compat/src/lib.rs index 3b96aced..3a8400dc 100644 --- a/bencher_compat/src/lib.rs +++ b/bencher_compat/src/lib.rs @@ -1,6 +1,6 @@ -pub use std::hint::black_box; -pub use criterion::Criterion; use criterion::measurement::WallTime; +pub use criterion::Criterion; +pub use std::hint::black_box; /// Stand-in for `bencher::Bencher` which uses Criterion.rs to perform the benchmark instead. pub struct Bencher<'a, 'b> { @@ -10,7 +10,8 @@ pub struct Bencher<'a, 'b> { impl<'a, 'b> Bencher<'a, 'b> { /// Callback for benchmark functions to run to perform the benchmark pub fn iter(&mut self, inner: F) - where F: FnMut() -> T + where + F: FnMut() -> T, { self.bencher.iter(inner); } @@ -46,6 +47,8 @@ macro_rules! benchmark_group { macro_rules! benchmark_main { ($($group_name:path),+) => { fn main() { + $crate::deaslr::maybe_reenter_without_aslr(); + $( $group_name(); )+ diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index b7268009..a4e71d7b 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -29,4 +29,6 @@ - [Comparison to Criterion.rs](./iai/comparison.md) - [Analysis Process](./analysis.md) - [Frequently Asked Questions](./faq.md) +- [Benchmarking Tips](./tips/tips.md) + - [Disabling ASLR](./tips/aslr.md) - [Migrating from 0.2.* to 0.3.*](./migrating_0_2_to_0_3.md) diff --git a/book/src/tips/aslr.md b/book/src/tips/aslr.md new file mode 100644 index 00000000..d6b81902 --- /dev/null +++ b/book/src/tips/aslr.md @@ -0,0 +1,38 @@ +# Disabling ASLR # + +Address-space layout randomization (ASLR) security hardening feature +is a known source of performance variance, it is a good idea +to make sure that it is not enabled when performing benchmarking. + +`criterion` crate, when built with with `deaslr` feature, +will provide a `criterion::deaslr::maybe_reenter_without_aslr()` function, +that will automatically try to disable ASLR for the current process, +and, if successful, re-execute the binary. + +If you are using `criterion_main!` macro, you do not need to do anything, +however, if you have your own `main` function, then you need to add +``` +criterion::deaslr::maybe_reenter_without_aslr(); +``` +as the first line of your `main()` function. + +Note that `personality(2)` may be forbidden by e.g. seccomp (which happens +by default if you are running in a Docker container). + +To globally disable ASLR on Linux, run +``` +echo 0 > /proc/sys/kernel/randomize_va_space +``` +or +``` +sysctl -w kernel.randomize_va_space=0 +``` +... or, you can add `norandmaps` to you linux kernel's command-line. + +To run a single benchmark with ASLR disabled on Linux, do: +``` +setarch `uname -m` -R ./a_benchmark +``` + +Note that for the information on how to disable ASLR on other operating systems, +please refer to their documentation. diff --git a/book/src/tips/tips.md b/book/src/tips/tips.md new file mode 100644 index 00000000..2216bb5c --- /dev/null +++ b/book/src/tips/tips.md @@ -0,0 +1,4 @@ +# Benchmarking Tips + +This chapter covers the tips that you can use to minimize performance variance, +and thus make benchmarking more deterministic. diff --git a/src/deaslr.rs b/src/deaslr.rs new file mode 100644 index 00000000..27e983ef --- /dev/null +++ b/src/deaslr.rs @@ -0,0 +1,78 @@ +//! This module deals with disablement of address-space-layout randomization +//! security hardening feature for the benchmark process, +//! which, for the purposes of benchmarking, would cause +//! subtle unreproducible uncontrollable randomness factors, +//! that would deteriorate quality-of-life of the benchmarks. + +/// This function does nothing on non-UNIX systems. +#[cfg(not(all(unix, feature = "deaslr")))] +pub fn maybe_reenter_without_aslr() { + // No-op. +} + +/// Disables ASLR for the current process and restarts the binary. +/// +/// If you using [`criterion_main!`](crate::criterion_main) macro, +/// it is done automatically, otherwise you may want to call +/// this function first thing in your `main` function. +#[cfg(all(unix, feature = "deaslr"))] +pub fn maybe_reenter_without_aslr() { + use nix::sys::personality; + use nix::sys::personality::Persona; + use std::ffi::CString; + use std::os::unix::ffi::OsStrExt; + + let Some(argc) = std::env::current_exe().ok() else { + // We are not guaranteed to know what our current executable is. + // On e.g. Hexagon simulator, argv may be NULL. + return; + }; + + let Ok(curr_personality) = personality::get() else { + // We should never fail to read-only query the current personality, + // but let's be cautious. + return; + }; + + if curr_personality.contains(Persona::ADDR_NO_RANDOMIZE) { + // If ASLR is already disabled, we have nothing more to do. + return; + } + + let proposed_personality = curr_personality.union(Persona::ADDR_NO_RANDOMIZE); + + let Ok(_prev_personality) = personality::set(proposed_personality) else { + // Have we failed to change the personality? That may happen. + return; + }; + + // Actually read what the new personality is. + let Ok(new_personality) = personality::get() else { + // We should never fail to read-only query the current personality, + // but let's be cautious. + return; + }; + + // Make sure the persona has been updated with the no-ASLR flag, + // otherwise we will try to reenter infinitely. + // This seems impossible, but can happen in some docker configurations. + if !new_personality.contains(Persona::ADDR_NO_RANDOMIZE) { + return; + } + + // We've succeeded in altering our personality, and can now [`execv`]. + // But it takes null-terminated [`CStr`]'s, + // whereas Rust's [`std::env::args_os`] are null-stripped. + + let argc = + CString::new(argc.into_os_string().as_bytes()).expect("executable name contained 0 byte"); + + let argv = std::env::args_os() + .skip(1) + .map(|s| CString::new(s.as_bytes()).expect("argument contained 0 byte")) + .collect::>(); + + let Err(_) = nix::unistd::execv(&argc, &argv); + // The exec() functions return only if an error has occurred, + // in which case we want to just continue as-is. +} diff --git a/src/lib.rs b/src/lib.rs index 7d6a15a8..0be51c2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,6 +44,7 @@ mod bencher; mod connection; #[cfg(feature = "csv_output")] mod csv_report; +pub mod deaslr; mod error; mod estimate; mod format; diff --git a/src/macros.rs b/src/macros.rs index 0edc8cab..9be9a1c5 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -116,6 +116,8 @@ macro_rules! criterion_group { macro_rules! criterion_main { ( $( $group:path ),+ $(,)* ) => { fn main() { + $crate::deaslr::maybe_reenter_without_aslr(); + $( $group(); )+