authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-07-09 16:34:41+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-07-18 13:34:34+02:00
log6c7a5da5a65b02cb7cebac7453c64c8f2d7a8afd
tree417fc2c9fc612e9cf80692de6c2544d66a42f6cf
parent873b762f4513a1e6bb72a6902c8a0f7e217e6da1

std.crypto.Blake3: fix hashParallel digest for non-aligned inputs

The parallel Merkle reduction only covered full chunks. When there was a trailing partial chunk, it was added at the end, which didn't produce the expeected tree when reduction merged the rightmost node. As in regular non-parallel hash, treat the partial chunk as the last leaf of the reduction instead. Add tests for a bunch of edge cases by the way. Fixes #36100

1 files changed, 44 insertions(+), 10 deletions(-)

lib/std/crypto/blake3.zig+44-10
......@@ -1004,7 +1004,10 @@ pub const Blake3 = struct {
10041004 return hash(b, out, options);
10051005 }
10061006
1007 const cvs = try allocator.alloc([8]u32, num_full_chunks);
1007 const remaining_bytes = b.len % chunk_length;
1008 const num_leaves = @divCeil(b.len, chunk_length);
1009
1010 const cvs = try allocator.alloc([8]u32, num_leaves);
10081011 defer allocator.free(cvs);
10091012
10101013 // Process chunks in parallel
......@@ -1028,8 +1031,16 @@ pub const Blake3 = struct {
10281031 }
10291032 try group.await(io);
10301033
1034 if (remaining_bytes > 0) {
1035 var chunk_state = ChunkState.init(key_words, flags);
1036 chunk_state.chunk_counter = num_full_chunks;
1037 chunk_state.update(b[num_full_chunks * chunk_length ..]);
1038 const output = chunk_state.output();
1039 cvs[num_full_chunks] = output.chainingValue();
1040 }
1041
10311042 // Build Merkle tree in parallel layers using ping-pong buffers
1032 const max_intermediate_size = (num_full_chunks + 1) / 2;
1043 const max_intermediate_size = @divCeil(num_leaves, 2);
10331044 const buffer0 = try allocator.alloc([8]u32, max_intermediate_size);
10341045 defer allocator.free(buffer0);
10351046 const buffer1 = try allocator.alloc([8]u32, max_intermediate_size);
......@@ -1064,14 +1075,6 @@ pub const Blake3 = struct {
10641075 // Finalize remaining small tree sequentially
10651076 var hasher = init_internal(key_words, flags);
10661077 for (current_level, 0..) |cv, i| hasher.pushCv(cv, i);
1067
1068 hasher.chunk.chunk_counter = num_full_chunks;
1069 const remaining_bytes = b.len % chunk_length;
1070 if (remaining_bytes > 0) {
1071 hasher.chunk.update(b[num_full_chunks * chunk_length ..]);
1072 hasher.mergeCvStack(hasher.chunk.chunk_counter);
1073 }
1074
10751078 hasher.final(out);
10761079 }
10771080
......@@ -1453,3 +1456,34 @@ test "BLAKE3 parallel vs sequential" {
14531456 try std.testing.expectEqualSlices(u8, &expected_keyed, &actual_keyed);
14541457 }
14551458}
1459
1460test "BLAKE3 parallel with partial trailing chunk" {
1461 const allocator = std.testing.allocator;
1462 const io = std.testing.io;
1463
1464 const test_sizes = [_]usize{
1465 3072 * 1024,
1466 3072 * 1024 + 1,
1467 3073 * 1024 + 1,
1468 3074 * 1024 + 1,
1469 3075 * 1024,
1470 3075 * 1024 + 1,
1471 3075 * 1024 + 1023,
1472 3077 * 1024 + 1,
1473 4095 * 1024 + 1,
1474 };
1475
1476 for (test_sizes) |size| {
1477 const input = try allocator.alloc(u8, size);
1478 defer allocator.free(input);
1479 for (input, 0..) |*byte, i| byte.* = @truncate(i);
1480
1481 var expected: [32]u8 = undefined;
1482 Blake3.hash(input, &expected, .{});
1483
1484 var actual: [32]u8 = undefined;
1485 try Blake3.hashParallel(input, &actual, .{}, allocator, io);
1486
1487 try std.testing.expectEqualSlices(u8, &expected, &actual);
1488 }
1489}