@@ -12,6 +12,14 @@ const log = std.log.scoped(.terminal_apc);
1212pub 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+
172234test "valid Kitty command" {
173235 if (comptime ! build_options .kitty_graphics ) return error .SkipZigTest ;
174236
0 commit comments