Exceptions incorrectly handled by default catch block in 64-bit C++Builder application

Viewed 111

My company has created a 64-bit NT Service application in C++Builder. One of our customers is encountering a problem where the service starts to log multiple unhandled C++ exceptions.

After many months of diagnostic builds and trial and error, we've determined that Delphi/C++ exceptions are no longer caught in the correct catch block but rather in the default catch block. When the service detects this scenario it runs a diagnostic in a new thread and logs the results. The diagnostic throws various exceptions in a loop and catches those exceptions.

Here is a simplified example:

// ESomeException descends from Exception

void TestThrowESomeException() {
  throw ESomeException("test");
}

void UnhandledCPPExceptionTest() {
  int eSomeExceptionCount = 0;
  int exceptionCount = 0;
  int unhandledCount = 0;
  for (int i = 0; i < 25; ++i) {
    try { TestThrowESomeException(); }
    catch (ESomeException&) { ++eSomeExceptionCount; }
    catch (Exception&) { ++exceptionCount; }
    catch (...) { ++unhandledCount; }
  }

  String msg = "Test results:\r\n";
  if (exceptionCount > 0 || unhandledCount > 0) {
    msg += String::Format(
      "eSomeExceptionCount = %d\r\n"
      "exceptionCount = %d\r\n"
      "unhandledCount = %d\r\n",
      ARRAYOFCONST((eSomeExceptionCount, exceptionCount, unhandledCount))
    );
  }
  else
    msg += "all tests succeeded";

  Logger::Add(msg);
}

When the server is not in the erroneous condition the log message will be:

Test results:
all tests succeeded

When the server is in the erroneous condition the log message will be something like:

Test results:
eSomeExceptionCount = 17
exceptionCount = 0
unhandledCount = 8

Analyzing these results shows that the ESomeException catch block was executed 17 times and the default block was executed 8 times.

We have not been able to reproduce this issue in any of our environments. The customer's server is an AWS EC2 instance with Windows Server 2012 R2. We do not know the AWS instance type, Windows update history, or the AMI used to create their server (if they used one). We have asked for more details and I will update this question once known. We replicated their instance in AWS using the information we know and were unable to reproduce the behavior.

Our project was built using RAD Studio 10.4 Update 2. It is configured to use runtime packages.

At this point, we are at a loss as to what steps to take to troubleshoot further. Has anyone encountered a similar problem with C++Builder 64-bit exception handling? Are there any steps we can take to gather more information to determine the cause? Could there be a setting in our project configuration that would help diagnose or fix this issue?

Edit 5/23

I have modified the diagnostic code to inspect the SEH exception information in attempt to determine why the catch isn't catching. Here is an example of the new test throw:

int exc_filter(EXCEPTION_POINTERS*) {
  return EXCEPTION_EXECUTE_HANDLER;
}
  EXCEPTION_POINTERS* xp;
  try {
    __try {
      try { TestThrowESomeException(); }
      catch (ESomeException const&) { ++eSomeExceptionCount; }
      catch (Exception const&) { ++exceptionCount; }
    }
    __except (exc_filter(xp = GetExceptionInformation())) {
      ++sehCount;
      diagStr = AnalyzeException(xp);
    }
  }
  catch (...) { ++unhandledCount; }

AnalyzeException analyzes the exception pointer and returns some details to hopefully figure out what is wrong with the exception object/structure:

bool IsDelphiExceptionCode(DWORD Code) {
  return Code == 0xEEDFACE || Code == 0xEEDFADE || Code == 0xEEFFACE || Code == 0xEEFFADE;
}

String AnalyzeException(EXCEPTION_POINTERS* Xp) {
  if (Xp == nullptr)
    return "XP is null";
  if (Xp->ExceptionRecord == nullptr)
    return "XP.ExceptionRecord is null";
  PEXCEPTION_RECORD xr = Xp->ExceptionRecord;

  String result;
  String codeStr = ExceptionCodeToString(xr->ExceptionCode);
  String hexStr = "0x" + IntToHex(static_cast <unsigned int> (xr->ExceptionCode));
  if (!codeStr.IsEmpty())
    codeStr += " (" +  hexStr + ")";
  else
    codeStr = hexStr;
  result +=
    "Code: " + codeStr +
    ", Flags: 0x" + IntToHex(static_cast <int> (xr->ExceptionFlags), 4) +
    ", Count: " + String(xr->NumberParameters) +
    ", [";
  for (int i = 0; i < EXCEPTION_MAXIMUM_PARAMETERS; ++i) {
    if (i > 0)
      result += ",";
    result += IntToHex(static_cast <int> (xr->ExceptionInformation[i]), 1);
  }
  result += "]";

  if (IsDelphiExceptionCode(xr->ExceptionCode)) {
    result += ", ";
    try {
      try {
        Exception* exception = reinterpret_cast <Exception*> (xr->ExceptionInformation[1]);
        if (exception == nullptr)
          result += "Exception is null";
        else {
          result += "Class: " + exception->ClassName() + ", ";
          result += "Message: " + exception->Message;
        }
      }
      catch (Exception const& e) {
        result += "Exception while inspecting: " + e.Message;
      }
    }
    catch (...) {
      result += "Exception while inspecting";
    }
  }

  return result;
}

It is my intention to gather as much detail as possible before opening a case with Embarcadero. It is possible that this problem is fixed in RAD Studio 11.1 but we have not finished evaluating that compiler for our production builds.

0 Answers
Related