diff mbox series

[kirkstone] git: fix CVE-2025-27614-CVE-2025-27613-CVE-2025-46334-CVE-2025-46835

Message ID 20250818123125.71140-1-hprajapati@mvista.com
State New
Headers show
Series [kirkstone] git: fix CVE-2025-27614-CVE-2025-27613-CVE-2025-46334-CVE-2025-46835 | expand

Commit Message

Hitendra Prajapati Aug. 18, 2025, 12:31 p.m. UTC
Upstream-Status: Backport from from https://github.com/git/git/commit/d61cfed2c23705fbeb9c0d08f59e75ee08738950

Signed-off-by: Hitendra Prajapati <hprajapati@mvista.com>
---
 ...-27613-CVE-2025-46334-CVE-2025-46835.patch | 2500 +++++++++++++++++
 meta/recipes-devtools/git/git_2.35.7.bb       |    1 +
 2 files changed, 2501 insertions(+)
 create mode 100644 meta/recipes-devtools/git/git/CVE-2025-27614-CVE-2025-27613-CVE-2025-46334-CVE-2025-46835.patch
diff mbox series

Patch

diff --git a/meta/recipes-devtools/git/git/CVE-2025-27614-CVE-2025-27613-CVE-2025-46334-CVE-2025-46835.patch b/meta/recipes-devtools/git/git/CVE-2025-27614-CVE-2025-27613-CVE-2025-46334-CVE-2025-46835.patch
new file mode 100644
index 0000000000..e08bf41b3c
--- /dev/null
+++ b/meta/recipes-devtools/git/git/CVE-2025-27614-CVE-2025-27613-CVE-2025-46334-CVE-2025-46835.patch
@@ -0,0 +1,2500 @@ 
+From: d61cfed2c23705fbeb9c0d08f59e75ee08738950 Merge: 664d4fa692 311d9ada3a
+Author: Taylor Blau <me@ttaylorr.com>
+Date:   Fri May 23 17:17:06 2025 -0400
+
+    Merge branch 'js/gitk-git-gui-harden-exec-open' into maint-2.43
+
+    This merges in fixes for CVE-2025-27614, CVE-2025-27613, CVE-2025-46334,
+    and CVE-2025-46835 targeting Gitk and Git GUI.
+
+    * js/gitk-git-gui-harden-exec-open: (41 commits)
+      git-gui: sanitize 'exec' arguments: convert new 'cygpath' calls
+      git-gui: do not mistake command arguments as redirection operators
+      git-gui: introduce function git_redir for git calls with redirections
+      git-gui: pass redirections as separate argument to git_read
+      git-gui: pass redirections as separate argument to _open_stdout_stderr
+      git-gui: convert git_read*, git_write to be non-variadic
+      git-gui: override exec and open only on Windows
+      gitk: sanitize 'open' arguments: revisit recently updated 'open' calls
+      git-gui: use git_read in githook_read
+      git-gui: sanitize $PATH on all platforms
+      git-gui: break out a separate function git_read_nice
+      git-gui: assure PATH has only absolute elements.
+      git-gui: remove option --stderr from git_read
+      git-gui: cleanup git-bash menu item
+      git-gui: sanitize 'exec' arguments: background
+      git-gui: avoid auto_execok in do_windows_shortcut
+      git-gui: sanitize 'exec' arguments: simple cases
+      git-gui: avoid auto_execok for git-bash menu item
+      git-gui: treat file names beginning with "|" as relative paths
+      git-gui: remove unused proc is_shellscript
+      git-gui: remove git config --list handling for git < 1.5.3
+      git-gui: remove special treatment of Windows from open_cmd_pipe
+      git-gui: remove HEAD detachment implementation for git < 1.5.3
+      git-gui: use only the configured shell
+      git-gui: remove Tcl 8.4 workaround on 2>@1 redirection
+      git-gui: make _shellpath usable on startup
+      git-gui: use [is_Windows], not bad _shellpath
+      git-gui: _which, only add .exe suffix if not present
+      gitk: encode arguments correctly with "open"
+      gitk: sanitize 'open' arguments: command pipeline
+      gitk: collect construction of blameargs into a single conditional
+      gitk: sanitize 'open' arguments: simple commands, readable and writable
+      gitk: sanitize 'open' arguments: simple commands with redirections
+      gitk: sanitize 'open' arguments: simple commands
+      gitk: sanitize 'exec' arguments: redirect to process
+      gitk: sanitize 'exec' arguments: redirections and background
+      gitk: sanitize 'exec' arguments: redirections
+      gitk: sanitize 'exec' arguments: 'eval exec'
+      gitk: sanitize 'exec' arguments: simple cases
+      gitk: have callers of diffcmd supply pipe symbol when necessary
+      gitk: treat file names beginning with "|" as relative paths
+      ...
+
+    Signed-off-by: Taylor Blau <me@ttaylorr.com>
+
+Upstream-Status: Backport from [https://github.com/git/git/commit/d61cfed2c23705fbeb9c0d08f59e75ee08738950]
+CVE: CVE-2025-27614, CVE-2025-27613, CVE-2025-46334, CVE-2025-46835
+Signed-off-by: Hitendra Prajapati <hprajapati@mvista.com>
+---
+ git-gui/git-gui.sh                   | 622 +++++++++++++++++----------
+ 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             |  16 +-
+ git-gui/lib/sshkey.tcl               |   7 +-
+ git-gui/lib/tools.tcl                |   7 +-
+ git-gui/lib/win32.tcl                |   9 +-
+ gitk-git/gitk                        | 298 ++++++++-----
+ 21 files changed, 667 insertions(+), 433 deletions(-)
+
+diff --git a/git-gui/git-gui.sh b/git-gui/git-gui.sh
+index 201524c..2f38291 100755
+--- a/git-gui/git-gui.sh
++++ b/git-gui/git-gui.sh
+@@ -24,7 +24,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+ 
+ You should have received a copy of the GNU General Public License
+-along with this program; if not, see <http://www.gnu.org/licenses/>.}]
++along with this program; if not, see <https://www.gnu.org/licenses/>.}]
+ 
+ ######################################################################
+ ##
+@@ -44,6 +44,211 @@ if {[catch {package require Tcl 8.5} err]
+ 
+ catch {rename send {}} ; # What an evil concept...
+ 
++######################################################################
++##
++## Enabling platform-specific code paths
++
++proc is_MacOSX {} {
++	if {[tk windowingsystem] eq {aqua}} {
++		return 1
++	}
++	return 0
++}
++
++proc is_Windows {} {
++	if {$::tcl_platform(platform) eq {windows}} {
++		return 1
++	}
++	return 0
++}
++
++set _iscygwin {}
++proc is_Cygwin {} {
++	global _iscygwin
++	if {$_iscygwin eq {}} {
++		if {[string match "CYGWIN_*" $::tcl_platform(os)]} {
++			set _iscygwin 1
++		} else {
++			set _iscygwin 0
++		}
++	}
++	return $_iscygwin
++}
++
++######################################################################
++##
++## 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 {}
++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
++
++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 suffix $_search_exe
++		}
++
++		foreach p $_search_path {
++			set p [file join $p $what$suffix]
++			if {[file exists $p]} {
++				return [file normalize $p]
++			}
++		}
++		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
++			}
++
++			# 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
++	}
++
++	# Override `exec` to avoid unsafe PATH lookup
++
++	rename exec real_exec
++
++	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
++	}
++
++	# Override `open` to avoid unsafe PATH lookup
++
++	rename open real_open
++
++	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]
++	}
++	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 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]
++	}
++	open $filename $flags
++}
++
++# End exec/open wrappers
++
+ ######################################################################
+ ##
+ ## locate our library
+@@ -144,14 +349,64 @@ unset oguimsg
+ 
+ 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]]]
+ 	}
+ }
+ 
++# 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 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]
++	}
++	open $filename $flags
++}
++
++# End exec/open wrappers
++
+ ######################################################################
+ ##
+ ## read only globals
+@@ -180,15 +435,37 @@ if {$_trace >= 0} {
+ # 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
+ }
+ 
+@@ -252,40 +529,6 @@ proc reponame {} {
+ 	return $::_reponame
+ }
+ 
+-proc is_MacOSX {} {
+-	if {[tk windowingsystem] eq {aqua}} {
+-		return 1
+-	}
+-	return 0
+-}
+-
+-proc is_Windows {} {
+-	if {$::tcl_platform(platform) eq {windows}} {
+-		return 1
+-	}
+-	return 0
+-}
+-
+-proc is_Cygwin {} {
+-	global _iscygwin
+-	if {$_iscygwin eq {}} {
+-		if {$::tcl_platform(platform) eq {windows}} {
+-			if {[catch {set p [exec cygpath --windir]} err]} {
+-				set _iscygwin 0
+-			} else {
+-				set _iscygwin 1
+-				# Handle MSys2 which is only cygwin when MSYSTEM is MSYS.
+-				if {[info exists ::env(MSYSTEM)] && $::env(MSYSTEM) ne "MSYS"} {
+-					set _iscygwin 0
+-				}
+-			}
+-		} else {
+-			set _iscygwin 0
+-		}
+-	}
+-	return $_iscygwin
+-}
+-
+ proc is_enabled {option} {
+ 	global enabled_options
+ 	if {[catch {set on $enabled_options($option)}]} {return 0}
+@@ -418,7 +661,7 @@ proc _git_cmd {name} {
+ 			# 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
+ 
+@@ -473,6 +716,9 @@ proc _which {what args} {
+ 
+ 	if {[is_Windows] && [lsearch -exact $args -script] >= 0} {
+ 		set suffix {}
++	} elseif {[is_Windows] && [string match *$_search_exe [string tolower $what]]} {
++		# The search string already has the file extension
++		set suffix {}
+ 	} else {
+ 		set suffix $_search_exe
+ 	}
+@@ -486,32 +732,14 @@ proc _which {what args} {
+ 	return {}
+ }
+ 
+-# 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]
+ }
+ 
+@@ -521,7 +749,7 @@ proc _lappend_nice {cmd_var} {
+ 
+ 	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 {}
+@@ -533,7 +761,11 @@ proc _lappend_nice {cmd_var} {
+ }
+ 
+ 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
+@@ -543,111 +775,47 @@ proc git {args} {
+ 	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
+-		}
++	_lappend_nice opt
+ 
+-		default {
+-			break
+-		}
++	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 pchook [gitdir hooks $hook_name]
+-	lappend args 2>@1
+-
+-	# On Windows [file executable] might lie so we need to ask
+-	# the shell if the hook is executable.  Yes that's annoying.
+-	#
+-	if {[is_Windows]} {
+-		upvar #0 _sh interp
+-		if {![info exists interp]} {
+-			set interp [_which sh]
+-		}
+-		if {$interp eq {}} {
+-			error "hook execution requires sh (not in PATH)"
+-		}
+-
+-		set scr {if test -x "$1";then exec "$@";fi}
+-		set sh_c [list $interp -c $scr $interp $pchook]
+-		return [_open_stdout_stderr [concat $sh_c $args]]
+-	}
+-
+-	if {[file executable $pchook]} {
+-		return [_open_stdout_stderr [concat [list $pchook] $args]]
+-	}
+-
+-	return {}
++	git_read [concat [list hook run --ignore-missing $hook_name --] $args] [list 2>@1]
+ }
+ 
+ proc kill_file_process {fd} {
+@@ -655,9 +823,9 @@ proc kill_file_process {fd} {
+ 
+ 	catch {
+ 		if {[is_Windows]} {
+-			exec taskkill /pid $process
++			safe_exec [list taskkill /pid $process]
+ 		} else {
+-			exec kill $process
++			safe_exec [list kill $process]
+ 		}
+ 	}
+ }
+@@ -683,7 +851,7 @@ proc sq {value} {
+ 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 {}
+@@ -1045,7 +1213,7 @@ You are using [git-version]:
+ ## 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 \
+@@ -1083,53 +1251,30 @@ unset -nocomplain idx fd
+ ##
+ ## 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) {}
+ 		}
+ 	}
+ }
+@@ -1412,7 +1557,7 @@ proc repository_state {ctvar hdvar mhvar} {
+ 	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
+ 		}
+@@ -1431,7 +1576,7 @@ proc PARENT {} {
+ 		return $p
+ 	}
+ 	if {$empty_tree eq {}} {
+-		set empty_tree [git mktree << {}]
++		set empty_tree [git_redir [list mktree] [list << {}]]
+ 	}
+ 	return $empty_tree
+ }
+@@ -1490,12 +1635,12 @@ proc rescan {after {honor_trustmtime 1}} {
+ 	} 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]
+@@ -1551,11 +1696,11 @@ proc rescan_stage2 {fd after} {
+ 	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
+@@ -1564,7 +1709,7 @@ proc rescan_stage2 {fd after} {
+ 	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
+@@ -1576,7 +1721,7 @@ proc load_message {file {encoding {}}} {
+ 
+ 	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 {}
+@@ -1600,23 +1745,23 @@ proc run_prepare_commit_msg_hook {} {
+ 	# 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
+@@ -2206,7 +2351,7 @@ proc do_gitk {revs {is_submodule false}} {
+ 			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
+@@ -2243,7 +2388,7 @@ proc do_git_gui {} {
+ 		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
+@@ -2272,16 +2417,18 @@ proc get_explorer {} {
+ 
+ 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
+@@ -2315,7 +2462,7 @@ proc do_quit {{rc {1}}} {
+ 			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
+@@ -2373,7 +2520,7 @@ proc do_quit {{rc {1}}} {
+ 	set ret_code $rc
+ 
+ 	# Briefly enable send again, working around Tk bug
+-	# http://sourceforge.net/tracker/?func=detail&atid=112997&aid=1821174&group_id=12997
++	# https://sourceforge.net/p/tktoolkit/bugs/2343/
+ 	tk appname [appname]
+ 
+ 	destroy .
+@@ -2759,17 +2906,16 @@ if {![is_bare]} {
+ 
+ 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]} {
+@@ -4134,7 +4280,7 @@ if {[winfo exists $ui_comm]} {
+ 				}
+ 			} 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 --git a/git-gui/lib/blame.tcl b/git-gui/lib/blame.tcl
+index 8441e10..d6fd8be 100644
+--- a/git-gui/lib/blame.tcl
++++ b/git-gui/lib/blame.tcl
+@@ -481,14 +481,14 @@ method _load {jump} {
+ 		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 @@ method _exec_blame {cur_w cur_d options cur_s} {
+ 	}
+ 
+ 	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 @@ method _showcommit {cur_w lno} {
+ 		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 @@ method _blameparent {} {
+ 		} 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 --git a/git-gui/lib/branch.tcl b/git-gui/lib/branch.tcl
+index 8b0c485..39e0f2d 100644
+--- a/git-gui/lib/branch.tcl
++++ b/git-gui/lib/branch.tcl
+@@ -7,7 +7,7 @@ proc load_all_heads {} {
+ 	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_heads {} {
+ 
+ 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 --git a/git-gui/lib/browser.tcl b/git-gui/lib/browser.tcl
+index a982983..6fc8d4d 100644
+--- a/git-gui/lib/browser.tcl
++++ b/git-gui/lib/browser.tcl
+@@ -196,7 +196,7 @@ method _ls {tree_id {name {}}} {
+ 	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 --git a/git-gui/lib/checkout_op.tcl b/git-gui/lib/checkout_op.tcl
+index 21ea768..87ed0b4 100644
+--- a/git-gui/lib/checkout_op.tcl
++++ b/git-gui/lib/checkout_op.tcl
+@@ -304,12 +304,12 @@ The rescan will be automatically started now.
+ 		_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 @@ method _readtree {} {
+ 		[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 @@ method _update_repo_state {} {
+ 	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 @@ method _confirm_reset {cur} {
+ 	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 --git a/git-gui/lib/choose_repository.tcl b/git-gui/lib/choose_repository.tcl
+index af1fee7..76224d9 100644
+--- a/git-gui/lib/choose_repository.tcl
++++ b/git-gui/lib/choose_repository.tcl
+@@ -662,8 +662,8 @@ method _do_clone2 {} {
+ 			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
+@@ -752,7 +752,7 @@ method _do_clone2 {} {
+ 			[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
+@@ -785,8 +785,8 @@ method _copy_files {objdir tocopy} {
+ 	}
+ 	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
+ 
+@@ -843,12 +843,12 @@ method _clone_refs {} {
+ 		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} {
+@@ -902,7 +902,7 @@ method _do_clone_full_end {ok} {
+ 
+ 		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
+@@ -978,13 +978,14 @@ method _do_clone_checkout {HEAD} {
+ 		[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 --git a/git-gui/lib/choose_rev.tcl b/git-gui/lib/choose_rev.tcl
+index 6dae793..8ae7e8a 100644
+--- a/git-gui/lib/choose_rev.tcl
++++ b/git-gui/lib/choose_rev.tcl
+@@ -146,14 +146,14 @@ constructor _new {path unmerged_only title} {
+ 	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 @@ constructor _new {path unmerged_only title} {
+ 	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 @@ method _reflog_last {name} {
+ 
+ 	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 --git a/git-gui/lib/commit.tcl b/git-gui/lib/commit.tcl
+index 11379f8..bb6056d 100644
+--- a/git-gui/lib/commit.tcl
++++ b/git-gui/lib/commit.tcl
+@@ -27,7 +27,7 @@ You are currently in the middle of a merge that has not been fully completed.  Y
+ 	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
+@@ -225,7 +225,7 @@ A good commit message has the following format:
+ 	# -- 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
+@@ -325,7 +325,7 @@ proc commit_commitmsg_wait {fd_ph curHEAD msg_p} {
+ 
+ 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]
+ }
+@@ -350,7 +350,7 @@ proc commit_committree {fd_wt curHEAD msg_p} {
+ 	# -- 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
+@@ -388,8 +388,8 @@ A rescan will be automatically started now.
+ 	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."]
+@@ -409,7 +409,7 @@ A rescan will be automatically started now.
+ 	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 --git a/git-gui/lib/console.tcl b/git-gui/lib/console.tcl
+index bb6b9c8..4715ce9 100644
+--- a/git-gui/lib/console.tcl
++++ b/git-gui/lib/console.tcl
+@@ -92,10 +92,9 @@ method _init {} {
+ 
+ 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
+ 	fileevent $fd_f readable [cb _read $fd_f $after]
+diff --git a/git-gui/lib/database.tcl b/git-gui/lib/database.tcl
+index 8578308..1fc0ea0 100644
+--- a/git-gui/lib/database.tcl
++++ b/git-gui/lib/database.tcl
+@@ -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 --git a/git-gui/lib/diff.tcl b/git-gui/lib/diff.tcl
+index 871ad48..8ec740e 100644
+--- a/git-gui/lib/diff.tcl
++++ b/git-gui/lib/diff.tcl
+@@ -202,7 +202,7 @@ proc show_other_diff {path w m cont_info} {
+ 					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]
+@@ -226,7 +226,7 @@ proc show_other_diff {path w m cont_info} {
+ 			$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]
+@@ -338,7 +338,7 @@ proc start_show_diff {cont_info {add_opts {}}} {
+ 		}
+ 	}
+ 
+-	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]]
+@@ -617,7 +617,7 @@ proc apply_or_revert_hunk {x y revert} {
+ 
+ 	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]} {
+@@ -853,7 +853,7 @@ proc apply_or_revert_range_or_line {x y revert} {
+ 
+ 	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
+@@ -890,7 +890,7 @@ proc undo_last_revert {} {
+ 
+ 	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 --git a/git-gui/lib/index.tcl b/git-gui/lib/index.tcl
+index d2ec24b..857864f 100644
+--- a/git-gui/lib/index.tcl
++++ b/git-gui/lib/index.tcl
+@@ -75,7 +75,7 @@ proc update_indexinfo {msg path_list after} {
+ 	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 @@ proc update_index {msg path_list after} {
+ 	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 @@ proc checkout_index {msg path_list after capture_error} {
+ 	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 --git a/git-gui/lib/merge.tcl b/git-gui/lib/merge.tcl
+index 664803c..44c3f93 100644
+--- a/git-gui/lib/merge.tcl
++++ b/git-gui/lib/merge.tcl
+@@ -93,7 +93,7 @@ method _start {} {
+ 	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 @@ method _start {} {
+ 		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 @@ Continue with resetting the current changes?"]
+ 	}
+ 
+ 	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 --git a/git-gui/lib/mergetool.tcl b/git-gui/lib/mergetool.tcl
+index e688b01..6b26726 100644
+--- a/git-gui/lib/mergetool.tcl
++++ b/git-gui/lib/mergetool.tcl
+@@ -88,7 +88,7 @@ proc merge_load_stages {path cont} {
+ 	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]
+@@ -293,7 +293,7 @@ proc merge_tool_get_stages {target stages} {
+ 	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
+@@ -343,9 +343,9 @@ proc merge_tool_start {cmdline target backup stages} {
+ 
+ 	# 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 --git a/git-gui/lib/remote.tcl b/git-gui/lib/remote.tcl
+index ef77ed7..cf796d1 100644
+--- a/git-gui/lib/remote.tcl
++++ b/git-gui/lib/remote.tcl
+@@ -32,7 +32,7 @@ proc all_tracking_branches {} {
+ 	}
+ 
+ 	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 @@ proc load_all_remotes {} {
+ 
+ 		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 @@ proc add_fetch_entry {r} {
+ 		}
+ 	} 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 @@ proc add_push_entry {r} {
+ 		}
+ 	} 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 --git a/git-gui/lib/remote_branch_delete.tcl b/git-gui/lib/remote_branch_delete.tcl
+index 5ba9fca..c8c99b1 100644
+--- a/git-gui/lib/remote_branch_delete.tcl
++++ b/git-gui/lib/remote_branch_delete.tcl
+@@ -308,7 +308,7 @@ method _load {cache uri} {
+ 		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 --git a/git-gui/lib/shortcut.tcl b/git-gui/lib/shortcut.tcl
+index 97d1d7a..d97be99 100644
+--- a/git-gui/lib/shortcut.tcl
++++ b/git-gui/lib/shortcut.tcl
+@@ -12,7 +12,7 @@ proc do_windows_shortcut {} {
+ 			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,7 +30,7 @@ proc do_cygwin_shortcut {} {
+ 	global argv0 _gitworktree
+ 
+ 	if {[catch {
+-		set desktop [exec cygpath \
++		set desktop [safe_exec [list cygpath \
+ 			--windows \
+ 			--absolute \
+ 			--long-name \
+@@ -48,14 +48,14 @@ proc do_cygwin_shortcut {} {
+ 			set fn ${fn}.lnk
+ 		}
+ 		if {[catch {
+-				set sh [exec cygpath \
++				set sh [safe_exec [list cygpath \
+ 					--windows \
+ 					--absolute \
+-					/bin/sh.exe]
+-				set me [exec cygpath \
++					/bin/sh.exe]]
++				set me [safe_exec [list cygpath \
+ 					--unix \
+ 					--absolute \
+-					$argv0]
++					$argv0]]
+ 				win32_create_lnk $fn [list \
+ 					$sh -c \
+ 					"CHERE_INVOKING=1 source /etc/profile;[sq $me] &" \
+@@ -86,7 +86,7 @@ proc do_macosx_app {} {
+ 
+ 				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">
+@@ -111,7 +111,7 @@ proc do_macosx_app {} {
+ </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 --git a/git-gui/lib/sshkey.tcl b/git-gui/lib/sshkey.tcl
+index 589ff8f..c3e681b 100644
+--- a/git-gui/lib/sshkey.tcl
++++ b/git-gui/lib/sshkey.tcl
+@@ -7,7 +7,7 @@ proc find_ssh_key {} {
+ 		~/.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 @@ proc make_ssh_key {w} {
+ 	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 --git a/git-gui/lib/tools.tcl b/git-gui/lib/tools.tcl
+index 413f1a1..48fddfd 100644
+--- a/git-gui/lib/tools.tcl
++++ b/git-gui/lib/tools.tcl
+@@ -110,14 +110,14 @@ proc tools_exec {fullname} {
+ 
+ 	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_exec {fullname} {
+ }
+ 
+ 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 --git a/git-gui/lib/win32.tcl b/git-gui/lib/win32.tcl
+index db91ab8..3aedae2 100644
+--- a/git-gui/lib/win32.tcl
++++ b/git-gui/lib/win32.tcl
+@@ -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 @@ proc win32_create_lnk {lnk_path lnk_exec lnk_dir} {
+ 	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 --git a/gitk-git/gitk b/gitk-git/gitk
+index 23d9dd1..1c8c9c0 100755
+--- a/gitk-git/gitk
++++ b/gitk-git/gitk
+@@ -9,6 +9,92 @@ exec wish "$0" -- "$@"
+ 
+ package require Tk
+ 
++
++# 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"}]
+@@ -134,7 +220,7 @@ proc unmerged_files {files} {
+     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
+@@ -296,7 +382,7 @@ proc parseviewrevs {view revs} {
+     } 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"]
+@@ -374,7 +460,7 @@ proc start_rev_list {view} {
+     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
+@@ -405,14 +491,16 @@ proc start_rev_list {view} {
+         if {$revs eq {}} {
+             return 0
+         }
+-        set args [concat $vflags($view) $revs]
++        set args $vflags($view)
+     } else {
++        set revs {}
+         set args $vorigargs($view)
+     }
+ 
+     if {[catch {
+-        set fd [open [concat | git log --no-color -z --pretty=raw $show_notes \
+-                        --parents --boundary $args "--" $files] 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
+@@ -446,9 +534,9 @@ proc stop_instance {inst} {
+         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}
+@@ -554,13 +642,18 @@ proc updatecommits {} {
+             set revs $newrevs
+             set vposids($view) [lsort -unique [concat $oldpos $vposids($view)]]
+         }
+-        set args [concat $vflags($view) $revs --not $oldpos]
++        set args $vflags($view)
++        foreach r $oldpos {
++                lappend revs "^$r"
++        }
+     } else {
++        set revs {}
+         set args $vorigargs($view)
+     }
+     if {[catch {
+-        set fd [open [concat | git log --no-color -z --pretty=raw $show_notes \
+-                        --parents --boundary $args "--" $vfilelimit($view)] 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
+@@ -1527,8 +1620,8 @@ proc getcommitlines {fd inst view updating}  {
+             # 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
+@@ -1648,7 +1741,7 @@ proc do_readcommit {id} {
+     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 != {}} {
+@@ -1784,7 +1877,7 @@ proc readrefs {} {
+     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
+     }
+@@ -1832,7 +1925,7 @@ proc readrefs {} {
+     set selectheadid {}
+     if {$selecthead ne {}} {
+         catch {
+-            set selectheadid [exec git rev-parse --verify $selecthead]
++            set selectheadid [safe_exec [list git rev-parse --verify $selecthead]]
+         }
+     }
+ }
+@@ -2092,7 +2185,7 @@ proc makewindow {} {
+             {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}
+         }}
+@@ -2874,7 +2967,7 @@ proc savestuff {w} {
+     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"
+             }
+@@ -2955,9 +3048,9 @@ proc savestuff {w} {
+ proc resizeclistpanes {win w} {
+     global oldwidth oldsash use_ttk
+     if {[info exists oldwidth($win)]} {
+-	if {[info exists oldsash($win)]} {
+-	    set s0 [lindex $oldsash($win) 0]
+-	    set s1 [lindex $oldsash($win) 1]
++        if {[info exists oldsash($win)]} {
++            set s0 [lindex $oldsash($win) 0]
++            set s1 [lindex $oldsash($win) 1]
+         } elseif {$use_ttk} {
+             set s0 [$win sashpos 0]
+             set s1 [$win sashpos 1]
+@@ -2991,8 +3084,10 @@ proc resizeclistpanes {win w} {
+         } else {
+             $win sash place 0 $sash0 [lindex $s0 1]
+             $win sash place 1 $sash1 [lindex $s1 1]
++            set sash0 [list $sash0 [lindex $s0 1]]
++            set sash1 [list $sash1 [lindex $s1 1]]
+         }
+-	set oldsash($win) [list $sash0 $sash1]
++        set oldsash($win) [list $sash0 $sash1]
+     }
+     set oldwidth($win) $w
+ }
+@@ -3000,8 +3095,8 @@ proc resizeclistpanes {win w} {
+ proc resizecdetpanes {win w} {
+     global oldwidth oldsash use_ttk
+     if {[info exists oldwidth($win)]} {
+-	if {[info exists oldsash($win)]} {
+-	    set s0 $oldsash($win)
++        if {[info exists oldsash($win)]} {
++            set s0 $oldsash($win)
+         } elseif {$use_ttk} {
+             set s0 [$win sashpos 0]
+         } else {
+@@ -3023,8 +3118,9 @@ proc resizecdetpanes {win w} {
+             $win sashpos 0 $sash0
+         } else {
+             $win sash place 0 $sash0 [lindex $s0 1]
++            set sash0 [list $sash0 [lindex $s0 1]]
+         }
+-	set oldsash($win) $sash0
++        set oldsash($win) $sash0
+     }
+     set oldwidth($win) $w
+ }
+@@ -3587,7 +3683,7 @@ proc gitknewtmpdir {} {
+             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]} {
+@@ -3609,7 +3705,7 @@ proc gitknewtmpdir {} {
+ 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
+         }
+@@ -3674,7 +3770,7 @@ proc external_diff {} {
+ 
+     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 {
+@@ -3778,7 +3874,7 @@ proc external_blame_diff {} {
+ # 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]
+@@ -3838,7 +3934,7 @@ proc external_blame {parent_idx {line {}}} {
+     # 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"
+     }
+ }
+@@ -3866,7 +3962,7 @@ proc show_line_source {} {
+                 # 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]} {
+@@ -3889,19 +3985,17 @@ proc show_line_source {} {
+         }
+         set line [lindex $h 1]
+     }
+-    set blameargs {}
++    set blamefile [file join $cdup $flist_menu_file]
+     if {$from_index ne {}} {
+-        lappend blameargs | git cat-file blob $from_index
+-    }
+-    lappend blameargs | git blame -p -L$line,+1
+-    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
+@@ -4826,8 +4920,8 @@ proc do_file_hl {serial} {
+         # 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 {}
+@@ -5256,8 +5350,8 @@ proc get_viewmainhead {view} {
+     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
+@@ -5322,14 +5416,14 @@ proc dodiffindex {} {
+     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]
+@@ -5354,11 +5448,11 @@ proc readdiffindex {fd serial inst} {
+     }
+ 
+     # 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]
+@@ -7147,8 +7241,8 @@ proc browseweb {url} {
+     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"
+     }
+ }
+@@ -7650,13 +7744,13 @@ proc gettree {id} {
+     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
+@@ -7720,13 +7814,13 @@ proc showfile {f} {
+         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
+         }
+@@ -7876,7 +7970,7 @@ proc diffcmd {ids flags} {
+     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 {
+@@ -7884,7 +7978,7 @@ proc diffcmd {ids flags} {
+             }
+         } 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
+             }
+@@ -7893,7 +7987,7 @@ proc diffcmd {ids flags} {
+         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} {
+@@ -7909,7 +8003,7 @@ proc diffcmd {ids flags} {
+         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
+ }
+@@ -7921,7 +8015,7 @@ proc gettreediffs {ids} {
+     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 {}
+@@ -8041,7 +8135,7 @@ proc getblobdiffs {ids} {
+     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
+     }
+@@ -8758,7 +8852,7 @@ proc gotocommit {} {
+                 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
+             }
+@@ -9064,10 +9158,8 @@ proc getpatchid {id} {
+ 
+     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"
+@@ -9163,14 +9255,14 @@ proc diffcommits {a b} {
+     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
+@@ -9310,10 +9402,7 @@ proc mkpatchgo {} {
+     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}
+@@ -9382,9 +9471,9 @@ proc domktag {} {
+     }
+     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
+@@ -9452,7 +9541,7 @@ proc copyreference {} {
+     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
+@@ -9502,7 +9591,7 @@ proc wrcomgo {} {
+     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}
+@@ -9606,7 +9695,7 @@ proc mkbrgo {top} {
+     nowbusy newbranch
+     update
+     if {[catch {
+-        eval exec git branch $cmdargs
++        safe_exec [concat git branch $cmdargs]
+     } err]} {
+         notbusy newbranch
+         error_popup $err
+@@ -9647,7 +9736,7 @@ proc mvbrgo {top prevname} {
+     nowbusy renamebranch
+     update
+     if {[catch {
+-        eval exec git branch $cmdargs
++        safe_exec [concat git branch $cmdargs]
+     } err]} {
+         notbusy renamebranch
+         error_popup $err
+@@ -9688,7 +9777,7 @@ proc exec_citool {tool_args {baseid {}}} {
+         }
+     }
+ 
+-    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
+@@ -9711,7 +9800,7 @@ proc cherrypick {} {
+     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)} \
+@@ -9773,7 +9862,7 @@ proc revert {} {
+     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] {
+@@ -9849,8 +9938,8 @@ proc resethead {} {
+     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
+@@ -9921,7 +10010,7 @@ proc cobranch {} {
+ 
+     # 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]
+@@ -9935,12 +10024,11 @@ proc cobranch {} {
+     } 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
+@@ -10006,7 +10094,7 @@ proc rmbranch {} {
+     }
+     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
+@@ -10197,7 +10285,7 @@ proc getallcommits {} {
+         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
+@@ -10206,7 +10294,7 @@ proc getallcommits {} {
+     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"
+@@ -10228,10 +10316,17 @@ proc getallcommits {} {
+             foreach id $seeds {
+                 lappend ids "^$id"
+             }
++            lappend ids "--"
+         }
+     }
+     if {$ids ne {}} {
+-        set fd [open [concat $cmd $ids] r]
++        if {$ids eq "--all"} {
++            set cmd [concat $cmd "--all"]
++            set fd [safe_open_command $cmd]
++        } else {
++            set cmd [concat $cmd --stdin]
++            set fd [safe_open_command_redirect $cmd [list "<<[join $ids "\n"]"]]
++        }
+         fconfigure $fd -blocking 0
+         incr allcommits
+         nowbusy allcommits
+@@ -10621,7 +10716,7 @@ proc savecache {} {
+     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
+     }
+@@ -11324,7 +11419,7 @@ proc add_tag_ctext {tag} {
+ 
+     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
+@@ -11927,7 +12022,7 @@ proc formatdate {d} {
+ }
+ 
+ # This list of encoding names and aliases is distilled from
+-# http://www.iana.org/assignments/character-sets.
++# https://www.iana.org/assignments/character-sets.
+ # Not all of them are supported by Tcl.
+ set encoding_aliases {
+     { ANSI_X3.4-1968 iso-ir-6 ANSI_X3.4-1986 ISO_646.irv:1991 ASCII
+@@ -12210,7 +12305,7 @@ proc gitattr {path attr default} {
+         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
+@@ -12237,7 +12332,7 @@ proc cache_gitattr {attr pathlist} {
+     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 "\""} {
+@@ -12290,11 +12385,11 @@ if {[catch {package require Tk 8.4} err]} {
+ 
+ # 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
+@@ -12443,7 +12538,7 @@ if {[tk windowingsystem] eq "aqua"} {
+ 
+ catch {
+     # follow the XDG base directory specification by default. See
+-    # http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
++    # https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
+     if {[info exists env(XDG_CONFIG_HOME)] && $env(XDG_CONFIG_HOME) ne ""} {
+         # XDG_CONFIG_HOME environment variable is set
+         set config_file [file join $env(XDG_CONFIG_HOME) git gitk]
+@@ -12539,7 +12634,7 @@ if {$selecthead eq "HEAD"} {
+ 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]
+@@ -12705,3 +12800,4 @@ getcommits {}
+ # indent-tabs-mode: t
+ # tab-width: 8
+ # End:
++
+-- 
+2.50.1
+
diff --git a/meta/recipes-devtools/git/git_2.35.7.bb b/meta/recipes-devtools/git/git_2.35.7.bb
index 765180a38d..3520b4db90 100644
--- a/meta/recipes-devtools/git/git_2.35.7.bb
+++ b/meta/recipes-devtools/git/git_2.35.7.bb
@@ -26,6 +26,7 @@  SRC_URI = "${KERNELORG_MIRROR}/software/scm/git/git-${PV}.tar.gz;name=tarball \
            file://CVE-2024-50349-0001.patch \
            file://CVE-2024-50349-0002.patch \
            file://CVE-2024-52006.patch \
+           file://CVE-2025-27614-CVE-2025-27613-CVE-2025-46334-CVE-2025-46835.patch \
            "
 
 S = "${WORKDIR}/git-${PV}"