From a2a8f56c41b3ba5834dacebbb5aeaea9d6301f7d Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Tue, 9 Jun 2026 01:54:10 -0700 Subject: [PATCH] fix(ec): reject a short/empty remote shard read instead of serving zeros doReadRemoteEcShardInterval accepted an immediate EOF or a short stream and returned success with a partly zero-filled, unvalidated buffer (the server stamps the identity only on chunks that carry bytes). A non-deleted interval must arrive whole: require n == len(buf), exempting the is_deleted short-circuit (n=0), matching readLocalEcShardInterval's local check. A short read now fails so the caller recovers from parity. --- weed/storage/store_ec.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/weed/storage/store_ec.go b/weed/storage/store_ec.go index c67cddf24..b5b860245 100644 --- a/weed/storage/store_ec.go +++ b/weed/storage/store_ec.go @@ -609,6 +609,16 @@ func (s *Store) doReadRemoteEcShardInterval(sourceDataNode pb.ServerAddress, nee return 0, is_deleted, fmt.Errorf("read ec shard %d.%d from %s: %v", vid, shardId, sourceDataNode, err) } + // A non-deleted interval must arrive whole: the server stamps EncodeTsNs only + // on chunks that carry bytes, so a short or empty stream (e.g. immediate EOF + // from a pre-upgrade or stale server) leaves the buffer partly zero-filled and + // unvalidated. Reject it so the caller recovers from parity. The is_deleted + // short-circuit legitimately returns n=0 with no data and is exempt, matching + // readLocalEcShardInterval's got==len(buf) rule for the local path. + if !is_deleted && n != len(buf) { + return n, is_deleted, fmt.Errorf("short read ec shard %d.%d from %s: got %d want %d", vid, shardId, sourceDataNode, n, len(buf)) + } + return }