diff --git a/src/terminal/apc/glyph.zig b/src/terminal/apc/glyph.zig index 13454a3f3..e86ee9f8a 100644 --- a/src/terminal/apc/glyph.zig +++ b/src/terminal/apc/glyph.zig @@ -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 //! diff --git a/src/terminal/apc/glyph/Glossary.zig b/src/terminal/apc/glyph/Glossary.zig index 28458dbb2..7200d162a 100644 --- a/src/terminal/apc/glyph/Glossary.zig +++ b/src/terminal/apc/glyph/Glossary.zig @@ -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; diff --git a/src/terminal/apc/glyph/request.zig b/src/terminal/apc/glyph/request.zig index 7bf4b6ba9..6b6d7ccd2 100644 --- a/src/terminal/apc/glyph/request.zig +++ b/src/terminal/apc/glyph/request.zig @@ -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" {