Skip to content

[libc++] [libc++abi] Regression: std::make_exception_ptr breaks catching ObjC objects on rethrow #135089

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

Open
rsesek opened this issue Apr 9, 2025 · 4 comments · May be fixed by #135386
Open
Labels
libc++abi libc++abi C++ Runtime Library. Not libc++. libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. objective-c

Comments

@rsesek
Copy link

rsesek commented Apr 9, 2025

This is a regression introduced in LLVM commit 51e91b6 (PR #65534). The following code used to work prior to the change:

#include <exception>
#import <Foundation/Foundation.h>

NSError* RecoverException(const std::exception_ptr& exc) {
  try {
    std::rethrow_exception(exc);
  } catch (NSError* error) {
    return error;
  } catch (...) {
  }
  return nil;
}

int main() {
  NSError* error = [NSError errorWithDomain:NSPOSIXErrorDomain code:EPERM userInfo:nil];
  std::exception_ptr exc = std::make_exception_ptr(error);
  NSError* recov = RecoverException(exc);
  if (recov) {
    printf("[+] PASS: %s\n", recov.description.UTF8String);
  } else {
    printf("[-] FAIL: Expected NSError, got nil\n");
  }
}

This kind of code may be written when working with an Objective-C++ codebase that uses exceptions, and NSError*/NSException* types need to be handled alongside C++ errors. I believe this is supported because Objective-C exceptions are interoperable with C++ exceptions.

This broke with the aforementioned commit because std::make_exception_ptr no longer internally throws the exception object and uses std::current_exception to capture the value. Instead, it directly allocates and populates the __cxxabiv1::__cxa_exception using the C++ type information. However, when an ObjC exception is thrown natively, it uses a different representation for the exception's typeinfo.

This is apparent when looking at the assembly output from clang++ -S. In the sample program above, the Catch TypeInfos in GCC_except_table0 for RecoverException() is a _OBJC_EHTYPE_$_NSError. This corresponds to the type stored in a struct objc_exception produced in the ObjC runtime when an object is thrown via objc_exception_throw.

But in the assembly produced after that commit, the template instantiation for std::exception_ptr std::make_exception_ptr<NSError*>(NSError*) produces an exception with a reference to the __ZTIP7NSError typeinfo. I.e., after the commit, the exception stores the C++ typeinfo for the object, rather than the ObjC typeinfo. When the exception is rethrown, the catch arms fail to match because the typeinfos between the catch arm and the exception object do not match.

Putting a breakpoint on RecoverException and then inspecting the argument shows this too. After the commit:

(lldb) p *(((__cxxabiv1::__cxa_exception*)exc.__ptr_)-1)
(__cxxabiv1::__cxa_exception) {
  reserve = 0x0000000000000000
  referenceCount = 1
  exceptionType = 0x00000001000040a0
  exceptionDestructor = 0x0000000100000b8c (cocoa-exc`std::exception_ptr std::make_exception_ptr[abi:ne180000]<NSError*>(NSError*)::'lambda'(void*)::__invoke(void*) at exception_ptr.h:91)
  unexpectedHandler = 0x0000000100141570 (libc++abi.1.dylib`demangling_unexpected_handler() at cxa_default_handlers.cpp:90)
  terminateHandler = 0x000000010014138c (libc++abi.1.dylib`demangling_terminate_handler() at cxa_default_handlers.cpp:39)
  nextException = nil
  handlerCount = 0
  handlerSwitchValue = 0
  actionRecord = 0x0000000000000000
  languageSpecificData = 0x0000000000000000
  catchTemp = 0x0000000000000000
  adjustedPtr = 0x0000000000000000
  unwindHeader = {
    exception_class = 4849336966747728640
    exception_cleanup = 0x0000000100177288 (libc++abi.1.dylib`__cxxabiv1::exception_cleanup_func(_Unwind_Reason_Code, _Unwind_Exception*) at cxa_exception.cpp:134)
    private_1 = 0
    private_2 = 0
  }
}
(lldb) p/s (((__cxxabiv1::__cxa_exception*)exc.__ptr_)-1)->exceptionType->name()
(const char *) "P7NSError" "P7NSError"
(lldb) x/2a (((__cxxabiv1::__cxa_exception*)exc.__ptr_)-1)->exceptionType
0x1000040a0: 0x0000000100183bc0 libc++abi.1.0.dylib`vtable for __cxxabiv1::__pointer_type_info + 16
0x1000040a8: 0x8000000100000cf7 (0x0000000100000cf7) cocoa-exc`typeinfo name for NSError*

Before the commit:

(lldb) p *(((__cxxabiv1::__cxa_exception*)exc.__ptr_)-1)
(__cxxabiv1::__cxa_exception) {
  reserve = 0x0000000000000000
  referenceCount = 1
  exceptionType = 0x00006000008c4088
  exceptionDestructor = 0x000000019290b1ec (libobjc.A.dylib`_objc_exception_destructor(void*))
  unexpectedHandler = 0x0000000192c8dd30 (libc++abi.dylib`demangling_unexpected_handler())
  terminateHandler = 0x0000000192914cd8 (libobjc.A.dylib`_objc_terminate())
  nextException = nil
  handlerCount = 0
  handlerSwitchValue = 1
  actionRecord = 0x0000000100000bc1 "\U00000001"
  languageSpecificData = 0x0000000100000bb0 "\xff\x9b\U00000015\U00000001\f\U0000001c\U00000004(\U00000001 ("
  catchTemp = 0x0000000100000a90
  adjustedPtr = 0x00006000008c4080
  unwindHeader = {
    exception_class = 4849336966747728640
    exception_cleanup = 0x0000000192ca1af4 (libc++abi.dylib`__cxxabiv1::exception_cleanup_func(_Unwind_Reason_Code, _Unwind_Exception*))
    private_1 = 0
    private_2 = 6171911792
  }
}
(lldb) p/s (((__cxxabiv1::__cxa_exception*)exc.__ptr_)-1)->exceptionType->name()
(const char *) "NSError" "NSError"
(lldb) x/2a (((__cxxabiv1::__cxa_exception*)exc.__ptr_)-1)->exceptionType
0x6000008c4088: 0x0000000202c44938 libobjc.A.dylib`objc_ehtype_vtable + 16
0x6000008c4090: 0x0000000194de7dfa "NSError"

Also filed as FB17179536.

@llvmbot llvmbot added libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. libc++abi libc++abi C++ Runtime Library. Not libc++. labels Apr 9, 2025
@itrofimow
Copy link
Contributor

Hi. Sorry for the late reply

I am not at all familiar with ObjC, so do I understand correctly that clang gives a special treatment to ObjC types when they are thrown/caught?
So before the commit, both std::make_exception_ptr<NSError*> and catch (NSError* error) were treated in ObjC-way, but after the commit in question std::make_exception_ptr<NSError*> no longer gets hooked by ObjC runtime due to it no longer throw-ing (which I assume is intercepted by ObjC runtime)?

@rsesek
Copy link
Author

rsesek commented Apr 11, 2025

Correct, when the compiler detects that the thrown object is an ObjC type, it emits a call to objc_exception_throw rather than __cxa_throw:

if (ThrowType->isObjCObjectPointerType()) {

As linked above, once objc_exception_throw constructs its own typeinfo, it then calls __cxa_throw to interop with the C++ exception system.

When std::make_exception_ptr just internally threw the exception type, that would let the ObjC runtime construct its own typeinfo because of the special treatment by the compiler. After the commit, directly constructing the __cxxabiv1::__cxa_exception treats the ObjC object as a plain C++ pointer type.

@jyknight
Copy link
Member

Perhaps make_exception_pointer could just do something like this, above the new code?

#ifdef __OBJC__
  if constexpr (std::is_convertible_v<_Ep, id> && !std::is_null_pointer_v<_Ep>) {
    try {
      throw __e;
    } catch (...) {
      return current_exception();
    }
#endif

@itrofimow
Copy link
Contributor

I was thinking about just disabling this optimization completely for ObjC, in the lines of https://github.com/llvm/llvm-project/pull/135386/files

Unfortunately, all the ObjC code would still need to be rebuilt against fixed version of libc++

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
libc++abi libc++abi C++ Runtime Library. Not libc++. libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. objective-c
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants