diff --git a/graphics-protocol.asciidoc b/graphics-protocol.asciidoc index 96199be6e..f5b79c6bb 100644 --- a/graphics-protocol.asciidoc +++ b/graphics-protocol.asciidoc @@ -280,6 +280,9 @@ x=1, y=1 is the top left cell). | `y` or `Y` | Delete all images that intersect the specified row, specified using the `y` key. | `z` or `Z` | Delete all images that have the specified z-index, specified using the `z` key. +|=== + + Some examples: ``` @@ -288,7 +291,15 @@ Some examples: _Ga=Z,z=-1\ # delete the images with z-index -1, also freeing up image data _Ga=P,x=3,y=4\ # delete all images that intersect the cell at (3, 4) ``` -|=== + +=== Image persistence and storage quotas + +In order to avoid *Denial-of-Service* attacks, terminal emulators should have a +maximum storage quota for image data. It should allow at least a few full +screen images. For example the quota in kitty is 320MB per buffer. When adding +a new image, if the total size exceeds the quota, the terminal emulator should +delete older images to make space for the new one. + == Control data reference diff --git a/kitty/graphics.c b/kitty/graphics.c index f4f092a8e..d3d0e0f82 100644 --- a/kitty/graphics.c +++ b/kitty/graphics.c @@ -18,6 +18,8 @@ #include #include +#define STORAGE_LIMIT (320 * (1024 * 1024)) + #define REPORT_ERROR(...) { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } static bool send_to_gpu = true; @@ -59,17 +61,19 @@ free_texture_func free_texture = NULL; send_image_to_gpu_func send_image_to_gpu = NULL; static inline void -free_image(Image *img) { +free_image(GraphicsManager *self, Image *img) { if (img->texture_id) free_texture(&img->texture_id); free_refs_data(img); free_load_data(&(img->load_data)); + self->used_storage -= img->used_storage; } + static void dealloc(GraphicsManager* self) { size_t i; if (self->images) { - for (i = 0; i < self->image_count; i++) free_image(self->images + i); + for (i = 0; i < self->image_count; i++) free_image(self, self->images + i); free(self->images); } free(self->render_data); @@ -114,9 +118,51 @@ img_by_client_id(GraphicsManager *self, uint32_t id) { return NULL; } +static inline void +remove_image(GraphicsManager *self, size_t idx) { + free_image(self, self->images + idx); + remove_from_array(self->images, sizeof(Image), idx, self->image_count--); + self->layers_dirty = true; +} + +static inline void +remove_images(GraphicsManager *self, bool(*predicate)(Image*), Image* skip_image) { + for (size_t i = self->image_count; i-- > 0;) { + Image *img = self->images + i; + if (img != skip_image && predicate(img)) { + remove_image(self, i); + } + } +} + // Loading image data {{{ +static bool +trim_predicate(Image *img) { + return !img->data_loaded || !img->refcnt; +} + + +static int +oldest_last(const void* a, const void *b) { + double ans = ((Image*)(b))->atime - ((Image*)(a))->atime; + return ans < 0 ? -1 : (ans == 0 ? 0 : 1); +} + +static inline void +apply_storage_quota(GraphicsManager *self, size_t storage_limit, Image *currently_added_image) { + // First remove unreferenced images, even if they have an id + remove_images(self, trim_predicate, currently_added_image); + if (self->used_storage < storage_limit) return; + + qsort(self->images, self->image_count, sizeof(Image), oldest_last); + while (self->used_storage > storage_limit && self->image_count > 0) { + remove_image(self, self->image_count - 1); + } + if (!self->image_count) self->used_storage = 0; // sanity check +} + static char add_response[512] = {0}; static bool has_add_respose = false; @@ -315,17 +361,6 @@ find_or_create_image(GraphicsManager *self, uint32_t id, bool *existing) { return self->images + self->image_count++; } -static inline void -remove_images(GraphicsManager *self, bool(*predicate)(Image*)) { - for (size_t i = self->image_count; i-- > 0;) { - if (predicate(self->images + i)) { - free_image(self->images + i); - remove_from_array(self->images, sizeof(Image), i, self->image_count--); - self->layers_dirty = true; - } - } -} - static Image* handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_t *payload, bool *is_dirty, uint32_t iid) { @@ -343,7 +378,7 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_ self->last_init_graphics_command.id = iid; self->loading_image = 0; if (g->data_width > 10000 || g->data_height > 10000) ABRT(EINVAL, "Image too large"); - remove_images(self, add_trim_predicate); + remove_images(self, add_trim_predicate, NULL); img = find_or_create_image(self, iid, &existing); if (existing) { free_load_data(&img->load_data); @@ -355,6 +390,7 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_ img->internal_id = internal_id_counter++; img->client_id = iid; } + img->atime = monotonic(); img->used_storage = 0; img->width = g->data_width; img->height = g->data_height; switch(fmt) { case PNG: @@ -480,6 +516,8 @@ handle_add_command(GraphicsManager *self, const GraphicsCommand *g, const uint8_ if (LIKELY(img->data_loaded && send_to_gpu)) { send_image_to_gpu(&img->texture_id, img->load_data.data, img->width, img->height, img->load_data.is_opaque, img->load_data.is_4byte_aligned); free_load_data(&img->load_data); + self->used_storage += required_sz; + img->used_storage = required_sz; } return img; #undef MAX_DATA_SZ @@ -537,6 +575,7 @@ handle_put_command(GraphicsManager *self, const GraphicsCommand *g, Cursor *c, b } } if (ref == NULL) ref = img->refs + img->refcnt++; + img->atime = monotonic(); ref->src_x = g->x_offset; ref->src_y = g->y_offset; ref->src_width = g->width ? g->width : img->width; ref->src_height = g->height ? g->height : img->height; ref->src_width = MIN(ref->src_width, img->width - (img->width > ref->src_x ? ref->src_x : img->width)); ref->src_height = MIN(ref->src_height, img->height - (img->height > ref->src_y ? ref->src_y : img->height)); @@ -644,10 +683,7 @@ filter_refs(GraphicsManager *self, const void* data, bool free_images, bool (*fi remove_from_array(img->refs, sizeof(ImageRef), j, img->refcnt--); } } - if (img->refcnt == 0 && (free_images || img->client_id == 0)) { - free_image(img); - remove_from_array(self->images, sizeof(Image), i, self->image_count--); - } + if (img->refcnt == 0 && (free_images || img->client_id == 0)) remove_image(self, i); } self->layers_dirty = true; } @@ -798,7 +834,8 @@ grman_handle_command(GraphicsManager *self, const GraphicsCommand *g, const uint image = handle_add_command(self, g, payload, is_dirty, iid); ret = create_add_response(self, image != NULL, g->action == 'q' ? q_iid: self->last_init_graphics_command.id); if (self->last_init_graphics_command.action == 'T' && image && image->data_loaded) handle_put_command(self, &self->last_init_graphics_command, c, is_dirty, image); - if (g->action == 'q') remove_images(self, add_trim_predicate); + if (g->action == 'q') remove_images(self, add_trim_predicate, NULL); + if (self->used_storage > STORAGE_LIMIT) apply_storage_quota(self, STORAGE_LIMIT, image); break; case 'p': if (!g->id) { diff --git a/kitty/graphics.h b/kitty/graphics.h index a86a0efe8..2db8ff0b4 100644 --- a/kitty/graphics.h +++ b/kitty/graphics.h @@ -50,6 +50,8 @@ typedef struct { ImageRef *refs; size_t refcnt, refcap; + double atime; + size_t used_storage; } Image; typedef struct { @@ -71,6 +73,7 @@ typedef struct { bool layers_dirty; size_t num_of_negative_refs, num_of_positive_refs; unsigned int last_scrolled_by; + size_t used_storage; } GraphicsManager; PyTypeObject GraphicsManager_Type;