Skip to content

Commit 06ec324

Browse files
dschoGit for Windows Build Agent
authored and
Git for Windows Build Agent
committed
Merge branch 'ctrl-c-with-CtrlRoutine'
This thing again... Background: when you hit Ctrl+C on Linux or macOS, a signal (SIGINT) is sent to the foreground process and its child processes. This signal can be intercepted by installing a signal handler for this specific signal. On Windows, there is no precise equivalent for this system. Instead, the Ctrl+C is translated by the current ConHost (i.e. the container running the Console processes) to a ConsoleCtrl event that is sent to all processes attached to that Console. If any of these processes installed a handler via SetConsoleCtrlHandler(), they can intercept that event (and avoid exiting or doing some cleanup work). On Linux and macOS (and every Unix flavor, really), processes can also be killed via the `kill` executable, which really just sends a signal to the process, typically SIGTERM. Processes can intercept that signal, too. To force processes to terminate, without giving them any chance to prevent that, SIGKILL can be sent. There is no equivalent for SIGTERM on Windows. To emulate SIGKILL on Windows, TerminateProcess() can be used, but it only kills one process (unlike SIGKILL, which is sent also to the child processes). In Git for Windows, we struggled with emulating SIGINT, SIGTERM and SIGKILL handling essentially since the beginning of the efforts to port Git to Windows. At least the SIGINT part of the problem becomes a lot worse when using a terminal window other than cmd.exe's: as long as using cmd.exe (AKA "ConHost"), Ctrl+C is handled entirely outside of our code. But with the big jump from v1.x to v2.x, Git for Windows not only switched from MSys to MSYS2, but also started using MinTTY as the default terminal window, which uses the MSYS2 runtime-provided pseudo terminals (inherited from Cygwin thanks to the MSYS2 runtime being a "friendly fork" of Cygwin). When Ctrl+C is pressed in MinTTY, all of the signaling has to be done by our code. The original code to handle Ctrl+C comes straight from Cygwin. It simply ignores the entire conundrum for non-Cygwin processes and simply calls TerminateProcess() on them, leaving spawned child processes running. The first attempt at fixing "the Ctrl+C problem" (with the symptom that interrupting `git clone ...` would not stop the actual download of the Git objects that was still running in a child process) was c4ba4e3357f. It simply enumerated all the processes' process IDs and parent process IDs and extracted the tree of (possibly transitive) child processes of the process to kill, then called TerminateProcess() on them. This solved the problem with interrupting `git clone`, but it did not address the problem that Git typically wants to "clean up" when being interrupted. In particular, Git installs atexit() and signal handlers to remove .lock files. The most common symptom was that a stale .git/index.lock file was still present after interrupting a Git process. Based on the idea presented in Dr Dobb's Journal in the article "A Safer Alternative to TerminateProcess()" by Andrew Tucker (July 1, 1999) http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547 we changed our handling to inject a remote thread calling ExitProcess() first, and fall back to TerminateProcess() the process tree instead: e9cb332976c This change was a little misguided in hindsight, as it only called TerminateProcess() on the process tree, but expected the atexit() handler of Git to take care of the child processes when killing the process via the remote ExitProcess() method. Therefore, we changed the strategy once again, to inject ExitProcess() threads into the child processes of the process to kill, too: 53e5c0313e1 (That commit also tries to handle Cygwin process among the child processes by sending Cygwin signals, but unfortunately that part of the commit was buggy.) This worked well for Git processes. However, Git Bash is used in all kinds of circumstances, including launching Maven, or node.js scripts that want to intercept SIGINT. Naturally, these callees have no idea that Git for Windows injects an ExitProcess() with exit code 130 (corresponding to 0x100 + SIGINT). Therefore, they never "got" the signal. So what is it that happens when ConHost generates a ConsoleCtrl event? This question was asked and answered in the excellent blog post at: http://stanislavs.org/stopping-command-line-applications-programatically-with-ctrl-c-events-from-net/#comment-2880 Essentially, the same happens as what we did with ExitProcess(): a remote thread gets injected, with the event type as parameter. Of course it is not ExitProcess() that is called, but CtrlRoutine(). This function lives in kernel32.dll, too, but it is not exported, i.e. GetProcAddress() won't find it. The trick proposed in the blog post (to send a test ConsoleCtrl event to the process itself, using a special handler that then inspects the stack trace to figure out the address of the caller) does not work for us, however: it would send a CTRL_BREAK_EVENT to *all* processes attached to the same Console, essentially killing MinTTY. But could we make this still work somehow? Yes, we could. We came up with yet another trick up our sleeves: instead of determining the address of kernel32!CtrlRoutine in our own process, we spawn a new one, with a new Console, to avoid killing MinTTY. To do that, we need a helper .exe, of course, which we put into /usr/libexec/. If this helper is not found, we fall back to the previous methods of injecting ExitProcess() or calling TerminateProcess(). This method (to spawn a helper .exe) has a further incidental benefit: by compiling 32-bit *and* 64-bit helpers and providing them as getprocaddr32.exe and getprocaddr64.exe, we can now also handle 32-bit processes in a 64-bit Git for Windows. Sadly not vice versa: calling CreateRemoteThread() on a 64-bit process from a 32-bit process seems to fail all the time (and require a lot of assembly hackery to fix that I am not really willing to include in Git for Windows' MSYS2 runtime). The current method was implemented in this commit: ca6188a7520 This is the hopeful final fix for git-for-windows/git#1491, git-for-windows/git#1470, git-for-windows/git#1248, git-for-windows/git#1239, git-for-windows/git#227, git-for-windows/git#1553, nodejs/node#16103, and plenty other tickets that petered out mostly due to a willingness of community members to leave all the hard work to a single, already overworked person. This fix also partially helps git-for-windows/git#1629 (only partially because the user wanted to quit the pager using Ctrl+C, which is not the intended consequence of a Ctrl+C: it should stop the Git process, but not the pager). Signed-off-by: Johannes Schindelin <[email protected]>
2 parents 2c2e0d5 + 72e9706 commit 06ec324

File tree

4 files changed

+261
-1
lines changed

4 files changed

+261
-1
lines changed

winsup/utils/Makefile.in

+21-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ prefix:=@prefix@
3535
exec_prefix:=@exec_prefix@
3636

3737
bindir:=@bindir@
38+
libexecdir = @libexecdir@
3839
program_transform_name:=@program_transform_name@
3940

4041
override INSTALL:=@INSTALL@
@@ -51,6 +52,8 @@ CYGWIN_LDFLAGS := -static -Wl,--enable-auto-import -L${WINDOWS_LIBDIR} $(LDLIBS)
5152
DEP_LDLIBS := $(cygwin_build)/libmsys-2.0.a
5253

5354
MINGW_CXX := @MINGW_CXX@
55+
MINGW32_CC := @MINGW32_CC@
56+
MINGW64_CC := @MINGW64_CC@
5457

5558
# List all binaries to be linked in Cygwin mode. Each binary on this list
5659
# must have a corresponding .o of the same name.
@@ -121,7 +124,7 @@ else
121124
all: warn_dumper
122125
endif
123126

124-
all: Makefile $(CYGWIN_BINS) $(MINGW_BINS)
127+
all: Makefile $(CYGWIN_BINS) $(MINGW_BINS) getprocaddr32.exe getprocaddr64.exe
125128

126129
# test harness support (note: the "MINGW_BINS +=" should come after the
127130
# "all:" above so that the testsuite is not run for "make" but only
@@ -160,6 +163,19 @@ $(CYGWIN_BINS): %.exe: %.o
160163
$(MINGW_BINS): $(DEP_LDLIBS)
161164
$(CYGWIN_BINS): $(DEP_LDLIBS)
162165

166+
# Must *not* use -O2 here, as it screws up the stack backtrace
167+
getprocaddr32.o: %32.o: %.c
168+
$(MINGW32_CC) -c -o $@ $<
169+
170+
getprocaddr32.exe: %.exe: %.o
171+
$(MINGW32_CC) -o $@ $^ -static -ldbghelp
172+
173+
getprocaddr64.o: %64.o: %.c
174+
$(MINGW64_CC) -c -o $@ $<
175+
176+
getprocaddr64.exe: %.exe: %.o
177+
$(MINGW64_CC) -o $@ $^ -static -ldbghelp
178+
163179
cygcheck.o cygpath.o module_info.o path.o ps.o regtool.o strace.o: loadlib.h
164180

165181
.PHONY: clean
@@ -177,6 +193,10 @@ install: all
177193
n=`echo $$i | sed '$(program_transform_name)'`; \
178194
$(INSTALL_PROGRAM) $$i $(DESTDIR)$(bindir)/$$n; \
179195
done
196+
/bin/mkdir -p ${DESTDIR}${libexecdir}
197+
for n in getprocaddr32 getprocaddr64; do \
198+
$(INSTALL_PROGRAM) $$n $(DESTDIR)$(libexecdir)/$$n; \
199+
done
180200

181201
$(cygwin_build)/libmsys-2.0.a: $(cygwin_build)/Makefile
182202
@$(MAKE) -C $(@D) $(@F)

winsup/utils/configure

+89
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,8 @@ ac_no_link=no
589589
ac_subst_vars='LTLIBOBJS
590590
LIBOBJS
591591
configure_args
592+
MINGW64_CC
593+
MINGW32_CC
592594
MINGW_CXX
593595
INSTALL_DATA
594596
INSTALL_SCRIPT
@@ -3303,6 +3305,93 @@ done
33033305
33043306
test -n "$MINGW_CXX" || as_fn_error $? "no acceptable mingw g++ found in \$PATH" "$LINENO" 5
33053307
3308+
for ac_prog in i686-w64-mingw32-gcc
3309+
do
3310+
# Extract the first word of "$ac_prog", so it can be a program name with args.
3311+
set dummy $ac_prog; ac_word=$2
3312+
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
3313+
$as_echo_n "checking for $ac_word... " >&6; }
3314+
if ${ac_cv_prog_MINGW32_CC+:} false; then :
3315+
$as_echo_n "(cached) " >&6
3316+
else
3317+
if test -n "$MINGW32_CC"; then
3318+
ac_cv_prog_MINGW32_CC="$MINGW32_CC" # Let the user override the test.
3319+
else
3320+
as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
3321+
for as_dir in $PATH
3322+
do
3323+
IFS=$as_save_IFS
3324+
test -z "$as_dir" && as_dir=.
3325+
for ac_exec_ext in '' $ac_executable_extensions; do
3326+
if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then
3327+
ac_cv_prog_MINGW32_CC="$ac_prog"
3328+
$as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5
3329+
break 2
3330+
fi
3331+
done
3332+
done
3333+
IFS=$as_save_IFS
3334+
3335+
fi
3336+
fi
3337+
MINGW32_CC=$ac_cv_prog_MINGW32_CC
3338+
if test -n "$MINGW32_CC"; then
3339+
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $MINGW32_CC" >&5
3340+
$as_echo "$MINGW32_CC" >&6; }
3341+
else
3342+
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
3343+
$as_echo "no" >&6; }
3344+
fi
3345+
3346+
3347+
test -n "$MINGW32_CC" && break
3348+
done
3349+
3350+
test -n "$MINGW32_CC" || as_fn_error $? "no acceptable mingw32 gcc found in \$PATH" "$LINENO" 5
3351+
for ac_prog in x86_64-w64-mingw32-gcc
3352+
do
3353+
# Extract the first word of "$ac_prog", so it can be a program name with args.
3354+
set dummy $ac_prog; ac_word=$2
3355+
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5
3356+
$as_echo_n "checking for $ac_word... " >&6; }
3357+
if ${ac_cv_prog_MINGW64_CC+:} false; then :
3358+
$as_echo_n "(cached) " >&6
3359+
else
3360+
if test -n "$MINGW64_CC"; then
3361+
ac_cv_prog_MINGW64_CC="$MINGW64_CC" # Let the user override the test.
3362+
else
3363+
as_save_IFS=$IFS; IFS=$PATH_SEPARATOR
3364+
for as_dir in $PATH
3365+
do
3366+
IFS=$as_save_IFS
3367+
test -z "$as_dir" && as_dir=.
3368+
for ac_exec_ext in '' $ac_executable_extensions; do
3369+
if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then
3370+
ac_cv_prog_MINGW64_CC="$ac_prog"
3371+
$as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5
3372+
break 2
3373+
fi
3374+
done
3375+
done
3376+
IFS=$as_save_IFS
3377+
3378+
fi
3379+
fi
3380+
MINGW64_CC=$ac_cv_prog_MINGW64_CC
3381+
if test -n "$MINGW64_CC"; then
3382+
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $MINGW64_CC" >&5
3383+
$as_echo "$MINGW64_CC" >&6; }
3384+
else
3385+
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
3386+
$as_echo "no" >&6; }
3387+
fi
3388+
3389+
3390+
test -n "$MINGW64_CC" && break
3391+
done
3392+
3393+
test -n "$MINGW64_CC" || as_fn_error $? "no acceptable mingw64 gcc found in \$PATH" "$LINENO" 5
3394+
33063395
33073396
33083397
configure_args=X

winsup/utils/configure.ac

+5
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ AC_PROG_INSTALL
3535
AC_CHECK_PROGS(MINGW_CXX, ${target_cpu}-w64-mingw32-g++)
3636
test -n "$MINGW_CXX" || AC_MSG_ERROR([no acceptable mingw g++ found in \$PATH])
3737

38+
AC_CHECK_PROGS(MINGW32_CC, i686-w64-mingw32-gcc)
39+
test -n "$MINGW32_CC" || AC_MSG_ERROR([no acceptable mingw32 gcc found in \$PATH])
40+
AC_CHECK_PROGS(MINGW64_CC, x86_64-w64-mingw32-gcc)
41+
test -n "$MINGW64_CC" || AC_MSG_ERROR([no acceptable mingw64 gcc found in \$PATH])
42+
3843
AC_EXEEXT
3944
AC_CONFIGURE_ARGS
4045
AC_CONFIG_FILES([Makefile])

winsup/utils/getprocaddr.c

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#include <stdio.h>
2+
#include <windows.h>
3+
4+
/**
5+
* To determine the address of kernel32!CtrlRoutine, we need to use
6+
* dbghelp.dll. But we want to avoid linking statically to that library because
7+
* the normal operation of cygwin-console-helper.exe (i.e. to allocate a hidden
8+
* Console) does not need it.
9+
*
10+
* Therefore, we declare the SYMBOL_INFOW structure here, load the dbghelp
11+
* library via LoadLibraryExA() and obtain the SymInitialize(), SymFromAddrW()
12+
* and SymCleanup() functions via GetProcAddr().
13+
*/
14+
15+
#include <dbghelp.h>
16+
17+
/* Avoid fprintf(), as it would try to reference '__getreent' */
18+
static void
19+
output (BOOL error, const char *fmt, ...)
20+
{
21+
va_list ap;
22+
char buffer[1024];
23+
24+
va_start (ap, fmt);
25+
vsnprintf (buffer, sizeof(buffer) - 1, fmt, ap);
26+
buffer[sizeof(buffer) - 1] = '\0';
27+
va_end (ap);
28+
WriteFile (GetStdHandle(error ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE),
29+
buffer, strlen (buffer), NULL, NULL);
30+
}
31+
32+
static WINAPI BOOL
33+
ctrl_handler(DWORD ctrl_type)
34+
{
35+
unsigned short count;
36+
void *address;
37+
HANDLE process;
38+
PSYMBOL_INFOW info;
39+
DWORD64 displacement;
40+
41+
count = CaptureStackBackTrace (1l /* skip this function */,
42+
1l /* return only one trace item */,
43+
&address, NULL);
44+
if (count != 1)
45+
{
46+
output (1, "Could not capture backtrace\n");
47+
return FALSE;
48+
}
49+
50+
process = GetCurrentProcess ();
51+
if (!SymInitialize (process, NULL, TRUE))
52+
{
53+
output (1, "Could not initialize symbols\n");
54+
return FALSE;
55+
}
56+
57+
info = (PSYMBOL_INFOW)
58+
malloc (sizeof(*info) + MAX_SYM_NAME * sizeof(wchar_t));
59+
if (!info)
60+
{
61+
output (1, "Could not allocate symbol info structure\n");
62+
return FALSE;
63+
}
64+
info->SizeOfStruct = sizeof(*info);
65+
info->MaxNameLen = MAX_SYM_NAME;
66+
67+
if (!SymFromAddrW (process, (DWORD64)(intptr_t)address, &displacement, info))
68+
{
69+
output (1, "Could not get symbol info\n");
70+
SymCleanup(process);
71+
return FALSE;
72+
}
73+
output (0, "%p\n", (void *)(intptr_t)info->Address);
74+
CloseHandle(GetStdHandle(STD_OUTPUT_HANDLE));
75+
SymCleanup(process);
76+
77+
exit(0);
78+
}
79+
80+
int
81+
main (int argc, char **argv)
82+
{
83+
char *end;
84+
85+
if (argc < 2)
86+
{
87+
output (1, "Need a function name\n");
88+
return 1;
89+
}
90+
91+
if (strcmp(argv[1], "CtrlRoutine"))
92+
{
93+
if (argc > 2)
94+
{
95+
output (1, "Unhandled option: %s\n", argv[2]);
96+
return 1;
97+
}
98+
99+
HINSTANCE kernel32 = GetModuleHandle ("kernel32");
100+
if (!kernel32)
101+
return 1;
102+
void *address = (void *) GetProcAddress (kernel32, argv[1]);
103+
if (!address)
104+
return 1;
105+
output (0, "%p\n", address);
106+
return 0;
107+
}
108+
109+
/* Special-case kernel32!CtrlRoutine */
110+
if (argc == 3 && !strcmp ("--alloc-console", argv[2]))
111+
{
112+
if (!FreeConsole () && GetLastError () != ERROR_INVALID_PARAMETER)
113+
{
114+
output (1, "Could not detach from current Console: %d\n",
115+
(int)GetLastError());
116+
return 1;
117+
}
118+
if (!AllocConsole ())
119+
{
120+
output (1, "Could not allocate a new Console\n");
121+
return 1;
122+
}
123+
}
124+
else if (argc > 2)
125+
{
126+
output (1, "Unhandled option: %s\n", argv[2]);
127+
return 1;
128+
}
129+
130+
if (!SetConsoleCtrlHandler (ctrl_handler, TRUE))
131+
{
132+
output (1, "Could not register Ctrl handler\n");
133+
return 1;
134+
}
135+
136+
if (!GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, 0))
137+
{
138+
output (1, "Could not simulate Ctrl+Break\n");
139+
return 1;
140+
}
141+
142+
/* Give the event 1sec time to print out the address */
143+
Sleep(1000);
144+
return 1;
145+
}
146+

0 commit comments

Comments
 (0)