krishvishal commented on code in PR #3746:
URL: https://github.com/apache/iggy/pull/3746#discussion_r3662113746


##########
core/server_common/src/send_messages2.rs:
##########
@@ -778,26 +906,84 @@ impl<'a> LegacyMessageRef<'a> {
     }
 }
 
-// Hash in storage order: header tail, payload, user headers (the message
-// sections follow the legacy wire layout).
-fn calculate_checksum_parts(header_tail: &[u8], payload: &[u8], user_headers: 
&[u8]) -> u64 {
+/// Batch checksum v2: streaming `XxHash3_64` over the six batch header meta
+/// fields followed by each message's stored 8-byte checksum field in message
+/// order - NOT the message bodies.
+///
+/// Bodies are bound only transitively: each per-message checksum already 
covers
+/// `header[8..48] || payload || user_headers`, so hashing the checksum fields
+/// binds every body byte IFF a reader also re-verifies the per-message
+/// checksums. Stamp (produce) hashes `N * 8` bytes instead of the whole blob;
+/// validating decoders pay the one body pass as the per-message verify in
+/// [`verify_and_recompute_batch_checksum`], which hashes the checksum-field
+/// bytes in the same order so its recompute matches a compute here.
+///
+/// Assumes a well-formed blob whose frames tile exactly; every compute site
+/// builds the blob and satisfies this.
+fn calculate_batch_checksum(header: &SendMessages2Header, blob: &[u8]) -> u64 {
     let mut hasher = XxHash3_64::new();
-    hasher.write(header_tail);
-    hasher.write(payload);
-    hasher.write(user_headers);
+    write_batch_header_fields(&mut hasher, header);
+    let batch = SendMessages2Ref {
+        header: *header,
+        blob,
+    };
+    for framed in batch.iter_with_offsets() {
+        hasher.write(&blob[framed.start..framed.start + 8]);
+    }

Review Comment:
   Trailing bytes past `batch_length` now reach disk and permanently desync the 
segment walk
   
   Changed lines: `core/server_common/src/send_messages2.rs:923-932` and 
`core/partitions/src/iggy_partition.rs:1965`. Both gates that used to catch 
this were removed by this PR; the unchanged lines they protected are 
`send_messages2.rs:807` and `:677`.
   
   Two pre-existing behaviours make an oversized request representable. 
`decode_batch_slice:677` tests `body.len() < header.total_size()`, a lower 
bound rather than an equality, and then clamps the blob to `blob_len`, so any 
suffix past `COMMAND_HEADER_SIZE + blob_len` is left uncovered. 
`stamp_prepare_for_persistence:807` slices 
`bytes[PREPARE_SPLIT_POINT..total_size]` off `PrepareHeader.size` rather than 
off `batch_length`. Neither line is touched by this PR.
   
   Reachability: `core/server-ng/src/dispatch.rs:404` sets `total_size = 
header_size + body.len()`, deriving `size` from the bytes actually received. A 
client that appends K junk bytes after a well-formed batch therefore produces 
`size = 512 + blob_len + K` with a correct `batch_length`, and nothing 
cross-checks the two.
   
   What changed is the outcome. Before this PR, `calculate_batch_checksum` was 
`hasher.write(blob)`, covering every byte of the slice handed to it, so the 
stamp hashed blob plus K while the validating flush walk at 
`iggy_partition.rs:1965` recomputed over `blob[..blob_len]` without K. The 
mismatch fired, the batch was never appended, and the error was logged. The 
corruption was caught before disk.
   
   This PR removes both gates. `:923-932` replaces the bulk write with a frame 
walk that stops after the last decodable frame, so the stamp and the read-back 
recompute now produce an identical value and K becomes invisible. `:1965` 
downgrades the flush walk to `decode_prepare_slice_trusted`, which performs 
neither the checksum comparison nor the tiling check, so nothing rejects 
`prepare.size - 512 != blob_len`.
   
   The consequence is durable. The flush writes `entry.slice(256..)` 
(`core/partitions/src/iggy_partition.rs:2568`), which is `256 + blob_len + K` 
bytes, while flush accounting advances by `batch.header.total_size()` 
(`:1999`), which is `256 + blob_len`. `walk_disk_chunk` then advances `cursor 
+= total_size` (`core/partitions/src/poll_plan.rs:863`), lands inside the junk, 
fails to decode, and every later batch in that segment becomes unreadable. 
Backups write the same bytes, because the receive gate truncates the blob to 
`blob_len` before verifying.
   
   Scope: the silent case is junk that fails frame decode, which covers any K 
below 48 bytes, and any K at or above 48 whose bytes do not satisfy `reserved 
== 0` plus self-consistent length fields. Junk that does decode as a frame is 
absorbed into the stamp and then mismatches on read-back, so it still fails 
loudly. Reaching any of this requires a non-conforming client; the official 
SDKs will not emit it.
   
   The fix is one line: bound the stamp blob by `command.blob_len()?`, or 
tighten `:677` to an equality.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to