mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-21 13:08:09 +00:00
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:
124
src/datastruct/limited_allocator.zig
Normal file
124
src/datastruct/limited_allocator.zig
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user