terminal/kitty: limit png decoder allocations

Limit individual allocator requests made by PNG decoders to the Kitty
graphics protocol's 400 MiB image ceiling. Add a reusable allocator
wrapper for callers that need per-request bounds.

PNG decoding previously used Wuffs' 4 GiB package limit and checked
the result only after allocation. A tiny PNG with oversized dimensions
could cause a multi-gigabyte RSS spike before being rejected.

Wrap decoder allocators with LimitedAllocator and translate limit
rejections to invalid image data while preserving genuine out-of-memory
errors. Add allocator boundary tests and regression coverage for a
crafted PNG below Wuffs' limit.
This commit is contained in:
Mitchell Hashimoto
2026-08-05 08:24:42 -07:00
parent f766f303a7
commit 590d669c4a
3 changed files with 201 additions and 3 deletions

View File

@@ -0,0 +1,124 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
/// An allocator that rejects any single allocation or resize larger than a
/// configured byte limit. The limit applies to each request independently,
/// not to the total amount of memory currently allocated.
pub const LimitedAllocator = struct {
child: Allocator,
limit: usize,
/// Set after an allocation, resize, or remap is rejected for exceeding
/// the limit. This remains set until the caller clears it.
limit_exceeded: bool = false,
pub fn init(child: Allocator, limit: usize) LimitedAllocator {
return .{
.child = child,
.limit = limit,
};
}
pub fn allocator(self: *LimitedAllocator) Allocator {
return .{
.ptr = self,
.vtable = &.{
.alloc = alloc,
.resize = resize,
.remap = remap,
.free = free,
},
};
}
fn check(self: *LimitedAllocator, size: usize) bool {
if (size <= self.limit) return true;
self.limit_exceeded = true;
return false;
}
fn alloc(
ctx: *anyopaque,
len: usize,
alignment: std.mem.Alignment,
ret_addr: usize,
) ?[*]u8 {
const self: *LimitedAllocator = @ptrCast(@alignCast(ctx));
if (!self.check(len)) return null;
return self.child.rawAlloc(len, alignment, ret_addr);
}
fn resize(
ctx: *anyopaque,
memory: []u8,
alignment: std.mem.Alignment,
new_len: usize,
ret_addr: usize,
) bool {
const self: *LimitedAllocator = @ptrCast(@alignCast(ctx));
if (!self.check(new_len)) return false;
return self.child.rawResize(memory, alignment, new_len, ret_addr);
}
fn remap(
ctx: *anyopaque,
memory: []u8,
alignment: std.mem.Alignment,
new_len: usize,
ret_addr: usize,
) ?[*]u8 {
const self: *LimitedAllocator = @ptrCast(@alignCast(ctx));
if (!self.check(new_len)) return null;
return self.child.rawRemap(memory, alignment, new_len, ret_addr);
}
fn free(
ctx: *anyopaque,
memory: []u8,
alignment: std.mem.Alignment,
ret_addr: usize,
) void {
const self: *LimitedAllocator = @ptrCast(@alignCast(ctx));
self.child.rawFree(memory, alignment, ret_addr);
}
};
test "LimitedAllocator allows allocations through the limit" {
const testing = std.testing;
var limited: LimitedAllocator = .init(testing.allocator, 8);
const alloc = limited.allocator();
const data = try alloc.alloc(u8, 8);
defer alloc.free(data);
try testing.expect(!limited.limit_exceeded);
}
test "LimitedAllocator rejects allocations before the child" {
const testing = std.testing;
var failing = testing.FailingAllocator.init(testing.allocator, .{
.fail_index = 0,
});
var limited: LimitedAllocator = .init(failing.allocator(), 8);
try testing.expectError(error.OutOfMemory, limited.allocator().alloc(u8, 9));
try testing.expect(limited.limit_exceeded);
try testing.expect(!failing.has_induced_failure);
}
test "LimitedAllocator rejects oversized resize and remap" {
const testing = std.testing;
var limited: LimitedAllocator = .init(testing.allocator, 8);
const alloc = limited.allocator();
const data = try alloc.alloc(u8, 8);
defer alloc.free(data);
try testing.expect(!alloc.resize(data, 9));
try testing.expect(limited.limit_exceeded);
limited.limit_exceeded = false;
try testing.expect(alloc.remap(data, 9) == null);
try testing.expect(limited.limit_exceeded);
}

View File

@@ -12,6 +12,7 @@ pub const BlockingQueue = blocking_queue.BlockingQueue;
pub const CacheTable = cache_table.CacheTable;
pub const CircBuf = circ_buf.CircBuf;
pub const IntrusiveDoublyLinkedList = intrusive_linked_list.DoublyLinkedList;
pub const LimitedAllocator = @import("limited_allocator.zig").LimitedAllocator;
pub const MessageData = @import("message_data.zig").MessageData;
pub const SegmentedPool = segmented_pool.SegmentedPool;
pub const SplitTree = split_tree.SplitTree;