new file mode 100644
@@ -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
+
@@ -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}"
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