Skip to content

rust bindings: keep argv[0] alive across benchmark reporting - #2237

Merged
dmah42 merged 1 commit into
google:mainfrom
Alearner12:main
Jun 30, 2026
Merged

rust bindings: keep argv[0] alive across benchmark reporting#2237
dmah42 merged 1 commit into
google:mainfrom
Alearner12:main

Conversation

@Alearner12

@Alearner12 Alearner12 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Rust bindings: keep argv[0] alive across benchmark reporting

Summary

The Rust binding constructs temporary C strings for benchmark::Initialize()
and passes their raw pointers into the C++ API. The C++ benchmark library keeps
argv[0] in BenchmarkReporter::Context::executable_name and reads it later
when console, JSON, or CSV reporters emit benchmark context.

This change makes the Rust C++ bridge keep a persistent copy of argv[0]
before calling benchmark::Initialize(), matching the lifetime expectation of
the C++ reporter context.

Bug

google_benchmark_rs::initialize() currently builds temporary CString
storage:

let mut c_args: Vec<CString> = args.iter()
    .map(|arg| CString::new(arg.as_str()).unwrap())
    .collect();

let mut c_ptrs: Vec<*mut c_char> = c_args.iter_mut()
    .map(|c| c.as_ptr() as *mut c_char)
    .collect();

Those buffers are dropped when initialize() returns. However,
benchmark::Initialize() stores argv[0] as a raw pointer:

BenchmarkReporter::Context::executable_name =
    ((argc != nullptr) && *argc > 0) ? argv[0] : "unknown";

The retained pointer is dereferenced later while printing the benchmark
context. Under ASan/UBSan, a small reproducer following the Rust binding's
lifetime pattern reports:

ERROR: AddressSanitizer: heap-use-after-free
READ of size 2
    #0 __interceptor_strlen
    #1 std::operator<<(std::ostream&, char const*)
    #2 benchmark::BenchmarkReporter::PrintBasicContext
    #3 benchmark::ConsoleReporter::ReportContext
    #4 RunBenchmarks
    #5 benchmark::RunSpecifiedBenchmarks

In an unsanitized run, allocator reuse can cause the reporter to print unrelated
later heap contents as the executable name in the benchmark context.

Fix

Convert the incoming size_t back to char** once in the bridge, copy
argv[0] into static storage, and replace argv[0] with that stable buffer
before delegating to benchmark::Initialize().

All other command-line arguments continue to be parsed and copied by the normal
C++ flag parsing path. The only raw argument pointer retained after
initialization is Context::executable_name.

Verification

Ran the Rust binding tests:

cargo test

Result:

test test_bindings ... ok
test result: ok. 1 passed; 0 failed

Security Report

Related Google VRP / IssueTracker report: 529810915

The Rust binding's initialize() creates temporary CString buffers that are freed on return, but benchmark::Initialize() stores argv[0] as a raw pointer in BenchmarkReporter::Context::executable_name. Later reporter calls dereference freed heap memory (use-after-free).

Copy argv[0] into static storage in the C++ bridge before calling benchmark::Initialize(), ensuring the retained pointer remains valid for the process lifetime.

AI assistance disclosure: AI tooling was used to assist with auditing, reproducer design, patch drafting, and PR text preparation.
@dmah42
dmah42 merged commit 882336a into google:main Jun 30, 2026
92 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants