huskies: merge 1202 bug Flaky test: matrix pull pull_rejects_tampered_artifact_sha256_mismatch fails nondeterministically in merge gates
This commit is contained in:
@@ -306,10 +306,23 @@ mod tests {
|
|||||||
/// Spawn a minimal HTTP server serving fixed responses for
|
/// Spawn a minimal HTTP server serving fixed responses for
|
||||||
/// `/manifest.json` and `/<artifact>` so `run_pull` can be exercised
|
/// `/manifest.json` and `/<artifact>` so `run_pull` can be exercised
|
||||||
/// end-to-end without a real network.
|
/// end-to-end without a real network.
|
||||||
|
///
|
||||||
|
/// `run_pull` issues its manifest fetch and artifact download
|
||||||
|
/// sequentially over one shared `reqwest::Client`, whose HTTP/1.1 pool
|
||||||
|
/// assumes keep-alive by default. Every response here closes its
|
||||||
|
/// connection after a single request, so we send `Connection: close` to
|
||||||
|
/// stop the client from ever trying to reuse a connection this server
|
||||||
|
/// has already torn down (a real source of intermittent "cannot reach"
|
||||||
|
/// fetch errors that raced ahead of the sha256 check). We also block on
|
||||||
|
/// a readiness signal fired from inside the accept loop so the returned
|
||||||
|
/// port is guaranteed to have an active acceptor before any caller
|
||||||
|
/// connects to it.
|
||||||
async fn spawn_mock_channel(manifest_body: Vec<u8>, artifact_body: Vec<u8>) -> u16 {
|
async fn spawn_mock_channel(manifest_body: Vec<u8>, artifact_body: Vec<u8>) -> u16 {
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
let port = listener.local_addr().unwrap().port();
|
let port = listener.local_addr().unwrap().port();
|
||||||
|
let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
let _ = ready_tx.send(());
|
||||||
loop {
|
loop {
|
||||||
let Ok((mut stream, _)) = listener.accept().await else {
|
let Ok((mut stream, _)) = listener.accept().await else {
|
||||||
break;
|
break;
|
||||||
@@ -327,13 +340,17 @@ mod tests {
|
|||||||
} else {
|
} else {
|
||||||
artifact_body
|
artifact_body
|
||||||
};
|
};
|
||||||
let response =
|
let response = format!(
|
||||||
format!("HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", body.len());
|
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||||
|
body.len()
|
||||||
|
);
|
||||||
let _ = stream.write_all(response.as_bytes()).await;
|
let _ = stream.write_all(response.as_bytes()).await;
|
||||||
let _ = stream.write_all(&body).await;
|
let _ = stream.write_all(&body).await;
|
||||||
|
let _ = stream.shutdown().await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
let _ = ready_rx.await;
|
||||||
port
|
port
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,10 +485,64 @@ mod tests {
|
|||||||
msg.contains("sha256 mismatch"),
|
msg.contains("sha256 mismatch"),
|
||||||
"expected sha256 failure: {msg}"
|
"expected sha256 failure: {msg}"
|
||||||
);
|
);
|
||||||
|
assert!(
|
||||||
|
!msg.contains("could not fetch manifest")
|
||||||
|
&& !msg.contains("could not download artifact"),
|
||||||
|
"manifest fetch and artifact download must succeed so the sha256 check is reached, \
|
||||||
|
not preempted by a fetch/download error: {msg}"
|
||||||
|
);
|
||||||
assert!(msg.contains("Nothing was installed"));
|
assert!(msg.contains("Nothing was installed"));
|
||||||
assert!(!dir.path().join("huskies-linux-arm64").exists());
|
assert!(!dir.path().join("huskies-linux-arm64").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stress variant of [`pull_rejects_tampered_artifact_sha256_mismatch`]:
|
||||||
|
/// repeats the same scenario against many independent mock channels to
|
||||||
|
/// catch any reintroduction of the fetch-ordering race under load.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pull_rejects_tampered_artifact_sha256_mismatch_stress() {
|
||||||
|
for _ in 0..50 {
|
||||||
|
let kp = make_keypair();
|
||||||
|
let pubkey = crate::node_identity::public_key_hex(&kp);
|
||||||
|
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(b"the real bytes");
|
||||||
|
let real_sha256: String = hasher
|
||||||
|
.finalize()
|
||||||
|
.iter()
|
||||||
|
.map(|b| format!("{b:02x}"))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let manifest = ReleaseManifest {
|
||||||
|
artifact: "huskies-linux-arm64".to_string(),
|
||||||
|
sha256: real_sha256,
|
||||||
|
version: "abc1234".to_string(),
|
||||||
|
channel: "stable".to_string(),
|
||||||
|
timestamp: 1,
|
||||||
|
};
|
||||||
|
let signed_manifest = signed(&kp, manifest);
|
||||||
|
let manifest_body = serde_json::to_vec(&signed_manifest).unwrap();
|
||||||
|
|
||||||
|
let port = spawn_mock_channel(manifest_body, b"tampered bytes".to_vec()).await;
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
let msg = run_pull(
|
||||||
|
"stable",
|
||||||
|
&format!("http://127.0.0.1:{port}"),
|
||||||
|
&pubkey,
|
||||||
|
None,
|
||||||
|
dir.path(),
|
||||||
|
"huskies-linux-arm64",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
msg.contains("sha256 mismatch"),
|
||||||
|
"expected sha256 failure on every iteration: {msg}"
|
||||||
|
);
|
||||||
|
assert!(!dir.path().join("huskies-linux-arm64").exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn pull_missing_manifest_endpoint_is_loud_error() {
|
async fn pull_missing_manifest_endpoint_is_loud_error() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user