Skip to content

Commit 4446dba

Browse files
authored
Misc APC improvements (#12349)
* Fix a memory leak when invalid Kitty graphics data is sent via APC (this is the only commit for backporting to 1.3.2) * Add `max_bytes` to limit size of buffered APC data by protocol to prevent DoS, default to reasonable values * libghostty: expose max bytes APC options
2 parents dcc39dc + 0069e28 commit 4446dba

4 files changed

Lines changed: 131 additions & 25 deletions

File tree

include/ghostty/vt/terminal.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,25 @@ typedef enum GHOSTTY_ENUM_TYPED {
573573
* Input type: bool*
574574
*/
575575
GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_SHARED_MEM = 18,
576+
577+
/**
578+
* Set the maximum bytes the APC handler will buffer for all protocols.
579+
* This prevents malicious input from causing unbounded memory allocation.
580+
* A NULL value pointer removes all overrides, reverting to the built-in
581+
* defaults.
582+
*
583+
* Input type: size_t*
584+
*/
585+
GHOSTTY_TERMINAL_OPT_APC_MAX_BYTES = 19,
586+
587+
/**
588+
* Set the maximum bytes the APC handler will buffer for Kitty graphics
589+
* protocol data. A NULL value pointer removes the override, reverting
590+
* to the built-in default.
591+
*
592+
* Input type: size_t*
593+
*/
594+
GHOSTTY_TERMINAL_OPT_APC_MAX_BYTES_KITTY = 20,
576595
GHOSTTY_TERMINAL_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
577596
} GhosttyTerminalOption;
578597

src/terminal/apc.zig

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ const log = std.log.scoped(.terminal_apc);
1212
pub const Handler = struct {
1313
state: State = .inactive,
1414

15+
/// Maximum bytes each APC protocol can buffer. This is to prevent
16+
/// malicious input from causing us to allocate too much memory.
17+
/// If you want to be lazy and set a single value for all protocols,
18+
/// use `.initFull`.
19+
max_bytes: std.EnumMap(Protocol, usize) = .initFullWith(.{
20+
.kitty = Protocol.defaultMaxBytes(.kitty),
21+
}),
22+
1523
pub fn deinit(self: *Handler) void {
1624
self.state.deinit();
1725
}
@@ -34,7 +42,11 @@ pub const Handler = struct {
3442
switch (byte) {
3543
// Kitty graphics protocol
3644
'G' => self.state = if (comptime build_options.kitty_graphics)
37-
.{ .kitty = kitty_gfx.CommandParser.init(alloc) }
45+
.{ .kitty = .init(
46+
alloc,
47+
self.max_bytes.get(.kitty) orelse
48+
Protocol.defaultMaxBytes(.kitty),
49+
) }
3850
else
3951
.ignore,
4052

@@ -46,6 +58,7 @@ pub const Handler = struct {
4658
.kitty => |*p| if (comptime build_options.kitty_graphics) {
4759
p.feed(byte) catch |err| {
4860
log.warn("kitty graphics protocol error: {}", .{err});
61+
p.deinit();
4962
self.state = .ignore;
5063
};
5164
} else unreachable,
@@ -106,8 +119,22 @@ pub const State = union(enum) {
106119
}
107120
};
108121

122+
/// Possible APC command types.
123+
pub const Protocol = enum {
124+
kitty,
125+
126+
/// Returns the default maximum bytes for the given protocol.
127+
pub fn defaultMaxBytes(self: Protocol) usize {
128+
return switch (self) {
129+
// Kitty graphics payloads can be very large (e.g. full images
130+
// encoded as base64), so the default is set to 65 MiB.
131+
.kitty => 65 * 1024 * 1024,
132+
};
133+
}
134+
};
135+
109136
/// Possible APC commands.
110-
pub const Command = union(enum) {
137+
pub const Command = union(Protocol) {
111138
kitty: if (build_options.kitty_graphics)
112139
kitty_gfx.Command
113140
else
@@ -169,6 +196,41 @@ test "Kitty command with overflow i32" {
169196
try testing.expect(h.end() == null);
170197
}
171198

199+
test "kitty feed error deinits parser" {
200+
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
201+
202+
const testing = std.testing;
203+
const alloc = testing.allocator;
204+
205+
// Feed a valid kitty command start to allocate parser state, then
206+
// trigger an error during feed via an integer overflow. The testing
207+
// allocator will detect leaks if deinit is not called.
208+
var h: Handler = .{};
209+
defer h.deinit();
210+
h.start();
211+
for ("Ga=p,i=10000000000;") |c| h.feed(alloc, c);
212+
try testing.expect(h.state == .ignore);
213+
}
214+
215+
test "kitty max bytes exceeded" {
216+
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
217+
218+
const testing = std.testing;
219+
const alloc = testing.allocator;
220+
221+
var h: Handler = .{ .max_bytes = .init(.{ .kitty = 4 }) };
222+
defer h.deinit();
223+
h.start();
224+
// 'G' identifies kitty, 'a=t;' moves to data state, then feed exceeds max_bytes.
225+
for ("Ga=t;") |c| h.feed(alloc, c);
226+
try testing.expect(h.state != .ignore);
227+
for ("abcd") |c| h.feed(alloc, c);
228+
try testing.expect(h.state != .ignore);
229+
// The 5th data byte exceeds the 4-byte limit.
230+
h.feed(alloc, 'e');
231+
try testing.expect(h.state == .ignore);
232+
}
233+
172234
test "valid Kitty command" {
173235
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
174236

src/terminal/c/terminal.zig

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const ZigTerminal = @import("../Terminal.zig");
77
const Stream = @import("../stream_terminal.zig").Stream;
88
const ScreenSet = @import("../ScreenSet.zig");
99
const PageList = @import("../PageList.zig");
10+
const apc = @import("../apc.zig");
1011
const kitty = @import("../kitty/key.zig");
1112
const kitty_gfx_c = @import("kitty_graphics.zig");
1213
const modes = @import("../modes.zig");
@@ -310,6 +311,8 @@ pub const Option = enum(c_int) {
310311
kitty_image_medium_file = 16,
311312
kitty_image_medium_temp_file = 17,
312313
kitty_image_medium_shared_mem = 18,
314+
apc_max_bytes = 19,
315+
apc_max_bytes_kitty = 20,
313316

314317
/// Input type expected for setting the option.
315318
pub fn InType(comptime self: Option) type {
@@ -331,6 +334,7 @@ pub const Option = enum(c_int) {
331334
.kitty_image_medium_temp_file,
332335
.kitty_image_medium_shared_mem,
333336
=> ?*const bool,
337+
.apc_max_bytes, .apc_max_bytes_kitty => ?*const usize,
334338
};
335339
}
336340
};
@@ -425,6 +429,19 @@ fn setTyped(
425429
}
426430
}
427431
},
432+
.apc_max_bytes => {
433+
wrapper.stream.handler.apc_handler.max_bytes = if (value) |ptr|
434+
.initFull(ptr.*)
435+
else
436+
.{};
437+
},
438+
.apc_max_bytes_kitty => {
439+
if (value) |ptr| {
440+
wrapper.stream.handler.apc_handler.max_bytes.put(.kitty, ptr.*);
441+
} else {
442+
wrapper.stream.handler.apc_handler.max_bytes.remove(.kitty);
443+
}
444+
},
428445
}
429446
return .success;
430447
}

0 commit comments

Comments
 (0)