Merge branch 'js/neuter-sideband' into seen

Invalidate control characters in sideband messages, to avoid
terminal state getting messed up.

Comments?

* js/neuter-sideband:
  sideband: delay sanitizing by default to Git v3.0
  sideband: offer to configure sanitizing on a per-URL basis
  sideband: add options to allow more control sequences to be passed through
  sideband: do allow ANSI color sequences by default
  sideband: introduce an "escape hatch" to allow control characters
  sideband: mask control characters
This commit is contained in:
Junio C Hamano
2026-02-23 16:25:40 -08:00
6 changed files with 341 additions and 2 deletions

View File

@@ -523,6 +523,8 @@ include::config/sequencer.adoc[]
include::config/showbranch.adoc[]
include::config/sideband.adoc[]
include::config/sparse.adoc[]
include::config/splitindex.adoc[]

View File

@@ -0,0 +1,39 @@
sideband.allowControlCharacters::
ifdef::with-breaking-changes[]
By default, control characters that are delivered via the sideband
are masked, except ANSI color sequences. This prevents potentially
endif::with-breaking-changes[]
ifndef::with-breaking-changes[]
By default, no control characters delivered via the sideband
are masked. This is unsafe and will change in Git v3.* to only
allow ANSI color sequences by default, preventing potentially
endif::with-breaking-changes[]
unwanted ANSI escape sequences from being sent to the terminal. Use
this config setting to override this behavior (the value can be
a comma-separated list of the following keywords):
+
--
`default`::
ifndef::with-breaking-changes[]
Allow any control sequence. This default is unsafe and will
change to `color` in Git v3.*.
endif::with-breaking-changes[]
`color`::
Allow ANSI color sequences, line feeds and horizontal tabs,
but mask all other control characters. This is the default.
`cursor:`:
Allow control sequences that move the cursor. This is
disabled by default.
`erase`::
Allow control sequences that erase charactrs. This is
disabled by default.
`false`::
Mask all control characters other than line feeds and
horizontal tabs.
`true`::
Allow all control characters to be sent to the terminal.
--
sideband.<url>.*::
Apply the `sideband.*` option selectively to specific URLs. The
same URL matching logic applies as for `http.<url>.*` settings.

View File

@@ -10,6 +10,7 @@
#include "help.h"
#include "pkt-line.h"
#include "write-or-die.h"
#include "urlmatch.h"
struct keyword_entry {
/*
@@ -26,6 +27,91 @@ static struct keyword_entry keywords[] = {
{ "error", GIT_COLOR_BOLD_RED },
};
static enum {
ALLOW_CONTROL_SEQUENCES_UNSET = -1,
ALLOW_NO_CONTROL_CHARACTERS = 0,
ALLOW_ANSI_COLOR_SEQUENCES = 1<<0,
ALLOW_ANSI_CURSOR_MOVEMENTS = 1<<1,
ALLOW_ANSI_ERASE = 1<<2,
ALLOW_ALL_CONTROL_CHARACTERS = 1<<3,
#ifdef WITH_BREAKING_CHANGES
ALLOW_DEFAULT_ANSI_SEQUENCES = ALLOW_ANSI_COLOR_SEQUENCES,
#else
ALLOW_DEFAULT_ANSI_SEQUENCES = ALLOW_ALL_CONTROL_CHARACTERS,
#endif
} allow_control_characters = ALLOW_CONTROL_SEQUENCES_UNSET;
static inline int skip_prefix_in_csv(const char *value, const char *prefix,
const char **out)
{
if (!skip_prefix(value, prefix, &value) ||
(*value && *value != ','))
return 0;
*out = value + !!*value;
return 1;
}
int sideband_allow_control_characters_config(const char *var, const char *value)
{
switch (git_parse_maybe_bool(value)) {
case 0:
allow_control_characters = ALLOW_NO_CONTROL_CHARACTERS;
return 0;
case 1:
allow_control_characters = ALLOW_ALL_CONTROL_CHARACTERS;
return 0;
default:
break;
}
allow_control_characters = ALLOW_NO_CONTROL_CHARACTERS;
while (*value) {
if (skip_prefix_in_csv(value, "default", &value))
allow_control_characters |= ALLOW_DEFAULT_ANSI_SEQUENCES;
else if (skip_prefix_in_csv(value, "color", &value))
allow_control_characters |= ALLOW_ANSI_COLOR_SEQUENCES;
else if (skip_prefix_in_csv(value, "cursor", &value))
allow_control_characters |= ALLOW_ANSI_CURSOR_MOVEMENTS;
else if (skip_prefix_in_csv(value, "erase", &value))
allow_control_characters |= ALLOW_ANSI_ERASE;
else if (skip_prefix_in_csv(value, "true", &value))
allow_control_characters = ALLOW_ALL_CONTROL_CHARACTERS;
else if (skip_prefix_in_csv(value, "false", &value))
allow_control_characters = ALLOW_NO_CONTROL_CHARACTERS;
else
warning(_("unrecognized value for '%s': '%s'"), var, value);
}
return 0;
}
static int sideband_config_callback(const char *var, const char *value,
const struct config_context *ctx UNUSED,
void *data UNUSED)
{
if (!strcmp(var, "sideband.allowcontrolcharacters"))
return sideband_allow_control_characters_config(var, value);
return 0;
}
void sideband_apply_url_config(const char *url)
{
struct urlmatch_config config = URLMATCH_CONFIG_INIT;
char *normalized_url;
if (!url)
BUG("must not call sideband_apply_url_config(NULL)");
config.section = "sideband";
config.collect_fn = sideband_config_callback;
normalized_url = url_normalize(url, &config.url);
repo_config(the_repository, urlmatch_config_entry, &config);
free(normalized_url);
string_list_clear(&config.vars, 1);
urlmatch_config_release(&config);
}
/* Returns a color setting (GIT_COLOR_NEVER, etc). */
static enum git_colorbool use_sideband_colors(void)
{
@@ -39,6 +125,14 @@ static enum git_colorbool use_sideband_colors(void)
if (use_sideband_colors_cached != GIT_COLOR_UNKNOWN)
return use_sideband_colors_cached;
if (allow_control_characters == ALLOW_CONTROL_SEQUENCES_UNSET) {
if (!repo_config_get_value(the_repository, "sideband.allowcontrolcharacters", &value))
sideband_allow_control_characters_config("sideband.allowcontrolcharacters", value);
if (allow_control_characters == ALLOW_CONTROL_SEQUENCES_UNSET)
allow_control_characters = ALLOW_DEFAULT_ANSI_SEQUENCES;
}
if (!repo_config_get_string_tmp(the_repository, key, &value))
use_sideband_colors_cached = git_config_colorbool(key, value);
else if (!repo_config_get_string_tmp(the_repository, "color.ui", &value))
@@ -66,6 +160,93 @@ void list_config_color_sideband_slots(struct string_list *list, const char *pref
list_config_item(list, prefix, keywords[i].keyword);
}
static int handle_ansi_sequence(struct strbuf *dest, const char *src, int n)
{
int i;
/*
* Valid ANSI color sequences are of the form
*
* ESC [ [<n> [; <n>]*] m
*
* These are part of the Select Graphic Rendition sequences which
* contain more than just color sequences, for more details see
* https://en.wikipedia.org/wiki/ANSI_escape_code#SGR.
*
* The cursor movement sequences are:
*
* ESC [ n A - Cursor up n lines (CUU)
* ESC [ n B - Cursor down n lines (CUD)
* ESC [ n C - Cursor forward n columns (CUF)
* ESC [ n D - Cursor back n columns (CUB)
* ESC [ n E - Cursor next line, beginning (CNL)
* ESC [ n F - Cursor previous line, beginning (CPL)
* ESC [ n G - Cursor to column n (CHA)
* ESC [ n ; m H - Cursor position (row n, col m) (CUP)
* ESC [ n ; m f - Same as H (HVP)
*
* The sequences to erase characters are:
*
*
* ESC [ 0 J - Clear from cursor to end of screen (ED)
* ESC [ 1 J - Clear from cursor to beginning of screen (ED)
* ESC [ 2 J - Clear entire screen (ED)
* ESC [ 3 J - Clear entire screen + scrollback (ED) - xterm extension
* ESC [ 0 K - Clear from cursor to end of line (EL)
* ESC [ 1 K - Clear from cursor to beginning of line (EL)
* ESC [ 2 K - Clear entire line (EL)
* ESC [ n M - Delete n lines (DL)
* ESC [ n P - Delete n characters (DCH)
* ESC [ n X - Erase n characters (ECH)
*
* For a comprehensive list of common ANSI Escape sequences, see
* https://www.xfree86.org/current/ctlseqs.html
*/
if (n < 3 || src[0] != '\x1b' || src[1] != '[')
return 0;
for (i = 2; i < n; i++) {
if (((allow_control_characters & ALLOW_ANSI_COLOR_SEQUENCES) &&
src[i] == 'm') ||
((allow_control_characters & ALLOW_ANSI_CURSOR_MOVEMENTS) &&
strchr("ABCDEFGHf", src[i])) ||
((allow_control_characters & ALLOW_ANSI_ERASE) &&
strchr("JKMPX", src[i]))) {
strbuf_add(dest, src, i + 1);
return i;
}
if (!isdigit(src[i]) && src[i] != ';')
break;
}
return 0;
}
static void strbuf_add_sanitized(struct strbuf *dest, const char *src, int n)
{
int i;
if ((allow_control_characters & ALLOW_ALL_CONTROL_CHARACTERS)) {
strbuf_add(dest, src, n);
return;
}
strbuf_grow(dest, n);
for (; n && *src; src++, n--) {
if (!iscntrl(*src) || *src == '\t' || *src == '\n') {
strbuf_addch(dest, *src);
} else if (allow_control_characters != ALLOW_NO_CONTROL_CHARACTERS &&
(i = handle_ansi_sequence(dest, src, n))) {
src += i;
n -= i;
} else {
strbuf_addch(dest, '^');
strbuf_addch(dest, *src == 0x7f ? '?' : 0x40 + *src);
}
}
}
/*
* Optionally highlight one keyword in remote output if it appears at the start
* of the line. This should be called for a single line only, which is
@@ -81,7 +262,7 @@ static void maybe_colorize_sideband(struct strbuf *dest, const char *src, int n)
int i;
if (!want_color_stderr(use_sideband_colors())) {
strbuf_add(dest, src, n);
strbuf_add_sanitized(dest, src, n);
return;
}
@@ -114,7 +295,7 @@ static void maybe_colorize_sideband(struct strbuf *dest, const char *src, int n)
}
}
strbuf_add(dest, src, n);
strbuf_add_sanitized(dest, src, n);
}

View File

@@ -30,4 +30,18 @@ int demultiplex_sideband(const char *me, int status,
void send_sideband(int fd, int band, const char *data, ssize_t sz, int packet_max);
/*
* Apply sideband configuration for the given URL. This should be called
* when a transport is created to allow URL-specific configuration of
* sideband behavior (e.g., sideband.<url>.allowControlCharacters).
*/
void sideband_apply_url_config(const char *url);
/*
* Parse and set the sideband allow control characters configuration.
* The var parameter should be the key name (without section prefix).
* Returns 0 if the variable was recognized and handled, non-zero otherwise.
*/
int sideband_allow_control_characters_config(const char *var, const char *value);
#endif

View File

@@ -98,4 +98,104 @@ test_expect_success 'fallback to color.ui' '
grep "<BOLD;RED>error<RESET>: error" decoded
'
if test_have_prereq WITH_BREAKING_CHANGES
then
TURN_ON_SANITIZING=already.turned=on
else
TURN_ON_SANITIZING=sideband.allowControlCharacters=color
fi
test_expect_success 'disallow (color) control sequences in sideband' '
write_script .git/color-me-surprised <<-\EOF &&
printf "error: Have you \\033[31mread\\033[m this?\\a\\n" >&2
exec "$@"
EOF
test_config_global uploadPack.packObjectsHook ./color-me-surprised &&
test_commit need-at-least-one-commit &&
git -c $TURN_ON_SANITIZING clone --no-local . throw-away 2>stderr &&
test_decode_color <stderr >decoded &&
test_grep RED decoded &&
test_grep "\\^G" stderr &&
tr -dc "\\007" <stderr >actual &&
test_must_be_empty actual &&
rm -rf throw-away &&
git -c sideband.allowControlCharacters=false \
clone --no-local . throw-away 2>stderr &&
test_decode_color <stderr >decoded &&
test_grep ! RED decoded &&
test_grep "\\^G" stderr &&
rm -rf throw-away &&
git -c sideband.allowControlCharacters clone --no-local . throw-away 2>stderr &&
test_decode_color <stderr >decoded &&
test_grep RED decoded &&
tr -dc "\\007" <stderr >actual &&
test_file_not_empty actual
'
test_decode_csi() {
awk '{
while (match($0, /\033/) != 0) {
printf "%sCSI ", substr($0, 1, RSTART-1);
$0 = substr($0, RSTART + RLENGTH, length($0) - RSTART - RLENGTH + 1);
}
print
}'
}
test_expect_success 'control sequences in sideband allowed by default (in Git v3.8)' '
write_script .git/color-me-surprised <<-\EOF &&
printf "error: \\033[31mcolor\\033[m\\033[Goverwrite\\033[Gerase\\033[K\\033?25l\\n" >&2
exec "$@"
EOF
test_config_global uploadPack.packObjectsHook ./color-me-surprised &&
test_commit need-at-least-one-commit-at-least &&
rm -rf throw-away &&
git -c $TURN_ON_SANITIZING clone --no-local . throw-away 2>stderr &&
test_decode_color <stderr >color-decoded &&
test_decode_csi <color-decoded >decoded &&
test_grep ! "CSI \\[K" decoded &&
test_grep ! "CSI \\[G" decoded &&
test_grep "\\^\\[?25l" decoded &&
rm -rf throw-away &&
git -c sideband.allowControlCharacters=erase,cursor,color \
clone --no-local . throw-away 2>stderr &&
test_decode_color <stderr >color-decoded &&
test_decode_csi <color-decoded >decoded &&
test_grep "RED" decoded &&
test_grep "CSI \\[K" decoded &&
test_grep "CSI \\[G" decoded &&
test_grep ! "\\^\\[\\[K" decoded &&
test_grep ! "\\^\\[\\[G" decoded
'
test_expect_success 'allow all control sequences for a specific URL' '
write_script .git/eraser <<-\EOF &&
printf "error: Ohai!\\r\\033[K" >&2
exec "$@"
EOF
test_config_global uploadPack.packObjectsHook ./eraser &&
test_commit one-more-please &&
rm -rf throw-away &&
git -c $TURN_ON_SANITIZING clone --no-local . throw-away 2>stderr &&
test_decode_color <stderr >color-decoded &&
test_decode_csi <color-decoded >decoded &&
test_grep ! "CSI \\[K" decoded &&
test_grep "\\^\\[\\[K" decoded &&
rm -rf throw-away &&
git -c sideband.allowControlCharacters=false \
-c "sideband.file://.allowControlCharacters=true" \
clone --no-local "file://$PWD" throw-away 2>stderr &&
test_decode_color <stderr >color-decoded &&
test_decode_csi <color-decoded >decoded &&
test_grep "CSI \\[K" decoded &&
test_grep ! "\\^\\[\\[K" decoded
'
test_done

View File

@@ -29,6 +29,7 @@
#include "object-name.h"
#include "color.h"
#include "bundle-uri.h"
#include "sideband.h"
static enum git_colorbool transport_use_color = GIT_COLOR_UNKNOWN;
static char transport_colors[][COLOR_MAXLEN] = {
@@ -1246,6 +1247,8 @@ struct transport *transport_get(struct remote *remote, const char *url)
ret->hash_algo = &hash_algos[GIT_HASH_SHA1_LEGACY];
sideband_apply_url_config(ret->url);
return ret;
}