authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-08-13 17:43:00-07:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2026-08-14 12:26:14+02:00
loge61d80414204d8b3d41c0a4c48b7935e269eadd8
treed6e8ee7afcba36aa58cfa085d1fe5dfee2db5ab9
parent7b890ff9789caf010639277168d5f5852a9d852e

flate.Decompress: Fix potential infinite loop when using limits

Previously, the code relied on being able to write an entire match at once. When combined with limited streaming, this could result in an infinite loop with e.g. streamExact. The fix here is to support writing partial matches up to the limit, and then continue writing the rest of the match on the next stream. Fixes https://github.com/ziglang/zig/issues/25032

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

lib/std/compress/flate/Decompress.zig+65-25
...@@ -37,10 +37,16 @@ const State = union(enum) {...@@ -37,10 +37,16 @@ const State = union(enum) {
37 stored_block: u16,37 stored_block: u16,
38 fixed_block,38 fixed_block,
39 fixed_block_literal: u8,39 fixed_block_literal: u8,
40 fixed_block_match: u16,40 fixed_block_match: struct {
41 distance: u16,
42 length: u16,
43 },
41 dynamic_block,44 dynamic_block,
42 dynamic_block_literal: u8,45 dynamic_block_literal: u8,
43 dynamic_block_match: u16,46 dynamic_block_match: struct {
47 distance: u16,
48 length: u16,
49 },
44 protocol_footer,50 protocol_footer,
45 end,51 end,
46};52};
...@@ -398,7 +404,8 @@ fn streamInner(d: *Decompress, w: *Writer, limit: std.Io.Limit) (Error || Reader...@@ -398,7 +404,8 @@ fn streamInner(d: *Decompress, w: *Writer, limit: std.Io.Limit) (Error || Reader
398404
399 // Match405 // Match
400 const length = try d.decodeLength(@intCast(sym - 257));406 const length = try d.decodeLength(@intCast(sym - 257));
401 continue :sw .{ .fixed_block_match = length };407 const distance = try d.decodeDistance(@bitReverse(try d.takeIntBits(u5)));
408 continue :sw .{ .fixed_block_match = .{ .length = length, .distance = distance } };
402 }409 }
403410
404 const byte: u8 = @intCast(sym);411 const byte: u8 = @intCast(sym);
...@@ -417,16 +424,21 @@ fn streamInner(d: *Decompress, w: *Writer, limit: std.Io.Limit) (Error || Reader...@@ -417,16 +424,21 @@ fn streamInner(d: *Decompress, w: *Writer, limit: std.Io.Limit) (Error || Reader
417 try w.writeBytePreserve(flate.history_len, symbol);424 try w.writeBytePreserve(flate.history_len, symbol);
418 continue :sw .fixed_block;425 continue :sw .fixed_block;
419 },426 },
420 .fixed_block_match => |length| {427 .fixed_block_match => |match| {
421 if (remaining >= length) {428 if (remaining >= match.length) {
422 @branchHint(.likely);429 @branchHint(.likely);
423 const distance = try d.decodeDistance(@bitReverse(try d.takeIntBits(u5)));430 try writeMatch(w, match.length, match.distance);
424 try writeMatch(w, length, distance);431 remaining -= match.length;
425 remaining -= length;
426 continue :sw .fixed_block;432 continue :sw .fixed_block;
427 } else {433 } else {
428 d.state = .{ .fixed_block_match = length };434 if (remaining > 0) {
429 return @backingInt(limit) - remaining;435 try writeMatch(w, @intCast(remaining), match.distance);
436 }
437 d.state = .{ .fixed_block_match = .{
438 .distance = match.distance,
439 .length = match.length - @as(u16, @intCast(remaining)),
440 } };
441 return @backingInt(limit);
430 }442 }
431 },443 },
432 // In larger archives most blocks are usually dynamic, so444 // In larger archives most blocks are usually dynamic, so
...@@ -447,7 +459,9 @@ fn streamInner(d: *Decompress, w: *Writer, limit: std.Io.Limit) (Error || Reader...@@ -447,7 +459,9 @@ fn streamInner(d: *Decompress, w: *Writer, limit: std.Io.Limit) (Error || Reader
447459
448 // Match460 // Match
449 const length = try d.decodeLength(@intCast(sym - 257));461 const length = try d.decodeLength(@intCast(sym - 257));
450 continue :sw .{ .dynamic_block_match = length };462 const dsm = try d.decodeSymbol(&d.dst_dec);
463 const distance = try d.decodeDistance(@intCast(dsm));
464 continue :sw .{ .dynamic_block_match = .{ .length = length, .distance = distance } };
451 }465 }
452466
453 const byte: u8 = @intCast(sym);467 const byte: u8 = @intCast(sym);
...@@ -466,17 +480,21 @@ fn streamInner(d: *Decompress, w: *Writer, limit: std.Io.Limit) (Error || Reader...@@ -466,17 +480,21 @@ fn streamInner(d: *Decompress, w: *Writer, limit: std.Io.Limit) (Error || Reader
466 try w.writeBytePreserve(flate.history_len, symbol);480 try w.writeBytePreserve(flate.history_len, symbol);
467 continue :sw .dynamic_block;481 continue :sw .dynamic_block;
468 },482 },
469 .dynamic_block_match => |length| {483 .dynamic_block_match => |match| {
470 if (remaining >= length) {484 if (remaining >= match.length) {
471 @branchHint(.likely);485 @branchHint(.likely);
472 remaining -= length;486 remaining -= match.length;
473 const dsm = try d.decodeSymbol(&d.dst_dec);487 try writeMatch(w, match.length, match.distance);
474 const distance = try d.decodeDistance(@intCast(dsm));
475 try writeMatch(w, length, distance);
476 continue :sw .dynamic_block;488 continue :sw .dynamic_block;
477 } else {489 } else {
478 d.state = .{ .dynamic_block_match = length };490 if (remaining > 0) {
479 return @backingInt(limit) - remaining;491 try writeMatch(w, @intCast(remaining), match.distance);
492 }
493 d.state = .{ .dynamic_block_match = .{
494 .distance = match.distance,
495 .length = match.length - @as(u16, @intCast(remaining)),
496 } };
497 return @backingInt(limit);
480 }498 }
481 },499 },
482 .protocol_footer => {500 .protocol_footer => {
...@@ -500,9 +518,11 @@ fn streamInner(d: *Decompress, w: *Writer, limit: std.Io.Limit) (Error || Reader...@@ -500,9 +518,11 @@ fn streamInner(d: *Decompress, w: *Writer, limit: std.Io.Limit) (Error || Reader
500518
501/// Write match (back-reference to the same data slice) starting at `distance`519/// Write match (back-reference to the same data slice) starting at `distance`
502/// back from current write position, and `length` of bytes.520/// back from current write position, and `length` of bytes.
521/// `length` may be less than the minimum match length to allow for writing
522/// partial matches, but must be greater than zero.
503fn writeMatch(w: *Writer, length: u16, distance: u16) !void {523fn writeMatch(w: *Writer, length: u16, distance: u16) !void {
504 if (w.end < distance) return error.InvalidMatch;524 if (w.end < distance) return error.InvalidMatch;
505 assert(length >= token.min_length);525 assert(length > 0);
506 assert(length <= token.max_length);526 assert(length <= token.max_length);
507 assert(distance >= token.min_distance);527 assert(distance >= token.min_distance);
508 assert(distance <= token.max_distance);528 assert(distance <= token.max_distance);
...@@ -1171,12 +1191,32 @@ fn testFailure(container: Container, in: []const u8, expected_err: anyerror) !vo...@@ -1171,12 +1191,32 @@ fn testFailure(container: Container, in: []const u8, expected_err: anyerror) !vo
1171}1191}
11721192
1173fn testDecompress(container: Container, compressed: []const u8, expected_plain: []const u8) !void {1193fn testDecompress(container: Container, compressed: []const u8, expected_plain: []const u8) !void {
1174 var in: std.Io.Reader = .fixed(compressed);
1175 var aw: std.Io.Writer.Allocating = .init(testing.allocator);1194 var aw: std.Io.Writer.Allocating = .init(testing.allocator);
1176 defer aw.deinit();1195 defer aw.deinit();
11771196
1178 var decompress: Decompress = .init(&in, container, &.{});1197 // Decompress once using the normal methods.
1179 const decompressed_len = try decompress.reader.streamRemaining(&aw.writer);1198 {
1180 try testing.expectEqual(expected_plain.len, decompressed_len);1199 var in: std.Io.Reader = .fixed(compressed);
1181 try testing.expectEqualSlices(u8, expected_plain, aw.written());1200 var decompress: Decompress = .init(&in, container, &.{});
1201 const decompressed_len = try decompress.reader.streamRemaining(&aw.writer);
1202 try testing.expectEqual(expected_plain.len, decompressed_len);
1203 try testing.expectEqualSlices(u8, expected_plain, aw.written());
1204 }
1205
1206 // Decompress again by streaming one byte at a time to check that there aren't
1207 // any problems with things like writing partial matches, etc.
1208 aw.clearRetainingCapacity();
1209 {
1210 var in: std.Io.Reader = .fixed(compressed);
1211 var decompress: Decompress = .init(&in, container, &.{});
1212 var decompressed_len: usize = 0;
1213 while (true) {
1214 decompressed_len += decompress.reader.stream(&aw.writer, .limited(1)) catch |err| switch (err) {
1215 error.EndOfStream => break,
1216 else => |e| return e,
1217 };
1218 }
1219 try testing.expectEqual(expected_plain.len, decompressed_len);
1220 try testing.expectEqualSlices(u8, expected_plain, aw.written());
1221 }
1182}1222}