mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
terminal/apc: limit glyf decode allocations
Limit individual allocations made while decoding registered glyf outlines to 64 KB. Carefully crafted glyf outlines could expand into ~768KB of memory per glossary entry, which adds up to hundreds of MB per terminal surface. Across many terminals this could cause issues. The 64KB number was chosen by inspecting every glyph across Apple symbols and Noto emoji and the largest single glyph found was 40KB. So, 64KB is generous while limiting each terminal to ~68MB of RAM for max glyph glossaries.
This commit is contained in:
@@ -133,9 +133,7 @@
|
||||
//! ## Glossary Capacity
|
||||
//!
|
||||
//! Each session holds at most 1024 registrations keyed by codepoint.
|
||||
//! Registrations live for the session duration. A 1025th registration
|
||||
//! evicts the oldest entry (FIFO). Sessions are isolated: two tabs may
|
||||
//! independently register the same codepoint.
|
||||
//! Each allocation made by the `glyf` decoder is limited to 64 KiB.
|
||||
//!
|
||||
//! ## Security: PUA-Only Restriction
|
||||
//!
|
||||
|
||||
@@ -20,6 +20,11 @@ const Constraint = FontGlyph.RenderOptions.Constraint;
|
||||
/// Defined by the specification.
|
||||
pub const max_entries = 1024;
|
||||
|
||||
/// Maximum size of any allocation made while decoding a glyf registration.
|
||||
/// This is limited by the dynamic allocations not the struct size so the
|
||||
/// actual size allowed is slightly larger, but worst case only by 10KB.
|
||||
pub const max_glyf_allocation = 64 * 1024; // 64 KiB
|
||||
|
||||
/// An empty glossary with no registered glyphs.
|
||||
pub const empty: Glossary = .{ .entries = .empty };
|
||||
|
||||
@@ -170,7 +175,7 @@ pub const Entry = struct {
|
||||
// Decode the payload into some usable glyph format for
|
||||
// future rasterization.
|
||||
const glyph: Glyph = switch (fmt) {
|
||||
.glyf => .{ .glyf = try req.decodeGlyfPayload(alloc) },
|
||||
.glyf => .{ .glyf = try req.decodeGlyfPayload(alloc, max_glyf_allocation) },
|
||||
.colrv0, .colrv1 => return error.UnsupportedFormat,
|
||||
};
|
||||
|
||||
@@ -273,6 +278,47 @@ fn testParseRegister(alloc: Allocator, data: []const u8) !RegisterReq {
|
||||
// and three on-curve points.
|
||||
const test_triangle_glyf_payload = "AAEAZABkA4QDhAACAAABAQEB9P5wAyADhPzgAAA=";
|
||||
|
||||
fn testRepeatedPointRegisterReq(
|
||||
alloc: Allocator,
|
||||
end_points: []const u16,
|
||||
) !RegisterReq {
|
||||
assert(end_points.len > 0);
|
||||
|
||||
var decoded: std.ArrayList(u8) = .empty;
|
||||
defer decoded.deinit(alloc);
|
||||
|
||||
// Simple glyph header with zero bounds.
|
||||
const contour_count: u16 = @intCast(end_points.len);
|
||||
try decoded.appendSlice(alloc, &.{ @truncate(contour_count >> 8), @truncate(contour_count) });
|
||||
try decoded.appendNTimes(alloc, 0, 8);
|
||||
for (end_points) |end_point| try decoded.appendSlice(
|
||||
alloc,
|
||||
&.{ @truncate(end_point >> 8), @truncate(end_point) },
|
||||
);
|
||||
try decoded.appendSlice(alloc, &.{ 0x00, 0x00 }); // No instructions.
|
||||
|
||||
// Maximum-length runs describe on-curve, zero-delta points compactly.
|
||||
var remaining: usize = @as(usize, end_points[end_points.len - 1]) + 1;
|
||||
while (remaining > 0) {
|
||||
const run = @min(remaining, 256);
|
||||
if (run == 1) {
|
||||
try decoded.append(alloc, 0x31);
|
||||
} else {
|
||||
try decoded.appendSlice(alloc, &.{ 0x39, @intCast(run - 1) });
|
||||
}
|
||||
remaining -= run;
|
||||
}
|
||||
|
||||
const Encoder = std.base64.standard.Encoder;
|
||||
const payload = try alloc.alloc(u8, Encoder.calcSize(decoded.items.len));
|
||||
defer alloc.free(payload);
|
||||
const encoded = Encoder.encode(payload, decoded.items);
|
||||
|
||||
const command = try std.fmt.allocPrint(alloc, "r;cp=e000;{s}", .{encoded});
|
||||
defer alloc.free(command);
|
||||
return try testParseRegister(alloc, command);
|
||||
}
|
||||
|
||||
fn testRegisterReq(alloc: Allocator, cp: u21) !RegisterReq {
|
||||
const data = try std.fmt.allocPrint(
|
||||
alloc,
|
||||
@@ -330,6 +376,31 @@ test "Entry init rejects invalid register payload" {
|
||||
try testing.expectError(error.MalformedPayload, Entry.init(alloc, req));
|
||||
}
|
||||
|
||||
test "Entry init rejects glyf allocation over limit" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// 5,462 points require a 65,544-byte points allocation.
|
||||
const req = try testRepeatedPointRegisterReq(alloc, &.{5461});
|
||||
defer alloc.free(req.raw);
|
||||
|
||||
try testing.expectError(error.PayloadTooLarge, Entry.init(alloc, req));
|
||||
}
|
||||
|
||||
test "Entry init accepts largest glyf points allocation under limit" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// 5,461 points require a 65,532-byte points allocation.
|
||||
const req = try testRepeatedPointRegisterReq(alloc, &.{ 0, 5460 });
|
||||
defer alloc.free(req.raw);
|
||||
|
||||
var entry = try Entry.init(alloc, req);
|
||||
defer entry.deinit(alloc);
|
||||
try testing.expectEqual(@as(usize, 5461), entry.glyph.glyf.points.len);
|
||||
try testing.expectEqual(@as(usize, 2), entry.glyph.glyf.contours.len);
|
||||
}
|
||||
|
||||
test "Glossary register overwrites and moves entry to newest position" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const LimitedAllocator = @import("../../../datastruct/main.zig").LimitedAllocator;
|
||||
const Glyf = @import("../../../font/opentype/glyf.zig").Glyf;
|
||||
const fraction = @import("../../fraction.zig");
|
||||
|
||||
@@ -272,7 +273,11 @@ pub const Request = union(enum) {
|
||||
};
|
||||
|
||||
/// Decode this request's base64 glyf payload into an owned outline.
|
||||
pub fn decodeGlyfPayload(self: Register, alloc: Allocator) DecodeError!Glyf.Outline {
|
||||
pub fn decodeGlyfPayload(
|
||||
self: Register,
|
||||
alloc: Allocator,
|
||||
max_allocation_bytes: usize,
|
||||
) DecodeError!Glyf.Outline {
|
||||
// Prep base64 decoding, initial validation.
|
||||
const Decoder = std.base64.standard.Decoder;
|
||||
const payload_bytes = self.payload();
|
||||
@@ -300,8 +305,16 @@ pub const Request = union(enum) {
|
||||
// decode call below. Glyf.Entry.decode returns an owned Outline, so
|
||||
// it is safe to free `data` before returning that outline.
|
||||
const glyf_entry = Glyf.Entry.init(data) catch return error.MalformedPayload;
|
||||
return glyf_entry.decode(alloc) catch |err| switch (err) {
|
||||
error.OutOfMemory => error.OutOfMemory,
|
||||
|
||||
// The points slice is the dominant decoded allocation. Limiting
|
||||
// every allocation made by the decoder prevents a compact record
|
||||
// from expanding into an unbounded persistent outline.
|
||||
var limited: LimitedAllocator = .init(alloc, max_allocation_bytes);
|
||||
return glyf_entry.decode(limited.allocator()) catch |err| switch (err) {
|
||||
error.OutOfMemory => if (limited.limit_exceeded)
|
||||
error.PayloadTooLarge
|
||||
else
|
||||
error.OutOfMemory,
|
||||
// Unsupported fields
|
||||
error.CompositeNotSupported => error.CompositeUnsupported,
|
||||
error.InstructionsNotSupported => error.HintingUnsupported,
|
||||
@@ -789,7 +802,10 @@ test "register decodes glyf payload" {
|
||||
var cmd = try testParse(testing.allocator, "r;cp=e0a0;AAAAAAAAAAAAAA==");
|
||||
defer cmd.deinit(testing.allocator);
|
||||
|
||||
var outline = try cmd.register.decodeGlyfPayload(testing.allocator);
|
||||
var outline = try cmd.register.decodeGlyfPayload(
|
||||
testing.allocator,
|
||||
std.math.maxInt(usize),
|
||||
);
|
||||
defer outline.deinit(testing.allocator);
|
||||
|
||||
try testing.expectEqual(@as(usize, 0), outline.points.len);
|
||||
@@ -802,7 +818,10 @@ test "register rejects malformed glyf payload" {
|
||||
var cmd = try testParse(testing.allocator, "r;cp=e0a0;%%%not-base64%%%");
|
||||
defer cmd.deinit(testing.allocator);
|
||||
|
||||
try testing.expectError(error.MalformedPayload, cmd.register.decodeGlyfPayload(testing.allocator));
|
||||
try testing.expectError(
|
||||
error.MalformedPayload,
|
||||
cmd.register.decodeGlyfPayload(testing.allocator, std.math.maxInt(usize)),
|
||||
);
|
||||
}
|
||||
|
||||
test "register response without payload" {
|
||||
|
||||
Reference in New Issue
Block a user