Control: tags 1108983 + patch
Control: tags 1108983 + pending

Dear maintainer,

I've prepared an NMU for git (versioned as 1:2.50.1-0.1) and uploaded
it to DELAYED/1. Please feel free to tell me if I should cancel it.

cu
Adrian
diffstat for git-2.50.0 git-2.50.1

 Documentation/RelNotes/2.43.7.adoc                  |   73 ++
 Documentation/RelNotes/2.44.4.adoc                  |    7 
 Documentation/RelNotes/2.45.4.adoc                  |    7 
 Documentation/RelNotes/2.46.4.adoc                  |    7 
 Documentation/RelNotes/2.47.3.adoc                  |    8 
 Documentation/RelNotes/2.48.2.adoc                  |    8 
 Documentation/RelNotes/2.49.1.adoc                  |   12 
 Documentation/RelNotes/2.50.1.adoc                  |    8 
 GIT-VERSION-GEN                                     |    2 
 RelNotes                                            |  447 -----------------
 bundle-uri.c                                        |   22 
 config.c                                            |    2 
 configure                                           |   18 
 contrib/credential/wincred/git-credential-wincred.c |   22 
 debian/changelog                                    |   17 
 git-gui/git-gui.sh                                  |  510 ++++++++++----------
 git-gui/lib/blame.tcl                               |   12 
 git-gui/lib/branch.tcl                              |    6 
 git-gui/lib/browser.tcl                             |    2 
 git-gui/lib/checkout_op.tcl                         |   25 
 git-gui/lib/choose_repository.tcl                   |   23 
 git-gui/lib/choose_rev.tcl                          |    8 
 git-gui/lib/commit.tcl                              |   14 
 git-gui/lib/console.tcl                             |    5 
 git-gui/lib/database.tcl                            |    2 
 git-gui/lib/diff.tcl                                |   12 
 git-gui/lib/index.tcl                               |    8 
 git-gui/lib/merge.tcl                               |    6 
 git-gui/lib/mergetool.tcl                           |    8 
 git-gui/lib/remote.tcl                              |    8 
 git-gui/lib/remote_branch_delete.tcl                |    2 
 git-gui/lib/shortcut.tcl                            |   14 
 git-gui/lib/sshkey.tcl                              |    7 
 git-gui/lib/tools.tcl                               |    7 
 git-gui/lib/win32.tcl                               |    9 
 gitk-git/gitk                                       |  277 ++++++----
 t/t1300-config.sh                                   |   11 
 t/t5558-clone-bundle-uri.sh                         |   23 
 t/t7450-bad-git-dotfiles.sh                         |   33 +
 version                                             |    2 
 40 files changed, 793 insertions(+), 901 deletions(-)

diff -Nru git-2.50.0/bundle-uri.c git-2.50.1/bundle-uri.c
--- git-2.50.0/bundle-uri.c	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/bundle-uri.c	2025-06-16 08:11:33.000000000 +0300
@@ -297,6 +297,28 @@
 	struct strbuf line = STRBUF_INIT;
 	int found_get = 0;
 
+	/*
+	 * The protocol we speak with git-remote-https(1) uses a space to
+	 * separate between URI and file, so the URI itself must not contain a
+	 * space. If it did, an adversary could change the location where the
+	 * downloaded file is being written to.
+	 *
+	 * Similarly, we use newlines to separate commands from one another.
+	 * Consequently, neither the URI nor the file must contain a newline or
+	 * otherwise an adversary could inject arbitrary commands.
+	 *
+	 * TODO: Restricting newlines in the target paths may break valid
+	 *       usecases, even if those are a bit more on the esoteric side.
+	 *       If this ever becomes a problem we should probably think about
+	 *       alternatives. One alternative could be to use NUL-delimited
+	 *       requests in git-remote-http(1). Another alternative could be
+	 *       to use URL quoting.
+	 */
+	if (strpbrk(uri, " \n"))
+		return error("bundle-uri: URI is malformed: '%s'", file);
+	if (strchr(file, '\n'))
+		return error("bundle-uri: filename is malformed: '%s'", file);
+
 	strvec_pushl(&cp.args, "git-remote-https", uri, NULL);
 	cp.err = -1;
 	cp.in = -1;
diff -Nru git-2.50.0/config.c git-2.50.1/config.c
--- git-2.50.0/config.c	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/config.c	2025-06-16 08:11:33.000000000 +0300
@@ -2940,7 +2940,7 @@
 	if (value[0] == ' ')
 		quote = "\"";
 	for (i = 0; value[i]; i++)
-		if (value[i] == ';' || value[i] == '#')
+		if (value[i] == ';' || value[i] == '#' || value[i] == '\r')
 			quote = "\"";
 	if (i && value[i - 1] == ' ')
 		quote = "\"";
diff -Nru git-2.50.0/configure git-2.50.1/configure
--- git-2.50.0/configure	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/configure	2025-06-16 08:11:33.000000000 +0300
@@ -1,6 +1,6 @@
 #! /bin/sh
 # Guess values for system-dependent variables and create Makefiles.
-# Generated by GNU Autoconf 2.72 for git 2.50.0.
+# Generated by GNU Autoconf 2.72 for git 2.50.1.
 #
 # Report bugs to <[email protected]>.
 #
@@ -604,8 +604,8 @@
 # Identity of this package.
 PACKAGE_NAME='git'
 PACKAGE_TARNAME='git'
-PACKAGE_VERSION='2.50.0'
-PACKAGE_STRING='git 2.50.0'
+PACKAGE_VERSION='2.50.1'
+PACKAGE_STRING='git 2.50.1'
 PACKAGE_BUGREPORT='[email protected]'
 PACKAGE_URL=''
 
@@ -1281,7 +1281,7 @@
   # Omit some internal or obsolete options to make the list less imposing.
   # This message is too long to be a string in the A/UX 3.1 sh.
   cat <<_ACEOF
-'configure' configures git 2.50.0 to adapt to many kinds of systems.
+'configure' configures git 2.50.1 to adapt to many kinds of systems.
 
 Usage: $0 [OPTION]... [VAR=VALUE]...
 
@@ -1343,7 +1343,7 @@
 
 if test -n "$ac_init_help"; then
   case $ac_init_help in
-     short | recursive ) echo "Configuration of git 2.50.0:";;
+     short | recursive ) echo "Configuration of git 2.50.1:";;
    esac
   cat <<\_ACEOF
 
@@ -1486,7 +1486,7 @@
 test -n "$ac_init_help" && exit $ac_status
 if $ac_init_version; then
   cat <<\_ACEOF
-git configure 2.50.0
+git configure 2.50.1
 generated by GNU Autoconf 2.72
 
 Copyright (C) 2023 Free Software Foundation, Inc.
@@ -1915,7 +1915,7 @@
 This file contains any messages produced by compilers while
 running configure, to aid debugging if configure makes a mistake.
 
-It was created by git $as_me 2.50.0, which was
+It was created by git $as_me 2.50.1, which was
 generated by GNU Autoconf 2.72.  Invocation command line was
 
   $ $0$ac_configure_args_raw
@@ -9315,7 +9315,7 @@
 # report actual input values of CONFIG_FILES etc. instead of their
 # values after options handling.
 ac_log="
-This file was extended by git $as_me 2.50.0, which was
+This file was extended by git $as_me 2.50.1, which was
 generated by GNU Autoconf 2.72.  Invocation command line was
 
   CONFIG_FILES    = $CONFIG_FILES
@@ -9374,7 +9374,7 @@
 cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
 ac_cs_config='$ac_cs_config_escaped'
 ac_cs_version="\\
-git config.status 2.50.0
+git config.status 2.50.1
 configured by $0, generated by GNU Autoconf 2.72,
   with options \\"\$ac_cs_config\\"
 
diff -Nru git-2.50.0/contrib/credential/wincred/git-credential-wincred.c git-2.50.1/contrib/credential/wincred/git-credential-wincred.c
--- git-2.50.0/contrib/credential/wincred/git-credential-wincred.c	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/contrib/credential/wincred/git-credential-wincred.c	2025-06-16 08:11:33.000000000 +0300
@@ -39,6 +39,14 @@
 static WCHAR *wusername, *password, *protocol, *host, *path, target[1024],
 	*password_expiry_utc, *oauth_refresh_token;
 
+static void target_append(const WCHAR *src)
+{
+	size_t avail = ARRAY_SIZE(target) - wcslen(target) - 1; /* -1 for NUL */
+	if (avail < wcslen(src))
+		die("target buffer overflow");
+	wcsncat(target, src, avail);
+}
+
 static void write_item(const char *what, LPCWSTR wbuf, int wlen)
 {
 	char *buf;
@@ -330,17 +338,17 @@
 
 	/* prepare 'target', the unique key for the credential */
 	wcscpy(target, L"git:");
-	wcsncat(target, protocol, ARRAY_SIZE(target));
-	wcsncat(target, L"://", ARRAY_SIZE(target));
+	target_append(protocol);
+	target_append(L"://");
 	if (wusername) {
-		wcsncat(target, wusername, ARRAY_SIZE(target));
-		wcsncat(target, L"@", ARRAY_SIZE(target));
+		target_append(wusername);
+		target_append(L"@");
 	}
 	if (host)
-		wcsncat(target, host, ARRAY_SIZE(target));
+		target_append(host);
 	if (path) {
-		wcsncat(target, L"/", ARRAY_SIZE(target));
-		wcsncat(target, path, ARRAY_SIZE(target));
+		target_append(L"/");
+		target_append(path);
 	}
 
 	if (!strcmp(argv[1], "get"))
diff -Nru git-2.50.0/debian/changelog git-2.50.1/debian/changelog
--- git-2.50.0/debian/changelog	2025-06-17 04:50:38.000000000 +0300
+++ git-2.50.1/debian/changelog	2025-07-29 20:54:28.000000000 +0300
@@ -1,3 +1,20 @@
+git (1:2.50.1-0.1) unstable; urgency=medium
+
+  * Non-maintainer upload.
+  * New upstream release.
+    - CVE-2025-27613: gitk: file creation/truncation after cloning
+      untrusted repository
+    - CVE-2025-27614: gitk: user can be tricked into running any
+      script after cloning untrusted repository
+    - CVE-2025-46835: git-gui: file creation/overwriting after
+      cloning untrusted repository
+    - CVE-2025-48384: script execution after cloning untrusted
+      repository
+    - CVE-2025-48385: protocol injection when fetching
+    - Closes: #1108983
+
+ -- Adrian Bunk <[email protected]>  Tue, 29 Jul 2025 20:54:28 +0300
+
 git (1:2.50.0-1) unstable; urgency=medium
 
   * new upstream release (see RelNotes/2.50.0.adoc).
diff -Nru git-2.50.0/Documentation/RelNotes/2.43.7.adoc git-2.50.1/Documentation/RelNotes/2.43.7.adoc
--- git-2.50.0/Documentation/RelNotes/2.43.7.adoc	1970-01-01 02:00:00.000000000 +0200
+++ git-2.50.1/Documentation/RelNotes/2.43.7.adoc	2025-06-16 08:11:33.000000000 +0300
@@ -0,0 +1,73 @@
+Git v2.43.7 Release Notes
+=========================
+
+This release includes fixes for CVE-2025-27613, CVE-2025-27614,
+CVE-2025-46334, CVE-2025-46835, CVE-2025-48384, CVE-2025-48385, and
+CVE-2025-48386.
+
+Fixes since v2.43.6
+-------------------
+
+ * CVE-2025-27613, Gitk:
+
+   When a user clones an untrusted repository and runs Gitk without
+   additional command arguments, any writable file can be created and
+   truncated. The option "Support per-file encoding" must have been
+   enabled. The operation "Show origin of this line" is affected as
+   well, regardless of the option being enabled or not.
+
+ * CVE-2025-27614, Gitk:
+
+   A Git repository can be crafted in such a way that a user who has
+   cloned the repository can be tricked into running any script
+   supplied by the attacker by invoking `gitk filename`, where
+   `filename` has a particular structure.
+
+ * CVE-2025-46334, Git GUI (Windows only):
+
+   A malicious repository can ship versions of sh.exe or typical
+   textconv filter programs such as astextplain. On Windows, path
+   lookup can find such executables in the worktree. These programs
+   are invoked when the user selects "Git Bash" or "Browse Files" from
+   the menu.
+
+ * CVE-2025-46835, Git GUI:
+
+   When a user clones an untrusted repository and is tricked into
+   editing a file located in a maliciously named directory in the
+   repository, then Git GUI can create and overwrite any writable
+   file.
+
+ * CVE-2025-48384, Git:
+
+   When reading a config value, Git strips any trailing carriage
+   return and line feed (CRLF). When writing a config entry, values
+   with a trailing CR are not quoted, causing the CR to be lost when
+   the config is later read.  When initializing a submodule, if the
+   submodule path contains a trailing CR, the altered path is read
+   resulting in the submodule being checked out to an incorrect
+   location. If a symlink exists that points the altered path to the
+   submodule hooks directory, and the submodule contains an executable
+   post-checkout hook, the script may be unintentionally executed
+   after checkout.
+
+ * CVE-2025-48385, Git:
+
+   When cloning a repository Git knows to optionally fetch a bundle
+   advertised by the remote server, which allows the server-side to
+   offload parts of the clone to a CDN. The Git client does not
+   perform sufficient validation of the advertised bundles, which
+   allows the remote side to perform protocol injection.
+
+   This protocol injection can cause the client to write the fetched
+   bundle to a location controlled by the adversary. The fetched
+   content is fully controlled by the server, which can in the worst
+   case lead to arbitrary code execution.
+
+ * CVE-2025-48386, Git:
+
+   The wincred credential helper uses a static buffer (`target`) as a
+   unique key for storing and comparing against internal storage. This
+   credential helper does not properly bounds check the available
+   space remaining in the buffer before appending to it with
+   `wcsncat()`, leading to potential buffer overflows.
diff -Nru git-2.50.0/Documentation/RelNotes/2.44.4.adoc git-2.50.1/Documentation/RelNotes/2.44.4.adoc
--- git-2.50.0/Documentation/RelNotes/2.44.4.adoc	1970-01-01 02:00:00.000000000 +0200
+++ git-2.50.1/Documentation/RelNotes/2.44.4.adoc	2025-06-16 08:11:33.000000000 +0300
@@ -0,0 +1,7 @@
+Git v2.44.4 Release Notes
+=========================
+
+This release merges up the fixes that appears in v2.43.7 to address
+the following CVEs: CVE-2025-27613, CVE-2025-27614, CVE-2025-46334,
+CVE-2025-46835, CVE-2025-48384, CVE-2025-48385, and CVE-2025-48386.
+See the release notes for v2.43.7 for details.
diff -Nru git-2.50.0/Documentation/RelNotes/2.45.4.adoc git-2.50.1/Documentation/RelNotes/2.45.4.adoc
--- git-2.50.0/Documentation/RelNotes/2.45.4.adoc	1970-01-01 02:00:00.000000000 +0200
+++ git-2.50.1/Documentation/RelNotes/2.45.4.adoc	2025-06-16 08:11:33.000000000 +0300
@@ -0,0 +1,7 @@
+Git v2.45.4 Release Notes
+=========================
+
+This release merges up the fixes that appears in v2.43.7, and v2.44.4
+to address the following CVEs: CVE-2025-27613, CVE-2025-27614,
+CVE-2025-46334, CVE-2025-46835, CVE-2025-48384, CVE-2025-48385, and
+CVE-2025-48386. See the release notes for v2.43.7 for details.
diff -Nru git-2.50.0/Documentation/RelNotes/2.46.4.adoc git-2.50.1/Documentation/RelNotes/2.46.4.adoc
--- git-2.50.0/Documentation/RelNotes/2.46.4.adoc	1970-01-01 02:00:00.000000000 +0200
+++ git-2.50.1/Documentation/RelNotes/2.46.4.adoc	2025-06-16 08:11:33.000000000 +0300
@@ -0,0 +1,7 @@
+Git v2.46.4 Release Notes
+=========================
+
+This release merges up the fixes that appears in v2.43.7, v2.44.4, and
+v2.45.4 to address the following CVEs: CVE-2025-27613, CVE-2025-27614,
+CVE-2025-46334, CVE-2025-46835, CVE-2025-48384, CVE-2025-48385, and
+CVE-2025-48386. See the release notes for v2.43.7 for details.
diff -Nru git-2.50.0/Documentation/RelNotes/2.47.3.adoc git-2.50.1/Documentation/RelNotes/2.47.3.adoc
--- git-2.50.0/Documentation/RelNotes/2.47.3.adoc	1970-01-01 02:00:00.000000000 +0200
+++ git-2.50.1/Documentation/RelNotes/2.47.3.adoc	2025-06-16 08:11:33.000000000 +0300
@@ -0,0 +1,8 @@
+Git v2.47.3 Release Notes
+=========================
+
+This release merges up the fixes that appears in v2.43.7, v2.44.4,
+v2.45.4, and v2.46.4 to address the following CVEs: CVE-2025-27613,
+CVE-2025-27614, CVE-2025-46334, CVE-2025-46835, CVE-2025-48384,
+CVE-2025-48385, and CVE-2025-48386. See the release notes for v2.43.7
+for details.
diff -Nru git-2.50.0/Documentation/RelNotes/2.48.2.adoc git-2.50.1/Documentation/RelNotes/2.48.2.adoc
--- git-2.50.0/Documentation/RelNotes/2.48.2.adoc	1970-01-01 02:00:00.000000000 +0200
+++ git-2.50.1/Documentation/RelNotes/2.48.2.adoc	2025-06-16 08:11:33.000000000 +0300
@@ -0,0 +1,8 @@
+Git v2.48.2 Release Notes
+=========================
+
+This release merges up the fixes that appears in v2.43.7, v2.44.4,
+v2.45.4, v2.46.4, and v2.47.3 to address the following CVEs:
+CVE-2025-27613, CVE-2025-27614, CVE-2025-46334, CVE-2025-46835,
+CVE-2025-48384, CVE-2025-48385, and CVE-2025-48386. See the release
+notes for v2.43.7 for details.
diff -Nru git-2.50.0/Documentation/RelNotes/2.49.1.adoc git-2.50.1/Documentation/RelNotes/2.49.1.adoc
--- git-2.50.0/Documentation/RelNotes/2.49.1.adoc	1970-01-01 02:00:00.000000000 +0200
+++ git-2.50.1/Documentation/RelNotes/2.49.1.adoc	2025-06-16 08:11:33.000000000 +0300
@@ -0,0 +1,12 @@
+Git v2.49.1 Release Notes
+=========================
+
+This release merges up the fixes that appear in v2.43.7, v2.44.4,
+v2.45.4, v2.46.4, v2.47.3, and v2.48.2 to address the following CVEs:
+CVE-2025-27613, CVE-2025-27614, CVE-2025-46334, CVE-2025-46835,
+CVE-2025-48384, CVE-2025-48385, and CVE-2025-48386. See the release
+notes for v2.43.7 for details.
+
+It also contains some updates to various CI bits to work around
+and/or to adjust to the deprecation of use of Ubuntu 20.04 GitHub
+Actions CI, updates to to Fedora base image.
diff -Nru git-2.50.0/Documentation/RelNotes/2.50.1.adoc git-2.50.1/Documentation/RelNotes/2.50.1.adoc
--- git-2.50.0/Documentation/RelNotes/2.50.1.adoc	1970-01-01 02:00:00.000000000 +0200
+++ git-2.50.1/Documentation/RelNotes/2.50.1.adoc	2025-06-16 08:11:33.000000000 +0300
@@ -0,0 +1,8 @@
+Git v2.50.1 Release Notes
+=========================
+
+This release merges up the fixes that appear in v2.43.7, v2.44.4,
+v2.45.4, v2.46.4, v2.47.3, v2.48.2, and v2.49.1 to address the
+following CVEs: CVE-2025-27613, CVE-2025-27614, CVE-2025-46334,
+CVE-2025-46835, CVE-2025-48384, CVE-2025-48385, and
+CVE-2025-48386. See the release notes for v2.43.7 for details.
diff -Nru git-2.50.0/git-gui/git-gui.sh git-2.50.1/git-gui/git-gui.sh
--- git-2.50.0/git-gui/git-gui.sh	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/git-gui.sh	2025-06-16 08:11:33.000000000 +0300
@@ -77,99 +77,178 @@
 
 ######################################################################
 ##
-## PATH lookup
+## PATH lookup. Sanitize $PATH, assure exec/open use only that
+
+if {[is_Windows]} {
+	set _path_sep {;}
+	set _search_exe .exe
+} else {
+	set _path_sep {:}
+	set _search_exe {}
+}
+
+if {[is_Windows]} {
+	set gitguidir [file dirname [info script]]
+	regsub -all ";" $gitguidir "\\;" gitguidir
+	set env(PATH) "$gitguidir;$env(PATH)"
+}
 
 set _search_path {}
-proc _which {what args} {
-	global env _search_exe _search_path
+set _path_seen [dict create]
+foreach p [split $env(PATH) $_path_sep] {
+	# Keep only absolute paths, getting rid of ., empty, etc.
+	if {[file pathtype $p] ne {absolute}} {
+		continue
+	}
+	# Keep only the first occurence of any duplicates.
+	set norm_p [file normalize $p]
+	if {[dict exists $_path_seen $norm_p]} {
+		continue
+	}
+	dict set _path_seen $norm_p 1
+	lappend _search_path $norm_p
+}
+unset _path_seen
 
-	if {$_search_path eq {}} {
-		if {[is_Windows]} {
-			set gitguidir [file dirname [info script]]
-			regsub -all ";" $gitguidir "\\;" gitguidir
-			set env(PATH) "$gitguidir;$env(PATH)"
-			set _search_path [split $env(PATH) {;}]
-			# Skip empty `PATH` elements
-			set _search_path [lsearch -all -inline -not -exact \
-				$_search_path ""]
-			set _search_exe .exe
+set env(PATH) [join $_search_path $_path_sep]
+
+if {[is_Windows]} {
+	proc _which {what args} {
+		global _search_exe _search_path
+
+		if {[lsearch -exact $args -script] >= 0} {
+			set suffix {}
+		} elseif {[string match *$_search_exe [string tolower $what]]} {
+			# The search string already has the file extension
+			set suffix {}
 		} else {
-			set _search_path [split $env(PATH) :]
-			set _search_exe {}
+			set suffix $_search_exe
 		}
-	}
 
-	if {[is_Windows] && [lsearch -exact $args -script] >= 0} {
-		set suffix {}
-	} else {
-		set suffix $_search_exe
+		foreach p $_search_path {
+			set p [file join $p $what$suffix]
+			if {[file exists $p]} {
+				return [file normalize $p]
+			}
+		}
+		return {}
 	}
 
-	foreach p $_search_path {
-		set p [file join $p $what$suffix]
-		if {[file exists $p]} {
-			return [file normalize $p]
+	proc sanitize_command_line {command_line from_index} {
+		set i $from_index
+		while {$i < [llength $command_line]} {
+			set cmd [lindex $command_line $i]
+			if {[llength [file split $cmd]] < 2} {
+				set fullpath [_which $cmd]
+				if {$fullpath eq ""} {
+					throw {NOT-FOUND} "$cmd not found in PATH"
+				}
+				lset command_line $i $fullpath
+			}
+
+			# handle piped commands, e.g. `exec A | B`
+			for {incr i} {$i < [llength $command_line]} {incr i} {
+				if {[lindex $command_line $i] eq "|"} {
+					incr i
+					break
+				}
+			}
 		}
+		return $command_line
 	}
-	return {}
-}
 
-proc sanitize_command_line {command_line from_index} {
-	set i $from_index
-	while {$i < [llength $command_line]} {
-		set cmd [lindex $command_line $i]
-		if {[llength [file split $cmd]] < 2} {
-			set fullpath [_which $cmd]
-			if {$fullpath eq ""} {
-				throw {NOT-FOUND} "$cmd not found in PATH"
-			}
-			lset command_line $i $fullpath
-		}
+	# Override `exec` to avoid unsafe PATH lookup
+
+	rename exec real_exec
 
-		# handle piped commands, e.g. `exec A | B`
-		for {incr i} {$i < [llength $command_line]} {incr i} {
-			if {[lindex $command_line $i] eq "|"} {
+	proc exec {args} {
+		# skip options
+		for {set i 0} {$i < [llength $args]} {incr i} {
+			set arg [lindex $args $i]
+			if {$arg eq "--"} {
 				incr i
 				break
 			}
+			if {[string range $arg 0 0] ne "-"} {
+				break
+			}
 		}
+		set args [sanitize_command_line $args $i]
+		uplevel 1 real_exec $args
 	}
-	return $command_line
-}
 
-# Override `exec` to avoid unsafe PATH lookup
+	# Override `open` to avoid unsafe PATH lookup
 
-rename exec real_exec
+	rename open real_open
 
-proc exec {args} {
-	# skip options
-	for {set i 0} {$i < [llength $args]} {incr i} {
-		set arg [lindex $args $i]
-		if {$arg eq "--"} {
-			incr i
-			break
-		}
-		if {[string range $arg 0 0] ne "-"} {
-			break
+	proc open {args} {
+		set arg0 [lindex $args 0]
+		if {[string range $arg0 0 0] eq "|"} {
+			set command_line [string trim [string range $arg0 1 end]]
+			lset args 0 "| [sanitize_command_line $command_line 0]"
 		}
+		uplevel 1 real_open $args
+	}
+
+} else {
+	# On non-Windows platforms, auto_execok, exec, and open are safe, and will
+	# use the sanitized search path. But, we need _which for these.
+
+	proc _which {what args} {
+		return [lindex [auto_execok $what] 0]
+	}
+}
+
+# Wrap exec/open to sanitize arguments
+
+# unsafe arguments begin with redirections or the pipe or background operators
+proc is_arg_unsafe {arg} {
+	regexp {^([<|>&]|2>)} $arg
+}
+
+proc make_arg_safe {arg} {
+	if {[is_arg_unsafe $arg]} {
+		set arg [file join . $arg]
 	}
-	set args [sanitize_command_line $args $i]
-	uplevel 1 real_exec $args
+	return $arg
 }
 
-# Override `open` to avoid unsafe PATH lookup
+proc make_arglist_safe {arglist} {
+	set res {}
+	foreach arg $arglist {
+		lappend res [make_arg_safe $arg]
+	}
+	return $res
+}
 
-rename open real_open
+# executes one command
+# no redirections or pipelines are possible
+# cmd is a list that specifies the command and its arguments
+# calls `exec` and returns its value
+proc safe_exec {cmd} {
+	eval exec [make_arglist_safe $cmd]
+}
 
-proc open {args} {
-	set arg0 [lindex $args 0]
-	if {[string range $arg0 0 0] eq "|"} {
-		set command_line [string trim [string range $arg0 1 end]]
-		lset args 0 "| [sanitize_command_line $command_line 0]"
+# executes one command in the background
+# no redirections or pipelines are possible
+# cmd is a list that specifies the command and its arguments
+# calls `exec` and returns its value
+proc safe_exec_bg {cmd} {
+	eval exec [make_arglist_safe $cmd] &
+}
+
+proc safe_open_file {filename flags} {
+	# a file name starting with "|" would attempt to run a process
+	# but such a file name must be treated as a relative path
+	# hide the "|" behind "./"
+	if {[string index $filename 0] eq "|"} {
+		set filename [file join . $filename]
 	}
-	uplevel 1 real_open $args
+	open $filename $flags
 }
 
+# End exec/open wrappers
+
 ######################################################################
 ##
 ## locate our library
@@ -270,11 +349,11 @@
 
 if {[tk windowingsystem] eq "aqua"} {
 	catch {
-		exec osascript -e [format {
+		safe_exec [list osascript -e [format {
 			tell application "System Events"
 				set frontmost of processes whose unix id is %d to true
 			end tell
-		} [pid]]
+		} [pid]]]
 	}
 }
 
@@ -304,15 +383,37 @@
 # branches).
 set _last_merged_branch {}
 
-proc shellpath {} {
-	global _shellpath env
-	if {[string match @@* $_shellpath]} {
-		if {[info exists env(SHELL)]} {
-			return $env(SHELL)
-		} else {
-			return /bin/sh
-		}
+# for testing, allow unconfigured _shellpath
+if {[string match @@* $_shellpath]} {
+	if {[info exists env(SHELL)]} {
+		set _shellpath $env(SHELL)
+	} else {
+		set _shellpath /bin/sh
 	}
+}
+
+if {[is_Windows]} {
+	set _shellpath [safe_exec [list cygpath -m $_shellpath]]
+}
+
+if {![file executable $_shellpath] || \
+	!([file pathtype $_shellpath] eq {absolute})} {
+	set errmsg "The defined shell ('$_shellpath') is not usable, \
+		it must be an absolute path to an executable."
+	puts stderr $errmsg
+
+	catch {wm withdraw .}
+	tk_messageBox \
+		-icon error \
+		-type ok \
+		-title "git-gui: configuration error" \
+		-message $errmsg
+	exit 1
+}
+
+
+proc shellpath {} {
+	global _shellpath
 	return $_shellpath
 }
 
@@ -494,7 +595,7 @@
 			# Tcl on Windows doesn't know it.
 			#
 			set p [gitexec git-$name]
-			set f [open $p r]
+			set f [safe_open_file $p r]
 			set s [gets $f]
 			close $f
 
@@ -524,32 +625,14 @@
 	return $v
 }
 
-# Test a file for a hashbang to identify executable scripts on Windows.
-proc is_shellscript {filename} {
-	if {![file exists $filename]} {return 0}
-	set f [open $filename r]
-	fconfigure $f -encoding binary
-	set magic [read $f 2]
-	close $f
-	return [expr {$magic eq "#!"}]
-}
-
-# Run a command connected via pipes on stdout.
+# Run a shell command connected via pipes on stdout.
 # This is for use with textconv filters and uses sh -c "..." to allow it to
-# contain a command with arguments. On windows we must check for shell
-# scripts specifically otherwise just call the filter command.
+# contain a command with arguments. We presume this
+# to be a shellscript that the configured shell (/bin/sh by default) knows
+# how to run.
 proc open_cmd_pipe {cmd path} {
-	global env
-	if {![file executable [shellpath]]} {
-		set exe [auto_execok [lindex $cmd 0]]
-		if {[is_shellscript [lindex $exe 0]]} {
-			set run [linsert [auto_execok sh] end -c "$cmd \"\$0\"" $path]
-		} else {
-			set run [concat $exe [lrange $cmd 1 end] $path]
-		}
-	} else {
-		set run [list [shellpath] -c "$cmd \"\$0\"" $path]
-	}
+	set run [list [shellpath] -c "$cmd \"\$0\"" $path]
+	set run [make_arglist_safe $run]
 	return [open |$run r]
 }
 
@@ -559,7 +642,7 @@
 
 	if {![info exists _nice]} {
 		set _nice [_which nice]
-		if {[catch {exec $_nice git version}]} {
+		if {[catch {safe_exec [list $_nice git version]}]} {
 			set _nice {}
 		} elseif {[is_Windows] && [file dirname $_nice] ne [file dirname $::_git]} {
 			set _nice {}
@@ -571,7 +654,11 @@
 }
 
 proc git {args} {
-	set fd [eval [list git_read] $args]
+	git_redir $args {}
+}
+
+proc git_redir {cmd redir} {
+	set fd [git_read $cmd $redir]
 	fconfigure $fd -translation binary -encoding utf-8
 	set result [string trimright [read $fd] "\n"]
 	close $fd
@@ -581,88 +668,47 @@
 	return $result
 }
 
-proc _open_stdout_stderr {cmd} {
-	_trace_exec $cmd
+proc safe_open_command {cmd {redir {}}} {
+	set cmd [make_arglist_safe $cmd]
+	_trace_exec [concat $cmd $redir]
 	if {[catch {
-			set fd [open [concat [list | ] $cmd] r]
-		} err]} {
-		if {   [lindex $cmd end] eq {2>@1}
-		    && $err eq {can not find channel named "1"}
-			} {
-			# Older versions of Tcl 8.4 don't have this 2>@1 IO
-			# redirect operator.  Fallback to |& cat for those.
-			# The command was not actually started, so its safe
-			# to try to start it a second time.
-			#
-			set fd [open [concat \
-				[list | ] \
-				[lrange $cmd 0 end-1] \
-				[list |& cat] \
-				] r]
-		} else {
-			error $err
-		}
+		set fd [open [concat [list | ] $cmd $redir] r]
+	} err]} {
+		error $err
 	}
 	fconfigure $fd -eofchar {}
 	return $fd
 }
 
-proc git_read {args} {
-	set opt [list]
-
-	while {1} {
-		switch -- [lindex $args 0] {
-		--nice {
-			_lappend_nice opt
-		}
-
-		--stderr {
-			lappend args 2>@1
-		}
+proc git_read {cmd {redir {}}} {
+	set cmdp [_git_cmd [lindex $cmd 0]]
+	set cmd [lrange $cmd 1 end]
 
-		default {
-			break
-		}
-
-		}
-
-		set args [lrange $args 1 end]
-	}
-
-	set cmdp [_git_cmd [lindex $args 0]]
-	set args [lrange $args 1 end]
-
-	return [_open_stdout_stderr [concat $opt $cmdp $args]]
+	return [safe_open_command [concat $cmdp $cmd] $redir]
 }
 
-proc git_write {args} {
+proc git_read_nice {cmd} {
 	set opt [list]
 
-	while {1} {
-		switch -- [lindex $args 0] {
-		--nice {
-			_lappend_nice opt
-		}
-
-		default {
-			break
-		}
+	_lappend_nice opt
 
-		}
+	set cmdp [_git_cmd [lindex $cmd 0]]
+	set cmd [lrange $cmd 1 end]
 
-		set args [lrange $args 1 end]
-	}
+	return [safe_open_command [concat $opt $cmdp $cmd]]
+}
 
-	set cmdp [_git_cmd [lindex $args 0]]
-	set args [lrange $args 1 end]
+proc git_write {cmd} {
+	set cmd [make_arglist_safe $cmd]
+	set cmdp [_git_cmd [lindex $cmd 0]]
+	set cmd [lrange $cmd 1 end]
 
-	_trace_exec [concat $opt $cmdp $args]
-	return [open [concat [list | ] $opt $cmdp $args] w]
+	_trace_exec [concat $cmdp $cmd]
+	return [open [concat [list | ] $cmdp $cmd] w]
 }
 
 proc githook_read {hook_name args} {
-	set cmd [concat git hook run --ignore-missing $hook_name -- $args 2>@1]
-	return [_open_stdout_stderr $cmd]
+	git_read [concat [list hook run --ignore-missing $hook_name --] $args] [list 2>@1]
 }
 
 proc kill_file_process {fd} {
@@ -670,9 +716,9 @@
 
 	catch {
 		if {[is_Windows]} {
-			exec taskkill /pid $process
+			safe_exec [list taskkill /pid $process]
 		} else {
-			exec kill $process
+			safe_exec [list kill $process]
 		}
 	}
 }
@@ -698,7 +744,7 @@
 proc load_current_branch {} {
 	global current_branch is_detached
 
-	set fd [open [gitdir HEAD] r]
+	set fd [safe_open_file [gitdir HEAD] r]
 	fconfigure $fd -translation binary -encoding utf-8
 	if {[gets $fd ref] < 1} {
 		set ref {}
@@ -1068,7 +1114,7 @@
 ## configure our library
 
 set idx [file join $oguilib tclIndex]
-if {[catch {set fd [open $idx r]} err]} {
+if {[catch {set fd [safe_open_file $idx r]} err]} {
 	catch {wm withdraw .}
 	tk_messageBox \
 		-icon error \
@@ -1106,53 +1152,30 @@
 ##
 ## config file parsing
 
-git-version proc _parse_config {arr_name args} {
-	>= 1.5.3 {
-		upvar $arr_name arr
-		array unset arr
-		set buf {}
-		catch {
-			set fd_rc [eval \
-				[list git_read config] \
-				$args \
-				[list --null --list]]
-			fconfigure $fd_rc -translation binary -encoding utf-8
-			set buf [read $fd_rc]
-			close $fd_rc
-		}
-		foreach line [split $buf "\0"] {
-			if {[regexp {^([^\n]+)\n(.*)$} $line line name value]} {
-				if {[is_many_config $name]} {
-					lappend arr($name) $value
-				} else {
-					set arr($name) $value
-				}
-			} elseif {[regexp {^([^\n]+)$} $line line name]} {
-				# no value given, but interpreting them as
-				# boolean will be handled as true
-				set arr($name) {}
-			}
-		}
-	}
-	default {
-		upvar $arr_name arr
-		array unset arr
-		catch {
-			set fd_rc [eval [list git_read config --list] $args]
-			while {[gets $fd_rc line] >= 0} {
-				if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
-					if {[is_many_config $name]} {
-						lappend arr($name) $value
-					} else {
-						set arr($name) $value
-					}
-				} elseif {[regexp {^([^=]+)$} $line line name]} {
-					# no value given, but interpreting them as
-					# boolean will be handled as true
-					set arr($name) {}
-				}
+proc _parse_config {arr_name args} {
+	upvar $arr_name arr
+	array unset arr
+	set buf {}
+	catch {
+		set fd_rc [git_read \
+			[concat config \
+			$args \
+			--null --list]]
+		fconfigure $fd_rc -translation binary -encoding utf-8
+		set buf [read $fd_rc]
+		close $fd_rc
+	}
+	foreach line [split $buf "\0"] {
+		if {[regexp {^([^\n]+)\n(.*)$} $line line name value]} {
+			if {[is_many_config $name]} {
+				lappend arr($name) $value
+			} else {
+				set arr($name) $value
 			}
-			close $fd_rc
+		} elseif {[regexp {^([^\n]+)$} $line line name]} {
+			# no value given, but interpreting them as
+			# boolean will be handled as true
+			set arr($name) {}
 		}
 	}
 }
@@ -1427,7 +1450,7 @@
 	set merge_head [gitdir MERGE_HEAD]
 	if {[file exists $merge_head]} {
 		set ct merge
-		set fd_mh [open $merge_head r]
+		set fd_mh [safe_open_file $merge_head r]
 		while {[gets $fd_mh line] >= 0} {
 			lappend mh $line
 		}
@@ -1446,7 +1469,7 @@
 		return $p
 	}
 	if {$empty_tree eq {}} {
-		set empty_tree [git mktree << {}]
+		set empty_tree [git_redir [list mktree] [list << {}]]
 	}
 	return $empty_tree
 }
@@ -1505,12 +1528,12 @@
 	} else {
 		set rescan_active 1
 		ui_status [mc "Refreshing file status..."]
-		set fd_rf [git_read update-index \
+		set fd_rf [git_read [list update-index \
 			-q \
 			--unmerged \
 			--ignore-missing \
 			--refresh \
-			]
+			]]
 		fconfigure $fd_rf -blocking 0 -translation binary
 		fileevent $fd_rf readable \
 			[list rescan_stage2 $fd_rf $after]
@@ -1550,11 +1573,11 @@
 	set rescan_active 2
 	ui_status [mc "Scanning for modified files ..."]
 	if {[git-version >= "1.7.2"]} {
-		set fd_di [git_read diff-index --cached --ignore-submodules=dirty -z [PARENT]]
+		set fd_di [git_read [list diff-index --cached --ignore-submodules=dirty -z [PARENT]]]
 	} else {
-		set fd_di [git_read diff-index --cached -z [PARENT]]
+		set fd_di [git_read [list diff-index --cached -z [PARENT]]]
 	}
-	set fd_df [git_read diff-files -z]
+	set fd_df [git_read [list diff-files -z]]
 
 	fconfigure $fd_di -blocking 0 -translation binary -encoding binary
 	fconfigure $fd_df -blocking 0 -translation binary -encoding binary
@@ -1563,7 +1586,7 @@
 	fileevent $fd_df readable [list read_diff_files $fd_df $after]
 
 	if {[is_config_true gui.displayuntracked]} {
-		set fd_lo [eval git_read ls-files --others -z $ls_others]
+		set fd_lo [git_read [concat ls-files --others -z $ls_others]]
 		fconfigure $fd_lo -blocking 0 -translation binary -encoding binary
 		fileevent $fd_lo readable [list read_ls_others $fd_lo $after]
 		incr rescan_active
@@ -1575,7 +1598,7 @@
 
 	set f [gitdir $file]
 	if {[file isfile $f]} {
-		if {[catch {set fd [open $f r]}]} {
+		if {[catch {set fd [safe_open_file $f r]}]} {
 			return 0
 		}
 		fconfigure $fd -eofchar {}
@@ -1599,23 +1622,23 @@
 	# it will be .git/MERGE_MSG (merge), .git/SQUASH_MSG (squash), or an
 	# empty file but existent file.
 
-	set fd_pcm [open [gitdir PREPARE_COMMIT_MSG] a]
+	set fd_pcm [safe_open_file [gitdir PREPARE_COMMIT_MSG] a]
 
 	if {[file isfile [gitdir MERGE_MSG]]} {
 		set pcm_source "merge"
-		set fd_mm [open [gitdir MERGE_MSG] r]
+		set fd_mm [safe_open_file [gitdir MERGE_MSG] r]
 		fconfigure $fd_mm -encoding utf-8
 		puts -nonewline $fd_pcm [read $fd_mm]
 		close $fd_mm
 	} elseif {[file isfile [gitdir SQUASH_MSG]]} {
 		set pcm_source "squash"
-		set fd_sm [open [gitdir SQUASH_MSG] r]
+		set fd_sm [safe_open_file [gitdir SQUASH_MSG] r]
 		fconfigure $fd_sm -encoding utf-8
 		puts -nonewline $fd_pcm [read $fd_sm]
 		close $fd_sm
 	} elseif {[file isfile [get_config commit.template]]} {
 		set pcm_source "template"
-		set fd_sm [open [get_config commit.template] r]
+		set fd_sm [safe_open_file [get_config commit.template] r]
 		fconfigure $fd_sm -encoding utf-8
 		puts -nonewline $fd_pcm [read $fd_sm]
 		close $fd_sm
@@ -2205,7 +2228,7 @@
 			unset env(GIT_DIR)
 			unset env(GIT_WORK_TREE)
 		}
-		eval exec $cmd $revs "--" "--" &
+		safe_exec_bg [concat $cmd $revs "--" "--"]
 
 		set env(GIT_DIR) $_gitdir
 		set env(GIT_WORK_TREE) $_gitworktree
@@ -2242,7 +2265,7 @@
 		set pwd [pwd]
 		cd $current_diff_path
 
-		eval exec $exe gui &
+		safe_exec_bg [concat $exe gui]
 
 		set env(GIT_DIR) $_gitdir
 		set env(GIT_WORK_TREE) $_gitworktree
@@ -2273,16 +2296,18 @@
 
 proc do_explore {} {
 	global _gitworktree
-	set explorer [get_explorer]
-	eval exec $explorer [list [file nativename $_gitworktree]] &
+	set cmd [get_explorer]
+	lappend cmd [file nativename $_gitworktree]
+	safe_exec_bg $cmd
 }
 
 # Open file relative to the working tree by the default associated app.
 proc do_file_open {file} {
 	global _gitworktree
-	set explorer [get_explorer]
+	set cmd [get_explorer]
 	set full_file_path [file join $_gitworktree $file]
-	exec $explorer [file nativename $full_file_path] &
+	lappend cmd [file nativename $full_file_path]
+	safe_exec_bg $cmd
 }
 
 set is_quitting 0
@@ -2316,7 +2341,7 @@
 			if {![string match amend* $commit_type]
 				&& $msg ne {}} {
 				catch {
-					set fd [open $save w]
+					set fd [safe_open_file $save w]
 					fconfigure $fd -encoding utf-8
 					puts -nonewline $fd $msg
 					close $fd
@@ -2760,17 +2785,16 @@
 
 if {[is_Windows]} {
 	# Use /git-bash.exe if available
-	set normalized [file normalize $::argv0]
-	regsub "/mingw../libexec/git-core/git-gui$" \
-		$normalized "/git-bash.exe" cmdLine
-	if {$cmdLine != $normalized && [file exists $cmdLine]} {
-		set cmdLine [list "Git Bash" $cmdLine &]
+	set _git_bash [safe_exec [list cygpath -m /git-bash.exe]]
+	if {[file executable $_git_bash]} {
+		set _bash_cmdline [list "Git Bash" $_git_bash]
 	} else {
-		set cmdLine [list "Git Bash" bash --login -l &]
+		set _bash_cmdline [list "Git Bash" bash --login -l]
 	}
 	.mbar.repository add command \
 		-label [mc "Git Bash"] \
-		-command {eval exec [auto_execok start] $cmdLine}
+		-command {safe_exec_bg [concat [list [_which cmd] /c start] $_bash_cmdline]}
+	unset _git_bash
 }
 
 if {[is_Windows] || ![is_bare]} {
@@ -4079,7 +4103,7 @@
 				}
 			} elseif {$m} {
 				catch {
-					set fd [open [gitdir GITGUI_BCK] w]
+					set fd [safe_open_file [gitdir GITGUI_BCK] w]
 					fconfigure $fd -encoding utf-8
 					puts -nonewline $fd $msg
 					close $fd
diff -Nru git-2.50.0/git-gui/lib/blame.tcl git-2.50.1/git-gui/lib/blame.tcl
--- git-2.50.0/git-gui/lib/blame.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/blame.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -481,14 +481,14 @@
 		if {$do_textconv ne 0} {
 			set fd [open_cmd_pipe $textconv $path]
 		} else {
-			set fd [open $path r]
+			set fd [safe_open_file $path r]
 		}
 		fconfigure $fd -eofchar {}
 	} else {
 		if {$do_textconv ne 0} {
-			set fd [git_read cat-file --textconv "$commit:$path"]
+			set fd [git_read [list cat-file --textconv "$commit:$path"]]
 		} else {
-			set fd [git_read cat-file blob "$commit:$path"]
+			set fd [git_read [list cat-file blob "$commit:$path"]]
 		}
 	}
 	fconfigure $fd \
@@ -617,7 +617,7 @@
 	}
 
 	lappend options -- $path
-	set fd [eval git_read --nice blame $options]
+	set fd [git_read_nice [concat blame $options]]
 	fconfigure $fd -blocking 0 -translation lf -encoding utf-8
 	fileevent $fd readable [cb _read_blame $fd $cur_w $cur_d]
 	set current_fd $fd
@@ -986,7 +986,7 @@
 		if {[catch {set msg $header($cmit,message)}]} {
 			set msg {}
 			catch {
-				set fd [git_read cat-file commit $cmit]
+				set fd [git_read [list cat-file commit $cmit]]
 				fconfigure $fd -encoding binary -translation lf
 				# By default commits are assumed to be in utf-8
 				set enc utf-8
@@ -1134,7 +1134,7 @@
 		} else {
 			set diffcmd [list diff-tree --unified=0 $cparent $cmit -- $new_path]
 		}
-		if {[catch {set fd [eval git_read $diffcmd]} err]} {
+		if {[catch {set fd [git_read $diffcmd]} err]} {
 			$status_operation stop [mc "Unable to display parent"]
 			error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
 			return
diff -Nru git-2.50.0/git-gui/lib/branch.tcl git-2.50.1/git-gui/lib/branch.tcl
--- git-2.50.0/git-gui/lib/branch.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/branch.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -7,7 +7,7 @@
 	set rh refs/heads
 	set rh_len [expr {[string length $rh] + 1}]
 	set all_heads [list]
-	set fd [git_read for-each-ref --format=%(refname) $rh]
+	set fd [git_read [list for-each-ref --format=%(refname) $rh]]
 	fconfigure $fd -translation binary -encoding utf-8
 	while {[gets $fd line] > 0} {
 		if {!$some_heads_tracking || ![is_tracking_branch $line]} {
@@ -21,10 +21,10 @@
 
 proc load_all_tags {} {
 	set all_tags [list]
-	set fd [git_read for-each-ref \
+	set fd [git_read [list for-each-ref \
 		--sort=-taggerdate \
 		--format=%(refname) \
-		refs/tags]
+		refs/tags]]
 	fconfigure $fd -translation binary -encoding utf-8
 	while {[gets $fd line] > 0} {
 		if {![regsub ^refs/tags/ $line {} name]} continue
diff -Nru git-2.50.0/git-gui/lib/browser.tcl git-2.50.1/git-gui/lib/browser.tcl
--- git-2.50.0/git-gui/lib/browser.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/browser.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -196,7 +196,7 @@
 	lappend browser_stack [list $tree_id $name]
 	$w conf -state disabled
 
-	set fd [git_read ls-tree -z $tree_id]
+	set fd [git_read [list ls-tree -z $tree_id]]
 	fconfigure $fd -blocking 0 -translation binary -encoding utf-8
 	fileevent $fd readable [cb _read $fd]
 }
diff -Nru git-2.50.0/git-gui/lib/checkout_op.tcl git-2.50.1/git-gui/lib/checkout_op.tcl
--- git-2.50.0/git-gui/lib/checkout_op.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/checkout_op.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -304,12 +304,12 @@
 		_readtree $this
 	} else {
 		ui_status [mc "Refreshing file status..."]
-		set fd [git_read update-index \
+		set fd [git_read [list update-index \
 			-q \
 			--unmerged \
 			--ignore-missing \
 			--refresh \
-			]
+			]]
 		fconfigure $fd -blocking 0 -translation binary
 		fileevent $fd readable [cb _refresh_wait $fd]
 	}
@@ -345,14 +345,15 @@
 		[mc "Updating working directory to '%s'..." [_name $this]] \
 		[mc "files checked out"]]
 
-	set fd [git_read --stderr read-tree \
+	set fd [git_read [list read-tree \
 		-m \
 		-u \
 		-v \
 		--exclude-per-directory=.gitignore \
 		$HEAD \
 		$new_hash \
-		]
+		] \
+		[list 2>@1]]
 	fconfigure $fd -blocking 0 -translation binary
 	fileevent $fd readable [cb _readtree_wait $fd $status_bar_operation]
 }
@@ -510,18 +511,8 @@
 	delete_this
 }
 
-git-version proc _detach_HEAD {log new} {
-	>= 1.5.3 {
-		git update-ref --no-deref -m $log HEAD $new
-	}
-	default {
-		set p [gitdir HEAD]
-		file delete $p
-		set fd [open $p w]
-		fconfigure $fd -translation lf -encoding utf-8
-		puts $fd $new
-		close $fd
-	}
+proc _detach_HEAD {log new} {
+	git update-ref --no-deref -m $log HEAD $new
 }
 
 method _confirm_reset {cur} {
@@ -582,7 +573,7 @@
 	pack $w.buttons.cancel -side right -padx 5
 	pack $w.buttons -side bottom -fill x -pady 10 -padx 10
 
-	set fd [git_read rev-list --pretty=oneline $cur ^$new_hash]
+	set fd [git_read [list rev-list --pretty=oneline $cur ^$new_hash]]
 	while {[gets $fd line] > 0} {
 		set abbr [string range $line 0 7]
 		set subj [string range $line 41 end]
diff -Nru git-2.50.0/git-gui/lib/choose_repository.tcl git-2.50.1/git-gui/lib/choose_repository.tcl
--- git-2.50.0/git-gui/lib/choose_repository.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/choose_repository.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -641,8 +641,8 @@
 			set pwd [pwd]
 			if {[catch {
 				file mkdir [gitdir objects info]
-				set f_in [open [file join $objdir info alternates] r]
-				set f_cp [open [gitdir objects info alternates] w]
+				set f_in [safe_open_file [file join $objdir info alternates] r]
+				set f_cp [safe_open_file [gitdir objects info alternates] w]
 				fconfigure $f_in -translation binary -encoding binary
 				fconfigure $f_cp -translation binary -encoding binary
 				cd $objdir
@@ -727,7 +727,7 @@
 			[cb _do_clone_tags]
 	}
 	shared {
-		set fd [open [gitdir objects info alternates] w]
+		set fd [safe_open_file [gitdir objects info alternates] w]
 		fconfigure $fd -translation binary
 		puts $fd $objdir
 		close $fd
@@ -760,8 +760,8 @@
 	}
 	foreach p $tocopy {
 		if {[catch {
-				set f_in [open [file join $objdir $p] r]
-				set f_cp [open [file join .git objects $p] w]
+				set f_in [safe_open_file [file join $objdir $p] r]
+				set f_cp [safe_open_file [file join .git objects $p] w]
 				fconfigure $f_in -translation binary -encoding binary
 				fconfigure $f_cp -translation binary -encoding binary
 
@@ -818,12 +818,12 @@
 		error_popup [mc "Not a Git repository: %s" [file tail $origin_url]]
 		return 0
 	}
-	set fd_in [git_read for-each-ref \
+	set fd_in [git_read [list for-each-ref \
 		--tcl \
-		{--format=list %(refname) %(objectname) %(*objectname)}]
+		{--format=list %(refname) %(objectname) %(*objectname)}]]
 	cd $pwd
 
-	set fd [open [gitdir packed-refs] w]
+	set fd [safe_open_file [gitdir packed-refs] w]
 	fconfigure $fd -translation binary
 	puts $fd "# pack-refs with: peeled"
 	while {[gets $fd_in line] >= 0} {
@@ -877,7 +877,7 @@
 
 		set HEAD {}
 		if {[file exists [gitdir FETCH_HEAD]]} {
-			set fd [open [gitdir FETCH_HEAD] r]
+			set fd [safe_open_file [gitdir FETCH_HEAD] r]
 			while {[gets $fd line] >= 0} {
 				if {[regexp "^(.{40})\t\t" $line line HEAD]} {
 					break
@@ -953,13 +953,14 @@
 		[mc "files"]]
 
 	set readtree_err {}
-	set fd [git_read --stderr read-tree \
+	set fd [git_read [list read-tree \
 		-m \
 		-u \
 		-v \
 		HEAD \
 		HEAD \
-		]
+		] \
+		[list 2>@1]]
 	fconfigure $fd -blocking 0 -translation binary
 	fileevent $fd readable [cb _readtree_wait $fd]
 }
diff -Nru git-2.50.0/git-gui/lib/choose_rev.tcl git-2.50.1/git-gui/lib/choose_rev.tcl
--- git-2.50.0/git-gui/lib/choose_rev.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/choose_rev.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -146,14 +146,14 @@
 	append fmt { %(*subject)}
 	append fmt {]}
 	set all_refn [list]
-	set fr_fd [git_read for-each-ref \
+	set fr_fd [git_read [list for-each-ref \
 		--tcl \
 		--sort=-taggerdate \
 		--format=$fmt \
 		refs/heads \
 		refs/remotes \
 		refs/tags \
-		]
+		]]
 	fconfigure $fr_fd -translation lf -encoding utf-8
 	while {[gets $fr_fd line] > 0} {
 		set line [eval $line]
@@ -176,7 +176,7 @@
 	close $fr_fd
 
 	if {$unmerged_only} {
-		set fr_fd [git_read rev-list --all ^$::HEAD]
+		set fr_fd [git_read [list rev-list --all ^$::HEAD]]
 		while {[gets $fr_fd sha1] > 0} {
 			if {[catch {set rlst $cmt_refn($sha1)}]} continue
 			foreach refn $rlst {
@@ -579,7 +579,7 @@
 
 	set last {}
 	if {[catch {set last [file mtime [gitdir $name]]}]
-	&& ![catch {set g [open [gitdir logs $name] r]}]} {
+	&& ![catch {set g [safe_open_file [gitdir logs $name] r]}]} {
 		fconfigure $g -translation binary
 		while {[gets $g line] >= 0} {
 			if {[regexp {> ([1-9][0-9]*) } $line line when]} {
diff -Nru git-2.50.0/git-gui/lib/commit.tcl git-2.50.1/git-gui/lib/commit.tcl
--- git-2.50.0/git-gui/lib/commit.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/commit.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -27,7 +27,7 @@
 	if {[catch {
 			set name ""
 			set email ""
-			set fd [git_read cat-file commit $curHEAD]
+			set fd [git_read [list cat-file commit $curHEAD]]
 			fconfigure $fd -encoding binary -translation lf
 			# By default commits are assumed to be in utf-8
 			set enc utf-8
@@ -236,7 +236,7 @@
 	# -- Build the message file.
 	#
 	set msg_p [gitdir GITGUI_EDITMSG]
-	set msg_wt [open $msg_p w]
+	set msg_wt [safe_open_file $msg_p w]
 	fconfigure $msg_wt -translation lf
 	setup_commit_encoding $msg_wt
 	puts $msg_wt $msg
@@ -336,7 +336,7 @@
 
 proc commit_writetree {curHEAD msg_p} {
 	ui_status [mc "Committing changes..."]
-	set fd_wt [git_read write-tree]
+	set fd_wt [git_read [list write-tree]]
 	fileevent $fd_wt readable \
 		[list commit_committree $fd_wt $curHEAD $msg_p]
 }
@@ -361,7 +361,7 @@
 	# -- Verify this wasn't an empty change.
 	#
 	if {$commit_type eq {normal}} {
-		set fd_ot [git_read cat-file commit $PARENT]
+		set fd_ot [git_read [list cat-file commit $PARENT]]
 		fconfigure $fd_ot -encoding binary -translation lf
 		set old_tree [gets $fd_ot]
 		close $fd_ot
@@ -399,8 +399,8 @@
 	foreach p [concat $PARENT $MERGE_HEAD] {
 		lappend cmd -p $p
 	}
-	lappend cmd <$msg_p
-	if {[catch {set cmt_id [eval git $cmd]} err]} {
+	set msgtxt [list <$msg_p]
+	if {[catch {set cmt_id [git_redir $cmd $msgtxt]} err]} {
 		catch {file delete $msg_p}
 		error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
 		ui_status [mc "Commit failed."]
@@ -420,7 +420,7 @@
 	if {$commit_type ne {normal}} {
 		append reflogm " ($commit_type)"
 	}
-	set msg_fd [open $msg_p r]
+	set msg_fd [safe_open_file $msg_p r]
 	setup_commit_encoding $msg_fd 1
 	gets $msg_fd subject
 	close $msg_fd
diff -Nru git-2.50.0/git-gui/lib/console.tcl git-2.50.1/git-gui/lib/console.tcl
--- git-2.50.0/git-gui/lib/console.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/console.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -92,10 +92,9 @@
 
 method exec {cmd {after {}}} {
 	if {[lindex $cmd 0] eq {git}} {
-		set fd_f [eval git_read --stderr [lrange $cmd 1 end]]
+		set fd_f [git_read [lrange $cmd 1 end] [list 2>@1]]
 	} else {
-		lappend cmd 2>@1
-		set fd_f [_open_stdout_stderr $cmd]
+		set fd_f [safe_open_command $cmd [list 2>@1]]
 	}
 	fconfigure $fd_f -blocking 0 -translation binary -encoding [encoding system]
 	fileevent $fd_f readable [cb _read $fd_f $after]
diff -Nru git-2.50.0/git-gui/lib/database.tcl git-2.50.1/git-gui/lib/database.tcl
--- git-2.50.0/git-gui/lib/database.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/database.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -3,7 +3,7 @@
 
 proc do_stats {} {
 	global use_ttk NS
-	set fd [git_read count-objects -v]
+	set fd [git_read [list count-objects -v]]
 	while {[gets $fd line] > 0} {
 		if {[regexp {^([^:]+): (\d+)$} $line _ name value]} {
 			set stats($name) $value
diff -Nru git-2.50.0/git-gui/lib/diff.tcl git-2.50.1/git-gui/lib/diff.tcl
--- git-2.50.0/git-gui/lib/diff.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/diff.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -191,7 +191,7 @@
 					set sz [string length $content]
 				}
 				file {
-					set fd [open $path r]
+					set fd [safe_open_file $path r]
 					fconfigure $fd \
 						-eofchar {} \
 						-encoding [get_path_encoding $path]
@@ -215,7 +215,7 @@
 			$ui_diff insert end \
 				"* [mc "Git Repository (subproject)"]\n" \
 				d_info
-		} elseif {![catch {set type [exec file $path]}]} {
+		} elseif {![catch {set type [safe_exec [list file $path]]}]} {
 			set n [string length $path]
 			if {[string equal -length $n $path $type]} {
 				set type [string range $type $n end]
@@ -327,7 +327,7 @@
 		}
 	}
 
-	if {[catch {set fd [eval git_read --nice $cmd]} err]} {
+	if {[catch {set fd [git_read_nice $cmd]} err]} {
 		set diff_active 0
 		unlock_index
 		ui_status [mc "Unable to display %s" [escape_path $path]]
@@ -603,7 +603,7 @@
 
 	if {[catch {
 		set enc [get_path_encoding $current_diff_path]
-		set p [eval git_write $apply_cmd]
+		set p [git_write $apply_cmd]
 		fconfigure $p -translation binary -encoding $enc
 		puts -nonewline $p $wholepatch
 		close $p} err]} {
@@ -839,7 +839,7 @@
 
 	if {[catch {
 		set enc [get_path_encoding $current_diff_path]
-		set p [eval git_write $apply_cmd]
+		set p [git_write $apply_cmd]
 		fconfigure $p -translation binary -encoding $enc
 		puts -nonewline $p $current_diff_header
 		puts -nonewline $p $wholepatch
@@ -876,7 +876,7 @@
 
 	if {[catch {
 		set enc $last_revert_enc
-		set p [eval git_write $apply_cmd]
+		set p [git_write $apply_cmd]
 		fconfigure $p -translation binary -encoding $enc
 		puts -nonewline $p $last_revert
 		close $p} err]} {
diff -Nru git-2.50.0/git-gui/lib/index.tcl git-2.50.1/git-gui/lib/index.tcl
--- git-2.50.0/git-gui/lib/index.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/index.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -75,7 +75,7 @@
 	if {$batch > 25} {set batch 25}
 
 	set status_bar_operation [$::main_status start $msg [mc "files"]]
-	set fd [git_write update-index -z --index-info]
+	set fd [git_write [list update-index -z --index-info]]
 	fconfigure $fd \
 		-blocking 0 \
 		-buffering full \
@@ -144,7 +144,7 @@
 	if {$batch > 25} {set batch 25}
 
 	set status_bar_operation [$::main_status start $msg [mc "files"]]
-	set fd [git_write update-index --add --remove -z --stdin]
+	set fd [git_write [list update-index --add --remove -z --stdin]]
 	fconfigure $fd \
 		-blocking 0 \
 		-buffering full \
@@ -218,13 +218,13 @@
 	if {$batch > 25} {set batch 25}
 
 	set status_bar_operation [$::main_status start $msg [mc "files"]]
-	set fd [git_write checkout-index \
+	set fd [git_write [list checkout-index \
 		--index \
 		--quiet \
 		--force \
 		-z \
 		--stdin \
-		]
+		]]
 	fconfigure $fd \
 		-blocking 0 \
 		-buffering full \
diff -Nru git-2.50.0/git-gui/lib/merge.tcl git-2.50.1/git-gui/lib/merge.tcl
--- git-2.50.0/git-gui/lib/merge.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/merge.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -93,7 +93,7 @@
 	set spec [$w_rev get_tracking_branch]
 	set cmit [$w_rev get_commit]
 
-	set fh [open [gitdir FETCH_HEAD] w]
+	set fh [safe_open_file [gitdir FETCH_HEAD] w]
 	fconfigure $fh -translation lf
 	if {$spec eq {}} {
 		set remote .
@@ -118,7 +118,7 @@
 		set cmd [list git]
 		lappend cmd merge
 		lappend cmd --strategy=recursive
-		lappend cmd [git fmt-merge-msg <[gitdir FETCH_HEAD]]
+		lappend cmd [git_redir [list fmt-merge-msg] [list <[gitdir FETCH_HEAD]]]
 		lappend cmd HEAD
 		lappend cmd $name
 	}
@@ -239,7 +239,7 @@
 	}
 
 	if {[ask_popup $op_question] eq {yes}} {
-		set fd [git_read --stderr read-tree --reset -u -v HEAD]
+		set fd [git_read [list read-tree --reset -u -v HEAD] [list 2>@1]]
 		fconfigure $fd -blocking 0 -translation binary
 		set status_bar_operation [$::main_status \
 			start \
diff -Nru git-2.50.0/git-gui/lib/mergetool.tcl git-2.50.1/git-gui/lib/mergetool.tcl
--- git-2.50.0/git-gui/lib/mergetool.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/mergetool.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -88,7 +88,7 @@
 	set merge_stages(3) {}
 	set merge_stages_buf {}
 
-	set merge_stages_fd [eval git_read ls-files -u -z -- {$path}]
+	set merge_stages_fd [git_read [list ls-files -u -z -- $path]]
 
 	fconfigure $merge_stages_fd -blocking 0 -translation binary -encoding binary
 	fileevent $merge_stages_fd readable [list read_merge_stages $merge_stages_fd $cont]
@@ -310,7 +310,7 @@
 	foreach fname $stages {
 		if {$merge_stages($i) eq {}} {
 			file delete $fname
-			catch { close [open $fname w] }
+			catch { close [safe_open_file $fname w] }
 		} else {
 			# A hack to support autocrlf properly
 			git checkout-index -f --stage=$i -- $target
@@ -360,9 +360,9 @@
 
 	# Force redirection to avoid interpreting output on stderr
 	# as an error, and launch the tool
-	lappend cmdline {2>@1}
+	set redir [list {2>@1}]
 
-	if {[catch { set mtool_fd [_open_stdout_stderr $cmdline] } err]} {
+	if {[catch { set mtool_fd [safe_open_command $cmdline $redir] } err]} {
 		delete_temp_files $mtool_tmpfiles
 		error_popup [mc "Could not start the merge tool:\n\n%s" $err]
 		return
diff -Nru git-2.50.0/git-gui/lib/remote_branch_delete.tcl git-2.50.1/git-gui/lib/remote_branch_delete.tcl
--- git-2.50.0/git-gui/lib/remote_branch_delete.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/remote_branch_delete.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -308,7 +308,7 @@
 		set full_list [list]
 		set head_cache($cache) [list]
 		set full_cache($cache) [list]
-		set active_ls [git_read ls-remote $uri]
+		set active_ls [git_read [list ls-remote $uri]]
 		fconfigure $active_ls \
 			-blocking 0 \
 			-translation lf \
diff -Nru git-2.50.0/git-gui/lib/remote.tcl git-2.50.1/git-gui/lib/remote.tcl
--- git-2.50.0/git-gui/lib/remote.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/remote.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -32,7 +32,7 @@
 	}
 
 	if {$pat ne {}} {
-		set fd [eval git_read for-each-ref --format=%(refname) $cmd]
+		set fd [git_read [concat for-each-ref --format=%(refname) $cmd]]
 		while {[gets $fd n] > 0} {
 			foreach spec $pat {
 				set dst [string range [lindex $spec 0] 0 end-2]
@@ -75,7 +75,7 @@
 
 		foreach name $all_remotes {
 			catch {
-				set fd [open [file join $rm_dir $name] r]
+				set fd [safe_open_file [file join $rm_dir $name] r]
 				while {[gets $fd line] >= 0} {
 					if {[regexp {^URL:[ 	]*(.+)$} $line line url]} {
 						set remote_url($name) $url
@@ -145,7 +145,7 @@
 		}
 	} else {
 		catch {
-			set fd [open [gitdir remotes $r] r]
+			set fd [safe_open_file [gitdir remotes $r] r]
 			while {[gets $fd n] >= 0} {
 				if {[regexp {^Pull:[ \t]*([^:]+):} $n]} {
 					set enable 1
@@ -182,7 +182,7 @@
 		}
 	} else {
 		catch {
-			set fd [open [gitdir remotes $r] r]
+			set fd [safe_open_file [gitdir remotes $r] r]
 			while {[gets $fd n] >= 0} {
 				if {[regexp {^Push:[ \t]*([^:]+):} $n]} {
 					set enable 1
diff -Nru git-2.50.0/git-gui/lib/shortcut.tcl git-2.50.1/git-gui/lib/shortcut.tcl
--- git-2.50.0/git-gui/lib/shortcut.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/shortcut.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -12,7 +12,7 @@
 			set fn ${fn}.lnk
 		}
 		# Use git-gui.exe if available (ie: git-for-windows)
-		set cmdLine [auto_execok git-gui.exe]
+		set cmdLine [list [_which git-gui]]
 		if {$cmdLine eq {}} {
 			set cmdLine [list [info nameofexecutable] \
 							 [file normalize $::argv0]]
@@ -30,8 +30,8 @@
 	global argv0 _gitworktree oguilib
 
 	if {[catch {
-		set desktop [exec cygpath \
-			--desktop]
+		set desktop [safe_exec [list cygpath \
+			--desktop]]
 		}]} {
 			set desktop .
 	}
@@ -50,14 +50,14 @@
 					"CHERE_INVOKING=1 \
 					source /etc/profile; \
 					git gui"}
-				exec /bin/mkshortcut.exe \
+				safe_exec [list /bin/mkshortcut.exe \
 					--arguments $shargs \
 					--desc "git-gui on $repodir" \
 					--icon $oguilib/git-gui.ico \
 					--name $fn \
 					--show min \
 					--workingdir $repodir \
-					/bin/sh.exe
+					/bin/sh.exe]
 			} err]} {
 			error_popup [strcat [mc "Cannot write shortcut:"] "\n\n$err"]
 		}
@@ -83,7 +83,7 @@
 
 				file mkdir $MacOS
 
-				set fd [open [file join $Contents Info.plist] w]
+				set fd [safe_open_file [file join $Contents Info.plist] w]
 				puts $fd {<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd";>
 <plist version="1.0">
@@ -108,7 +108,7 @@
 </plist>}
 				close $fd
 
-				set fd [open $exe w]
+				set fd [safe_open_file $exe w]
 				puts $fd "#!/bin/sh"
 				foreach name [lsort [array names env]] {
 					set value $env($name)
diff -Nru git-2.50.0/git-gui/lib/sshkey.tcl git-2.50.1/git-gui/lib/sshkey.tcl
--- git-2.50.0/git-gui/lib/sshkey.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/sshkey.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -7,7 +7,7 @@
 		~/.ssh/id_rsa.pub ~/.ssh/identity.pub
 	} {
 		if {[file exists $name]} {
-			set fh    [open $name r]
+			set fh    [safe_open_file $name r]
 			set cont  [read $fh]
 			close $fh
 			return [list $name $cont]
@@ -83,9 +83,10 @@
 	set sshkey_title [mc "Generating..."]
 	$w.header.gen configure -state disabled
 
-	set cmdline [list sh -c {echo | ssh-keygen -q -t rsa -f ~/.ssh/id_rsa 2>&1}]
+	set cmdline [list [shellpath] -c \
+		{echo | ssh-keygen -q -t rsa -f ~/.ssh/id_rsa 2>&1}]
 
-	if {[catch { set sshkey_fd [_open_stdout_stderr $cmdline] } err]} {
+	if {[catch { set sshkey_fd [safe_open_command $cmdline] } err]} {
 		error_popup [mc "Could not start ssh-keygen:\n\n%s" $err]
 		return
 	}
diff -Nru git-2.50.0/git-gui/lib/tools.tcl git-2.50.1/git-gui/lib/tools.tcl
--- git-2.50.0/git-gui/lib/tools.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/tools.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -110,14 +110,14 @@
 
 	set cmdline $repo_config(guitool.$fullname.cmd)
 	if {[is_config_true "guitool.$fullname.noconsole"]} {
-		tools_run_silent [list sh -c $cmdline] \
+		tools_run_silent [list [shellpath] -c $cmdline] \
 				 [list tools_complete $fullname {}]
 	} else {
 		regsub {/} $fullname { / } title
 		set w [console::new \
 			[mc "Tool: %s" $title] \
 			[mc "Running: %s" $cmdline]]
-		console::exec $w [list sh -c $cmdline] \
+		console::exec $w [list [shellpath] -c $cmdline] \
 				 [list tools_complete $fullname $w]
 	}
 
@@ -130,8 +130,7 @@
 }
 
 proc tools_run_silent {cmd after} {
-	lappend cmd 2>@1
-	set fd [_open_stdout_stderr $cmd]
+	set fd [safe_open_command $cmd [list 2>@1]]
 
 	fconfigure $fd -blocking 0 -translation binary
 	fileevent $fd readable [list tools_consume_input $fd $after]
diff -Nru git-2.50.0/git-gui/lib/win32.tcl git-2.50.1/git-gui/lib/win32.tcl
--- git-2.50.0/git-gui/lib/win32.tcl	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/git-gui/lib/win32.tcl	2025-06-16 08:11:33.000000000 +0300
@@ -2,11 +2,11 @@
 # Copyright (C) 2007 Shawn Pearce
 
 proc win32_read_lnk {lnk_path} {
-	return [exec cscript.exe \
+	return [safe_exec [list cscript.exe \
 		/E:jscript \
 		/nologo \
 		[file join $::oguilib win32_shortcut.js] \
-		$lnk_path]
+		$lnk_path]]
 }
 
 proc win32_create_lnk {lnk_path lnk_exec lnk_dir} {
@@ -15,12 +15,13 @@
 	set lnk_args [lrange $lnk_exec 1 end]
 	set lnk_exec [lindex $lnk_exec 0]
 
-	eval [list exec wscript.exe \
+	set cmd [list wscript.exe \
 		/E:jscript \
 		/nologo \
 		[file nativename [file join $oguilib win32_shortcut.js]] \
 		$lnk_path \
 		[file nativename [file join $oguilib git-gui.ico]] \
 		$lnk_dir \
-		$lnk_exec] $lnk_args
+		$lnk_exec]
+	safe_exec [concat $cmd $lnk_args]
 }
diff -Nru git-2.50.0/gitk-git/gitk git-2.50.1/gitk-git/gitk
--- git-2.50.0/gitk-git/gitk	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/gitk-git/gitk	2025-06-16 08:11:33.000000000 +0300
@@ -113,6 +113,91 @@
 
 # End of safe PATH lookup stuff
 
+# Wrap exec/open to sanitize arguments
+
+# unsafe arguments begin with redirections or the pipe or background operators
+proc is_arg_unsafe {arg} {
+    regexp {^([<|>&]|2>)} $arg
+}
+
+proc make_arg_safe {arg} {
+    if {[is_arg_unsafe $arg]} {
+        set arg [file join . $arg]
+    }
+    return $arg
+}
+
+proc make_arglist_safe {arglist} {
+    set res {}
+    foreach arg $arglist {
+        lappend res [make_arg_safe $arg]
+    }
+    return $res
+}
+
+# executes one command
+# no redirections or pipelines are possible
+# cmd is a list that specifies the command and its arguments
+# calls `exec` and returns its value
+proc safe_exec {cmd} {
+    eval exec [make_arglist_safe $cmd]
+}
+
+# executes one command with redirections
+# no pipelines are possible
+# cmd is a list that specifies the command and its arguments
+# redir is a list that specifies redirections (output, background, constant(!) commands)
+# calls `exec` and returns its value
+proc safe_exec_redirect {cmd redir} {
+    eval exec [make_arglist_safe $cmd] $redir
+}
+
+proc safe_open_file {filename flags} {
+    # a file name starting with "|" would attempt to run a process
+    # but such a file name must be treated as a relative path
+    # hide the "|" behind "./"
+    if {[string index $filename 0] eq "|"} {
+        set filename [file join . $filename]
+    }
+    open $filename $flags
+}
+
+# opens a command pipeline for reading
+# cmd is a list that specifies the command and its arguments
+# calls `open` and returns the file id
+proc safe_open_command {cmd} {
+    open |[make_arglist_safe $cmd] r
+}
+
+# opens a command pipeline for reading and writing
+# cmd is a list that specifies the command and its arguments
+# calls `open` and returns the file id
+proc safe_open_command_rw {cmd} {
+    open |[make_arglist_safe $cmd] r+
+}
+
+# opens a command pipeline for reading with redirections
+# cmd is a list that specifies the command and its arguments
+# redir is a list that specifies redirections
+# calls `open` and returns the file id
+proc safe_open_command_redirect {cmd redir} {
+    set cmd [make_arglist_safe $cmd]
+    open |[concat $cmd $redir] r
+}
+
+# opens a pipeline with several commands for reading
+# cmds is a list of lists, each of which specifies a command and its arguments
+# calls `open` and returns the file id
+proc safe_open_pipeline {cmds} {
+    set cmd {}
+    foreach subcmd $cmds {
+        set cmd [concat $cmd | [make_arglist_safe $subcmd]]
+    }
+    open $cmd r
+}
+
+# End exec/open wrappers
+
 proc hasworktree {} {
     return [expr {[exec git rev-parse --is-bare-repository] == "false" &&
                   [exec git rev-parse --is-inside-git-dir] == "false"}]
@@ -238,7 +323,7 @@
     set mlist {}
     set nr_unmerged 0
     if {[catch {
-        set fd [open "| git ls-files -u" r]
+        set fd [safe_open_command {git ls-files -u}]
     } err]} {
         show_error {} . "[mc "Couldn't get list of unmerged files:"] $err"
         exit 1
@@ -400,7 +485,7 @@
     } elseif {[lsearch -exact $revs --all] >= 0} {
         lappend revs HEAD
     }
-    if {[catch {set ids [eval exec git rev-parse $revs]} err]} {
+    if {[catch {set ids [safe_exec [concat git rev-parse $revs]]} err]} {
         # we get stdout followed by stderr in $err
         # for an unknown rev, git rev-parse echoes it and then errors out
         set errlines [split $err "\n"]
@@ -457,16 +542,6 @@
     return $ret
 }
 
-# Escapes a list of filter paths to be passed to git log via stdin. Note that
-# paths must not be quoted.
-proc escape_filter_paths {paths} {
-    set escaped [list]
-    foreach path $paths {
-        lappend escaped [string map {\\ \\\\ "\ " "\\\ "} $path]
-    }
-    return $escaped
-}
-
 # Start off a git log process and arrange to read its output
 proc start_rev_list {view} {
     global startmsecs commitidx viewcomplete curview
@@ -488,7 +563,7 @@
     set args $viewargs($view)
     if {$viewargscmd($view) ne {}} {
         if {[catch {
-            set str [exec sh -c $viewargscmd($view)]
+            set str [safe_exec [list sh -c $viewargscmd($view)]]
         } err]} {
             error_popup "[mc "Error executing --argscmd command:"] $err"
             return 0
@@ -526,10 +601,9 @@
     }
 
     if {[catch {
-        set fd [open [concat | git log --no-color -z --pretty=raw $show_notes \
-                        --parents --boundary $args --stdin \
-                        "<<[join [concat $revs "--" \
-                                [escape_filter_paths $files]] "\\n"]"] r]
+        set fd [safe_open_command_redirect [concat git log --no-color -z --pretty=raw $show_notes \
+                        --parents --boundary $args --stdin] \
+                        [list "<<[join [concat $revs "--" $files] "\n"]"]]
     } err]} {
         error_popup "[mc "Error executing git log:"] $err"
         return 0
@@ -563,9 +637,9 @@
         set pid [pid $fd]
 
         if {$::tcl_platform(platform) eq {windows}} {
-            exec taskkill /pid $pid
+            safe_exec [list taskkill /pid $pid]
         } else {
-            exec kill $pid
+            safe_exec [list kill $pid]
         }
     }
     catch {close $fd}
@@ -680,11 +754,9 @@
         set args $vorigargs($view)
     }
     if {[catch {
-        set fd [open [concat | git log --no-color -z --pretty=raw $show_notes \
-                        --parents --boundary $args --stdin \
-                        "<<[join [concat $revs "--" \
-                                [escape_filter_paths \
-                                        $vfilelimit($view)]] "\\n"]"] r]
+        set fd [safe_open_command_redirect [concat git log --no-color -z --pretty=raw $show_notes \
+                        --parents --boundary $args --stdin] \
+                        [list "<<[join [concat $revs "--" $vfilelimit($view)] "\n"]"]]
     } err]} {
         error_popup "[mc "Error executing git log:"] $err"
         return
@@ -1651,8 +1723,8 @@
             # and if we already know about it, using the rewritten
             # parent as a substitute parent for $id's children.
             if {![catch {
-                set rwid [exec git rev-list --first-parent --max-count=1 \
-                              $id -- $vfilelimit($view)]
+                set rwid [safe_exec [list git rev-list --first-parent --max-count=1 \
+                              $id -- $vfilelimit($view)]]
             }]} {
                 if {$rwid ne {} && [info exists varcid($view,$rwid)]} {
                     # use $rwid in place of $id
@@ -1772,7 +1844,7 @@
     global tclencoding
 
     # Invoke git-log to handle automatic encoding conversion
-    set fd [open [concat | git log --no-color --pretty=raw -1 $id] r]
+    set fd [safe_open_command [concat git log --no-color --pretty=raw -1 $id]]
     # Read the results using i18n.logoutputencoding
     fconfigure $fd -translation lf -eofchar {}
     if {$tclencoding != {}} {
@@ -1908,7 +1980,7 @@
     foreach v {tagids idtags headids idheads otherrefids idotherrefs} {
         unset -nocomplain $v
     }
-    set refd [open [list | git show-ref -d] r]
+    set refd [safe_open_command [list git show-ref -d]]
     if {$tclencoding != {}} {
         fconfigure $refd -encoding $tclencoding
     }
@@ -1956,7 +2028,7 @@
     set selectheadid {}
     if {$selecthead ne {}} {
         catch {
-            set selectheadid [exec git rev-parse --verify $selecthead]
+            set selectheadid [safe_exec [list git rev-parse --verify $selecthead]]
         }
     }
 }
@@ -2220,7 +2292,7 @@
             {mc "Reread re&ferences" command rereadrefs}
             {mc "&List references" command showrefs -accelerator F2}
             {xx "" separator}
-            {mc "Start git &gui" command {exec git gui &}}
+            {mc "Start git &gui" command {safe_exec_redirect [list git gui] [list &]}}
             {xx "" separator}
             {mc "&Quit" command doquit -accelerator Meta1-Q}
         }}
@@ -3007,7 +3079,7 @@
     set remove_tmp 0
     if {[catch {
         set try_count 0
-        while {[catch {set f [open $config_file_tmp {WRONLY CREAT EXCL}]}]} {
+        while {[catch {set f [safe_open_file $config_file_tmp {WRONLY CREAT EXCL}]}]} {
             if {[incr try_count] > 50} {
                 error "Unable to write config file: $config_file_tmp exists"
             }
@@ -3723,7 +3795,7 @@
             set tmpdir $gitdir
         }
         set gitktmpformat [file join $tmpdir ".gitk-tmp.XXXXXX"]
-        if {[catch {set gitktmpdir [exec mktemp -d $gitktmpformat]}]} {
+        if {[catch {set gitktmpdir [safe_exec [list mktemp -d $gitktmpformat]]}]} {
             set gitktmpdir [file join $gitdir [format ".gitk-tmp.%s" [pid]]]
         }
         if {[catch {file mkdir $gitktmpdir} err]} {
@@ -3745,7 +3817,7 @@
 proc save_file_from_commit {filename output what} {
     global nullfile
 
-    if {[catch {exec git show $filename -- > $output} err]} {
+    if {[catch {safe_exec_redirect [list git show $filename --] [list > $output]} err]} {
         if {[string match "fatal: bad revision *" $err]} {
             return $nullfile
         }
@@ -3810,7 +3882,7 @@
 
     if {$difffromfile ne {} && $difftofile ne {}} {
         set cmd [list [shellsplit $extdifftool] $difffromfile $difftofile]
-        if {[catch {set fl [open |$cmd r]} err]} {
+        if {[catch {set fl [safe_open_command $cmd]} err]} {
             file delete -force $diffdir
             error_popup "$extdifftool: [mc "command failed:"] $err"
         } else {
@@ -3914,7 +3986,7 @@
 # Find the SHA1 ID of the blob for file $fname in the index
 # at stage 0 or 2
 proc index_sha1 {fname} {
-    set f [open [list | git ls-files -s $fname] r]
+    set f [safe_open_command [list git ls-files -s $fname]]
     while {[gets $f line] >= 0} {
         set info [lindex [split $line "\t"] 0]
         set stage [lindex $info 2]
@@ -3974,7 +4046,7 @@
     # being given an absolute path...
     set f [make_relative $f]
     lappend cmdline $base_commit $f
-    if {[catch {eval exec $cmdline &} err]} {
+    if {[catch {safe_exec_redirect $cmdline [list &]} err]} {
         error_popup "[mc "git gui blame: command failed:"] $err"
     }
 }
@@ -4002,7 +4074,7 @@
                 # must be a merge in progress...
                 if {[catch {
                     # get the last line from .git/MERGE_HEAD
-                    set f [open [file join $gitdir MERGE_HEAD] r]
+                    set f [safe_open_file [file join $gitdir MERGE_HEAD] r]
                     set id [lindex [split [read $f] "\n"] end-1]
                     close $f
                 } err]} {
@@ -4025,19 +4097,17 @@
         }
         set line [lindex $h 1]
     }
-    set blameargs {}
-    if {$from_index ne {}} {
-        lappend blameargs | git cat-file blob $from_index
-    }
-    lappend blameargs | git blame -p -L$line,+1
+    set blamefile [file join $cdup $flist_menu_file]
     if {$from_index ne {}} {
-        lappend blameargs --contents -
+        set blameargs [list \
+            [list git cat-file blob $from_index] \
+            [list git blame -p -L$line,+1 --contents - -- $blamefile]]
     } else {
-        lappend blameargs $id
+        set blameargs [list \
+            [list git blame -p -L$line,+1 $id -- $blamefile]]
     }
-    lappend blameargs -- [file join $cdup $flist_menu_file]
     if {[catch {
-        set f [open $blameargs r]
+        set f [safe_open_pipeline $blameargs]
     } err]} {
         error_popup [mc "Couldn't start git blame: %s" $err]
         return
@@ -4962,8 +5032,8 @@
         # must be "containing:", i.e. we're searching commit info
         return
     }
-    set cmd [concat | git diff-tree -r -s --stdin $gdtargs]
-    set filehighlight [open $cmd r+]
+    set cmd [concat git diff-tree -r -s --stdin $gdtargs]
+    set filehighlight [safe_open_command_rw $cmd]
     fconfigure $filehighlight -blocking 0
     filerun $filehighlight readfhighlight
     set fhl_list {}
@@ -5392,8 +5462,8 @@
     global viewmainheadid vfilelimit viewinstances mainheadid
 
     catch {
-        set rfd [open [concat | git rev-list -1 $mainheadid \
-                           -- $vfilelimit($view)] r]
+        set rfd [safe_open_command [concat git rev-list -1 $mainheadid \
+                           -- $vfilelimit($view)]]
         set j [reg_instance $rfd]
         lappend viewinstances($view) $j
         fconfigure $rfd -blocking 0
@@ -5458,14 +5528,14 @@
     if {!$showlocalchanges || !$hasworktree} return
     incr lserial
     if {[package vcompare $git_version "1.7.2"] >= 0} {
-        set cmd "|git diff-index --cached --ignore-submodules=dirty HEAD"
+        set cmd "git diff-index --cached --ignore-submodules=dirty HEAD"
     } else {
-        set cmd "|git diff-index --cached HEAD"
+        set cmd "git diff-index --cached HEAD"
     }
     if {$vfilelimit($curview) ne {}} {
         set cmd [concat $cmd -- $vfilelimit($curview)]
     }
-    set fd [open $cmd r]
+    set fd [safe_open_command $cmd]
     fconfigure $fd -blocking 0
     set i [reg_instance $fd]
     filerun $fd [list readdiffindex $fd $lserial $i]
@@ -5490,11 +5560,11 @@
     }
 
     # now see if there are any local changes not checked in to the index
-    set cmd "|git diff-files"
+    set cmd "git diff-files"
     if {$vfilelimit($curview) ne {}} {
         set cmd [concat $cmd -- $vfilelimit($curview)]
     }
-    set fd [open $cmd r]
+    set fd [safe_open_command $cmd]
     fconfigure $fd -blocking 0
     set i [reg_instance $fd]
     filerun $fd [list readdifffiles $fd $serial $i]
@@ -7283,8 +7353,8 @@
     global web_browser
 
     if {$web_browser eq {}} return
-    # Use eval here in case $web_browser is a command plus some arguments
-    if {[catch {eval exec $web_browser [list $url] &} err]} {
+    # Use concat here in case $web_browser is a command plus some arguments
+    if {[catch {safe_exec_redirect [concat $web_browser [list $url]] [list &]} err]} {
         error_popup "[mc "Error starting web browser:"] $err"
     }
 }
@@ -7790,13 +7860,13 @@
     if {![info exists treefilelist($id)]} {
         if {![info exists treepending]} {
             if {$id eq $nullid} {
-                set cmd [list | git ls-files]
+                set cmd [list git ls-files]
             } elseif {$id eq $nullid2} {
-                set cmd [list | git ls-files --stage -t]
+                set cmd [list git ls-files --stage -t]
             } else {
-                set cmd [list | git ls-tree -r $id]
+                set cmd [list git ls-tree -r $id]
             }
-            if {[catch {set gtf [open $cmd r]}]} {
+            if {[catch {set gtf [safe_open_command $cmd]}]} {
                 return
             }
             set treepending $id
@@ -7860,13 +7930,13 @@
         return
     }
     if {$diffids eq $nullid} {
-        if {[catch {set bf [open $f r]} err]} {
+        if {[catch {set bf [safe_open_file $f r]} err]} {
             puts "oops, can't read $f: $err"
             return
         }
     } else {
         set blob [lindex $treeidlist($diffids) $i]
-        if {[catch {set bf [open [concat | git cat-file blob $blob] r]} err]} {
+        if {[catch {set bf [safe_open_command [concat git cat-file blob $blob]]} err]} {
             puts "oops, error reading blob $blob: $err"
             return
         }
@@ -8016,7 +8086,7 @@
     if {$i >= 0} {
         if {[llength $ids] > 1 && $j < 0} {
             # comparing working directory with some specific revision
-            set cmd [concat | git diff-index $flags]
+            set cmd [concat git diff-index $flags]
             if {$i == 0} {
                 lappend cmd -R [lindex $ids 1]
             } else {
@@ -8024,7 +8094,7 @@
             }
         } else {
             # comparing working directory with index
-            set cmd [concat | git diff-files $flags]
+            set cmd [concat git diff-files $flags]
             if {$j == 1} {
                 lappend cmd -R
             }
@@ -8033,7 +8103,7 @@
         if {[package vcompare $git_version "1.7.2"] >= 0} {
             set flags "$flags --ignore-submodules=dirty"
         }
-        set cmd [concat | git diff-index --cached $flags]
+        set cmd [concat git diff-index --cached $flags]
         if {[llength $ids] > 1} {
             # comparing index with specific revision
             if {$j == 0} {
@@ -8049,7 +8119,7 @@
         if {$log_showroot} {
             lappend flags --root
         }
-        set cmd [concat | git diff-tree -r $flags $ids]
+        set cmd [concat git diff-tree -r $flags $ids]
     }
     return $cmd
 }
@@ -8061,7 +8131,7 @@
     if {$limitdiffs && $vfilelimit($curview) ne {}} {
             set cmd [concat $cmd -- $vfilelimit($curview)]
     }
-    if {[catch {set gdtf [open $cmd r]}]} return
+    if {[catch {set gdtf [safe_open_command $cmd]}]} return
 
     set treepending $ids
     set treediff {}
@@ -8181,7 +8251,7 @@
     if {$limitdiffs && $vfilelimit($curview) ne {}} {
         set cmd [concat $cmd -- $vfilelimit($curview)]
     }
-    if {[catch {set bdf [open $cmd r]} err]} {
+    if {[catch {set bdf [safe_open_command $cmd]} err]} {
         error_popup [mc "Error getting diffs: %s" $err]
         return
     }
@@ -8899,7 +8969,7 @@
                 set id [lindex $matches 0]
             }
         } else {
-            if {[catch {set id [exec git rev-parse --verify $sha1string]}]} {
+            if {[catch {set id [safe_exec [list git rev-parse --verify $sha1string]]}]} {
                 error_popup [mc "Revision %s is not known" $sha1string]
                 return
             }
@@ -9205,10 +9275,8 @@
 
     if {![info exists patchids($id)]} {
         set cmd [diffcmd [list $id] {-p --root}]
-        # trim off the initial "|"
-        set cmd [lrange $cmd 1 end]
         if {[catch {
-            set x [eval exec $cmd | git patch-id]
+            set x [safe_exec_redirect $cmd [list | git patch-id]]
             set patchids($id) [lindex $x 0]
         }]} {
             set patchids($id) "error"
@@ -9304,14 +9372,14 @@
     set fna [file join $tmpdir "commit-[string range $a 0 7]"]
     set fnb [file join $tmpdir "commit-[string range $b 0 7]"]
     if {[catch {
-        exec git diff-tree -p --pretty $a >$fna
-        exec git diff-tree -p --pretty $b >$fnb
+        safe_exec_redirect [list git diff-tree -p --pretty $a] [list >$fna]
+        safe_exec_redirect [list git diff-tree -p --pretty $b] [list >$fnb]
     } err]} {
         error_popup [mc "Error writing commit to file: %s" $err]
         return
     }
     if {[catch {
-        set fd [open "| diff -U$diffcontext $fna $fnb" r]
+        set fd [safe_open_command "diff -U$diffcontext $fna $fnb"]
     } err]} {
         error_popup [mc "Error diffing commits: %s" $err]
         return
@@ -9451,10 +9519,7 @@
     set newid [$patchtop.tosha1 get]
     set fname [$patchtop.fname get]
     set cmd [diffcmd [list $oldid $newid] -p]
-    # trim off the initial "|"
-    set cmd [lrange $cmd 1 end]
-    lappend cmd >$fname &
-    if {[catch {eval exec $cmd} err]} {
+    if {[catch {safe_exec_redirect $cmd [list >$fname &]} err]} {
         error_popup "[mc "Error creating patch:"] $err" $patchtop
     }
     catch {destroy $patchtop}
@@ -9523,9 +9588,9 @@
     }
     if {[catch {
         if {$msg != {}} {
-            exec git tag -a -m $msg $tag $id
+            safe_exec [list git tag -a -m $msg $tag $id]
         } else {
-            exec git tag $tag $id
+            safe_exec [list git tag $tag $id]
         }
     } err]} {
         error_popup "[mc "Error creating tag:"] $err" $mktagtop
@@ -9593,7 +9658,7 @@
     if {$autosellen < 40} {
         lappend cmd --abbrev=$autosellen
     }
-    set reference [eval exec $cmd $rowmenuid]
+    set reference [safe_exec [concat $cmd $rowmenuid]]
 
     clipboard clear
     clipboard append $reference
@@ -9643,7 +9708,7 @@
     set id [$wrcomtop.sha1 get]
     set cmd "echo $id | [$wrcomtop.cmd get]"
     set fname [$wrcomtop.fname get]
-    if {[catch {exec sh -c $cmd >$fname &} err]} {
+    if {[catch {safe_exec_redirect [list sh -c $cmd] [list >$fname &]} err]} {
         error_popup "[mc "Error writing commit:"] $err" $wrcomtop
     }
     catch {destroy $wrcomtop}
@@ -9747,7 +9812,7 @@
     nowbusy newbranch
     update
     if {[catch {
-        eval exec git branch $cmdargs
+        safe_exec [concat git branch $cmdargs]
     } err]} {
         notbusy newbranch
         error_popup $err
@@ -9788,7 +9853,7 @@
     nowbusy renamebranch
     update
     if {[catch {
-        eval exec git branch $cmdargs
+        safe_exec [concat git branch $cmdargs]
     } err]} {
         notbusy renamebranch
         error_popup $err
@@ -9829,7 +9894,7 @@
         }
     }
 
-    eval exec git citool $tool_args &
+    safe_exec_redirect [concat git citool $tool_args] [list &]
 
     array unset env GIT_AUTHOR_*
     array set env $save_env
@@ -9852,7 +9917,7 @@
     update
     # Unfortunately git-cherry-pick writes stuff to stderr even when
     # no error occurs, and exec takes that as an indication of error...
-    if {[catch {exec sh -c "git cherry-pick -r $rowmenuid 2>&1"} err]} {
+    if {[catch {safe_exec [list sh -c "git cherry-pick -r $rowmenuid 2>&1"]} err]} {
         notbusy cherrypick
         if {[regexp -line \
                  {Entry '(.*)' (would be overwritten by merge|not uptodate)} \
@@ -9914,7 +9979,7 @@
     nowbusy revert [mc "Reverting"]
     update
 
-    if [catch {exec git revert --no-edit $rowmenuid} err] {
+    if [catch {safe_exec [list git revert --no-edit $rowmenuid]} err] {
         notbusy revert
         if [regexp {files would be overwritten by merge:(\n(( |\t)+[^\n]+\n)+)}\
                 $err match files] {
@@ -9990,8 +10055,8 @@
     bind $w <Visibility> "grab $w; focus $w"
     tkwait window $w
     if {!$confirm_ok} return
-    if {[catch {set fd [open \
-            [list | git reset --$resettype $rowmenuid 2>@1] r]} err]} {
+    if {[catch {set fd [safe_open_command_redirect \
+            [list git reset --$resettype $rowmenuid] [list 2>@1]]} err]} {
         error_popup $err
     } else {
         dohidelocalchanges
@@ -10062,7 +10127,7 @@
 
     # check the tree is clean first??
     set newhead $headmenuhead
-    set command [list | git checkout]
+    set command [list git checkout]
     if {[string match "remotes/*" $newhead]} {
         set remote $newhead
         set newhead [string range $newhead [expr [string last / $newhead] + 1] end]
@@ -10076,12 +10141,11 @@
     } else {
         lappend command $newhead
     }
-    lappend command 2>@1
     nowbusy checkout [mc "Checking out"]
     update
     dohidelocalchanges
     if {[catch {
-        set fd [open $command r]
+        set fd [safe_open_command_redirect $command [list 2>@1]]
     } err]} {
         notbusy checkout
         error_popup $err
@@ -10147,7 +10211,7 @@
     }
     nowbusy rmbranch
     update
-    if {[catch {exec git branch -D $head} err]} {
+    if {[catch {safe_exec [list git branch -D $head]} err]} {
         notbusy rmbranch
         error_popup $err
         return
@@ -10338,7 +10402,7 @@
         set cachedarcs 0
         set allccache [file join $gitdir "gitk.cache"]
         if {![catch {
-            set f [open $allccache r]
+            set f [safe_open_file $allccache r]
             set allcwait 1
             getcache $f
         }]} return
@@ -10347,7 +10411,7 @@
     if {$allcwait} {
         return
     }
-    set cmd [list | git rev-list --parents]
+    set cmd [list git rev-list --parents]
     set allcupdate [expr {$seeds ne {}}]
     if {!$allcupdate} {
         set ids "--all"
@@ -10375,10 +10439,11 @@
     if {$ids ne {}} {
         if {$ids eq "--all"} {
             set cmd [concat $cmd "--all"]
+            set fd [safe_open_command $cmd]
         } else {
-            set cmd [concat $cmd --stdin "<<[join $ids "\\n"]"]
+            set cmd [concat $cmd --stdin]
+            set fd [safe_open_command_redirect $cmd [list "<<[join $ids "\n"]"]]
         }
-        set fd [open $cmd r]
         fconfigure $fd -blocking 0
         incr allcommits
         nowbusy allcommits
@@ -10768,7 +10833,7 @@
     set cachearc 0
     set cachedarcs $nextarc
     catch {
-        set f [open $allccache w]
+        set f [safe_open_file $allccache w]
         puts $f [list 1 $cachedarcs]
         run writecache $f
     }
@@ -11471,7 +11536,7 @@
 
     if {![info exists cached_tagcontent($tag)]} {
         catch {
-            set cached_tagcontent($tag) [exec git cat-file -p $tag]
+            set cached_tagcontent($tag) [safe_exec [list git cat-file -p $tag]]
         }
     }
     $ctext insert end "[mc "Tag"]: $tag\n" bold
@@ -12382,7 +12447,7 @@
         set r $path_attr_cache($attr,$path)
     } else {
         set r "unspecified"
-        if {![catch {set line [exec git check-attr $attr -- $path]}]} {
+        if {![catch {set line [safe_exec [list git check-attr $attr -- $path]]}]} {
             regexp "(.*): $attr: (.*)" $line m f r
         }
         set path_attr_cache($attr,$path) $r
@@ -12409,7 +12474,7 @@
     while {$newlist ne {}} {
         set head [lrange $newlist 0 [expr {$lim - 1}]]
         set newlist [lrange $newlist $lim end]
-        if {![catch {set rlist [eval exec git check-attr $attr -- $head]}]} {
+        if {![catch {set rlist [safe_exec [concat git check-attr $attr -- $head]]}]} {
             foreach row [split $rlist "\n"] {
                 if {[regexp "(.*): $attr: (.*)" $row m path value]} {
                     if {[string index $path 0] eq "\""} {
@@ -12461,11 +12526,11 @@
 
 # on OSX bring the current Wish process window to front
 if {[tk windowingsystem] eq "aqua"} {
-    exec osascript -e [format {
+    safe_exec [list osascript -e [format {
         tell application "System Events"
             set frontmost of processes whose unix id is %d to true
         end tell
-    } [pid] ]
+    } [pid] ]]
 }
 
 # Unset GIT_TRACE var if set
@@ -12713,7 +12778,7 @@
 if {$i >= [llength $argv] && $revtreeargs ne {}} {
     # no -- on command line, but some arguments (other than --argscmd)
     if {[catch {
-        set f [eval exec git rev-parse --no-revs --no-flags $revtreeargs]
+        set f [safe_exec [concat git rev-parse --no-revs --no-flags $revtreeargs]]
         set cmdline_files [split $f "\n"]
         set n [llength $cmdline_files]
         set revtreeargs [lrange $revtreeargs 0 end-$n]
diff -Nru git-2.50.0/GIT-VERSION-GEN git-2.50.1/GIT-VERSION-GEN
--- git-2.50.0/GIT-VERSION-GEN	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/GIT-VERSION-GEN	2025-06-16 08:11:33.000000000 +0300
@@ -1,6 +1,6 @@
 #!/bin/sh
 
-DEF_VER=v2.50.0
+DEF_VER=v2.50.1
 
 LF='
 '
diff -Nru git-2.50.0/RelNotes git-2.50.1/RelNotes
--- git-2.50.0/RelNotes	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/RelNotes	2025-06-16 08:11:33.000000000 +0300
@@ -1,441 +1,8 @@
-Git v2.50 Release Notes
-=======================
+Git v2.50.1 Release Notes
+=========================
 
-UI, Workflows & Features
-------------------------
-
- * A post-processing filter for "diff --raw" output has been
-   introduced.
-
- * "git repack" learned "--combine-cruft-below-size" option that
-   controls how cruft-packs are combined.
-
- * TCP keepalive behaviour on http transports can now be configured by
-   calling cURL library.
-
- * Incrementally updating multi-pack index files.
-
- * "git reflog" learns "drop" subcommand, that discards the entire
-   reflog data for a ref.
-
- * A new userdiff driver for ".ini" format configuration files has
-   been added.
-
- * The job to coalesce loose objects into packfiles in "git
-   maintenance" now has configurable batch size.
-
- * "git clone" still gave the message about the default branch name;
-   this message has been turned into an advice message that can be
-   turned off.
-
- * "git rev-list" learns machine-parsable output format that delimits
-   each field with NUL.
-
- * "git maintenance" learns a new task to expire reflog entries.
-
- * Auth-related (and unrelated) error handling in send-email has been
-   made more robust.
-
- * Updating multiple references have only been possible in an all-or-nothing
-   fashion with transactions, but it can be more efficient to batch
-   multiple updates even when some of them are allowed to fail in a
-   best-effort manner.  A new "best effort batches of updates" mode
-   has been introduced.
-
- * "git help --build-options" reports SHA-1 and SHA-256 backends used
-   in the build.
-
- * "git cat-file --batch" and friends learned to allow "--filter=" to
-   omit certain objects, just like the transport layer does.
-
- * "git blame --porcelain" mode now talks about unblamable lines and
-   lines that are blamed to an ignored commit.
-
- * The build procedure installs bash (but not zsh) completion script.
-
- * send-email has been updated to work better with Outlook's SMTP server.
-
- * "git diff --minimal" used to give non-minimal output when its
-   optimization kicked in, which has been disabled.
-
- * "git index-pack --fix-thin" used to abort to prevent a cycle in
-   delta chains from forming in a corner case even when there is no
-   such cycle.
-
- * Make repository clean-up tasks that "gc" can do available to "git
-   maintenance" front-end.
-
- * Bundle-URI feature did not use refs recorded in the bundle other
-   than normal branches as anchoring points to optimize the follow-up
-   fetch during "git clone"; now it is told to utilize all.
-
- * The `send-email` documentation has been updated with OAuth2.0
-   related examples.
-
- * Two of the "scalar" subcommands that add a repository that hasn't
-   been under "scalar"'s control are taught an option not to enable the
-   scheduled maintenance on it.
-
- * The userdiff pattern for shell scripts has been updated to cope
-   with more bash-isms.
-
- * "git merge-tree" learned an option to see if it resolves cleanly
-   without actually creating a result.
-
- * The commit title in the "rebase -i" todo file are now prefixed with
-   '#', just like a merge commit being replayed.
-
- * "git receive-pack" optionally learns not to care about connectivity
-   check, which can be useful when the repository arranges to ensure
-   connectivity by some other means.
-
- * "git notes --help" documentation updates.
-
-
-Performance, Internal Implementation, Development Support etc.
---------------------------------------------------------------
-
- * A handful of built-in command implementations have been rewritten
-   to use the repository instance supplied by git.c:run_builtin(), its
-   caller.
-
- * "git fsck" becomes more careful when checking the refs.
-
- * "git fast-export | git fast-import" learns to deal with commit and
-   tag objects with embedded signatures a bit better.  This is highly
-   experimental and the format of the data stream may change in the
-   future without compatibility guarantees.
-
- * The code paths to check whether a refname X is available (by seeing
-   if another ref X/Y exists, etc.) have been optimized.
-
- * First step of deprecating and removing merge-recursive.
-
- * In protocol v2 where the refs advertisement is constrained, we try
-   to tell the server side not to limit the advertisement when there
-   is no specific need to, which has been the source of confusion and
-   recent bugs.  Revamp the logic to simplify.
-
- * Update meson based build procedure for breaking changes support.
-
- * Enable -Wunreachable-code for developer builds.
-
- * Ensure what we write in assert() does not have side effects,
-   and introduce ASSERT() macro to mark those that cannot be
-   mechanically checked for lack of side effects.
-
- * Give more meaningful error return values from block writer layer of
-   the reftable ref-API backend.
-
- * Make the code in reftable library less reliant on the service
-   routines it used to borrow from Git proper, to make it easier to
-   use by external users of the library.
-
- * CI update.
-
- * The object layer has been updated to take an explicit repository
-   instance as a parameter in more code paths.
-
- * Some warnings from "-Wsign-compare" for builtin/rm.c have been
-   squelched.
-
- * A few traditional unit tests have been rewritten to use the clar
-   framework.
-
- * Some warnings from "-Wsign-compare" for pathspec.c have been
-   squelched.
-
- * "make test" used to have a hard dependency on (basic) Perl; tests
-   have been rewritten help environment with NO_PERL test the build as
-   much as possible.
-
- * Remove remnants of the recursive merge strategy backend, which was
-   superseded by the ort merge strategy.
-
- * Optimize the code to dedup references recorded in a bundle file.
-
- * Update parse-options API to catch mistakes to pass address of an
-   integral variable of a wrong type/size.
-
- * Since a call to repo_config() can be called with repo set to NULL
-   these days, a command that is marked as RUN_SETUP in the builtin
-   command table does not have to check repo with NULL before making
-   the call.
-
- * Overhaul of the reftable API.
-
- * Reduce requirement for Perl in our documentation build and a few
-   scripts.
-
- * The build procedure based on Meson learned to drive the
-   benchmarking tests.
-
- * Code clean-up for meson-based build infrastructure.
-
- * Add an equivalent to "make hdr-check" target to meson based builds.
-
- * Further code clean-up in the object-store layer.
-
- * Build performance fix.
-
- * Teach "git send-email" to also consult `hostname -f` for mail
-   domain to compute the identity given to SMTP servers.
-
- * The dependency on the_repository variable has been reduced from the
-   code paths in "git replay".
-
- * Support to create a loose object file with unknown object type has
-   been dropped.
-
- * The code path to access the "packed-refs" file while "fsck" is
-   taught to mmap the file, instead of reading the whole file into
-   memory.
-
- * Assorted fixes for issues found with CodeQL.
-
- * Remove the leftover hints to the test framework to mark tests that
-   do not pass the leak checker tests, as they should no longer be
-   needed.
-
- * When a stale .midx file refers to .pack files that no longer exist,
-   we ended up checking for these non-existent files repeatedly, which
-   has been optimized by memoizing the non-existence.
-
- * Build settings have been improved for BSD based systems.
-
- * Newer version of libcURL detected curl_easy_setopt() calls we made
-   with platform-natural "int" when we should have used "long", which
-   all have been corrected.
-
- * Tests that compare $HOME and $(pwd), which should be the same
-   directory unless the tests chdir's around, would fail when the user
-   enters the test directory via symbolic links, which has been
-   corrected.
-
-
-Fixes since v2.49
------------------
-
- * The refname exclusion logic in the packed-ref backend has been
-   broken for some time, which confused upload-pack to advertise
-   different set of refs.  This has been corrected.
-   (merge 10e8a9352b tb/refs-exclude-fixes later to maint).
-
- * The merge-recursive and merge-ort machinery crashed in corner cases
-   when certain renames are involved.
-   (merge 3adba40858 en/merge-process-renames-crash-fix later to maint).
-
- * Certain "cruft" objects would have never been refreshed when there
-   are multiple cruft packs in the repository, which has been
-   corrected.
-   (merge 08f612ba70 tb/multi-cruft-pack-refresh-fix later to maint).
-
- * The xdiff code on 32-bit platform misbehaved when an insanely large
-   context size is given, which has been corrected.
-   (merge d39e28e68c rs/xdiff-context-length-fix later to maint).
-
- * GitHub Actions CI switched on a CI/CD variable that does not exist
-   when choosing what packages to install etc., which has been
-   corrected.
-   (merge ee89f7c79d kn/ci-meson-check-build-docs-fix later to maint).
-
- * Using "git name-rev --stdin" as an example, improve the framework to
-   prepare tests to pretend to be in the future where the breaking
-   changes have already happened.
-   (merge de3dec1187 jc/name-rev-stdin later to maint).
-
- * An earlier code refactoring of the hash machinery missed a few
-   required calls to init_fn.
-   (merge d39f04b638 jh/hash-init-fixes later to maint).
-
- * A documentation page was left out from formatting and installation,
-   which has been corrected.
-   (merge ae85116f18 pw/build-breaking-changes-doc later to maint).
-
- * The bash command line completion script (in contrib/) has been
-   updated to cope with remote repository nicknames with slashes in
-   them.
-   (merge 778d2f1760 dm/completion-remote-names-fix later to maint).
-
- * "Dubious ownership" checks on Windows has been tightened up.
-   (merge 5bb88e89ef js/mingw-admins-are-special later to maint).
-
- * Layout configuration in vimdiff backend didn't work as advertised,
-   which has been corrected.
-   (merge 93bab2d04b fr/vimdiff-layout-fixes later to maint).
-
- * Fix our use of zlib corner cases.
-   (merge 1cb2f293f5 jk/zlib-inflate-fixes later to maint).
-
- * Fix lockfile contention in reftable code on Windows.
-   (merge 0a3dceabf1 ps/mingw-creat-excl-fix later to maint).
-
- * "git-merge-file" documentation source, which has lines that look
-   like conflict markers, lacked custom conflict marker size defined,
-   which has been corrected..
-   (merge d3b5832381 pw/custom-conflict-marker-size-for-merge-related-docs later to maint).
-
- * Squelch false-positive from sparse.
-   (merge da87b58014 dd/sparse-glibc-workaround later to maint).
-
- * Adjust to the deprecation of use of Ubuntu 20.04 GitHub Actions CI.
-   (merge 832d9f6d0b js/ci-github-update-ubuntu later to maint).
-
- * Work around CI breakage due to fedora base image getting updated.
-   (merge 8a471a663b js/ci-fedora-gawk later to maint).
-
- * A ref transaction corner case fix.
-   (merge b9fadeead7 jt/ref-transaction-abort-fix later to maint).
-
- * Random build fixes.
-   (merge 85e1d6819f ps/misc-build-fixes later to maint).
-
- * "git fetch [<remote>]" with only the configured fetch refspec
-   should be the only thing to update refs/remotes/<remote>/HEAD,
-   but the code was overly eager to do so in other cases.
-
- * Incorrect sorting of refs with bytes with high-bit set on platforms
-   with signed char led to a BUG, which has been corrected.
-
- * "make perf" fixes.
-   (merge 1665f12fa0 pb/perf-test-fixes later to maint).
-
- * Doc mark-up updates.
-   (merge 5a5565ec44 ja/doc-reset-mv-rm-markup-updates later to maint).
-
- * Work around false positive from CodeQL checker.
-   (merge 0f558141ed js/range-check-codeql-workaround later to maint).
-
- * "git log --{left,right}-only A...B", when A and B does not share
-   any common ancestor, now behaves as expected.
-   (merge e7ef4be7c2 mh/left-right-limited later to maint).
-
- * Document the convention to disable hooks altogether by setting the
-   hooksPath configuration variable to /dev/null.
-   (merge 1b2eee94f1 ds/doc-disable-hooks later to maint).
-
- * Make sure outage of third-party sites that supply P4, Git-LFS, and
-   JGit we use for testing would not prevent our CI jobs from running
-   at all.
-
- * Various build tweaks, including CSPRNG selection on some platforms.
-   (merge cdda67de03 rj/build-tweaks later to maint).
-
- * Developer support fix..
-   (merge 32b74b9809 js/git-perf-env-override later to maint).
-
- * Fix for scheduled maintenance tasks on platforms using launchctl.
-   (merge eb2d7beb0e jh/gc-launchctl-schedule-fix later to maint).
-
- * Update to arm64 Windows port (part of which had been reverted as it
-   broke builds for existing platforms, which may need to be redone in
-   future releases).
-
- * hashmap API clean-up to ensure hashmap_clear() leaves a cleared map
-   in a reusable state.
-   (merge 9481877de3 en/hashmap-clear-fix later to maint).
-
- * "git mv a a/b dst" would ask to move the directory 'a' itself, as
-   well as its contents, in a single destination directory, which is
-   a contradicting request that is impossible to satisfy. This case is
-   now detected and the command errors out.
-   (merge 974f0d4664 ps/mv-contradiction-fix later to maint).
-
- * Further refinement on CI messages when an optional external
-   software is unavailable (e.g. due to third-party service outage).
-   (merge 956acbefbd jc/ci-skip-unavailable-external-software later to maint).
-
- * Test result aggregation did not work in Meson based CI jobs.
-   (merge bd38ed5be1 ps/ci-test-aggreg-fix-for-meson later to maint).
-
- * Code clean-up around stale CI elements and building with Visual Studio.
-   (merge a7b060f67f js/ci-buildsystems-cleanup later to maint).
-
- * "git add 'f?o'" did not add 'foo' if 'f?o', an unusual pathname,
-   also existed on the working tree, which has been corrected.
-   (merge ec727e189c kj/glob-path-with-special-char later to maint).
-
- * The fallback implementation of open_nofollow() depended on
-   open("symlink", O_NOFOLLOW) to set errno to ELOOP, but a few BSD
-   derived systems use different errno, which has been worked around.
-   (merge f47bcc3413 cf/wrapper-bsd-eloop later to maint).
-
- * Use-after-free fix in the sequencer.
-   (merge 5dbaec628d pw/sequencer-reflog-use-after-free later to maint).
-
- * win+Meson CI pipeline, unlike other pipelines for Windows,
-   used to build artifacts in developer mode, which has been changed to
-   build them in release mode for consistency.
-   (merge 184abdcf05 js/ci-build-win-in-release-mode later to maint).
-
- * CI settings at GitLab has been updated to run MSVC based Meson job
-   automatically (as opposed to be done only upon manual request).
-   (merge 6389579b2f ps/ci-gitlab-enable-msvc-meson-job later to maint).
-
- * "git apply" and "git add -i/-p" code paths no longer unnecessarily
-   expand sparse-index while working.
-   (merge ecf9ba20e3 ds/sparse-apply-add-p later to maint).
-
- * Avoid adding directory path to a sparse-index tree entries to the
-   name-hash, since they would bloat the hashtable without anybody
-   querying for them.  This was done already for a single threaded
-   part of the code, but now the multi-threaded code also does the
-   same.
-   (merge 2e60aabc75 am/sparse-index-name-hash-fix later to maint).
-
- * Recent versions of Perl started warning against "! A =~ /pattern/"
-   which does not negate the result of the matching.  As it turns out
-   that the problematic function is not even called, it was removed.
-   (merge 67cae845d2 op/cvsserver-perl-warning later to maint).
-
- * "git apply --index/--cached" when applying a deletion patch in
-   reverse failed to give the mode bits of the path "removed" by the
-   patch to the file it creates, which has been corrected.
-
- * "git verify-refs" errored out in a repository in which
-   linked worktrees were prepared with Git 2.43 or lower.
-   (merge d5b3c38b8a sj/ref-contents-check-fix later to maint).
-
- * Update total_ram() function on BSD variants.
-
- * Update online_cpus() function on BSD variants.
-
- * Revert a botched bswap.h change that broke ntohll() functions on
-   big-endian systems with __builtin_bswap32/64().
-
- * Fixes for GitHub Actions Coverity job.
-   (merge 3cc4fc1ebd js/github-ci-win-coverity-fix later to maint).
-
- * Other code cleanup, docfix, build fix, etc.
-   (merge 227c4f33a0 ja/doc-block-delimiter-markup-fix later to maint).
-   (merge 2bfd3b3685 ab/decorate-code-cleanup later to maint).
-   (merge 5337daddc7 am/dir-dedup-decl-of-repository later to maint).
-   (merge 554051d691 en/diff-rename-follow-fix later to maint).
-   (merge a18c18b470 en/random-cleanups later to maint).
-   (merge 5af21c9acb hj/doc-rev-list-ancestry-fix later to maint).
-   (merge 26d76ca284 aj/doc-restore-p-update later to maint).
-   (merge 2c0dcb9754 cc/lop-remote later to maint).
-   (merge 7b399322a2 ja/doc-branch-markup later to maint).
-   (merge ee434e1807 pw/doc-pack-refs-markup-fix later to maint).
-   (merge c000918eb7 tb/bitamp-typofix later to maint).
-   (merge fa8cd29676 js/imap-send-peer-cert-verify later to maint).
-   (merge 98b423bc1c rs/clear-commit-marks-simplify later to maint).
-   (merge 133d065dd6 ta/bulk-checkin-signed-compare-false-warning-fix later to maint).
-   (merge d2827dc31e es/meson-build-skip-coccinelle later to maint).
-   (merge ee8edb7156 dk/vimdiff-doc-fix later to maint).
-   (merge 107d889303 md/t1403-path-is-file later to maint).
-   (merge abd4192b07 js/comma-semicolon-confusion later to maint).
-   (merge 27b7264206 ab/environment-clean-header later to maint).
-   (merge ff4a749354 as/typofix-in-env-h-header later to maint).
-   (merge 86eef3541e az/tighten-string-array-constness later to maint).
-   (merge 25292c301d lo/remove-log-reencode-from-rev-info later to maint).
-   (merge 1aa50636fd jk/p5332-testfix later to maint).
-   (merge 42cf4ac552 ps/ci-resurrect-p4-on-github later to maint).
-   (merge 104add8368 js/diff-codeql-false-positive-workaround later to maint).
-   (merge f62977b93c en/get-tree-entry-doc later to maint).
-   (merge e5dd0a05ed ly/am-split-stgit-leakfix later to maint).
-   (merge bac220e154 rc/t1001-test-path-is-file later to maint).
-   (merge 91db6c735d ly/reftable-writer-leakfix later to maint).
-   (merge 20e4e9ad0b jc/doc-synopsis-option-markup later to maint).
-   (merge cddcee7f64 es/meson-configure-build-options-fix later to maint).
-   (merge cea9f55f00 wk/sparse-checkout-doc-fix later to maint).
+This release merges up the fixes that appear in v2.43.7, v2.44.4,
+v2.45.4, v2.46.4, v2.47.3, v2.48.2, and v2.49.1 to address the
+following CVEs: CVE-2025-27613, CVE-2025-27614, CVE-2025-46334,
+CVE-2025-46835, CVE-2025-48384, CVE-2025-48385, and
+CVE-2025-48386. See the release notes for v2.43.7 for details.
diff -Nru git-2.50.0/t/t1300-config.sh git-2.50.1/t/t1300-config.sh
--- git-2.50.0/t/t1300-config.sh	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/t/t1300-config.sh	2025-06-16 08:11:33.000000000 +0300
@@ -2851,4 +2851,15 @@
 
 done
 
+test_expect_success 'writing value with trailing CR not stripped on read' '
+	test_when_finished "rm -rf cr-test" &&
+
+	printf "bar\r\n" >expect &&
+	git init cr-test &&
+	git -C cr-test config set core.foo $(printf "bar\r") &&
+	git -C cr-test config get core.foo >actual &&
+
+	test_cmp expect actual
+'
+
 test_done
diff -Nru git-2.50.0/t/t5558-clone-bundle-uri.sh git-2.50.1/t/t5558-clone-bundle-uri.sh
--- git-2.50.0/t/t5558-clone-bundle-uri.sh	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/t/t5558-clone-bundle-uri.sh	2025-06-16 08:11:33.000000000 +0300
@@ -1279,6 +1279,29 @@
 		trace-mult.txt >bundle-fetches &&
 	test_line_count = 1 bundle-fetches
 '
+
+test_expect_success 'bundles with space in URI are rejected' '
+	test_when_finished "rm -rf busted repo" &&
+	mkdir -p "$HOME/busted/ /$HOME/repo/.git/objects/bundles" &&
+	git clone --bundle-uri="$HTTPD_URL/bogus $HOME/busted/" "$HTTPD_URL/smart/fetch.git" repo 2>err &&
+	test_grep "error: bundle-uri: URI is malformed: " err &&
+	find busted -type f >files &&
+	test_must_be_empty files
+'
+
+test_expect_success 'bundles with newline in URI are rejected' '
+	test_when_finished "rm -rf busted repo" &&
+	git clone --bundle-uri="$HTTPD_URL/bogus\nget $HTTPD_URL/bogus $HOME/busted" "$HTTPD_URL/smart/fetch.git" repo 2>err &&
+	test_grep "error: bundle-uri: URI is malformed: " err &&
+	test_path_is_missing "$HOME/busted"
+'
+
+test_expect_success 'bundles with newline in target path are rejected' '
+	git clone --bundle-uri="$HTTPD_URL/bogus" "$HTTPD_URL/smart/fetch.git" "$(printf "escape\nget $HTTPD_URL/bogus .")" 2>err &&
+	test_grep "error: bundle-uri: filename is malformed: " err &&
+	test_path_is_missing escape
+'
+
 # Do not add tests here unless they use the HTTP server, as they will
 # not run unless the HTTP dependencies exist.
 
diff -Nru git-2.50.0/t/t7450-bad-git-dotfiles.sh git-2.50.1/t/t7450-bad-git-dotfiles.sh
--- git-2.50.0/t/t7450-bad-git-dotfiles.sh	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/t/t7450-bad-git-dotfiles.sh	2025-06-16 08:11:33.000000000 +0300
@@ -372,4 +372,37 @@
 	test_path_is_missing nested_checkout/thing2/.git
 '
 
+test_expect_success SYMLINKS,!WINDOWS,!MINGW 'submodule must not checkout into different directory' '
+	test_when_finished "rm -rf sub repo bad-clone" &&
+
+	git init sub &&
+	write_script sub/post-checkout <<-\EOF &&
+	touch "$PWD/foo"
+	EOF
+	git -C sub add post-checkout &&
+	git -C sub commit -m hook &&
+
+	git init repo &&
+	git -C repo -c protocol.file.allow=always submodule add "$PWD/sub" sub &&
+	git -C repo mv sub $(printf "sub\r") &&
+
+	# Ensure config values containing CR are wrapped in quotes.
+	git config unset -f repo/.gitmodules submodule.sub.path &&
+	printf "\tpath = \"sub\r\"\n" >>repo/.gitmodules &&
+
+	git config unset -f repo/.git/modules/sub/config core.worktree &&
+	{
+		printf "[core]\n" &&
+		printf "\tworktree = \"../../../sub\r\"\n"
+	} >>repo/.git/modules/sub/config &&
+
+	ln -s .git/modules/sub/hooks repo/sub &&
+	git -C repo add -A &&
+	git -C repo commit -m submodule &&
+
+	git -c protocol.file.allow=always clone --recurse-submodules repo bad-clone &&
+	! test -f "$PWD/foo" &&
+	test -f $(printf "bad-clone/sub\r/post-checkout")
+'
+
 test_done
diff -Nru git-2.50.0/version git-2.50.1/version
--- git-2.50.0/version	2025-06-16 08:42:57.000000000 +0300
+++ git-2.50.1/version	2025-06-16 08:11:33.000000000 +0300
@@ -1 +1 @@
-2.50.0
+2.50.1

Reply via email to