Skip to content

Commit bde0894

Browse files
art049claude
andcommitted
feat: pin downloaded binaries with sha256 verification
Every binary the runner downloads at install time (the patched valgrind .deb, the memtrack/exec-harness/mongo-tracer installer scripts) is now SHA-256-pinned. URLs and expected hashes live together in a new `PinnedBinary` enum, and the download helper rejects the install if the bytes don't match. Bumping a pinned version requires updating both the version constant and the matching hash; CONTRIBUTING.md documents the regeneration workflow. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4eac647 commit bde0894

10 files changed

Lines changed: 179 additions & 95 deletions

File tree

CONTRIBUTING.md

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,24 @@ cargo release -p exec-harness --execute beta
3535

3636
After releasing `memtrack` or `exec-harness`, you **must** update the version references in the runner code:
3737

38-
1. **For memtrack**: Update `MEMTRACK_CODSPEED_VERSION` in `src/executor/memory/executor.rs`:
38+
1. **For memtrack**: Update `MEMTRACK_VERSION` in `src/binary_pins.rs` and the matching SHA-256 in `PinnedBinary::sha256` (see [Pinned binary hashes](#pinned-binary-hashes) below).
3939

40-
```rust
41-
const MEMTRACK_CODSPEED_VERSION: &str = "X.Y.Z"; // Update to new version
42-
```
43-
44-
2. **For exec-harness**: Update `EXEC_HARNESS_VERSION` in `src/executor/orchestrator.rs`:
45-
```rust
46-
const EXEC_HARNESS_VERSION: &str = "X.Y.Z"; // Update to new version
47-
```
40+
2. **For exec-harness**: Update `EXEC_HARNESS_VERSION` in `src/binary_pins.rs` and the matching SHA-256 in `PinnedBinary::sha256`.
4841

4942
These constants are used by the runner to download and install the correct versions of the binaries from GitHub releases.
5043

44+
### Pinned binary hashes
45+
46+
Every binary the runner downloads at install time (the patched valgrind `.deb`, the memtrack installer, the exec-harness installer, the mongo-tracer installer) is SHA-256-pinned. URLs and hashes live together in `PinnedBinary` in `src/binary_pins.rs`.
47+
48+
When you bump a pinned version, regenerate the hash for each affected URL and update the matching match arm in `PinnedBinary::sha256`:
49+
50+
```bash
51+
curl -sL '<url>' | sha256sum
52+
```
53+
54+
For valgrind, that is one hash per supported `(distro_version, arch)` combination.
55+
5156
### Releasing the Main Runner
5257

5358
The main runner (`codspeed-runner`) should be released after ensuring all dependency versions are correct.
@@ -56,8 +61,10 @@ The main runner (`codspeed-runner`) should be released after ensuring all depend
5661

5762
**Verify binary version references**: Check that version constants in the runner code match the released versions:
5863

59-
- `MEMTRACK_CODSPEED_VERSION` in `src/executor/memory/executor.rs`
60-
- `EXEC_HARNESS_VERSION` in `src/executor/orchestrator.rs`
64+
- `MEMTRACK_VERSION` in `src/binary_pins.rs`
65+
- `EXEC_HARNESS_VERSION` in `src/binary_pins.rs`
66+
67+
Also confirm the SHA-256 entries in `PinnedBinary::sha256` (in `src/binary_pins.rs`) match the released artifacts.
6168

6269
#### Release Command
6370

src/binary_installer/mod.rs

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,26 @@
1-
use crate::cli::run::helpers::download_file;
1+
use crate::binary_pins::PinnedBinary;
2+
use crate::cli::run::helpers::download_pinned_file;
23
use crate::prelude::*;
34
use semver::Version;
45
use std::process::Command;
56
use tempfile::NamedTempFile;
6-
use url::Url;
77

88
mod versions;
99

10-
/// Ensure a binary is installed, or install it from a runner's GitHub release using the installer script.
10+
/// Ensure a binary is installed, or install it from a `PinnedBinary` installer script.
1111
///
1212
/// This function checks if the binary is already installed with the correct version.
13-
/// If not, it downloads and executes an installer script from the CodSpeed runner repository.
13+
/// If not, it downloads and executes the pinned installer script.
1414
///
1515
/// # Arguments
1616
/// * `binary_name` - The binary command name (e.g., "codspeed-memtrack", "codspeed-exec-harness")
1717
/// * `version` - The version to install (e.g., "4.4.2-alpha.2")
18-
/// * `get_installer_url` - A closure that returns the URL to download the installer script.
19-
pub async fn ensure_binary_installed<F>(
18+
/// * `installer` - The `PinnedBinary` installer to download.
19+
pub async fn ensure_binary_installed(
2020
binary_name: &str,
2121
version: &str,
22-
get_installer_url: F,
23-
) -> Result<()>
24-
where
25-
F: FnOnce() -> String,
26-
{
22+
installer: PinnedBinary,
23+
) -> Result<()> {
2724
if is_command_installed(
2825
binary_name,
2926
Version::parse(version).context("Invalid version format")?,
@@ -32,13 +29,11 @@ where
3229
return Ok(());
3330
}
3431

35-
let installer_url = Url::parse(&get_installer_url()).context("Invalid installer URL")?;
32+
debug!("Downloading installer for {binary_name}");
3633

37-
debug!("Downloading installer from: {installer_url}");
38-
39-
// Download the installer script to a temporary file
34+
// Download the installer script to a temporary file (with sha256 verification)
4035
let temp_file = NamedTempFile::new().context("Failed to create temporary file")?;
41-
download_file(&installer_url, temp_file.path()).await?;
36+
download_pinned_file(installer, temp_file.path()).await?;
4237

4338
// Execute the installer script
4439
let output = Command::new("sh")

src/binary_pins.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Pinned downloads. Each `PinnedBinary` variant carries the URL and the
2+
// expected SHA-256 of the file at that URL — bumping a version requires
3+
// updating both. See CONTRIBUTING.md for the regeneration workflow.
4+
5+
pub const VALGRIND_DEB_VERSION: &str = "3.26.0-0codspeed0";
6+
pub const MEMTRACK_VERSION: &str = "1.2.3";
7+
pub const EXEC_HARNESS_VERSION: &str = "1.3.0";
8+
pub const MONGODB_TRACER_VERSION: &str = "cs-mongo-tracer-v0.2.0";
9+
10+
/// A binary the runner downloads at install time. The download helper looks
11+
/// up the URL and SHA-256 via `url()` and `sha256()` and rejects the install
12+
/// if the bytes don't match.
13+
#[derive(Debug, Clone, Copy)]
14+
pub enum PinnedBinary {
15+
ValgrindDeb {
16+
distro_version: &'static str,
17+
arch: &'static str,
18+
},
19+
MemtrackInstaller,
20+
ExecHarnessInstaller,
21+
MongoTracerInstaller,
22+
}
23+
24+
impl PinnedBinary {
25+
pub fn url(&self) -> String {
26+
match self {
27+
PinnedBinary::ValgrindDeb {
28+
distro_version,
29+
arch,
30+
} => format!(
31+
"https://github.com/CodSpeedHQ/valgrind-codspeed/releases/download/{VALGRIND_DEB_VERSION}/valgrind_{VALGRIND_DEB_VERSION}_ubuntu-{distro_version}_{arch}.deb"
32+
),
33+
PinnedBinary::MemtrackInstaller => format!(
34+
"https://github.com/CodSpeedHQ/codspeed/releases/download/memtrack-v{MEMTRACK_VERSION}/memtrack-installer.sh"
35+
),
36+
PinnedBinary::ExecHarnessInstaller => format!(
37+
"https://github.com/CodSpeedHQ/codspeed/releases/download/exec-harness-v{EXEC_HARNESS_VERSION}/exec-harness-installer.sh"
38+
),
39+
PinnedBinary::MongoTracerInstaller => format!(
40+
"https://codspeed-public-assets.s3.eu-west-1.amazonaws.com/mongo-tracer/{MONGODB_TRACER_VERSION}/cs-mongo-tracer-installer.sh"
41+
),
42+
}
43+
}
44+
45+
pub fn sha256(&self) -> &'static str {
46+
match self {
47+
PinnedBinary::ValgrindDeb {
48+
distro_version,
49+
arch,
50+
} => match (*distro_version, *arch) {
51+
("22.04", "amd64") => {
52+
"e0743f01668d664a97d85903057da7557dbf2dbbf0ceeb88e6b67ae9fb2f392f"
53+
}
54+
("24.04", "amd64") => {
55+
"2f02fd8e9377168310258ea03b356f4fa4beda3557e50da85681a3df889a886a"
56+
}
57+
("22.04", "arm64") => {
58+
"f24fc0676b8e0fd16de8efb8ab2a1dec0083304469883c03e553fb881dd5df29"
59+
}
60+
("24.04", "arm64") => {
61+
"32a48910da4b094192ef5c83e6526712fbf03b3d76a12e84423cbf6cc9ecbadf"
62+
}
63+
(d, a) => unreachable!("unknown valgrind target {d}/{a}"),
64+
},
65+
PinnedBinary::MemtrackInstaller => {
66+
"67f30ebe17d5da4246b51d8663394026385d95203ff09e81289772159e969603"
67+
}
68+
PinnedBinary::ExecHarnessInstaller => {
69+
"75cbff4fdaefe98927d24fff43fd600c621eb1263b0c40b0fd32c68fa6d88ebd"
70+
}
71+
PinnedBinary::MongoTracerInstaller => {
72+
"685f1d540cb24c2aa6f447991958339c6b70ec7664df2dba2713b8b3d77687e7"
73+
}
74+
}
75+
}
76+
}

src/cli/run/helpers/download_file.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
use crate::binary_pins::PinnedBinary;
12
use crate::{prelude::*, request_client::REQUEST_CLIENT};
23
use std::path::Path;
34

45
use url::Url;
56

6-
pub async fn download_file(url: &Url, path: &Path) -> Result<()> {
7+
async fn download_file(url: &Url, path: &Path) -> Result<()> {
78
debug!("Downloading file: {url}");
89
let response = REQUEST_CLIENT
910
.get(url.clone())
@@ -23,3 +24,26 @@ pub async fn download_file(url: &Url, path: &Path) -> Result<()> {
2324
.map_err(|e| anyhow!("Failed to write to file: {}, {}", path.display(), e))?;
2425
Ok(())
2526
}
27+
28+
/// Download a `PinnedBinary` and verify its bytes against the SHA-256
29+
/// declared by `PinnedBinary::sha256`. On mismatch the partial file is
30+
/// removed and an error is returned.
31+
pub async fn download_pinned_file(binary: PinnedBinary, path: &Path) -> Result<()> {
32+
let url_str = binary.url();
33+
let url = Url::parse(&url_str).context("failed to parse pinned URL")?;
34+
download_file(&url, path).await?;
35+
36+
let actual = sha256::try_digest(path)
37+
.with_context(|| format!("failed to compute sha256 of {}", path.display()))?;
38+
let expected = binary.sha256();
39+
40+
if actual != expected {
41+
let _ = std::fs::remove_file(path);
42+
bail!(
43+
"Hash mismatch for {url_str}: expected {expected}, got {actual}. The downloaded file has been deleted."
44+
);
45+
}
46+
47+
debug!("Verified sha256 of {url_str}");
48+
Ok(())
49+
}

src/cli/run/helpers/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ mod format_memory;
55
mod get_env_var;
66
mod parse_git_remote;
77

8-
pub(crate) use download_file::download_file;
8+
pub(crate) use download_file::download_pinned_file;
99
pub(crate) use find_repository_root::find_repository_root;
1010
pub(crate) use format_duration::format_duration;
1111
pub(crate) use format_memory::format_memory;

src/executor/memory/setup.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use crate::binary_installer::ensure_binary_installed;
2+
use crate::binary_pins::{self, PinnedBinary};
23
use crate::executor::{ToolInstallStatus, ToolStatus};
34
use crate::prelude::*;
45
use std::process::Command;
56

67
pub const MEMTRACK_COMMAND: &str = "codspeed-memtrack";
7-
pub const MEMTRACK_CODSPEED_VERSION: &str = "1.2.3";
8+
pub const MEMTRACK_CODSPEED_VERSION: &str = binary_pins::MEMTRACK_VERSION;
89

910
pub fn get_memtrack_status() -> ToolStatus {
1011
let tool_name = MEMTRACK_COMMAND.to_string();
@@ -70,16 +71,10 @@ pub fn get_memtrack_status() -> ToolStatus {
7071
}
7172

7273
pub async fn install_memtrack() -> Result<()> {
73-
let get_memtrack_installer_url = || {
74-
format!(
75-
"https://github.com/CodSpeedHQ/codspeed/releases/download/memtrack-v{MEMTRACK_CODSPEED_VERSION}/memtrack-installer.sh"
76-
)
77-
};
78-
7974
ensure_binary_installed(
8075
MEMTRACK_COMMAND,
8176
MEMTRACK_CODSPEED_VERSION,
82-
get_memtrack_installer_url,
77+
PinnedBinary::MemtrackInstaller,
8378
)
8479
.await
8580
}

src/executor/orchestrator.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use super::{ExecutionContext, ExecutorName, get_executor_from_mode, run_executor};
22
use crate::api_client::CodSpeedAPIClient;
33
use crate::binary_installer::ensure_binary_installed;
4+
use crate::binary_pins::{self, PinnedBinary};
45
use crate::cli::exec::multi_targets;
56
use crate::cli::run::logger::Logger;
67
use crate::executor::config::BenchmarkTarget;
@@ -18,7 +19,7 @@ use std::collections::BTreeMap;
1819
use std::path::{Path, PathBuf};
1920

2021
pub const EXEC_HARNESS_COMMAND: &str = "exec-harness";
21-
pub const EXEC_HARNESS_VERSION: &str = "1.3.0";
22+
pub const EXEC_HARNESS_VERSION: &str = binary_pins::EXEC_HARNESS_VERSION;
2223

2324
/// Shared orchestration state created once per CLI invocation.
2425
///
@@ -82,11 +83,11 @@ impl Orchestrator {
8283
.collect();
8384

8485
if !exec_targets.is_empty() {
85-
ensure_binary_installed(EXEC_HARNESS_COMMAND, EXEC_HARNESS_VERSION, || {
86-
format!(
87-
"https://github.com/CodSpeedHQ/codspeed/releases/download/exec-harness-v{EXEC_HARNESS_VERSION}/exec-harness-installer.sh"
88-
)
89-
})
86+
ensure_binary_installed(
87+
EXEC_HARNESS_COMMAND,
88+
EXEC_HARNESS_VERSION,
89+
PinnedBinary::ExecHarnessInstaller,
90+
)
9091
.await?;
9192

9293
let pipe_cmd = multi_targets::build_exec_targets_pipe_command(&exec_targets)?;

0 commit comments

Comments
 (0)