From 17db13eba417480745de7fd10f871fb52ed0dd8e Mon Sep 17 00:00:00 2001 From: not-matthias Date: Mon, 26 Oct 2026 17:32:40 +0100 Subject: [PATCH 1/2] feat: seed Go's global math/rand deterministically Set GODEBUG=randautoseed=0 on benchmark processes in every mode, so the global math/rand source starts from Seed(1) instead of a random seed and code that draws from it behaves the same on every run. The setting is prepended to the user's GODEBUG. Go uses the last occurrence of a setting, so the user's other settings are kept and an explicit randautoseed from the user still wins. Refs COD-4013 --- crates/runner-shared/src/runtime_env/go.rs | 34 +++++++++++++++++++++ crates/runner-shared/src/runtime_env/mod.rs | 2 ++ 2 files changed, 36 insertions(+) create mode 100644 crates/runner-shared/src/runtime_env/go.rs diff --git a/crates/runner-shared/src/runtime_env/go.rs b/crates/runner-shared/src/runtime_env/go.rs new file mode 100644 index 000000000..ba00d8ce9 --- /dev/null +++ b/crates/runner-shared/src/runtime_env/go.rs @@ -0,0 +1,34 @@ +const GODEBUG: &str = "GODEBUG"; +/// Starts the global `math/rand` source from `Seed(1)` instead of a random seed. +const RANDAUTOSEED_OFF: &str = "randautoseed=0"; + +/// Go env: +/// - `GODEBUG=randautoseed=0`: deterministic global `math/rand` in every mode. +pub(super) fn env() -> Vec<(&'static str, String)> { + let user_godebug = std::env::var(GODEBUG).ok(); + vec![(GODEBUG, godebug(user_godebug.as_deref()))] +} + +/// Prepends our settings to the user's `GODEBUG`. Go uses the last occurrence +/// of a setting, so an explicit `randautoseed` from the user still wins. +fn godebug(user_godebug: Option<&str>) -> String { + match user_godebug { + Some(user) if !user.is_empty() => format!("{RANDAUTOSEED_OFF},{user}"), + _ => RANDAUTOSEED_OFF.to_owned(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn keeps_user_settings_after_ours() { + assert_eq!(godebug(None), "randautoseed=0"); + assert_eq!(godebug(Some("")), "randautoseed=0"); + assert_eq!( + godebug(Some("gctrace=1,randautoseed=1")), + "randautoseed=0,gctrace=1,randautoseed=1" + ); + } +} diff --git a/crates/runner-shared/src/runtime_env/mod.rs b/crates/runner-shared/src/runtime_env/mod.rs index 8b70f1b16..fc3b2631f 100644 --- a/crates/runner-shared/src/runtime_env/mod.rs +++ b/crates/runner-shared/src/runtime_env/mod.rs @@ -1,5 +1,6 @@ use crate::measurement_mode::MeasurementMode; +mod go; mod java; mod python; @@ -10,5 +11,6 @@ pub fn env(mode: MeasurementMode) -> Vec<(&'static str, String)> { let mut env = vec![(RUNNER_MODE_ENV, mode.runner_mode_env_value().to_owned())]; env.extend(python::env(mode)); env.extend(java::env(mode)); + env.extend(go::env()); env } From 8c128244942da6197d619434608afec3d7ac2d0f Mon Sep 17 00:00:00 2001 From: not-matthias Date: Mon, 26 Oct 2026 17:32:51 +0100 Subject: [PATCH 2/2] feat: disable ASLR for walltime and memory benchmarks Simulation already runs under setarch --addr-no-randomize. Walltime and memory kept ASLR on, so the memory layout and address-seeded hashes (e.g. abseil's absl::Hash) changed on every run and added variance. Wrap the innermost benchmark command in setarch for both modes. The kernel clears ADDR_NO_RANDOMIZE when exec'ing setuid or file-capability binaries, so setarch sits inside sudo/systemd-run for walltime and inside the shell command that memtrack runs for memory mode. setarch is probed once. When it fails, e.g. because Docker's default seccomp profile blocks personality(ADDR_NO_RANDOMIZE), the runner warns and runs the benchmark unchanged instead of failing. The probe is skipped off Linux. Refs COD-4013 --- src/executor/helpers/aslr.rs | 61 ++++++++++++++++++++++++++++++ src/executor/helpers/mod.rs | 1 + src/executor/memory/executor.rs | 9 +++-- src/executor/valgrind/measure.rs | 5 ++- src/executor/wall_time/executor.rs | 2 + 5 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 src/executor/helpers/aslr.rs diff --git a/src/executor/helpers/aslr.rs b/src/executor/helpers/aslr.rs new file mode 100644 index 000000000..0a1dc42df --- /dev/null +++ b/src/executor/helpers/aslr.rs @@ -0,0 +1,61 @@ +//! Disables ASLR for benchmark processes, so the memory layout and address-seeded +//! hashes (e.g. abseil's `absl::Hash`) stay the same across runs. + +use crate::executor::helpers::command::CommandBuilder; +use crate::prelude::*; +use std::env::consts::ARCH; +use std::process::{Command, Stdio}; +use std::sync::LazyLock; + +pub const SETARCH: &str = "setarch"; +pub const ADDR_NO_RANDOMIZE_FLAG: &str = "--addr-no-randomize"; + +/// Wraps `cmd` in `setarch --addr-no-randomize`. +/// +/// The kernel clears the flag when exec'ing setuid or file-capability binaries +/// (`sudo`, `memtrack`), so this must wrap the innermost benchmark command. +/// +/// Leaves `cmd` unchanged off Linux, or when `setarch` fails, e.g. because +/// Docker's default seccomp profile blocks `personality(ADDR_NO_RANDOMIZE)`. +pub fn wrap_with_aslr_disabled(cmd: &mut CommandBuilder) { + if !can_disable_aslr() { + return; + } + cmd.wrap(SETARCH, [ARCH, ADDR_NO_RANDOMIZE_FLAG]); +} + +/// [`wrap_with_aslr_disabled`] for a shell command string, for callers that run +/// their command through a shell. Returns `command` unchanged on the same +/// conditions, so no extra shell is spawned when ASLR stays on. +pub fn shell_command_with_aslr_disabled(command: String) -> String { + if !can_disable_aslr() { + return command; + } + shell_words::join([ + SETARCH, + ARCH, + ADDR_NO_RANDOMIZE_FLAG, + "bash", + "-c", + &command, + ]) +} + +fn can_disable_aslr() -> bool { + cfg!(target_os = "linux") && *CAN_DISABLE_ASLR +} + +static CAN_DISABLE_ASLR: LazyLock = LazyLock::new(|| { + let can_disable = Command::new(SETARCH) + .args([ARCH, ADDR_NO_RANDOMIZE_FLAG, "true"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_ok_and(|status| status.success()); + if !can_disable { + warn!( + "Failed to disable ASLR with `{SETARCH} {ARCH} {ADDR_NO_RANDOMIZE_FLAG}`, memory addresses will vary between runs" + ); + } + can_disable +}); diff --git a/src/executor/helpers/mod.rs b/src/executor/helpers/mod.rs index 084302a1b..beb63fb65 100644 --- a/src/executor/helpers/mod.rs +++ b/src/executor/helpers/mod.rs @@ -1,4 +1,5 @@ pub mod apt; +pub mod aslr; #[cfg(target_os = "linux")] pub mod capabilities; pub mod command; diff --git a/src/executor/memory/executor.rs b/src/executor/memory/executor.rs index 1cfbe4775..52cd4696a 100644 --- a/src/executor/memory/executor.rs +++ b/src/executor/memory/executor.rs @@ -2,6 +2,7 @@ use crate::executor::ExecutorName; use crate::executor::ExecutorSupport; use crate::executor::PrivilegeStatus; use crate::executor::ToolStatus; +use crate::executor::helpers::aslr::shell_command_with_aslr_disabled; use crate::executor::helpers::command::CommandBuilder; use crate::executor::helpers::env::{build_path_env, get_base_injected_env}; use crate::executor::helpers::get_bench_command::get_bench_command; @@ -58,9 +59,11 @@ impl MemoryExecutor { let (ipc_server, server_name) = ipc::IpcOneShotServer::new()?; // A file-capability binary runs in glibc secure-execution mode, which strips - // LD_* variables. The forwarded environment has to be re-sourced below that - // boundary, inside the benchmark, to survive. - let bench_command = get_bench_command(&execution_context.config)?; + // LD_* variables and clears the ASLR personality flag. The forwarded + // environment and `setarch` have to sit below that boundary, inside the + // benchmark, to survive. + let bench_command = + shell_command_with_aslr_disabled(get_bench_command(&execution_context.config)?); let (bench_command, env_file) = prefix_command_with_env(&bench_command, &extra_env)?; // Build the memtrack command diff --git a/src/executor/valgrind/measure.rs b/src/executor/valgrind/measure.rs index fca89e776..d85a54a06 100644 --- a/src/executor/valgrind/measure.rs +++ b/src/executor/valgrind/measure.rs @@ -1,6 +1,7 @@ use crate::executor::ExecutorConfig; use crate::executor::RunnerMode; use crate::executor::config::SimulationTool; +use crate::executor::helpers::aslr::{ADDR_NO_RANDOMIZE_FLAG, SETARCH}; use crate::executor::helpers::env::{build_path_env, get_base_injected_env}; use crate::executor::helpers::get_bench_command::get_bench_command; use crate::executor::helpers::run_command_with_log_pipe::run_command_with_log_pipe; @@ -130,8 +131,8 @@ pub async fn measure( } // Create the command - let mut cmd = Command::new("setarch"); - cmd.arg(ARCH).arg("--addr-no-randomize"); + let mut cmd = Command::new(SETARCH); + cmd.arg(ARCH).arg(ADDR_NO_RANDOMIZE_FLAG); // Configure the environment cmd.envs(get_base_injected_env( RunnerMode::Simulation, diff --git a/src/executor/wall_time/executor.rs b/src/executor/wall_time/executor.rs index d6bc1d487..740064d82 100644 --- a/src/executor/wall_time/executor.rs +++ b/src/executor/wall_time/executor.rs @@ -7,6 +7,7 @@ use crate::executor::Executor; use crate::executor::ExecutorConfig; use crate::executor::ToolStatus; use crate::executor::config::WalltimeProfiler; +use crate::executor::helpers::aslr::wrap_with_aslr_disabled; use crate::executor::helpers::command::CommandBuilder; use crate::executor::helpers::env::{build_path_env, get_base_injected_env}; use crate::executor::helpers::get_bench_command::get_bench_command; @@ -81,6 +82,7 @@ impl WallTimeExecutor { let mut bench_cmd = CommandBuilder::new("bash"); bench_cmd.arg(script_file.path()); + wrap_with_aslr_disabled(&mut bench_cmd); let (mut bench_cmd, env_file) = wrap_with_env(bench_cmd, &extra_env)?; if let Some(cwd) = &config.working_directory {