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) {
3737 stored_block: u16,
3838 fixed_block,
3939 fixed_block_literal: u8,
40 fixed_block_match: u16,
40 fixed_block_match: struct {
41 distance: u16,
42 length: u16,
43 },
4144 dynamic_block,
4245 dynamic_block_literal: u8,
43 dynamic_block_match: u16,
46 dynamic_block_match: struct {
47 distance: u16,
48 length: u16,
49 },
4450 protocol_footer,
4551 end,
4652};
......@@ -398,7 +404,8 @@ fn streamInner(d: *Decompress, w: *Writer, limit: std.Io.Limit) (Error || Reader
398404
399405 // Match
400406 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 } };
402409 }
403410
404411 const byte: u8 = @intCast(sym);
......@@ -417,16 +424,21 @@ fn streamInner(d: *Decompress, w: *Writer, limit: std.Io.Limit) (Error || Reader
417424 try w.writeBytePreserve(flate.history_len, symbol);
418425 continue :sw .fixed_block;
419426 },
420 .fixed_block_match => |length| {
421 if (remaining >= length) {
427 .fixed_block_match => |match| {
428 if (remaining >= match.length) {
422429 @branchHint(.likely);
423 const distance = try d.decodeDistance(@bitReverse(try d.takeIntBits(u5)));
424 try writeMatch(w, length, distance);
425 remaining -= length;
430 try writeMatch(w, match.length, match.distance);
431 remaining -= match.length;
426432 continue :sw .fixed_block;
427433 } else {
428 d.state = .{ .fixed_block_match = length };
429 return @backingInt(limit) - remaining;
434 if (remaining > 0) {
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);
430442 }
431443 },
432444 // 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
447459
448460 // Match
449461 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 } };
451465 }
452466
453467 const byte: u8 = @intCast(sym);
......@@ -466,17 +480,21 @@ fn streamInner(d: *Decompress, w: *Writer, limit: std.Io.Limit) (Error || Reader
466480 try w.writeBytePreserve(flate.history_len, symbol);
467481 continue :sw .dynamic_block;
468482 },
469 .dynamic_block_match => |length| {
470 if (remaining >= length) {
483 .dynamic_block_match => |match| {
484 if (remaining >= match.length) {
471485 @branchHint(.likely);
472 remaining -= length;
473 const dsm = try d.decodeSymbol(&d.dst_dec);
474 const distance = try d.decodeDistance(@intCast(dsm));
475 try writeMatch(w, length, distance);
486 remaining -= match.length;
487 try writeMatch(w, match.length, match.distance);
476488 continue :sw .dynamic_block;
477489 } else {
478 d.state = .{ .dynamic_block_match = length };
479 return @backingInt(limit) - remaining;
490 if (remaining > 0) {
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);
480498 }
481499 },
482500 .protocol_footer => {
......@@ -500,9 +518,11 @@ fn streamInner(d: *Decompress, w: *Writer, limit: std.Io.Limit) (Error || Reader
500518
501519/// Write match (back-reference to the same data slice) starting at `distance`
502520/// 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.
503523fn writeMatch(w: *Writer, length: u16, distance: u16) !void {
504524 if (w.end < distance) return error.InvalidMatch;
505 assert(length >= token.min_length);
525 assert(length > 0);
506526 assert(length <= token.max_length);
507527 assert(distance >= token.min_distance);
508528 assert(distance <= token.max_distance);
......@@ -1171,12 +1191,32 @@ fn testFailure(container: Container, in: []const u8, expected_err: anyerror) !vo
11711191}
11721192
11731193fn testDecompress(container: Container, compressed: []const u8, expected_plain: []const u8) !void {
1174 var in: std.Io.Reader = .fixed(compressed);
11751194 var aw: std.Io.Writer.Allocating = .init(testing.allocator);
11761195 defer aw.deinit();
11771196
1178 var decompress: Decompress = .init(&in, container, &.{});
1179 const decompressed_len = try decompress.reader.streamRemaining(&aw.writer);
1180 try testing.expectEqual(expected_plain.len, decompressed_len);
1181 try testing.expectEqualSlices(u8, expected_plain, aw.written());
1197 // Decompress once using the normal methods.
1198 {
1199 var in: std.Io.Reader = .fixed(compressed);
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 }
11821222}