diff --git a/crates/targets/src/store.rs b/crates/targets/src/store.rs index 4a19295ae..0a98524c5 100644 --- a/crates/targets/src/store.rs +++ b/crates/targets/src/store.rs @@ -919,6 +919,22 @@ where } } + // The filename's item count is untrusted. Reject a payload that contains + // more items than advertised instead of returning success and allowing the + // caller to delete the entry with trailing events still in the file. + match deserializer.next() { + None => {} + Some(Ok(_)) => { + return Err(StoreError::Deserialization(format!( + "Batch for key {key} contains more than {} items", + key.item_count + ))); + } + Some(Err(e)) => { + return Err(StoreError::Deserialization(format!("Failed to deserialize trailing batch item: {e}"))); + } + } + if items.is_empty() && key.item_count > 0 { return Err(StoreError::Deserialization("No items found".to_string())); } @@ -1381,6 +1397,39 @@ mod tests { let _ = store.delete(); } + #[test] + fn get_multiple_errors_on_batch_with_trailing_items_instead_of_partial_success() { + let dir = temp_store_dir("trailing-batch-items"); + let store = QueueStore::::new_with_compression(&dir, 8, ".test", false); + store.open().unwrap(); + + let items = vec!["aa".to_string(), "bb".to_string(), "cc".to_string()]; + let original_key = store.put_multiple(items).unwrap(); + assert_eq!(original_key.item_count, 3); + + // Keep the three-item payload but make its filename claim that it contains + // only two items, simulating a corrupt or otherwise untrusted queue key. + let original_path = store.file_path(&original_key); + let advertised_key = Key { + item_count: 2, + ..original_key + }; + let advertised_path = store.file_path(&advertised_key); + std::fs::rename(&original_path, &advertised_path).unwrap(); + + let err = store.get_multiple(&advertised_key).unwrap_err(); + assert!( + matches!(err, StoreError::Deserialization(_)), + "expected Deserialization error, got {err:?}" + ); + + // Because get_multiple failed, the batch entry remains available for + // inspection or recovery instead of being silently discarded. + assert!(advertised_path.exists()); + + let _ = store.delete(); + } + #[test] fn concurrent_put_raw_respects_entry_limit() { let dir = temp_store_dir("concurrent-limit"); diff --git a/docs/testing/ci-gates.md b/docs/testing/ci-gates.md index 20874320f..2635367e0 100644 --- a/docs/testing/ci-gates.md +++ b/docs/testing/ci-gates.md @@ -286,6 +286,8 @@ workflow, builds the current checkout, runs the mixed-version and rollback E2E lanes, and fails unless all three raw G09 artifacts are measured, revision-bound, and role-bound. Use `--source-binary` for a custom previous-release binary on another platform, or `--test mixed-version|rollback` while narrowing a failure. +It performs a free-space preflight before building so a saturated validation +host fails before producing partial evidence. When the real release lanes have produced their dedicated artifacts, validate the complete hard-gate bundle with: diff --git a/scripts/run_scanner_heal_g09_upgrade_evidence.sh b/scripts/run_scanner_heal_g09_upgrade_evidence.sh index 2be5a6e28..4c3efd01b 100755 --- a/scripts/run_scanner_heal_g09_upgrade_evidence.sh +++ b/scripts/run_scanner_heal_g09_upgrade_evidence.sh @@ -8,6 +8,7 @@ SOURCE_REPOSITORY="${RUSTFS_UPGRADE_SOURCE_REPOSITORY:-rustfs/rustfs}" SOURCE_VERSION="${RUSTFS_UPGRADE_SOURCE_VERSION:-1.0.0-rc.5}" SOURCE_ASSET="${RUSTFS_UPGRADE_SOURCE_ASSET:-rustfs-linux-x86_64-gnu-v1.0.0-rc.5.zip}" SOURCE_SHA256="${RUSTFS_UPGRADE_SOURCE_SHA256:-3ee8df71e8edcfada533be452c4135868f697bc515460ae97b027313eade7a3d}" +MIN_FREE_KIB="${RUSTFS_G09_MIN_FREE_KIB:-6291456}" RUN_DIR="" SOURCE_DIR="" @@ -39,7 +40,9 @@ Options: -h, --help Show this help The default pinned release asset is Linux x86_64. Use --source-binary when -running against a custom previous-release binary on another platform. +running against a custom previous-release binary on another platform. The +script requires at least 6 GiB free by default; override +RUSTFS_G09_MIN_FREE_KIB only for a deliberately smaller diagnostic run. USAGE } @@ -122,6 +125,21 @@ verify_sha256() { fi } +ensure_min_free_space() { + local path="$1" + local available + mkdir -p "$path" + available="$(df -Pk "$path" | awk 'NR == 2 { print $4 }')" + if [[ -z "$available" ]]; then + echo "could not determine free space for $path" >&2 + exit 1 + fi + if (( available < MIN_FREE_KIB )); then + echo "insufficient free space for G09 evidence run at $path: need ${MIN_FREE_KIB} KiB, found ${available} KiB" >&2 + exit 1 + fi +} + resolve_source_binary() { if [[ -n "$SOURCE_BINARY" ]]; then SOURCE_BINARY="$(normalize_path "$SOURCE_BINARY")" @@ -326,6 +344,7 @@ if [[ "$PLAN_ONLY" == 1 ]]; then echo "tests=${CASES[*]}" echo "source_repository=$SOURCE_REPOSITORY" echo "source_version=$SOURCE_VERSION" + echo "min_free_kib=$MIN_FREE_KIB" if [[ -n "$SOURCE_BINARY" ]]; then echo "source_binary=$(normalize_path "$SOURCE_BINARY")" else @@ -349,6 +368,11 @@ if [[ -e "$RUN_DIR" ]]; then exit 1 fi mkdir -p "$RUN_DIR/logs" +if [[ -n "${TMPDIR:-}" ]]; then + mkdir -p "$TMPDIR" + ensure_min_free_space "$TMPDIR" +fi +ensure_min_free_space "$RUN_DIR" SOURCE_BINARY="$(resolve_source_binary)" export RUSTFS_UPGRADE_SOURCE_BINARY="$SOURCE_BINARY"