fix(ecstore): fail closed on unverifiable data quorum (#6903)

fix(ecstore): require verification source for degraded GET

Fail closed when reconstruction has only an exact decode quorum, because no surplus source remains to validate the rebuilt data. Cover both erasure engines and the data-shards-only rollout gate.

Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-08-31 05:07:47 +08:00
committed by GitHub
parent 489408c0b0
commit 47ad69b691
3 changed files with 132 additions and 12 deletions
@@ -933,8 +933,29 @@ impl Erasure {
}
pub(crate) fn decode_data_with_reconstruction_verification(&self, shards: &mut [Option<Vec<u8>>]) -> io::Result<()> {
self.decode_data_with_reconstruction_verification_policy(shards, false)
}
pub(crate) fn decode_data_with_reconstruction_verification_for_lockstep(
&self,
shards: &mut [Option<Vec<u8>>],
) -> io::Result<()> {
self.decode_data_with_reconstruction_verification_policy(shards, true)
}
fn decode_data_with_reconstruction_verification_policy(
&self,
shards: &mut [Option<Vec<u8>>],
require_surplus_source: bool,
) -> io::Result<()> {
let missing_data_source = shards.iter().take(self.data_shards).any(|shard| shard.is_none());
let available_shards = shards.iter().filter(|shard| shard.is_some()).count();
if require_surplus_source && missing_data_source && available_shards == self.data_shards {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"insufficient source shards to verify reconstructed data",
));
}
let source_parity = if missing_data_source && available_shards > self.data_shards {
shards
.iter()
@@ -1868,6 +1889,31 @@ mod tests {
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn decode_data_with_verification_scopes_exact_quorum_to_lockstep() {
for uses_legacy in [false, true] {
let erasure = Erasure::new_with_options(3, 2, 128, uses_legacy);
let data = b"verified reads must not accept reconstruction without a surplus source";
let encoded = erasure.encode_data(data).expect("encode should succeed");
let mut exact_quorum = optional_shards(&encoded);
exact_quorum[0] = None;
exact_quorum[erasure.total_shard_count() - 1] = None;
let mut default_shards = exact_quorum.clone();
erasure
.decode_data_with_reconstruction_verification(&mut default_shards)
.expect("default decode must preserve exact-quorum reconstruction");
assert_eq!(default_shards[0].as_deref(), Some(encoded[0].as_ref()));
let err = erasure
.decode_data_with_reconstruction_verification_for_lockstep(&mut exact_quorum)
.expect_err("data-shards-only lockstep must reject an exact decode quorum");
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert!(err.to_string().contains("insufficient source shards"));
}
}
#[test]
fn verify_data_and_parity_rejects_missing_and_mismatched_shards() {
let erasure = Erasure::new(4, 2, 128);