2

The question

Given a subprocess started in python with code similar to:

import subprocess
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.communicate()
print('Return code: {}'.format(p.returncode))

According to the official documentation, it is possible to check whether the subprocess was terminated by a signal:

A negative value -N indicates that the child was terminated by signal N (POSIX only).

But only on POSIX platforms.

Is there a way to do check if a process was terminated by a signal (do not care which one) on Windows platforms?

Background

I am running into this issue while running the tests of googletest. The break-on-failure CLI flag test fails on Windows platforms (VC14, VS2017) but works well on POSIX ones (2x Ubuntu, 2x macOS).

Manually on the command line, I get these results:

> .\googletest-break-on-failure-unittest_.exe --gtest_break_on_failure
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from Foo
[ RUN      ] Foo.Bar
<some path>\googletest\test\googletest-break-on-failure-unittest_.cc(52): error: Expected equality of these values:
  2
  3

> echo %ERRORLEVEL%
-2147483645

However, the python wrapper that calls this test receives 2147483651 (positive number).

(I just added a print before this line)

Note that these refer to the numbers 0xFFFFFFFF80000003 (negative number) and 0x‭80000003‬ (positive number) in hex and that the return code was not processed any further. (See here)

Why would the return code be changed like this?

PS: Yes, I have checked that GTEST_OS_WINDOWS and GTEST_HAS_SEH are true in the C++ code.

eldruin
  • 332
  • 3
  • 16

1 Answers1

2

CMD's ERRORLEVEL environment variable is a 32-bit signed value. The positive range is 0x00000000-0x7FFFFFFF (0 up to 2147483647), and the negative range is 0x80000000-0xFFFFFFFF (-2147483648 up to -1). When a process exits due to an unhandled exception, the status is usually the exception code, which is typically a 32-bit, signed NTSTATUS value. That said, nothing stops us from calling ExitProcess or TerminateProcess with an unsigned exit code that exceeds 0x7FFFFFFF, so a negative ERRORLEVEL in CMD by itself does not mean that the process exited abnormally.

In this case, -2147483645 is the NTSTATUS code STATUS_BREAKPOINT (0x80000003). This originates from the gtest_break_on_failure option of Google Test, which calls WinAPI DebugBreak. For the x86 architecture, the latter function simply executes an int 3 instruction (software interrupt 3), which is trapped in the kernel to raise a breakpoint exception.

Normally without an attached debugger, this would execute the default Windows unhandled exception handler, which calls on Windows Error Reporting (see below). But the code linked in the question sets ExitWithExceptionCode as the application's unhandled exception filter. This filter simply exits via exit(exception_pointers->ExceptionRecord->ExceptionCode).

As to Unix signals, the Windows kernel doesn't implement them. The C runtime implements the six that are required by standard C. In a console application, the standard SIGINT signal is associated with the console's CTRL_C_EVENT. The non-standard SIGBREAK signal is used for other console control events, including CTRL_BREAK_EVENT and CTRL_CLOSE_EVENT. The default handler for these events calls ExitProcess(STATUS_CONTROL_C_EXIT). Thus, to literally answer the question, to determine whether a process was killed by a console 'signal', check for STATUS_CONTROL_C_EXIT (0xC000013A or -1073741510).


Handling an Unhandled Exception

  • If a debugger is attached to the process debug port, the kernel sends it a first-chance exception event. If it doesn't handle the exception, the kernel dispatches the exception to the thread's chain of vectored exception handlers and structured exception handlers (stack-based, checked in reverse from the current frame). We will assume that the exception is not handled by any of them.

  • The last frame checked is the frame of the thread startup function, RtlUserThreadStart. The handler for this frame, _C_specific_handler, is passed a dispatch record that allows it determine the scope's exception filter. This filter in turn calls the unhandled exception filter of the process, given it was previously set via RtlSetUnhandledExceptionFilter. At process startup, the initialization routine of kernelbase.dll sets it to the aptly named function, UnhandledExceptionFilter.

  • If a debugger is attached, UnhandledExceptionFilter returns EXCEPTION_CONTINUE_SEARCH. Subsequently, the kernel sends the debugger a second-chance exception event. If the debugger doesn't handle the exception, the kernel tries sending the event to the subsystem, i.e. the Windows session server, csrss.exe. The server in turn relays the fault to the Windows Error Reporting (WER) service, which will ultimately terminate the process.

  • If no debugger is attached, UnhandledExceptionFilter calls the application's unhandled exception filter, if it was previously set via SetUnhandledExceptionFilter. If the application filter returns EXCEPTION_CONTINUE_EXECUTION or EXCEPTION_EXECUTE_HANDLER, then UnhandledExceptionFilter returns this value to the frame handler.

  • If the application hasn't set its own filter or if its filter returns EXCEPTION_CONTINUE_SEARCH, then UnhandledExceptionFilter next checks the job, process, and thread error modes for the flag SEM_NOGPFAULTERRORBOX. This flag disables error reporting, in which case the filter simply returns EXCEPTION_EXECUTE_HANDLER.

  • If error reporting is allowed, UnhandledExceptionFilter calls on the Windows Error Reporting (WER) service. With WER in the loop, the behavior ultimately depends on how it's configured in the registry, group policy, and the default behavior for the current Windows version. WER may create and attach a debugger process, based on the system "AeDebug" settings. If it attaches a debugger, UnhandledExceptionFilter returns EXCEPTION_CONTINUE_SEARCH, just like it would had a debugger already been attached. Otherwise it returns EXCEPTION_EXECUTE_HANDLER.

  • Ultimately if it executes the exception handler for an 'unhandled' exception, the stack is first unwound to the RtlUserThreadStart frame via RtlUnwindEx, which will call any finally termination handlers in the intervening frames. The exception handler's execution context is restored via RtlRestoreContext, and the exception code gets set in the integer return register (e.g. rax in x64). At last, the handler self-terminates via NtTerminateProcess(NtCurrentProcess(), exceptionCode).

eldruin
  • 332
  • 3
  • 16
Eryk Sun
  • 33,190
  • 5
  • 92
  • 111