8

In the following code, the function foo calls itself recursively once. The inner call causes an access violation to be raised. The outer call catches the exception.

#include <windows.h>
#include <stdio.h>

void foo(int cont)
{
    __try
    {
        __try
        {
            __try
            {
                if (!cont)
                    *(int *)0 = 0;
                foo(cont - 1);
            }
            __finally
            {
                printf("inner finally %d\n", cont);
            }
        }
        __except (!cont? EXCEPTION_CONTINUE_SEARCH: EXCEPTION_EXECUTE_HANDLER)
        {
            printf("except %d\n", cont);
        }
    }
    __finally
    {
        printf("outer finally %d\n", cont);
    }
}

int main()
{
    __try
    {
        foo(1);
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        printf("main\n");
    }
    return 0;
}

The expected output here should be

inner finally 0
outer finally 0
inner finally 1
except 1
outer finally 1

However, outer finally 0 is conspicuously missing from the real output. Is this a bug or is there some detail I'm overlooking?

For completeness, happens with VS2015, compiling for x64. Surprisingly it doesn't happen on x86, leading me to believe that it is really a bug.

avakar
  • 32,009
  • 9
  • 68
  • 103
  • This might technically fall under the purview of undefined behavior, as you're assigning to a null pointer. Have you tried throwing a regular exception using `RaiseException`? – OmnipotentEntity Sep 27 '15 at 11:35
  • 1
    Well, not good. It is not a new issue, VS2013 behaves the same way. Looks like a structural limitation of /SAFESEH to me, specific to recursion, it works fine in a non-recursive case. Pretty doubtful anybody here can fix this problem, best to ping connect.microsoft.com about it. – Hans Passant Sep 27 '15 at 11:54
  • @OmnipotentEntity: Assigning to a null pointer is *undefined behavior*, as far as the C++ Language Standard is concerned. On the Windows platform, however, this is well defined: The instruction raises an access violation, that is communicated to user code through an SEH exception. – IInspectable Sep 27 '15 at 12:12
  • 1
    @IInspectable It's the compiler rather than the platform that makes this well defined. – David Heffernan Sep 27 '15 at 12:25
  • @DavidHeffernan: SEH is a system service on Windows, built into the OS. It's SEH, that makes the outcome of writing to a null pointer well defined. The compiler makes SEH accessible through special keywords, and implements stack unwinding semantics, if requested, but it's not the compiler, that turns undefined behavior into well defined behavior. – IInspectable Sep 27 '15 at 14:08
  • 2
    @IInspectable No. It's the compiler that decides to emit the code. It could emit code to do something different on a null dereference. No compiler does. – David Heffernan Sep 27 '15 at 14:11
  • 1
    @DavidHeffernan: The compiler doesn't emit anything special. `*(int*)0=0;` produces the instruction `mov dword ptr [0],0`. SEH is a system service, and the OS makes the outcome well defined. – IInspectable Sep 27 '15 at 14:25
  • 1
    @IInspectable The compiler vendor made a choice to emit that code. A compiler could exist that did something different. – David Heffernan Sep 27 '15 at 14:32
  • @DavidHeffernan: There's nothing compiler-specific under [Exception Dispatching](https://msdn.microsoft.com/en-us/library/windows/desktop/ms679327.aspx), because SEH is a system service. The document describes, what happens in case of a hardware or software exception. None of this requires compiler support. It's SEH, implemented in the system, that makes the outcome of writing to address 0 well defined. You need compiler (and CRT) support, if you decide to **handle** the exception. – IInspectable Sep 27 '15 at 16:09
  • 2
    @IInspectable unless the compiler decides to emit code to do something different when dereferencing nullptr – David Heffernan Sep 27 '15 at 16:27
  • 1
    @DavidHeffernan: Stop turning around the question, until your wrong sounds right. SEH is implemented in the system. That's what I pointed out in my initial comment. SEH is, what turns the undefined behavior into well defined behavior. No compiler is involved. Please accept, that you can be wrong. There's nothing to be gained from this discussion anymore. I'm out. – IInspectable Sep 27 '15 at 17:07
  • 1
    Guys, it doesn't matter, the behavior is exactly the same if I raise the exception with `RaiseException`. – avakar Sep 27 '15 at 17:11
  • @HansPassant, it turns out that there are sources for _C_specific_handler in the installation dir of vs2015. There's a piece of code that explicitely stops the unwinding once the "target handler" is found. However, this happens for all frames, not just the target frame. I'll go through connect, thanks. – avakar Sep 27 '15 at 17:13
  • 1
    I've submitted https://connect.microsoft.com/VisualStudio/feedback/details/1842143, let's see how long it will take MS to delete the report... – avakar Sep 27 '15 at 17:23
  • @IInspectable I accept that I can be wrong. – David Heffernan Sep 27 '15 at 17:37
  • 1
    Although not explicitly mentioned in the [Visual Studio 2015 Update 3](https://www.visualstudio.com/en-us/news/releasenotes/vs2015-update3-vs) release notes, the bug seems to have been fixed (possibly even with [Update 2](https://www.visualstudio.com/news/vs2015-update2-vs), but I couldn't check). You might want to write up an answer. – IInspectable Sep 06 '16 at 15:20

1 Answers1

1

exist and more simply example (we can remove inner try/finally block:

void foo(int cont)
{
    __try
    {
        __try
        {
            if (!cont) *(int *)0 = 0;
            foo(cont - 1);
        }
        __except (cont? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
        {
            printf("except %d\n", cont);
        }
    }
    __finally
    {
        printf("finally %d\n", cont);
    }
}

with output

except 1
finally 1

so finally 0 block not executed. but in not recursive case - no bug:

__try
{
    foo(0);
} 
__except(EXCEPTION_EXECUTE_HANDLER)
{
    printf("except\n");
}

output:

finally 0
except

this is bug in next function

EXCEPTION_DISPOSITION
__C_specific_handler (
    _In_ PEXCEPTION_RECORD ExceptionRecord,
    _In_ PVOID EstablisherFrame,
    _Inout_ PCONTEXT ContextRecord,
    _Inout_ PDISPATCHER_CONTEXT DispatcherContext
    );

old implementation of this function with bug here :

                    //
                    // try/except - exception filter (JumpTarget != 0).
                    // After the exception filter is called, the exception
                    // handler clause is executed by the call to unwind
                    // above. Having reached this point in the scan of the
                    // scope tables, any other termination handlers will
                    // be outside the scope of the try/except.
                    //

                    if (TargetPc == ScopeTable->ScopeRecord[Index].JumpTarget) { // bug
                        break;
                    }

if we have latest VC compiler/libraries installed, search for chandler.c (in my install in located at \VC\crt\src\amd64\chandler.c )

and in file can view now next code:

                if (TargetPc == ScopeTable->ScopeRecord[Index].JumpTarget
                    // Terminate only when we are at the Target frame;
                    // otherwise, continue search for outer finally:
                    && IS_TARGET_UNWIND(ExceptionRecord->ExceptionFlags)
                    ) {
                    break;
                }

so additional condition is added IS_TARGET_UNWIND(ExceptionRecord->ExceptionFlags) which fix this bug

__C_specific_handler implemented in different crt libraries (in some case with static link, in some case will be imported from vcruntime*.dll or msvcrt.dll (was forwarded to ntdll.dll)). also ntdll.dll export this function - however in latest win10 builds(14393) it still not fixed

RbMm
  • 31,280
  • 3
  • 35
  • 56