terminal/kitty: add Limits to restrict capabilities of image transfer

This commit is contained in:
Mitchell Hashimoto
2026-04-05 07:05:51 -07:00
parent 29e3de737e
commit 6a99c248d0
2 changed files with 220 additions and 14 deletions

View File

@@ -112,7 +112,7 @@ fn query(alloc: Allocator, cmd: *const Command) Response {
};
// Attempt to load the image. If we cannot, then set an appropriate error.
var loading = LoadingImage.init(alloc, cmd) catch |err| {
var loading = LoadingImage.init(alloc, cmd, .all) catch |err| {
encodeError(&result, err);
return result;
};
@@ -322,7 +322,7 @@ fn loadAndAddImage(
}
break :loading loading.*;
} else try .init(alloc, cmd);
} else try .init(alloc, cmd, .all);
// We only want to deinit on error. If we're chunking, then we don't
// want to deinit at all. If we're not chunking, then we'll deinit

View File

@@ -44,10 +44,38 @@ pub const LoadingImage = struct {
/// used if q isn't set on subsequent chunks.
quiet: command.Command.Quiet,
/// The limits of the Kitty Graphics protocol we should allow.
///
/// This can be used to restrict the type of images and other
/// parameters for resource or security reasons. Note that depending
/// on how libghostty is compiled, some of these may be fully unsupported
/// and ignored (e.g. "file" on wasm32-freestanding).
pub const Limits = packed struct {
file: bool,
temporary_file: bool,
shared_memory: bool,
pub const all: Limits = .{
.file = true,
.temporary_file = true,
.shared_memory = true,
};
pub const direct: Limits = .{
.file = false,
.temporary_file = false,
.shared_memory = false,
};
};
/// Initialize a chunked immage from the first image transmission.
/// If this is a multi-chunk image, this should only be the FIRST
/// chunk.
pub fn init(alloc: Allocator, cmd: *const command.Command) !LoadingImage {
pub fn init(
alloc: Allocator,
cmd: *const command.Command,
limits: Limits,
) !LoadingImage {
// Build our initial image from the properties sent via the control.
// These can be overwritten by the data loading process. For example,
// PNG loading sets the width/height from the data.
@@ -72,6 +100,26 @@ pub const LoadingImage = struct {
return result;
}
// Verify our capabilities and limits allow this.
{
// Special case if we don't support decoding PNGs and the format
// is a PNG we can save a lot of memory/effort buffering the
// data but failing up front.
if (t.format == .png and
sys.decode_png == null)
{
return error.UnsupportedMedium;
}
// Verify the medium is allowed
switch (t.medium) {
.direct => unreachable,
.file => if (!limits.file) return error.UnsupportedMedium,
.temporary_file => if (!limits.temporary_file) return error.UnsupportedMedium,
.shared_memory => if (!limits.shared_memory) return error.UnsupportedMedium,
}
}
// Otherwise, the payload data is guaranteed to be a path.
if (comptime builtin.os.tag != .windows) {
@@ -523,7 +571,7 @@ test "image load with invalid RGB data" {
.data = try alloc.dupe(u8, "AAAA"),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .all);
defer loading.deinit(alloc);
}
@@ -541,7 +589,7 @@ test "image load with image too wide" {
.data = try alloc.dupe(u8, "AAAA"),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .all);
defer loading.deinit(alloc);
try testing.expectError(error.DimensionsTooLarge, loading.complete(alloc));
}
@@ -560,7 +608,7 @@ test "image load with image too tall" {
.data = try alloc.dupe(u8, "AAAA"),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .all);
defer loading.deinit(alloc);
try testing.expectError(error.DimensionsTooLarge, loading.complete(alloc));
}
@@ -584,7 +632,7 @@ test "image load: rgb, zlib compressed, direct" {
),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .all);
defer loading.deinit(alloc);
var img = try loading.complete(alloc);
defer img.deinit(alloc);
@@ -612,7 +660,7 @@ test "image load: rgb, not compressed, direct" {
),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .all);
defer loading.deinit(alloc);
var img = try loading.complete(alloc);
defer img.deinit(alloc);
@@ -641,7 +689,7 @@ test "image load: rgb, zlib compressed, direct, chunked" {
.data = try alloc.dupe(u8, data[0..1024]),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .all);
defer loading.deinit(alloc);
// Read our remaining chunks
@@ -677,7 +725,7 @@ test "image load: rgb, zlib compressed, direct, chunked with zero initial chunk"
} },
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .all);
defer loading.deinit(alloc);
// Read our remaining chunks
@@ -721,7 +769,7 @@ test "image load: temporary file without correct path" {
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
try testing.expectError(error.TemporaryFileNotNamedCorrectly, LoadingImage.init(alloc, &cmd));
try testing.expectError(error.TemporaryFileNotNamedCorrectly, LoadingImage.init(alloc, &cmd, .all));
// Temporary file should still be there
try tmp_dir.dir.access(path, .{});
@@ -754,7 +802,7 @@ test "image load: rgb, not compressed, temporary file" {
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .all);
defer loading.deinit(alloc);
var img = try loading.complete(alloc);
defer img.deinit(alloc);
@@ -791,7 +839,7 @@ test "image load: rgb, not compressed, regular file" {
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .all);
defer loading.deinit(alloc);
var img = try loading.complete(alloc);
defer img.deinit(alloc);
@@ -828,7 +876,7 @@ test "image load: png, not compressed, regular file" {
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd);
var loading = try LoadingImage.init(alloc, &cmd, .all);
defer loading.deinit(alloc);
var img = try loading.complete(alloc);
defer img.deinit(alloc);
@@ -836,3 +884,161 @@ test "image load: png, not compressed, regular file" {
try testing.expect(img.format == .rgba);
try tmp_dir.dir.access(path, .{});
}
test "limits: direct medium always allowed" {
const testing = std.testing;
const alloc = testing.allocator;
var cmd: command.Command = .{
.control = .{ .transmit = .{
.format = .rgb,
.medium = .direct,
.width = 1,
.height = 1,
.image_id = 31,
} },
.data = try alloc.dupe(u8, "AAAA"),
};
defer cmd.deinit(alloc);
// Direct medium should work even with the most restrictive limits
var loading = try LoadingImage.init(alloc, &cmd, .direct);
defer loading.deinit(alloc);
}
test "limits: file medium blocked by limits" {
const testing = std.testing;
const alloc = testing.allocator;
var tmp_dir = try temp_dir.TempDir.init();
defer tmp_dir.deinit();
const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
try tmp_dir.dir.writeFile(.{
.sub_path = "image.data",
.data = data,
});
var buf: [std.fs.max_path_bytes]u8 = undefined;
const path = try tmp_dir.dir.realpath("image.data", &buf);
var cmd: command.Command = .{
.control = .{ .transmit = .{
.format = .rgb,
.medium = .file,
.compression = .none,
.width = 20,
.height = 15,
.image_id = 31,
} },
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
try testing.expectError(error.UnsupportedMedium, LoadingImage.init(alloc, &cmd, .direct));
}
test "limits: file medium allowed by limits" {
const testing = std.testing;
const alloc = testing.allocator;
var tmp_dir = try temp_dir.TempDir.init();
defer tmp_dir.deinit();
const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
try tmp_dir.dir.writeFile(.{
.sub_path = "image.data",
.data = data,
});
var buf: [std.fs.max_path_bytes]u8 = undefined;
const path = try tmp_dir.dir.realpath("image.data", &buf);
var cmd: command.Command = .{
.control = .{ .transmit = .{
.format = .rgb,
.medium = .file,
.compression = .none,
.width = 20,
.height = 15,
.image_id = 31,
} },
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd, .{
.file = true,
.temporary_file = false,
.shared_memory = false,
});
defer loading.deinit(alloc);
}
test "limits: temporary file medium blocked by limits" {
const testing = std.testing;
const alloc = testing.allocator;
var tmp_dir = try temp_dir.TempDir.init();
defer tmp_dir.deinit();
const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
try tmp_dir.dir.writeFile(.{
.sub_path = "tty-graphics-protocol-image.data",
.data = data,
});
var buf: [std.fs.max_path_bytes]u8 = undefined;
const path = try tmp_dir.dir.realpath("tty-graphics-protocol-image.data", &buf);
var cmd: command.Command = .{
.control = .{ .transmit = .{
.format = .rgb,
.medium = .temporary_file,
.compression = .none,
.width = 20,
.height = 15,
.image_id = 31,
} },
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
try testing.expectError(error.UnsupportedMedium, LoadingImage.init(alloc, &cmd, .{
.file = true,
.temporary_file = false,
.shared_memory = true,
}));
// File should still exist since we blocked before reading
try tmp_dir.dir.access("tty-graphics-protocol-image.data", .{});
}
test "limits: temporary file medium allowed by limits" {
const testing = std.testing;
const alloc = testing.allocator;
var tmp_dir = try temp_dir.TempDir.init();
defer tmp_dir.deinit();
const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data");
try tmp_dir.dir.writeFile(.{
.sub_path = "tty-graphics-protocol-image.data",
.data = data,
});
var buf: [std.fs.max_path_bytes]u8 = undefined;
const path = try tmp_dir.dir.realpath("tty-graphics-protocol-image.data", &buf);
var cmd: command.Command = .{
.control = .{ .transmit = .{
.format = .rgb,
.medium = .temporary_file,
.compression = .none,
.width = 20,
.height = 15,
.image_id = 31,
} },
.data = try alloc.dupe(u8, path),
};
defer cmd.deinit(alloc);
var loading = try LoadingImage.init(alloc, &cmd, .{
.file = false,
.temporary_file = true,
.shared_memory = false,
});
defer loading.deinit(alloc);
}