diff options
author | Junio C Hamano <gitster@pobox.com> | 2025-07-07 15:08:10 -0700 |
---|---|---|
committer | Junio C Hamano <gitster@pobox.com> | 2025-07-07 15:08:10 -0700 |
commit | 038143def708a65455172a87432aee27da2d80c4 (patch) | |
tree | a6271d7824fe36b364f6f19f2437de785820aab8 | |
parent | 41905d60226a0346b22f0d0d99428c746a5a3b14 (diff) | |
parent | d82adb61ba2fd11d8f2587fca1b6bd7925ce4044 (diff) |
Sync with Git 2.50.1
35 files changed, 758 insertions, 450 deletions
diff --git a/Documentation/RelNotes/2.43.7.adoc b/Documentation/RelNotes/2.43.7.adoc new file mode 100644 index 0000000000..95702a036e --- /dev/null +++ b/Documentation/RelNotes/2.43.7.adoc @@ -0,0 +1,73 @@ +Git v2.43.7 Release Notes +========================= + +This release includes fixes for CVE-2025-27613, CVE-2025-27614, +CVE-2025-46334, CVE-2025-46835, CVE-2025-48384, CVE-2025-48385, and +CVE-2025-48386. + +Fixes since v2.43.6 +------------------- + + * CVE-2025-27613, Gitk: + + When a user clones an untrusted repository and runs Gitk without + additional command arguments, any writable file can be created and + truncated. The option "Support per-file encoding" must have been + enabled. The operation "Show origin of this line" is affected as + well, regardless of the option being enabled or not. + + * CVE-2025-27614, Gitk: + + A Git repository can be crafted in such a way that a user who has + cloned the repository can be tricked into running any script + supplied by the attacker by invoking `gitk filename`, where + `filename` has a particular structure. + + * CVE-2025-46334, Git GUI (Windows only): + + A malicious repository can ship versions of sh.exe or typical + textconv filter programs such as astextplain. On Windows, path + lookup can find such executables in the worktree. These programs + are invoked when the user selects "Git Bash" or "Browse Files" from + the menu. + + * CVE-2025-46835, Git GUI: + + When a user clones an untrusted repository and is tricked into + editing a file located in a maliciously named directory in the + repository, then Git GUI can create and overwrite any writable + file. + + * CVE-2025-48384, Git: + + When reading a config value, Git strips any trailing carriage + return and line feed (CRLF). When writing a config entry, values + with a trailing CR are not quoted, causing the CR to be lost when + the config is later read. When initializing a submodule, if the + submodule path contains a trailing CR, the altered path is read + resulting in the submodule being checked out to an incorrect + location. If a symlink exists that points the altered path to the + submodule hooks directory, and the submodule contains an executable + post-checkout hook, the script may be unintentionally executed + after checkout. + + * CVE-2025-48385, Git: + + When cloning a repository Git knows to optionally fetch a bundle + advertised by the remote server, which allows the server-side to + offload parts of the clone to a CDN. The Git client does not + perform sufficient validation of the advertised bundles, which + allows the remote side to perform protocol injection. + + This protocol injection can cause the client to write the fetched + bundle to a location controlled by the adversary. The fetched + content is fully controlled by the server, which can in the worst + case lead to arbitrary code execution. + + * CVE-2025-48386, Git: + + The wincred credential helper uses a static buffer (`target`) as a + unique key for storing and comparing against internal storage. This + credential helper does not properly bounds check the available + space remaining in the buffer before appending to it with + `wcsncat()`, leading to potential buffer overflows. diff --git a/Documentation/RelNotes/2.44.4.adoc b/Documentation/RelNotes/2.44.4.adoc new file mode 100644 index 0000000000..8db4d5b537 --- /dev/null +++ b/Documentation/RelNotes/2.44.4.adoc @@ -0,0 +1,7 @@ +Git v2.44.4 Release Notes +========================= + +This release merges up the fixes that appears in v2.43.7 to address +the following CVEs: CVE-2025-27613, CVE-2025-27614, CVE-2025-46334, +CVE-2025-46835, CVE-2025-48384, CVE-2025-48385, and CVE-2025-48386. +See the release notes for v2.43.7 for details. diff --git a/Documentation/RelNotes/2.45.4.adoc b/Documentation/RelNotes/2.45.4.adoc new file mode 100644 index 0000000000..5b50d8daf0 --- /dev/null +++ b/Documentation/RelNotes/2.45.4.adoc @@ -0,0 +1,7 @@ +Git v2.45.4 Release Notes +========================= + +This release merges up the fixes that appears in v2.43.7, and v2.44.4 +to address the following CVEs: CVE-2025-27613, CVE-2025-27614, +CVE-2025-46334, CVE-2025-46835, CVE-2025-48384, CVE-2025-48385, and +CVE-2025-48386. See the release notes for v2.43.7 for details. diff --git a/Documentation/RelNotes/2.46.4.adoc b/Documentation/RelNotes/2.46.4.adoc new file mode 100644 index 0000000000..622f4c752f --- /dev/null +++ b/Documentation/RelNotes/2.46.4.adoc @@ -0,0 +1,7 @@ +Git v2.46.4 Release Notes +========================= + +This release merges up the fixes that appears in v2.43.7, v2.44.4, and +v2.45.4 to address the following CVEs: CVE-2025-27613, CVE-2025-27614, +CVE-2025-46334, CVE-2025-46835, CVE-2025-48384, CVE-2025-48385, and +CVE-2025-48386. See the release notes for v2.43.7 for details. diff --git a/Documentation/RelNotes/2.47.3.adoc b/Documentation/RelNotes/2.47.3.adoc new file mode 100644 index 0000000000..bc2a2b833b --- /dev/null +++ b/Documentation/RelNotes/2.47.3.adoc @@ -0,0 +1,8 @@ +Git v2.47.3 Release Notes +========================= + +This release merges up the fixes that appears in v2.43.7, v2.44.4, +v2.45.4, and v2.46.4 to address the following CVEs: CVE-2025-27613, +CVE-2025-27614, CVE-2025-46334, CVE-2025-46835, CVE-2025-48384, +CVE-2025-48385, and CVE-2025-48386. See the release notes for v2.43.7 +for details. diff --git a/Documentation/RelNotes/2.48.2.adoc b/Documentation/RelNotes/2.48.2.adoc new file mode 100644 index 0000000000..f3f2f90c2b --- /dev/null +++ b/Documentation/RelNotes/2.48.2.adoc @@ -0,0 +1,8 @@ +Git v2.48.2 Release Notes +========================= + +This release merges up the fixes that appears in v2.43.7, v2.44.4, +v2.45.4, v2.46.4, and v2.47.3 to address the following CVEs: +CVE-2025-27613, CVE-2025-27614, CVE-2025-46334, CVE-2025-46835, +CVE-2025-48384, CVE-2025-48385, and CVE-2025-48386. See the release +notes for v2.43.7 for details. diff --git a/Documentation/RelNotes/2.49.1.adoc b/Documentation/RelNotes/2.49.1.adoc new file mode 100644 index 0000000000..c619e8b495 --- /dev/null +++ b/Documentation/RelNotes/2.49.1.adoc @@ -0,0 +1,12 @@ +Git v2.49.1 Release Notes +========================= + +This release merges up the fixes that appear in v2.43.7, v2.44.4, +v2.45.4, v2.46.4, v2.47.3, and v2.48.2 to address the following CVEs: +CVE-2025-27613, CVE-2025-27614, CVE-2025-46334, CVE-2025-46835, +CVE-2025-48384, CVE-2025-48385, and CVE-2025-48386. See the release +notes for v2.43.7 for details. + +It also contains some updates to various CI bits to work around +and/or to adjust to the deprecation of use of Ubuntu 20.04 GitHub +Actions CI, updates to to Fedora base image. diff --git a/Documentation/RelNotes/2.50.1.adoc b/Documentation/RelNotes/2.50.1.adoc new file mode 100644 index 0000000000..aa4a71adbc --- /dev/null +++ b/Documentation/RelNotes/2.50.1.adoc @@ -0,0 +1,8 @@ +Git v2.50.1 Release Notes +========================= + +This release merges up the fixes that appear in v2.43.7, v2.44.4, +v2.45.4, v2.46.4, v2.47.3, v2.48.2, and v2.49.1 to address the +following CVEs: CVE-2025-27613, CVE-2025-27614, CVE-2025-46334, +CVE-2025-46835, CVE-2025-48384, CVE-2025-48385, and +CVE-2025-48386. See the release notes for v2.43.7 for details. diff --git a/bundle-uri.c b/bundle-uri.c index c9d65aa0ce..8708d189e3 100644 --- a/bundle-uri.c +++ b/bundle-uri.c @@ -297,6 +297,28 @@ static int download_https_uri_to_file(const char *file, const char *uri) struct strbuf line = STRBUF_INIT; int found_get = 0; + /* + * The protocol we speak with git-remote-https(1) uses a space to + * separate between URI and file, so the URI itself must not contain a + * space. If it did, an adversary could change the location where the + * downloaded file is being written to. + * + * Similarly, we use newlines to separate commands from one another. + * Consequently, neither the URI nor the file must contain a newline or + * otherwise an adversary could inject arbitrary commands. + * + * TODO: Restricting newlines in the target paths may break valid + * usecases, even if those are a bit more on the esoteric side. + * If this ever becomes a problem we should probably think about + * alternatives. One alternative could be to use NUL-delimited + * requests in git-remote-http(1). Another alternative could be + * to use URL quoting. + */ + if (strpbrk(uri, " \n")) + return error("bundle-uri: URI is malformed: '%s'", file); + if (strchr(file, '\n')) + return error("bundle-uri: filename is malformed: '%s'", file); + strvec_pushl(&cp.args, "git-remote-https", uri, NULL); cp.err = -1; cp.in = -1; @@ -2935,7 +2935,7 @@ static ssize_t write_pair(int fd, const char *key, const char *value, if (value[0] == ' ') quote = "\""; for (i = 0; value[i]; i++) - if (value[i] == ';' || value[i] == '#') + if (value[i] == ';' || value[i] == '#' || value[i] == '\r') quote = "\""; if (i && value[i - 1] == ' ') quote = "\""; diff --git a/contrib/credential/wincred/git-credential-wincred.c b/contrib/credential/wincred/git-credential-wincred.c index 04145b5118..5683846b4b 100644 --- a/contrib/credential/wincred/git-credential-wincred.c +++ b/contrib/credential/wincred/git-credential-wincred.c @@ -39,6 +39,14 @@ static void *xmalloc(size_t size) static WCHAR *wusername, *password, *protocol, *host, *path, target[1024], *password_expiry_utc, *oauth_refresh_token; +static void target_append(const WCHAR *src) +{ + size_t avail = ARRAY_SIZE(target) - wcslen(target) - 1; /* -1 for NUL */ + if (avail < wcslen(src)) + die("target buffer overflow"); + wcsncat(target, src, avail); +} + static void write_item(const char *what, LPCWSTR wbuf, int wlen) { char *buf; @@ -330,17 +338,17 @@ int main(int argc, char *argv[]) /* prepare 'target', the unique key for the credential */ wcscpy(target, L"git:"); - wcsncat(target, protocol, ARRAY_SIZE(target)); - wcsncat(target, L"://", ARRAY_SIZE(target)); + target_append(protocol); + target_append(L"://"); if (wusername) { - wcsncat(target, wusername, ARRAY_SIZE(target)); - wcsncat(target, L"@", ARRAY_SIZE(target)); + target_append(wusername); + target_append(L"@"); } if (host) - wcsncat(target, host, ARRAY_SIZE(target)); + target_append(host); if (path) { - wcsncat(target, L"/", ARRAY_SIZE(target)); - wcsncat(target, path, ARRAY_SIZE(target)); + target_append(L"/"); + target_append(path); } if (!strcmp(argv[1], "get")) diff --git a/git-gui/git-gui.sh b/git-gui/git-gui.sh index 28572c889c..c77c05edde 100755 --- a/git-gui/git-gui.sh +++ b/git-gui/git-gui.sh @@ -77,99 +77,178 @@ proc is_Cygwin {} { ###################################################################### ## -## PATH lookup +## PATH lookup. Sanitize $PATH, assure exec/open use only that + +if {[is_Windows]} { + set _path_sep {;} + set _search_exe .exe +} else { + set _path_sep {:} + set _search_exe {} +} + +if {[is_Windows]} { + set gitguidir [file dirname [info script]] + regsub -all ";" $gitguidir "\\;" gitguidir + set env(PATH) "$gitguidir;$env(PATH)" +} set _search_path {} -proc _which {what args} { - global env _search_exe _search_path +set _path_seen [dict create] +foreach p [split $env(PATH) $_path_sep] { + # Keep only absolute paths, getting rid of ., empty, etc. + if {[file pathtype $p] ne {absolute}} { + continue + } + # Keep only the first occurence of any duplicates. + set norm_p [file normalize $p] + if {[dict exists $_path_seen $norm_p]} { + continue + } + dict set _path_seen $norm_p 1 + lappend _search_path $norm_p +} +unset _path_seen - if {$_search_path eq {}} { - if {[is_Windows]} { - set gitguidir [file dirname [info script]] - regsub -all ";" $gitguidir "\\;" gitguidir - set env(PATH) "$gitguidir;$env(PATH)" - set _search_path [split $env(PATH) {;}] - # Skip empty `PATH` elements - set _search_path [lsearch -all -inline -not -exact \ - $_search_path ""] - set _search_exe .exe +set env(PATH) [join $_search_path $_path_sep] + +if {[is_Windows]} { + proc _which {what args} { + global _search_exe _search_path + + if {[lsearch -exact $args -script] >= 0} { + set suffix {} + } elseif {[string match *$_search_exe [string tolower $what]]} { + # The search string already has the file extension + set suffix {} } else { - set _search_path [split $env(PATH) :] - set _search_exe {} + set suffix $_search_exe } - } - if {[is_Windows] && [lsearch -exact $args -script] >= 0} { - set suffix {} - } else { - set suffix $_search_exe - } - - foreach p $_search_path { - set p [file join $p $what$suffix] - if {[file exists $p]} { - return [file normalize $p] + foreach p $_search_path { + set p [file join $p $what$suffix] + if {[file exists $p]} { + return [file normalize $p] + } } + return {} } - 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" + 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 + } } - lset command_line $i $fullpath } + return $command_line + } + + # Override `exec` to avoid unsafe PATH lookup + + rename exec real_exec - # handle piped commands, e.g. `exec A | B` - for {incr i} {$i < [llength $command_line]} {incr i} { - if {[lindex $command_line $i] eq "|"} { + proc exec {args} { + # skip options + for {set i 0} {$i < [llength $args]} {incr i} { + set arg [lindex $args $i] + if {$arg eq "--"} { incr i break } + if {[string range $arg 0 0] ne "-"} { + break + } } + set args [sanitize_command_line $args $i] + uplevel 1 real_exec $args } - return $command_line -} -# Override `exec` to avoid unsafe PATH lookup + # Override `open` to avoid unsafe PATH lookup -rename exec real_exec + rename open real_open -proc exec {args} { - # skip options - for {set i 0} {$i < [llength $args]} {incr i} { - set arg [lindex $args $i] - if {$arg eq "--"} { - incr i - break - } - if {[string range $arg 0 0] ne "-"} { - break + proc open {args} { + set arg0 [lindex $args 0] + if {[string range $arg0 0 0] eq "|"} { + set command_line [string trim [string range $arg0 1 end]] + lset args 0 "| [sanitize_command_line $command_line 0]" } + uplevel 1 real_open $args + } + +} else { + # On non-Windows platforms, auto_execok, exec, and open are safe, and will + # use the sanitized search path. But, we need _which for these. + + proc _which {what args} { + return [lindex [auto_execok $what] 0] } - set args [sanitize_command_line $args $i] - uplevel 1 real_exec $args } -# Override `open` to avoid unsafe PATH lookup +# Wrap exec/open to sanitize arguments -rename open real_open +# unsafe arguments begin with redirections or the pipe or background operators +proc is_arg_unsafe {arg} { + regexp {^([<|>&]|2>)} $arg +} -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]" +proc make_arg_safe {arg} { + if {[is_arg_unsafe $arg]} { + set arg [file join . $arg] } - uplevel 1 real_open $args + 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 @@ -270,11 +349,11 @@ 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]]] } } @@ -304,15 +383,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 } @@ -494,7 +595,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 @@ -524,32 +625,14 @@ proc _git_cmd {name} { return $v } -# Test a file for a hashbang to identify executable scripts on Windows. -proc is_shellscript {filename} { - if {![file exists $filename]} {return 0} - set f [open $filename r] - fconfigure $f -encoding binary - set magic [read $f 2] - close $f - return [expr {$magic eq "#!"}] -} - -# Run a command connected via pipes on stdout. +# Run a shell command connected via pipes on stdout. # This is for use with textconv filters and uses sh -c "..." to allow it to -# contain a command with arguments. On windows we must check for shell -# scripts specifically otherwise just call the filter command. +# contain a command with arguments. We presume this +# to be a shellscript that the configured shell (/bin/sh by default) knows +# how to run. proc open_cmd_pipe {cmd path} { - global env - if {![file executable [shellpath]]} { - set exe [auto_execok [lindex $cmd 0]] - if {[is_shellscript [lindex $exe 0]]} { - set run [linsert [auto_execok sh] end -c "$cmd \"\$0\"" $path] - } else { - set run [concat $exe [lrange $cmd 1 end] $path] - } - } else { - set run [list [shellpath] -c "$cmd \"\$0\"" $path] - } + set run [list [shellpath] -c "$cmd \"\$0\"" $path] + set run [make_arglist_safe $run] return [open |$run r] } @@ -559,7 +642,7 @@ 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 {} @@ -571,7 +654,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 @@ -581,88 +668,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 - } - - default { - break - } - - } - - set args [lrange $args 1 end] - } +proc git_read {cmd {redir {}}} { + set cmdp [_git_cmd [lindex $cmd 0]] + set cmd [lrange $cmd 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 cmd [concat git hook run --ignore-missing $hook_name -- $args 2>@1] - return [_open_stdout_stderr $cmd] + git_read [concat [list hook run --ignore-missing $hook_name --] $args] [list 2>@1] } proc kill_file_process {fd} { @@ -670,9 +716,9 @@ 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] } } } @@ -698,7 +744,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 {} @@ -1068,7 +1114,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 \ @@ -1106,53 +1152,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) {} } } } @@ -1427,7 +1450,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 } @@ -1446,7 +1469,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 } @@ -1505,12 +1528,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] @@ -1550,11 +1573,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 @@ -1563,7 +1586,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 @@ -1575,7 +1598,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 {} @@ -1599,23 +1622,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 @@ -2205,7 +2228,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 @@ -2242,7 +2265,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 @@ -2273,16 +2296,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 @@ -2316,7 +2341,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 @@ -2760,17 +2785,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]} { @@ -4079,7 +4103,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 8441e109be..d6fd8bea91 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 8b0c485889..39e0f2dc98 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 a982983667..6fc8d4d637 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 21ea768d80..87ed0b4858 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 d23abedcb3..5b361cc424 100644 --- a/git-gui/lib/choose_repository.tcl +++ b/git-gui/lib/choose_repository.tcl @@ -641,8 +641,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 @@ -727,7 +727,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 @@ -760,8 +760,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 @@ -818,12 +818,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} { @@ -877,7 +877,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 @@ -953,13 +953,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 6dae7937d5..8ae7e8a5c4 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 a570f9cdc6..60d66172a1 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 @@ -236,7 +236,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 @@ -336,7 +336,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] } @@ -361,7 +361,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 @@ -399,8 +399,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."] @@ -420,7 +420,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 fafafb81f1..a017cfeadd 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 -encoding [encoding system] fileevent $fd_f readable [cb _read $fd_f $after] diff --git a/git-gui/lib/database.tcl b/git-gui/lib/database.tcl index 85783081e0..1fc0ea00b3 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 d657bfec05..84f0468c7c 100644 --- a/git-gui/lib/diff.tcl +++ b/git-gui/lib/diff.tcl @@ -191,7 +191,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] @@ -215,7 +215,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] @@ -327,7 +327,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]] @@ -603,7 +603,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]} { @@ -839,7 +839,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 @@ -876,7 +876,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 d2ec24bd80..857864ff2b 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 664803cf3f..44c3f93584 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 8b8c16b1d6..2c9bb3af40 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] @@ -310,7 +310,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 @@ -360,9 +360,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 ef77ed7399..cf796d1601 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 5ba9fcadd1..c8c99b17a8 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 674a41f5e0..1d01d9cbfa 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,8 +30,8 @@ proc do_cygwin_shortcut {} { global argv0 _gitworktree oguilib if {[catch { - set desktop [exec cygpath \ - --desktop] + set desktop [safe_exec [list cygpath \ + --desktop]] }]} { set desktop . } @@ -50,14 +50,14 @@ proc do_cygwin_shortcut {} { "CHERE_INVOKING=1 \ source /etc/profile; \ git gui"} - exec /bin/mkshortcut.exe \ + safe_exec [list /bin/mkshortcut.exe \ --arguments $shargs \ --desc "git-gui on $repodir" \ --icon $oguilib/git-gui.ico \ --name $fn \ --show min \ --workingdir $repodir \ - /bin/sh.exe + /bin/sh.exe] } err]} { error_popup [strcat [mc "Cannot write shortcut:"] "\n\n$err"] } @@ -83,7 +83,7 @@ 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"> @@ -108,7 +108,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 589ff8f78a..c3e681b899 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 413f1a1700..48fddfd814 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 db91ab84a5..3aedae2f13 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 19689765cd..5be8b2aeb0 100755 --- a/gitk-git/gitk +++ b/gitk-git/gitk @@ -113,6 +113,91 @@ if {[is_Windows]} { # End of safe PATH lookup stuff +# Wrap exec/open to sanitize arguments + +# unsafe arguments begin with redirections or the pipe or background operators +proc is_arg_unsafe {arg} { + regexp {^([<|>&]|2>)} $arg +} + +proc make_arg_safe {arg} { + if {[is_arg_unsafe $arg]} { + set arg [file join . $arg] + } + return $arg +} + +proc make_arglist_safe {arglist} { + set res {} + foreach arg $arglist { + lappend res [make_arg_safe $arg] + } + return $res +} + +# executes one command +# no redirections or pipelines are possible +# cmd is a list that specifies the command and its arguments +# calls `exec` and returns its value +proc safe_exec {cmd} { + eval exec [make_arglist_safe $cmd] +} + +# executes one command with redirections +# no pipelines are possible +# cmd is a list that specifies the command and its arguments +# redir is a list that specifies redirections (output, background, constant(!) commands) +# calls `exec` and returns its value +proc safe_exec_redirect {cmd redir} { + eval exec [make_arglist_safe $cmd] $redir +} + +proc safe_open_file {filename flags} { + # a file name starting with "|" would attempt to run a process + # but such a file name must be treated as a relative path + # hide the "|" behind "./" + if {[string index $filename 0] eq "|"} { + set filename [file join . $filename] + } + open $filename $flags +} + +# opens a command pipeline for reading +# cmd is a list that specifies the command and its arguments +# calls `open` and returns the file id +proc safe_open_command {cmd} { + open |[make_arglist_safe $cmd] r +} + +# opens a command pipeline for reading and writing +# cmd is a list that specifies the command and its arguments +# calls `open` and returns the file id +proc safe_open_command_rw {cmd} { + open |[make_arglist_safe $cmd] r+ +} + +# opens a command pipeline for reading with redirections +# cmd is a list that specifies the command and its arguments +# redir is a list that specifies redirections +# calls `open` and returns the file id +proc safe_open_command_redirect {cmd redir} { + set cmd [make_arglist_safe $cmd] + open |[concat $cmd $redir] r +} + +# opens a pipeline with several commands for reading +# cmds is a list of lists, each of which specifies a command and its arguments +# calls `open` and returns the file id +proc safe_open_pipeline {cmds} { + set cmd {} + foreach subcmd $cmds { + set cmd [concat $cmd | [make_arglist_safe $subcmd]] + } + open $cmd r +} + +# End exec/open wrappers + proc hasworktree {} { return [expr {[exec git rev-parse --is-bare-repository] == "false" && [exec git rev-parse --is-inside-git-dir] == "false"}] @@ -238,7 +323,7 @@ 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 @@ -400,7 +485,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"] @@ -457,16 +542,6 @@ proc parseviewrevs {view revs} { return $ret } -# Escapes a list of filter paths to be passed to git log via stdin. Note that -# paths must not be quoted. -proc escape_filter_paths {paths} { - set escaped [list] - foreach path $paths { - lappend escaped [string map {\\ \\\\ "\ " "\\\ "} $path] - } - return $escaped -} - # Start off a git log process and arrange to read its output proc start_rev_list {view} { global startmsecs commitidx viewcomplete curview @@ -488,7 +563,7 @@ 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 @@ -526,10 +601,9 @@ proc start_rev_list {view} { } if {[catch { - set fd [open [concat | git log --no-color -z --pretty=raw $show_notes \ - --parents --boundary $args --stdin \ - "<<[join [concat $revs "--" \ - [escape_filter_paths $files]] "\\n"]"] r] + set fd [safe_open_command_redirect [concat git log --no-color -z --pretty=raw $show_notes \ + --parents --boundary $args --stdin] \ + [list "<<[join [concat $revs "--" $files] "\n"]"]] } err]} { error_popup "[mc "Error executing git log:"] $err" return 0 @@ -563,9 +637,9 @@ 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} @@ -680,11 +754,9 @@ proc updatecommits {} { set args $vorigargs($view) } if {[catch { - set fd [open [concat | git log --no-color -z --pretty=raw $show_notes \ - --parents --boundary $args --stdin \ - "<<[join [concat $revs "--" \ - [escape_filter_paths \ - $vfilelimit($view)]] "\\n"]"] r] + set fd [safe_open_command_redirect [concat git log --no-color -z --pretty=raw $show_notes \ + --parents --boundary $args --stdin] \ + [list "<<[join [concat $revs "--" $vfilelimit($view)] "\n"]"]] } err]} { error_popup "[mc "Error executing git log:"] $err" return @@ -1651,8 +1723,8 @@ 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 @@ -1772,7 +1844,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 != {}} { @@ -1908,7 +1980,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 } @@ -1956,7 +2028,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]] } } } @@ -2220,7 +2292,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} }} @@ -3007,7 +3079,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" } @@ -3723,7 +3795,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]} { @@ -3745,7 +3817,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 } @@ -3810,7 +3882,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 { @@ -3914,7 +3986,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] @@ -3974,7 +4046,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" } } @@ -4002,7 +4074,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]} { @@ -4025,19 +4097,17 @@ proc show_line_source {} { } set line [lindex $h 1] } - set blameargs {} - if {$from_index ne {}} { - lappend blameargs | git cat-file blob $from_index - } - lappend blameargs | git blame -p -L$line,+1 + set blamefile [file join $cdup $flist_menu_file] if {$from_index ne {}} { - lappend blameargs --contents - + set blameargs [list \ + [list git cat-file blob $from_index] \ + [list git blame -p -L$line,+1 --contents - -- $blamefile]] } else { - lappend blameargs $id + set blameargs [list \ + [list git blame -p -L$line,+1 $id -- $blamefile]] } - lappend blameargs -- [file join $cdup $flist_menu_file] if {[catch { - set f [open $blameargs r] + set f [safe_open_pipeline $blameargs] } err]} { error_popup [mc "Couldn't start git blame: %s" $err] return @@ -4962,8 +5032,8 @@ 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 {} @@ -5392,8 +5462,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 @@ -5458,14 +5528,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] @@ -5490,11 +5560,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] @@ -7283,8 +7353,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" } } @@ -7790,13 +7860,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 @@ -7860,13 +7930,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 } @@ -8016,7 +8086,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 { @@ -8024,7 +8094,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 } @@ -8033,7 +8103,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} { @@ -8049,7 +8119,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 } @@ -8061,7 +8131,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 {} @@ -8181,7 +8251,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 } @@ -8899,7 +8969,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 } @@ -9205,10 +9275,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" @@ -9304,14 +9372,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 @@ -9451,10 +9519,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} @@ -9523,9 +9588,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 @@ -9593,7 +9658,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 @@ -9643,7 +9708,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} @@ -9747,7 +9812,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 @@ -9788,7 +9853,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 @@ -9829,7 +9894,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 @@ -9852,7 +9917,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)} \ @@ -9914,7 +9979,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] { @@ -9990,8 +10055,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 @@ -10062,7 +10127,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] @@ -10076,12 +10141,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 @@ -10147,7 +10211,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 @@ -10338,7 +10402,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 @@ -10347,7 +10411,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" @@ -10375,10 +10439,11 @@ proc getallcommits {} { if {$ids ne {}} { if {$ids eq "--all"} { set cmd [concat $cmd "--all"] + set fd [safe_open_command $cmd] } else { - set cmd [concat $cmd --stdin "<<[join $ids "\\n"]"] + set cmd [concat $cmd --stdin] + set fd [safe_open_command_redirect $cmd [list "<<[join $ids "\n"]"]] } - set fd [open $cmd r] fconfigure $fd -blocking 0 incr allcommits nowbusy allcommits @@ -10768,7 +10833,7 @@ 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 } @@ -11471,7 +11536,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 @@ -12382,7 +12447,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 @@ -12409,7 +12474,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 "\""} { @@ -12461,11 +12526,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 @@ -12713,7 +12778,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] diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 51a85e83c2..f856821839 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -2851,4 +2851,15 @@ test_expect_success 'writing to stdin is rejected' ' done +test_expect_success 'writing value with trailing CR not stripped on read' ' + test_when_finished "rm -rf cr-test" && + + printf "bar\r\n" >expect && + git init cr-test && + git -C cr-test config set core.foo $(printf "bar\r") && + git -C cr-test config get core.foo >actual && + + test_cmp expect actual +' + test_done diff --git a/t/t5558-clone-bundle-uri.sh b/t/t5558-clone-bundle-uri.sh index 9b211a626b..7a0943bd36 100755 --- a/t/t5558-clone-bundle-uri.sh +++ b/t/t5558-clone-bundle-uri.sh @@ -1279,6 +1279,29 @@ test_expect_success 'bundles are downloaded once during fetch --all' ' trace-mult.txt >bundle-fetches && test_line_count = 1 bundle-fetches ' + +test_expect_success 'bundles with space in URI are rejected' ' + test_when_finished "rm -rf busted repo" && + mkdir -p "$HOME/busted/ /$HOME/repo/.git/objects/bundles" && + git clone --bundle-uri="$HTTPD_URL/bogus $HOME/busted/" "$HTTPD_URL/smart/fetch.git" repo 2>err && + test_grep "error: bundle-uri: URI is malformed: " err && + find busted -type f >files && + test_must_be_empty files +' + +test_expect_success 'bundles with newline in URI are rejected' ' + test_when_finished "rm -rf busted repo" && + git clone --bundle-uri="$HTTPD_URL/bogus\nget $HTTPD_URL/bogus $HOME/busted" "$HTTPD_URL/smart/fetch.git" repo 2>err && + test_grep "error: bundle-uri: URI is malformed: " err && + test_path_is_missing "$HOME/busted" +' + +test_expect_success 'bundles with newline in target path are rejected' ' + git clone --bundle-uri="$HTTPD_URL/bogus" "$HTTPD_URL/smart/fetch.git" "$(printf "escape\nget $HTTPD_URL/bogus .")" 2>err && + test_grep "error: bundle-uri: filename is malformed: " err && + test_path_is_missing escape +' + # Do not add tests here unless they use the HTTP server, as they will # not run unless the HTTP dependencies exist. diff --git a/t/t7450-bad-git-dotfiles.sh b/t/t7450-bad-git-dotfiles.sh index 9367794641..14b5743b96 100755 --- a/t/t7450-bad-git-dotfiles.sh +++ b/t/t7450-bad-git-dotfiles.sh @@ -372,4 +372,37 @@ test_expect_success 'checkout -f --recurse-submodules must not use a nested gitd test_path_is_missing nested_checkout/thing2/.git ' +test_expect_success SYMLINKS,!WINDOWS,!MINGW 'submodule must not checkout into different directory' ' + test_when_finished "rm -rf sub repo bad-clone" && + + git init sub && + write_script sub/post-checkout <<-\EOF && + touch "$PWD/foo" + EOF + git -C sub add post-checkout && + git -C sub commit -m hook && + + git init repo && + git -C repo -c protocol.file.allow=always submodule add "$PWD/sub" sub && + git -C repo mv sub $(printf "sub\r") && + + # Ensure config values containing CR are wrapped in quotes. + git config unset -f repo/.gitmodules submodule.sub.path && + printf "\tpath = \"sub\r\"\n" >>repo/.gitmodules && + + git config unset -f repo/.git/modules/sub/config core.worktree && + { + printf "[core]\n" && + printf "\tworktree = \"../../../sub\r\"\n" + } >>repo/.git/modules/sub/config && + + ln -s .git/modules/sub/hooks repo/sub && + git -C repo add -A && + git -C repo commit -m submodule && + + git -c protocol.file.allow=always clone --recurse-submodules repo bad-clone && + ! test -f "$PWD/foo" && + test -f $(printf "bad-clone/sub\r/post-checkout") +' + test_done |