authorgravatar for aspatelakbar@gmail.comakbarhusain <aspatelakbar@gmail.com> 2026-08-31 01:43:02+02:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-08-31 01:43:02+02:00
logfbdf84138ee8320c3edcd396f229e152307baebf
tree2ee3de833daf8ddd75e0c7f50fdad3c3bef12c50
parent8a6f4ae1827a5b6b28b07f57852008ea001bf02e

Fix data truncation in writeSplatHeaderLimitFinish (#36672)

fixes #36637 writeSplatHeaderLimitFinish batches up to 8 slices in a fixed `vecs: [8][]const u8` buffer before draining. When more than 8 slices (header plus data elements) were present, the loop did `break :v` once the buffer filled, silently dropping the remaining data and the splat pattern. Now the full batch is drained so processing continues for the remaining slices, with `total` accumulating the bytes written across batches. Also updated test `writeSplatHeader splatting avoids buffer aliasing temptation` which had incorrect expectations. Co-authored-by: Ryan Liptak <squeek502@hotmail.com> Reviewed-on: https://codeberg.org/ziglang/zig/pulls/36672 Reviewed-by: Ryan Liptak <squeek502@noreply.codeberg.org>

1 files changed, 34 insertions(+), 25 deletions(-)

lib/std/Io/Writer.zig+34-25
...@@ -269,36 +269,43 @@ fn writeSplatHeaderLimitFinish(...@@ -269,36 +269,43 @@ fn writeSplatHeaderLimitFinish(
269 limit: usize,269 limit: usize,
270) Error!usize {270) Error!usize {
271 var remaining = limit;271 var remaining = limit;
272 var total: usize = 0;
272 var vecs: [8][]const u8 = undefined;273 var vecs: [8][]const u8 = undefined;
273 var i: usize = 0;274 var i: usize = 0;
274 v: {275 if (header.len != 0) {
275 if (header.len != 0) {276 const copy_len = @min(header.len, remaining);
276 const copy_len = @min(header.len, remaining);277 vecs[i] = header[0..copy_len];
277 vecs[i] = header[0..copy_len];278 i += 1;
278 i += 1;279 remaining -= copy_len;
279 remaining -= copy_len;280 if (remaining == 0) {
280 if (remaining == 0) break :v;281 return w.vtable.drain(w, (&vecs)[0..i], 1);
281 }282 }
282 for (data[0 .. data.len - 1]) |buf| {283 }
283 if (buf.len == 0) continue;284 for (data[0 .. data.len - 1]) |buf| {
284 const copy_len = @min(buf.len, remaining);285 if (buf.len == 0) continue;
285 vecs[i] = buf[0..copy_len];286 const copy_len = @min(buf.len, remaining);
286 i += 1;287 vecs[i] = buf[0..copy_len];
287 remaining -= copy_len;288 i += 1;
288 if (remaining == 0) break :v;289 remaining -= copy_len;
289 if (vecs.len - i == 0) break :v;290 if (remaining == 0) {
291 return w.vtable.drain(w, (&vecs)[0..i], 1);
290 }292 }
291 const pattern = data[data.len - 1];293 if (i == vecs.len) {
292 if (splat == 1 or remaining < pattern.len) {294 total += try w.vtable.drain(w, &vecs, 1);
293 vecs[i] = pattern[0..@min(remaining, pattern.len)];295 i = 0;
294 i += 1;
295 break :v;
296 }296 }
297 vecs[i] = pattern;297 }
298 const pattern = data[data.len - 1];
299 if (splat == 1 or remaining < pattern.len) {
300 vecs[i] = pattern[0..@min(remaining, pattern.len)];
298 i += 1;301 i += 1;
299 return w.vtable.drain(w, (&vecs)[0..i], @min(remaining / pattern.len, splat));302 total += try w.vtable.drain(w, (&vecs)[0..i], 1);
303 return total;
300 }304 }
301 return w.vtable.drain(w, (&vecs)[0..i], 1);305 vecs[i] = pattern;
306 i += 1;
307 total += try w.vtable.drain(w, (&vecs)[0..i], @min(remaining / pattern.len, splat));
308 return total;
302}309}
303310
304const SplatHeaderTestCase = struct {311const SplatHeaderTestCase = struct {
...@@ -357,6 +364,8 @@ test "fixed writer writeSplatHeaderLimit" {...@@ -357,6 +364,8 @@ test "fixed writer writeSplatHeaderLimit" {
357364
358 // allocating writer that needs to expand capacity during splat365 // allocating writer that needs to expand capacity during splat
359 try testWriteSplatHeaderLimit(.{ .writer_type = .allocating, .buf_len = 8, .header = "hhhh", .data = &.{"PP"}, .splat = 3, .limit = 100, .expected_res = .{ .written = 10 }, .expected_buf_content = "hhhhPPPPPP" });366 try testWriteSplatHeaderLimit(.{ .writer_type = .allocating, .buf_len = 8, .header = "hhhh", .data = &.{"PP"}, .splat = 3, .limit = 100, .expected_res = .{ .written = 10 }, .expected_buf_content = "hhhhPPPPPP" });
367 try testWriteSplatHeaderLimit(.{ .writer_type = .allocating, .buf_len = 2, .header = "", .data = &.{ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "X", "Y", "ZZ" }, .splat = 2, .limit = 100, .expected_res = .{ .written = 16 }, .expected_buf_content = "0123456789XYZZZZ" });
368 try testWriteSplatHeaderLimit(.{ .writer_type = .allocating, .buf_len = 2, .header = "", .data = &.{ "0", "1", "2", "", "", "3", "4" }, .splat = 2, .limit = 4, .expected_res = .{ .written = 4 }, .expected_buf_content = "0123" });
360}369}
361370
362test "writeSplatHeader splatting avoids buffer aliasing temptation" {371test "writeSplatHeader splatting avoids buffer aliasing temptation" {
...@@ -367,9 +376,9 @@ test "writeSplatHeader splatting avoids buffer aliasing temptation" {...@@ -367,9 +376,9 @@ test "writeSplatHeader splatting avoids buffer aliasing temptation" {
367 const n = try aw.writer.writeSplatHeader("header which is longer than buf ", &.{376 const n = try aw.writer.writeSplatHeader("header which is longer than buf ", &.{
368 "1", "2", "3", "4", "5", "6", "foo", "bar", "foo",377 "1", "2", "3", "4", "5", "6", "foo", "bar", "foo",
369 }, 3);378 }, 3);
370 try testing.expectEqual(41, n);379 try testing.expectEqual(53, n);
371 try testing.expectEqualStrings(380 try testing.expectEqualStrings(
372 "header which is longer than buf 123456foo",381 "header which is longer than buf 123456foobarfoofoofoo",
373 aw.writer.buffered(),382 aw.writer.buffered(),
374 );383 );
375}384}