Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP Try harder to handle Ctrl+C #18

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

dscho
Copy link
Member

@dscho dscho commented May 30, 2018

It has been reported in git-for-windows/git#1648 that our way to simulate SIGINT sometimes fails to do anything.

This PR fixes that. See the commit message for an extensive description of the culprit and its resolution.

TODOs before merging:

  • Explain in the commit message that the most likely reason that Ctrl+C is ignored in those situations is described in https://msdn.microsoft.com/en-us/library/windows/desktop/ms682425.aspx:

    When a process is created with CREATE_NEW_PROCESS_GROUP specified, an implicit call to SetConsoleCtrlHandler(NULL,TRUE) is made on behalf of the new process; this means that the new process has CTRL+C disabled. This lets shells handle CTRL+C themselves, and selectively pass that signal on to sub-processes. CTRL+BREAK is not disabled, and may be used to interrupt the process/process group.

  • Test with a 32-bit runtime, in particular whether it manages to interrupt a 64-bit process.

  • Load the IsProcessCritical() symbol, and where available (i.e. Windows 8.1 and later), use it to avoid interrupting processes that should never be interrupted.

  • Identify more tickets that would benefit from this fix, and mention them in the commit message, too.

dscho added 2 commits May 30, 2018 08:15
It seems that the "signal" associated with a Ctrl+C (i.e. a
CTRL_C_EVENT) is sometimes not passed through to the registered handler.

This symptom occurs e.g. when spawning a non-MSYS2 process in Bash
through env.exe, such as is commonly the case when starting ruby scripts
via the shebang line `#!/usr/bin/env ruby` (which usually calls
/mingw64/bin/ruby.exe). The unexpected behavior is that a CtrlRoutine
thread can be executed successfully but does not result in the process'
handler to be called, let alone in the process being terminated.

Even more curious is that a CTRL_BREAK_EVENT is delivered without
problems.

A rather intense few days of quality time with GDB and DuckDuckGo turned
into the following analysis of the issue:

It turns out that after calling EnterCriticalSection(), CtrlRoutine()
asks whether it has been passed 0 (CTRL_C_EVENT) as a parameter and in
that case, jumps to conditional code (hex addresses removed from GDB's
disassembly to save on space):

<KERNELBASE!CtrlRoutine+91>:  callq  *0x17c9af(%rip)
<KERNELBASE!CtrlRoutine+97>:  andl   $0x0,0x24(%rsp)
<KERNELBASE!CtrlRoutine+102>: test   %ebx,%ebx
<KERNELBASE!CtrlRoutine+104>: je     <KERNELBASE!CtrlRoutine+163>

That conditional code reads thusly:

<KERNELBASE!CtrlRoutine+163>: mov    %gs:0x60,%rax
<KERNELBASE!CtrlRoutine+172>: mov    0x20(%rax),%rcx
<KERNELBASE!CtrlRoutine+176>: testb  $0x1,0x18(%rcx)
<KERNELBASE!CtrlRoutine+180>: jne    <KERNELBASE!CtrlRoutine+216>
<KERNELBASE!CtrlRoutine+182>: jmp    <KERNELBASE!CtrlRoutine+106>

This code looks at %gs:0x60, which according to
https://en.wikipedia.org/wiki/Win32_Thread_Information_Block is the PEB
(Process Environment Block). Then it accesses the field of the PEB at
offset 0x20, which is the ProcessParameters field according to
https://msdn.microsoft.com/en-us/library/windows/desktop/aa813706.aspx

These process parameters are described here:
http://undocumented.ntinternals.net/UserMode/Structures/RTL_USER_PROCESS_PARAMETERS.html

Sadly, it is unclear what the field at offset 0x18 (named
`ConsoleFlags`) does, but from the disassembly it is clear that if it
has its least significant bit set, CtrlRoutine() simply cleans up and
exits. The handler(s) only get a chance to run when that bit is 0. (Note
that ReactOS expects *all* of ConsoleFlags to be different from 1:
https://github.com/reactos/reactos/blob/ae9702fce/dll/win32/kernel32/client/console/console.c#L169)

Note: When a 64-bit process asks for the PEB of a 32-bit process, it
does receive a copy of *a 64-bit version* of it. This 64-bit version
points to a copy of the USER_PROCESS_INFORMATION struct that is
compatible with 64-bit, but writing to it will not have any effect! We
instead need to access the 32-bit PEB, which has a pointer to the 32-bit
version of the process information, where we modify the ConsoleFlags. We
are using a special trick to obtain the 32-bit PEB inspired by
https://stackoverflow.com/a/23842609: asking NtQueryInformation() for
the ProcessWow64Information will return the 64-bit address of the 32-bit
PEB, if there is any (indicating that the process in question is a WoW
processes i.e. 32-bit processes running on 64-bit Windows).

This closes git-for-windows/git#1648 and
git-for-windows/git#1470

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Also TODO: test on 32-bit

Also TODO: identify more tickets on GitHub that would probably be fixed
by this topic branch

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
@dscho
Copy link
Member Author

dscho commented May 30, 2018

@orgads this is the PR. Please have a look...

@dscho
Copy link
Member Author

dscho commented Jun 1, 2018

@orgads I really could use your help...

@orgads
Copy link

orgads commented Jun 1, 2018

Sure! I didn't forget you, I just had a heavy workload in the past few days. I'll try to test it by Sunday.

@dscho
Copy link
Member Author

dscho commented Jun 1, 2018

I didn't forget you, I just had a heavy workload in the past few days. I'll try to test it by Sunday.

Thank you!

@orgads
Copy link

orgads commented Jun 3, 2018

I had to comment the TODO and add #include <winternl.h> to exit_process.h, and add -lntdll to one of the Makefiles to compile it.

This solves the env case, which is not interrupted as expected. For the other case (direct execution of ruby, it interrupts, but seems to not deliver the signal to the process. The process is terminated, but without the expected exception:

$ env ruby -e 'sleep 10'
-e:1:in `sleep': Interrupt
        from -e:1:in `<main>'

$ ruby -e 'sleep 10'

$

(before this change, env did not interrupt at all, and direct ruby interrupted with the exception)

@orgads
Copy link

orgads commented Sep 2, 2018

Any progress with this?

@dscho
Copy link
Member Author

dscho commented Sep 3, 2018

Any progress with this?

Unfortunately not. It has been on the back burner and will remain there for the time being. :-(

@orgads
Copy link

orgads commented Dec 13, 2018

Do you plan to complete this commit? This bug is the most annoying one for me.

It looks like it is 80-90% done, isn't it?

@dscho
Copy link
Member Author

dscho commented Dec 14, 2018

Do you plan to complete this commit? This bug is the most annoying one for me.

Hmm, I seem to not find the time to finish this any time soon.

It looks like it is 80-90% done, isn't it?

The biggest problem here is that it might make Git Bash look like malware to some anti-malware software, as it digs pretty deep into internals of other processes.

So I started wondering whether this is really the right approach, and whether we should not rather have a simpler (and more documented, although not quite as satisfying) strategy of trying to kill the process with a remote thread, then see whether the process is still around, falling back to terminating it.

This strategy would not be quite as satisfying because it would fail to emulate the Ctrl+C behavior in CMD.

Any thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants