From 2d11b36a24d7bdd056fac48fb8d5e8f274a1dcf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 15 May 2026 08:29:07 +0200 Subject: [PATCH 01/17] changelog: add new 'unreleased' section --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index abc3ca70..5792c240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +* [Unreleased](#unreleased) * [1.27.0](#1-27-0) * [1.26.1](#1-26-1) * [1.26.0](#1-26-0) @@ -69,6 +70,16 @@ * [1.2.0](#1-2-0) +## Unreleased +### Added +### Changed +### Deprecated +### Removed +### Fixed +### Security +### Contributors + + ## 1.27.0 ### Added From f35e60577fde44a7360415448a380702d2ac0ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 17 May 2026 15:03:56 +0200 Subject: [PATCH 02/17] project: add .clangd, where we set -Wno-c2y-extensions We make use of __COUNTER__, which recent versions of clang emit a warning for (unless you compile with -std=c2y). Our meson.build already handles this for the actual build, but if your build is using gcc, -Wno-c2y-extensions isn't added to the build rules (since gcc doesn't support the flag). --- .clangd | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .clangd diff --git a/.clangd b/.clangd new file mode 100644 index 00000000..336c0f12 --- /dev/null +++ b/.clangd @@ -0,0 +1,5 @@ +# -*- yaml -*- + +CompileFlags: + Add: + - -Wno-c2y-extensions From 5335cec3228dab421b2abba4f178125d88ad2fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 22 May 2026 10:49:22 +0200 Subject: [PATCH 03/17] uri-parse: add a bunch of unit tests --- uri.c | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/uri.c b/uri.c index febe2e47..3f3beb53 100644 --- a/uri.c +++ b/uri.c @@ -269,3 +269,150 @@ hostname_is_localhost(const char *hostname) streq(hostname, "localhost") || streq(hostname, this_host))); } + +UNITTEST +{ + /* Valid URI, all components */ + const char uri[] = "http://john:wick@www.foobar.com:80/this/is/the%20path?query1=abc&query2=def#fragment"; + + char *scheme, *user, *password, *host, *path, *query, *fragment; + uint16_t port = 0; + + uri_parse( + uri, sizeof(uri) - 1, &scheme, &user, &password, &host, &port, &path, + &query, &fragment); + xassert(streq(scheme, "http")); free(scheme); + xassert(streq(user, "john")); free(user); + xassert(streq(password, "wick")); free(password); + xassert(streq(host, "www.foobar.com")); free(host); + xassert(port == 80); + xassert(streq(path, "/this/is/the path")); free(path); + xassert(streq(query, "query1=abc&query2=def")); free(query); + xassert(streq(fragment, "fragment")); free(fragment); +} + +UNITTEST +{ + /* Valid URI, all components. Using file scheme, so query+fragment is treated as part of the path */ + const char uri[] = "file://john:wick@www.foobar.com:80/this/is/the%20path?query1=abc&query2=def#fragment"; + + char *scheme, *user, *password, *host, *path, *query, *fragment; + uint16_t port = 0; + + uri_parse( + uri, sizeof(uri) - 1, &scheme, &user, &password, &host, &port, &path, + &query, &fragment); + xassert(streq(scheme, "file")); free(scheme); + xassert(streq(user, "john")); free(user); + xassert(streq(password, "wick")); free(password); + xassert(streq(host, "www.foobar.com")); free(host); + xassert(port == 80); + xassert(streq(path, "/this/is/the path?query1=abc&query2=def#fragment")); free(path); + xassert(query == NULL); + xassert(fragment == NULL); +} + +UNITTEST +{ + /* Test a valid URI containing all components, except password */ + const char uri[] = "http://john@www.foobar.com:80/this/is/the%20path?query1=abc&query2=def#fragment"; + + char *scheme, *user, *password, *host, *path, *query, *fragment; + uint16_t port = 0; + + uri_parse( + uri, sizeof(uri) - 1, &scheme, &user, &password, &host, &port, &path, + &query, &fragment); + xassert(streq(scheme, "http")); free(scheme); + xassert(streq(user, "john")); free(user); + xassert(password == NULL); + xassert(streq(host, "www.foobar.com")); free(host); + xassert(port == 80); + xassert(streq(path, "/this/is/the path")); free(path); + xassert(streq(query, "query1=abc&query2=def")); free(query); + xassert(streq(fragment, "fragment")); free(fragment); +} + +UNITTEST +{ + /* Valid URI, all components, except user+password */ + const char uri[] = "http://www.foobar.com:80/this/is/the%20path?query1=abc&query2=def#fragment"; + + char *scheme, *user, *password, *host, *path, *query, *fragment; + uint16_t port = 0; + + uri_parse( + uri, sizeof(uri) - 1, &scheme, &user, &password, &host, &port, &path, + &query, &fragment); + xassert(streq(scheme, "http")); free(scheme); + xassert(user == NULL); + xassert(password == NULL); + xassert(streq(host, "www.foobar.com")); free(host); + xassert(port == 80); + xassert(streq(path, "/this/is/the path")); free(path); + xassert(streq(query, "query1=abc&query2=def")); free(query); + xassert(streq(fragment, "fragment")); free(fragment); +} + +UNITTEST +{ + /* Valid URI, fragment, no query */ + const char uri[] = "http://www.foobar.com:80/this/is/the%20path#fragment"; + + char *scheme, *user, *password, *host, *path, *query, *fragment; + uint16_t port = 0; + + uri_parse( + uri, sizeof(uri) - 1, &scheme, &user, &password, &host, &port, &path, + &query, &fragment); + xassert(streq(scheme, "http")); free(scheme); + xassert(user == NULL); + xassert(password == NULL); + xassert(streq(host, "www.foobar.com")); free(host); + xassert(port == 80); + xassert(streq(path, "/this/is/the path")); free(path); + xassert(query == NULL); + xassert(streq(fragment, "fragment")); free(fragment); +} + +UNITTEST +{ + /* Valid URI, query, no fragment */ + const char uri[] = "http://www.foobar.com:80/this/is/the%20path?query1=abc&query2=def"; + + char *scheme, *user, *password, *host, *path, *query, *fragment; + uint16_t port = 0; + + uri_parse( + uri, sizeof(uri) - 1, &scheme, &user, &password, &host, &port, &path, + &query, &fragment); + xassert(streq(scheme, "http")); free(scheme); + xassert(user == NULL); + xassert(password == NULL); + xassert(streq(host, "www.foobar.com")); free(host); + xassert(port == 80); + xassert(streq(path, "/this/is/the path")); free(path); + xassert(streq(query, "query1=abc&query2=def")); free(query); + xassert(fragment == NULL); +} + +UNITTEST +{ + /* Malformed URI, fragment before query (treated as part of path instead) */ + const char uri[] = "http://www.foobar.com:80/this/is/the%20path#fragment?query1=abc&query2=def"; + + char *scheme, *user, *password, *host, *path, *query, *fragment; + uint16_t port = 0; + + uri_parse( + uri, sizeof(uri) - 1, &scheme, &user, &password, &host, &port, &path, + &query, &fragment); + xassert(streq(scheme, "http")); free(scheme); + xassert(user == NULL); + xassert(password == NULL); + xassert(streq(host, "www.foobar.com")); free(host); + xassert(port == 80); + xassert(streq(path, "/this/is/the path#fragment?query1=abc&query2=def")); free(path); + xassert(query == NULL); + xassert(fragment == NULL); +} From 2eaa7beba1f35b31019a992de35be8a54b9ac028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 22 May 2026 10:59:26 +0200 Subject: [PATCH 04/17] uri-parse: fix out-of-bounds read with malformed %-encoded content If the input URI ends with a trailing '%' (or a trailing '%N'), we read outside the provided buffer. On NULL terminated input, this happened to work out since we'd correctly detect an invalid %-sequence as soon as we read the NULL terminator. On input that is not NULL terminated, we're out of luck. This patch fixes this by also checking we have enough input left to even _try_ to read the %-digits. Also add unit tests for this particular case. Closes #2353 --- CHANGELOG.md | 7 +++++++ uri.c | 31 ++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5792c240..a4c22bf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,13 @@ ### Deprecated ### Removed ### Fixed + +* Out-of-bounds read when parsing URIs with malformed %-encoded + content ([#2353][2353]). + +[2353]: https://codeberg.org/dnkl/foot/issues/2353 + + ### Security ### Contributors diff --git a/uri.c b/uri.c index 3f3beb53..ddbfacdd 100644 --- a/uri.c +++ b/uri.c @@ -195,7 +195,9 @@ uri_parse(const char *uri, size_t len, encoded_len -= prefix_len; decoded_len += prefix_len; - if (hex2nibble(next[1]) <= 15 && hex2nibble(next[2]) <= 15) { + if (encoded_len >= 3 && + hex2nibble(next[1]) <= 15 && hex2nibble(next[2]) <= 15) + { *p++ = hex2nibble(next[1]) << 4 | hex2nibble(next[2]); decoded_len++; encoded_len -= 3; @@ -416,3 +418,30 @@ UNITTEST xassert(query == NULL); xassert(fragment == NULL); } + +UNITTEST +{ + /* Malformed URI, trailing '%' */ + const char uri[] = "file:///%ABNOT-PART-OF-INPUT"; + char *path; + uri_parse(uri, 9, NULL, NULL, NULL, NULL, NULL, &path, NULL, NULL); + xassert(streq(path, "/%")); free(path); +} + +UNITTEST +{ + /* Malformed URI, trailing '%2' */ + const char uri[] = "file:///%2ANOT-PART-OF-INPUT"; + char *path; + uri_parse(uri, 10, NULL, NULL, NULL, NULL, NULL, &path, NULL, NULL); + xassert(streq(path, "/%2")); free(path); +} + +UNITTEST +{ + /* Malformed URI, trailing '%ag' */ + const char uri[] = "file:///%ag"; + char *path; + uri_parse(uri, sizeof(uri) - 1, NULL, NULL, NULL, NULL, NULL, &path, NULL, NULL); + xassert(streq(path, "/%ag")); free(path); +} From b18d8aa2f165a3b07c28f43de6812e057ba3233e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 22 May 2026 11:39:04 +0200 Subject: [PATCH 05/17] csi: DECCRA: clamp and verify destination rectangle coordinates dst_right was already being clamped, but not dst_left. In addition to that, reject the sequence if dst_left > dst_right, or dst_bottom > dst_top. This is already being done for the source rectangle, in params_to_rectangular_area(). Closes #2352 --- CHANGELOG.md | 3 +++ csi.c | 17 ++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4c22bf6..93f80251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,8 +79,11 @@ * Out-of-bounds read when parsing URIs with malformed %-encoded content ([#2353][2353]). +* DECCRA not clamping or verifying the destination rectangle + ([#2352][2352]). [2353]: https://codeberg.org/dnkl/foot/issues/2353 +[2352]: https://codeberg.org/dnkl/foot/issues/2352 ### Security diff --git a/csi.c b/csi.c index 87af215e..176cc3ac 100644 --- a/csi.c +++ b/csi.c @@ -774,7 +774,7 @@ params_to_rectangular_area(const struct terminal *term, int first_idx, int rel_bottom = vt_param_get(term, first_idx + 2, term->rows) - 1; *right = min(vt_param_get(term, first_idx + 3, term->cols) - 1, term->cols - 1); - if (rel_top > rel_bottom || *left > *right) + if (unlikely(rel_top > rel_bottom || *left > *right)) return false; *top = term_row_rel_to_abs(term, rel_top); @@ -2005,9 +2005,8 @@ csi_dispatch(struct terminal *term, uint8_t final) } int src_page = vt_param_get(term, 4, 1); - int dst_rel_top = vt_param_get(term, 5, 1) - 1; - int dst_left = vt_param_get(term, 6, 1) - 1; + int dst_left = min(vt_param_get(term, 6, 1) - 1, term->cols - 1); int dst_page = vt_param_get(term, 7, 1); if (unlikely(src_page != 1 || dst_page != 1)) { @@ -2021,6 +2020,18 @@ csi_dispatch(struct terminal *term, uint8_t final) int dst_top = term_row_rel_to_abs(term, dst_rel_top); int dst_bottom = term_row_rel_to_abs(term, dst_rel_bottom); + if (unlikely(dst_left > dst_right || dst_bottom > dst_top)) + break; + + /* + * src validated by params_to_rectangular_area() + * dst validated above + */ + xassert(src_bottom - src_top >= 0); + xassert(dst_bottom - dst_top >= 0); + xassert(src_right - src_left >= 0); + xassert(dst_right - dst_left >= 0); + /* Target area outside the screen is clipped */ const size_t row_count = min(src_bottom - src_top, dst_bottom - dst_top) + 1; From 4bf60d0fbcad533c5131288617caae67342c1065 Mon Sep 17 00:00:00 2001 From: CismonX Date: Thu, 21 May 2026 02:38:16 +0800 Subject: [PATCH 06/17] selection: do not copy empty text Copy-on-select (configured with 'selection-target') may accidentally clear the clipboard, if the user drags the mouse a little bit when clicking inside a terminal window. Now we only copy if there is actual text being selected. Closes #2327 --- CHANGELOG.md | 2 ++ selection.c | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93f80251..464f096f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,9 +81,11 @@ content ([#2353][2353]). * DECCRA not clamping or verifying the destination rectangle ([#2352][2352]). +* Empty selection clearing the clipboard ([#2327][2327]). [2353]: https://codeberg.org/dnkl/foot/issues/2353 [2352]: https://codeberg.org/dnkl/foot/issues/2352 +[2327]: https://codeberg.org/dnkl/foot/issues/2327 ### Security diff --git a/selection.c b/selection.c index 0a479ee8..bb0d3f1b 100644 --- a/selection.c +++ b/selection.c @@ -1948,6 +1948,9 @@ static const struct zwp_primary_selection_source_v1_listener primary_selection_s bool text_to_clipboard(struct seat *seat, struct terminal *term, char *text, uint32_t serial) { + if (text == NULL || text[0] == '\0') + return false; + xassert(serial != 0); struct wl_clipboard *clipboard = &seat->clipboard; @@ -2418,6 +2421,9 @@ selection_from_clipboard(struct seat *seat, struct terminal *term, uint32_t seri bool text_to_primary(struct seat *seat, struct terminal *term, char *text, uint32_t serial) { + if (text == NULL || text[0] == '\0') + return false; + if (term->wl->primary_selection_device_manager == NULL) return false; From 8038adedf0e3280897ae8181597384b375f9fa32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 12 Jun 2026 13:50:41 +0200 Subject: [PATCH 07/17] meson: require xkbcommon >= 1.6.0 We use XKB_KEYSYM_MAX, which was added in 1.6.0. In other words, this requirement isn't really new, but now we've formalized it in meson.build. Closes #2379 --- CHANGELOG.md | 4 ++++ meson.build | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 464f096f..16bf715d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,10 +82,14 @@ * DECCRA not clamping or verifying the destination rectangle ([#2352][2352]). * Empty selection clearing the clipboard ([#2327][2327]). +* Require xkbcommon >= 1.6.0. This has been the case for a while, due + to our use of `XKB_KEYSYM_MAX`. Now it is formalized in + `meson.build` ([#2379][2379]). [2353]: https://codeberg.org/dnkl/foot/issues/2353 [2352]: https://codeberg.org/dnkl/foot/issues/2352 [2327]: https://codeberg.org/dnkl/foot/issues/2327 +[2379]: https://codeberg.org/dnkl/foot/issues/2379 ### Security diff --git a/meson.build b/meson.build index e4e83302..12f2a514 100644 --- a/meson.build +++ b/meson.build @@ -151,7 +151,7 @@ wayland_protocols = dependency('wayland-protocols', version: '>=1.41', default_options: ['tests=false']) wayland_client = dependency('wayland-client') wayland_cursor = dependency('wayland-cursor') -xkb = dependency('xkbcommon', version: '>=1.0.0') +xkb = dependency('xkbcommon', version: '>=1.6.0') fontconfig = dependency('fontconfig') utf8proc = dependency('libutf8proc', required: get_option('grapheme-clustering')) From f66a020bbae3df4b10030a263571defb5c5ff01d Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Thu, 4 Jun 2026 09:43:48 +1000 Subject: [PATCH 08/17] Fix #2377: DECCRA: swapped row-bounds check dropping multi-row copies `dst_bottom > dst_top` holds for every multi-row destination (rows increase downward), so the guard rejected all multi-row copies. Flip to `dst_top > dst_bottom`, mirroring the sibling column check and DEC STD 070. --- csi.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csi.c b/csi.c index 176cc3ac..1f417155 100644 --- a/csi.c +++ b/csi.c @@ -2020,7 +2020,7 @@ csi_dispatch(struct terminal *term, uint8_t final) int dst_top = term_row_rel_to_abs(term, dst_rel_top); int dst_bottom = term_row_rel_to_abs(term, dst_rel_bottom); - if (unlikely(dst_left > dst_right || dst_bottom > dst_top)) + if (unlikely(dst_left > dst_right || dst_top > dst_bottom)) break; /* From d7742d031276a19160dc9317ff6fb5aac861d853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 12 Jun 2026 15:34:12 +0200 Subject: [PATCH 09/17] term: do not allow codepoint merging into grapheme clusters directly after a cursor move That is, only do grapheme clustering when printing codepoints in sequence, without any cursor movements in between. Closes #2383 --- CHANGELOG.md | 7 +++++++ terminal.c | 13 ++++++++++++- terminal.h | 2 ++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16bf715d..803408af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,13 @@ ## Unreleased ### Added ### Changed + +* Do not allow codepoints to be merged into grapheme clusters directly + after a cursor move ([#2383][2383]). + +[2383]: https://codeberg.org/dnkl/foot/issues/2383 + + ### Deprecated ### Removed ### Fixed diff --git a/terminal.c b/terminal.c index 8eafbcbe..374d4371 100644 --- a/terminal.c +++ b/terminal.c @@ -4086,6 +4086,10 @@ term_print(struct terminal *term, char32_t wc, int width, bool insert_mode_disab xassert(!grid->cursor.lcf); grid->cursor.point.col = col; + +#if defined(FOOT_GRAPHEME_CLUSTERING) + term->vt.codepoint_merging_ok = true; +#endif } static void @@ -4126,6 +4130,9 @@ ascii_printer_fast(struct terminal *term, char32_t wc) xassert(!grid->cursor.lcf); grid->cursor.point.col = col; +#if defined(FOOT_GRAPHEME_CLUSTERING) + term->vt.codepoint_merging_ok = true; +#endif if (unlikely(row->extra != NULL)) { grid_row_uri_range_erase(row, uri_start, uri_start); @@ -4210,7 +4217,11 @@ term_process_and_print_non_ascii(struct terminal *term, char32_t wc) { int width = c32width(wc); bool insert_mode_disable = false; - const bool grapheme_clustering = term->grapheme_shaping; + const bool grapheme_clustering = term->grapheme_shaping +#if defined(FOOT_GRAPHEME_CLUSTERING) + && term->vt.codepoint_merging_ok +#endif + ; #if !defined(FOOT_GRAPHEME_CLUSTERING) xassert(!grapheme_clustering); diff --git a/terminal.h b/terminal.h index 446d5f23..7e95697e 100644 --- a/terminal.h +++ b/terminal.h @@ -264,6 +264,7 @@ struct vt { char32_t last_printed; #if defined(FOOT_GRAPHEME_CLUSTERING) utf8proc_int32_t grapheme_state; + bool codepoint_merging_ok; #endif char32_t utf8; struct { @@ -994,5 +995,6 @@ static inline void term_reset_grapheme_state(struct terminal *term) { #if defined(FOOT_GRAPHEME_CLUSTERING) term->vt.grapheme_state = 0; + term->vt.codepoint_merging_ok = false; #endif } From 382e9a31c5c631258da8f6ebb17f1b9aecb92e6b Mon Sep 17 00:00:00 2001 From: CismonX Date: Sun, 24 May 2026 06:47:49 +0800 Subject: [PATCH 10/17] selection: fix block selection direction update This patch fixes a condition check in selection_update(), so that during block selection, the selection area no longer gets incorrectly updated when selecting back across the starting column. --- CHANGELOG.md | 2 ++ selection.c | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 803408af..04dfecc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,8 @@ * Require xkbcommon >= 1.6.0. This has been the case for a while, due to our use of `XKB_KEYSYM_MAX`. Now it is formalized in `meson.build` ([#2379][2379]). +* Block selection area incorrectly updated when selecting back + across the starting column. [2353]: https://codeberg.org/dnkl/foot/issues/2353 [2352]: https://codeberg.org/dnkl/foot/issues/2352 diff --git a/selection.c b/selection.c index bb0d3f1b..4a62369d 100644 --- a/selection.c +++ b/selection.c @@ -1145,7 +1145,7 @@ selection_update(struct terminal *term, int col, int row) struct coord *pivot_end = &term->selection.pivot.end; if (term->selection.kind == SELECTION_BLOCK) { - if (new_end.col > pivot_start->col) + if (new_end.col >= pivot_start->col) new_direction = SELECTION_RIGHT; else new_direction = SELECTION_LEFT; From 66ec9fad881ac3ddafc3faf2b598345473c6fd7c Mon Sep 17 00:00:00 2001 From: CismonX Date: Mon, 25 May 2026 06:31:04 +0800 Subject: [PATCH 11/17] csi: refactor CHT/CBT Eliminate the outer loop, so that when moving the cursor multiple tab stops, we no longer iterate the tab stop list all over again. This also fixes a DoS flaw when passing a very large value as CHT/CBT argument, which may hang the terminal. Closes #2360 --- CHANGELOG.md | 3 +++ csi.c | 39 +++++++++++++++++++++------------------ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04dfecc8..f5ff4c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,11 +94,14 @@ `meson.build` ([#2379][2379]). * Block selection area incorrectly updated when selecting back across the starting column. +* Passing a very large value as CHT/CBT argument hangs the terminal + ([#2360][2360]). [2353]: https://codeberg.org/dnkl/foot/issues/2353 [2352]: https://codeberg.org/dnkl/foot/issues/2352 [2327]: https://codeberg.org/dnkl/foot/issues/2327 [2379]: https://codeberg.org/dnkl/foot/issues/2379 +[2360]: https://codeberg.org/dnkl/foot/issues/2360 ### Security diff --git a/csi.c b/csi.c index 1f417155..a82c0e2c 100644 --- a/csi.c +++ b/csi.c @@ -1171,37 +1171,40 @@ csi_dispatch(struct terminal *term, uint8_t final) case 'I': { /* CHT - Tab Forward (param is number of tab stops to move through) */ - for (int i = 0; i < vt_param_get(term, 0, 1); i++) { - int new_col = term->cols - 1; - tll_foreach(term->tab_stops, it) { - if (it->item > term->grid->cursor.point.col) { - new_col = it->item; + int count = vt_param_get(term, 0, 1); + int new_col = term->grid->cursor.point.col; + tll_foreach(term->tab_stops, it) { + if (it->item > new_col) { + if (--count < 0) { break; } + new_col = it->item; } - xassert(new_col >= term->grid->cursor.point.col); - - bool lcf = term->grid->cursor.lcf; - term_cursor_right(term, new_col - term->grid->cursor.point.col); - term->grid->cursor.lcf = lcf; } + xassert(new_col >= term->grid->cursor.point.col); + + bool lcf = term->grid->cursor.lcf; + term_cursor_right(term, new_col - term->grid->cursor.point.col); + term->grid->cursor.lcf = lcf; break; } - case 'Z': + case 'Z': { /* CBT - Back tab (param is number of tab stops to move back through) */ - for (int i = 0; i < vt_param_get(term, 0, 1); i++) { - int new_col = 0; - tll_rforeach(term->tab_stops, it) { - if (it->item < term->grid->cursor.point.col) { - new_col = it->item; + int count = vt_param_get(term, 0, 1); + int new_col = term->grid->cursor.point.col; + tll_rforeach(term->tab_stops, it) { + if (it->item < new_col) { + if (--count < 0) { break; } + new_col = it->item; } - xassert(term->grid->cursor.point.col >= new_col); - term_cursor_left(term, term->grid->cursor.point.col - new_col); } + xassert(term->grid->cursor.point.col >= new_col); + term_cursor_left(term, term->grid->cursor.point.col - new_col); break; + } case 'h': case 'l': { From 7aa880654a97a4c73b9711f72071f7da05266a53 Mon Sep 17 00:00:00 2001 From: fhqh Date: Sun, 24 May 2026 12:37:44 +0200 Subject: [PATCH 12/17] Import official Dracula (dark) / Alucard (light) theme Some colors in dark theme have slightly different I assume more correct values then previously --- CHANGELOG.md | 2 ++ themes/dracula | 56 ++++++++++++++++++++++++++++++++++---------------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5ff4c44..ef9c986a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,8 @@ * Do not allow codepoints to be merged into grapheme clusters directly after a cursor move ([#2383][2383]). +* Dracula theme updated to latest official, and light theme alucard + added. [2383]: https://codeberg.org/dnkl/foot/issues/2383 diff --git a/themes/dracula b/themes/dracula index 82994203..93dd5f06 100644 --- a/themes/dracula +++ b/themes/dracula @@ -1,23 +1,43 @@ # -*- conf -*- -# Dracula +# Dracula / Alucard +# Source: https://github.com/dracula/foot [colors-dark] -cursor=282a36 f8f8f2 foreground=f8f8f2 background=282a36 -regular0=000000 # black -regular1=ff5555 # red -regular2=50fa7b # green -regular3=f1fa8c # yellow -regular4=bd93f9 # blue -regular5=ff79c6 # magenta -regular6=8be9fd # cyan -regular7=bfbfbf # white -bright0=4d4d4d # bright black -bright1=ff6e67 # bright red -bright2=5af78e # bright green -bright3=f4f99d # bright yellow -bright4=caa9fa # bright blue -bright5=ff92d0 # bright magenta -bright6=9aedfe # bright cyan -bright7=e6e6e6 # bright white \ No newline at end of file +regular0=21222c # black +regular1=ff5555 # red +regular2=50fa7b # green +regular3=f1fa8c # yellow +regular4=bd93f9 # blue +regular5=ff79c6 # magenta +regular6=8be9fd # cyan +regular7=f8f8f2 # white +bright0=6272a4 # bright black +bright1=ff6e6e # bright red +bright2=69ff94 # bright green +bright3=ffffa5 # bright yellow +bright4=d6acff # bright blue +bright5=ff92df # bright magenta +bright6=a4ffff # bright cyan +bright7=ffffff # bright white + +[colors-light] +foreground=1f1f1f +background=fffbeb +regular0=fffbeb # white +regular1=cb3a2a # red +regular2=14710a # green +regular3=846e15 # yellow +regular4=644ac9 # blue +regular5=a3144d # magenta +regular6=036a96 # cyan +regular7=1f1f1f # white +bright0=6c664b # bright black +bright1=d74c3d # bright red +bright2=198d0c # bright green +bright3=9e841a # bright yellow +bright4=7862d0 # bright blue +bright5=bf185a # bright magenta +bright6=047fb4 # bright cyan +bright7=2c2b31 # bright white From 35f30e64518e9a14fb9db273a676e614f63d5d10 Mon Sep 17 00:00:00 2001 From: lumerue Date: Wed, 3 Jun 2026 20:58:12 +0300 Subject: [PATCH 13/17] add ayu-dark --- themes/ayu-dark | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 themes/ayu-dark diff --git a/themes/ayu-dark b/themes/ayu-dark new file mode 100644 index 00000000..89d2896c --- /dev/null +++ b/themes/ayu-dark @@ -0,0 +1,26 @@ +# -*- conf -*- +# theme: Ayu Dark +# upstream: https://github.com/ayu-theme/ayu-colors/blob/master/themes/dark.yaml + +[colors-dark] +background=0d1017 +foreground=bfbdb6 +cursor=0d1017 ffb454 + +regular0=0a0000 # black +regular1=d75b63 # red +regular2=94c230 # green +regular3=e79e3a # yellow +regular4=40abe7 # blue +regular5=bc90e7 # magenta +regular6=7ecfb5 # cyan +regular7=ffffff # white + +bright0=0a0000 # bright black +bright1=f07178 # bright red +bright2=aad94c # bright green +bright3=ffb454 # bright yellow +bright4=59c2ff # bright blue +bright5=d2a6ff # bright magenta +bright6=95e6cb # bright cyan +bright7=ffffff # bright white From c9c448e61157cf2798f7c571e0e057273d7c3b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 12 Jun 2026 18:00:20 +0200 Subject: [PATCH 14/17] sixel: clamp pan/pad to 5 Pad is always (in real life) 1, and no-one should be setting pan to anything larger than 5 (the largest value possible to set with the sixel init parameters). FWIW, no-one really uses anything but 1 here either. Clamping ensures we don't blow up allocations to something huge, or worse, trigger an overflow that results in a pixel height of 0, causing a division-by-zero. Closes #2371 --- CHANGELOG.md | 2 ++ sixel.c | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef9c986a..d92676d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,8 +78,10 @@ after a cursor move ([#2383][2383]). * Dracula theme updated to latest official, and light theme alucard added. +* Sixels: pan/pad clamped to 5 ([#2371][2371]). [2383]: https://codeberg.org/dnkl/foot/issues/2383 +[2371]: https://codeberg.org/dnkl/foot/issues/2371 ### Deprecated diff --git a/sixel.c b/sixel.c index 187f1348..0c1ccd1b 100644 --- a/sixel.c +++ b/sixel.c @@ -1888,8 +1888,8 @@ decgra(struct terminal *term, uint8_t c) unsigned ph = nparams > 2 ? term->sixel.params[2] : 0; unsigned pv = nparams > 3 ? term->sixel.params[3] : 0; - pan = pan > 0 ? pan : 1; - pad = pad > 0 ? pad : 1; + pan = pan > 0 ? min(pan, 5) : 1; + pad = pad > 0 ? min(pad, 5) : 1; if (likely(term->sixel.image.width == 0 && term->sixel.image.height == 0)) From 75e201608ba7aeac87af26906aaaaba55c63b92e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 12 Jun 2026 18:09:02 +0200 Subject: [PATCH 15/17] sixel: fix NULL deref when using a shared palette and gamma-correct blending This fixes a copy-paste error where we read/modified/wrote the private-palette instead of the shared palette. Since the private palette is NULL in this case, that meant a crash. Closes #2370 --- CHANGELOG.md | 3 +++ sixel.c | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d92676d2..bcf8b5ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,12 +100,15 @@ across the starting column. * Passing a very large value as CHT/CBT argument hangs the terminal ([#2360][2360]). +* Sixel: crash when using a shared palette and gamma-correct blending + has been enabled, or foot is using 10-bit surface ([#2370][2370]). [2353]: https://codeberg.org/dnkl/foot/issues/2353 [2352]: https://codeberg.org/dnkl/foot/issues/2352 [2327]: https://codeberg.org/dnkl/foot/issues/2327 [2379]: https://codeberg.org/dnkl/foot/issues/2379 [2360]: https://codeberg.org/dnkl/foot/issues/2360 +[2370]: https://codeberg.org/dnkl/foot/issues/2370 ### Security diff --git a/sixel.c b/sixel.c index 0c1ccd1b..2bf47b7e 100644 --- a/sixel.c +++ b/sixel.c @@ -169,10 +169,10 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) if (term->sixel.linear_blending || term->sixel.use_10bit) { for (size_t i = 0; i < active_palette_entries; i++) { - uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff; - uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff; - uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff; - term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b); + uint8_t r = (term->sixel.shared_palette[i] >> 16) & 0xff; + uint8_t g = (term->sixel.shared_palette[i] >> 8) & 0xff; + uint8_t b = (term->sixel.shared_palette[i] >> 0) & 0xff; + term->sixel.shared_palette[i] = color_decode_srgb(term, r, g, b); } } } else { From 8b047852f3fcb26f4bc4c1dece0089e90cb645ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 12 Jun 2026 18:21:51 +0200 Subject: [PATCH 16/17] osc: kitty text-size: bail out if text is zero-length This fixes a crash further down, where we try to lookup an existing compose sequence. Closes #2364 --- CHANGELOG.md | 3 +++ osc.c | 3 +++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcf8b5ca..cde48392 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,8 @@ ([#2360][2360]). * Sixel: crash when using a shared palette and gamma-correct blending has been enabled, or foot is using 10-bit surface ([#2370][2370]). +* Kitty text-size protocol: fix crash when text is zero-length + ([#2364][2364]). [2353]: https://codeberg.org/dnkl/foot/issues/2353 [2352]: https://codeberg.org/dnkl/foot/issues/2352 @@ -109,6 +111,7 @@ [2379]: https://codeberg.org/dnkl/foot/issues/2379 [2360]: https://codeberg.org/dnkl/foot/issues/2360 [2370]: https://codeberg.org/dnkl/foot/issues/2370 +[2364]: https://codeberg.org/dnkl/foot/issues/2364 ### Security diff --git a/osc.c b/osc.c index 95af5bff..05281d0e 100644 --- a/osc.c +++ b/osc.c @@ -1149,6 +1149,9 @@ kitty_text_size(struct terminal *term, char *string) *text = '\0'; text++; + if (text[0] == '\0') + return; + char32_t *wchars = ambstoc32(text); if (wchars == NULL) return; From ca73ec71d5747d2c0c68b8b562cee3bb80b773ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 12 Jun 2026 18:49:48 +0200 Subject: [PATCH 17/17] selection: escape quotes in file names being DnD:ed on the command line Closes #2363 --- CHANGELOG.md | 2 ++ selection.c | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cde48392..f1820317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,6 +104,8 @@ has been enabled, or foot is using 10-bit surface ([#2370][2370]). * Kitty text-size protocol: fix crash when text is zero-length ([#2364][2364]). +* Escape quotes in file names being DnD:ed on the command line + ([#2363][2363]). [2353]: https://codeberg.org/dnkl/foot/issues/2353 [2352]: https://codeberg.org/dnkl/foot/issues/2352 diff --git a/selection.c b/selection.c index 4a62369d..dad81994 100644 --- a/selection.c +++ b/selection.c @@ -2098,7 +2098,16 @@ decode_one_uri(struct clipboard_receive *ctx, char *uri, size_t len) if (ctx->quote_paths) ctx->cb("'", 1, ctx->user); - ctx->cb(path, strlen(path), ctx->user); + char *path_remaining = path; + for (char *next_quote = strchr(path_remaining, '\''); + next_quote != NULL; + path_remaining = next_quote + 1, + next_quote = strchr(path_remaining, '\'')) + { + ctx->cb(path_remaining, next_quote - path_remaining, ctx->user); + ctx->cb("\\'", 2, ctx->user); + } + ctx->cb(path_remaining, strlen(path_remaining), ctx->user); if (ctx->quote_paths) ctx->cb("'", 1, ctx->user);