[libc++abi] Handle catch null pointer-to-object (#68076)
This addresses cases (currently failing) where we throw a null
pointer-to-object and fixes #64953.
We are trying to satisfy the following bullet from the C++ ABI 15.3:
* the handler is of type cv1 T* cv2 and E is a pointer type that can be
converted to the type of the handler by either or both of:
- a standard pointer conversion (4.10 [conv.ptr]) not involving
conversions to private or protected or ambiguous classes.
- a qualification conversion.
The existing implementation assesses the ambiguity of bases by computing
the offsets to them; ambiguous cases are then when the same base appears
at different offsets. The computation of offset includes indirecting
through the vtables to find the offsets to virtual bases.
When the thrown pointer points to a real object, this is quite efficient
since, if the base is found, and it is not ambiguous and on a public
path, the offset is needed to return the adjusted pointer (and the
indirections are not particularly expensive to compute).
However, when we throw a null pointer-to-object, this scheme is no
longer applicable (and the code currently bypasses the relevant
computations, leading to the incorrect catches reported in the issue).
-----
The solution proposed here takes a composite approach:
1. When the pointer-to-object points to a real instance (well, at least,
it is determined to be non-null), we use the existing scheme.
2. When the pointer-to-object is null:
* We note that there is no real object.
* When we are processing non-virtual bases, we continue to compute the
offsets, but for a notional dummy object based at 0. This is OK, since
we never need to access the object content for non-virtual bases.
* When we are processing a path with one or more virtual bases, we
remember a cookie corresponding to the inner-most virtual base found so
far (and set the notional offset to 0). Offsets to inner non-virtual
bases are then computed as normal.
A base is then ambiguous iff:
* There is a recorded virtual base cookie and that is different from the
current one or,
* The non-virtual base offsets differ.
When a handler for a pointer succeeds in catching a base pointer for a
thrown null pointer-to-object, we still return a nullptr (so the
adjustment to the pointer is not required and need not be computed).
Since we noted that there was no object when starting the search for
ambiguous bases, we know that we can skip the pointer adjustment.
This was originally uploaded as https://reviews.llvm.org/D158769.
Fixes #64953
2024-01-08 22:11:14 +00:00
|
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
//
|
|
|
|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
|
|
// See https://llvm.org/LICENSE.txt for license information.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
//
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
//
|
|
|
|
// This test case checks specifically the cases under bullet 3.3:
|
|
|
|
//
|
|
|
|
// C++ ABI 15.3:
|
|
|
|
// A handler is a match for an exception object of type E if
|
|
|
|
// * The handler is of type cv T or cv T& and E and T are the same type
|
|
|
|
// (ignoring the top-level cv-qualifiers), or
|
|
|
|
// * the handler is of type cv T or cv T& and T is an unambiguous base
|
|
|
|
// class of E, or
|
|
|
|
// > * the handler is of type cv1 T* cv2 and E is a pointer type that can <
|
|
|
|
// > be converted to the type of the handler by either or both of <
|
|
|
|
// > o a standard pointer conversion (4.10 [conv.ptr]) not involving <
|
|
|
|
// > conversions to private or protected or ambiguous classes <
|
|
|
|
// > o a qualification conversion <
|
|
|
|
// * the handler is a pointer or pointer to member type and E is
|
|
|
|
// std::nullptr_t
|
|
|
|
//
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
|
|
|
|
// UNSUPPORTED: no-exceptions
|
2024-06-28 10:40:35 -05:00
|
|
|
|
|
|
|
// This test requires the fix to https://github.com/llvm/llvm-project/issues/64953,
|
|
|
|
// which landed in d5f84e6 and is in the libc++abi built library.
|
|
|
|
// XFAIL: using-built-library-before-llvm-18
|
[libc++abi] Handle catch null pointer-to-object (#68076)
This addresses cases (currently failing) where we throw a null
pointer-to-object and fixes #64953.
We are trying to satisfy the following bullet from the C++ ABI 15.3:
* the handler is of type cv1 T* cv2 and E is a pointer type that can be
converted to the type of the handler by either or both of:
- a standard pointer conversion (4.10 [conv.ptr]) not involving
conversions to private or protected or ambiguous classes.
- a qualification conversion.
The existing implementation assesses the ambiguity of bases by computing
the offsets to them; ambiguous cases are then when the same base appears
at different offsets. The computation of offset includes indirecting
through the vtables to find the offsets to virtual bases.
When the thrown pointer points to a real object, this is quite efficient
since, if the base is found, and it is not ambiguous and on a public
path, the offset is needed to return the adjusted pointer (and the
indirections are not particularly expensive to compute).
However, when we throw a null pointer-to-object, this scheme is no
longer applicable (and the code currently bypasses the relevant
computations, leading to the incorrect catches reported in the issue).
-----
The solution proposed here takes a composite approach:
1. When the pointer-to-object points to a real instance (well, at least,
it is determined to be non-null), we use the existing scheme.
2. When the pointer-to-object is null:
* We note that there is no real object.
* When we are processing non-virtual bases, we continue to compute the
offsets, but for a notional dummy object based at 0. This is OK, since
we never need to access the object content for non-virtual bases.
* When we are processing a path with one or more virtual bases, we
remember a cookie corresponding to the inner-most virtual base found so
far (and set the notional offset to 0). Offsets to inner non-virtual
bases are then computed as normal.
A base is then ambiguous iff:
* There is a recorded virtual base cookie and that is different from the
current one or,
* The non-virtual base offsets differ.
When a handler for a pointer succeeds in catching a base pointer for a
thrown null pointer-to-object, we still return a nullptr (so the
adjustment to the pointer is not required and need not be computed).
Since we noted that there was no object when starting the search for
ambiguous bases, we know that we can skip the pointer adjustment.
This was originally uploaded as https://reviews.llvm.org/D158769.
Fixes #64953
2024-01-08 22:11:14 +00:00
|
|
|
|
|
|
|
#include <exception>
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <assert.h>
|
|
|
|
#include <stdio.h>
|
|
|
|
|
|
|
|
struct Base {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
struct Base2 {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
struct Derived1 : Base {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
struct Derived2 : Base {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
struct Derived3 : Base2 {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
struct Private : private Base {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
struct Protected : protected Base {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
struct Virtual1 : virtual Base {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
struct Virtual2 : virtual Base {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct Ambiguous1 : Derived1, Derived2 {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
struct Ambiguous2 : Derived1, Private {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
struct Ambiguous3 : Derived1, Protected {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct NoPublic1 : Private, Base2 {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
struct NoPublic2 : Protected, Base2 {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct Catchable1 : Derived3, Derived1 {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
struct Catchable2 : Virtual1, Virtual2 {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
struct Catchable3 : virtual Base, Virtual2 {
|
|
|
|
int b;
|
|
|
|
};
|
|
|
|
|
|
|
|
// Check that, when we have a null pointer-to-object that we catch a nullptr.
|
|
|
|
template <typename T // Handler type
|
|
|
|
,
|
|
|
|
typename E // Thrown exception type
|
|
|
|
>
|
|
|
|
void assert_catches() {
|
|
|
|
try {
|
|
|
|
throw static_cast<E>(0);
|
|
|
|
printf("%s\n", __PRETTY_FUNCTION__);
|
|
|
|
assert(false && "Statements after throw must be unreachable");
|
|
|
|
} catch (T t) {
|
|
|
|
assert(t == nullptr);
|
|
|
|
return;
|
|
|
|
} catch (...) {
|
|
|
|
printf("%s\n", __PRETTY_FUNCTION__);
|
|
|
|
assert(false && "Should not have entered catch-all");
|
|
|
|
}
|
|
|
|
|
|
|
|
printf("%s\n", __PRETTY_FUNCTION__);
|
|
|
|
assert(false && "The catch should have returned");
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename T // Handler type
|
|
|
|
,
|
|
|
|
typename E // Thrown exception type
|
|
|
|
>
|
|
|
|
void assert_cannot_catch() {
|
|
|
|
try {
|
|
|
|
throw static_cast<E>(0);
|
|
|
|
printf("%s\n", __PRETTY_FUNCTION__);
|
|
|
|
assert(false && "Statements after throw must be unreachable");
|
|
|
|
} catch (T t) {
|
|
|
|
printf("%s\n", __PRETTY_FUNCTION__);
|
|
|
|
assert(false && "Should not have entered the catch");
|
|
|
|
} catch (...) {
|
|
|
|
assert(true);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
printf("%s\n", __PRETTY_FUNCTION__);
|
|
|
|
assert(false && "The catch-all should have returned");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check that when we have a pointer-to-actual-object we, in fact, get the
|
|
|
|
// adjusted pointer to the base class.
|
|
|
|
template <typename T // Handler type
|
|
|
|
,
|
|
|
|
typename O // Object type
|
|
|
|
>
|
|
|
|
void assert_catches_bp() {
|
|
|
|
O* o = new (O);
|
|
|
|
try {
|
|
|
|
throw o;
|
|
|
|
printf("%s\n", __PRETTY_FUNCTION__);
|
|
|
|
assert(false && "Statements after throw must be unreachable");
|
|
|
|
} catch (T t) {
|
|
|
|
assert(t == static_cast<T>(o));
|
|
|
|
//__builtin_printf("o = %p t = %p\n", o, t);
|
|
|
|
delete o;
|
|
|
|
return;
|
|
|
|
} catch (...) {
|
|
|
|
printf("%s\n", __PRETTY_FUNCTION__);
|
|
|
|
assert(false && "Should not have entered catch-all");
|
|
|
|
}
|
|
|
|
|
|
|
|
printf("%s\n", __PRETTY_FUNCTION__);
|
|
|
|
assert(false && "The catch should have returned");
|
|
|
|
}
|
|
|
|
|
|
|
|
void f1() {
|
|
|
|
assert_catches<Base*, Catchable1*>();
|
|
|
|
assert_catches<Base*, Catchable2*>();
|
|
|
|
assert_catches<Base*, Catchable3*>();
|
|
|
|
}
|
|
|
|
|
|
|
|
void f2() {
|
|
|
|
assert_cannot_catch<Base*, Ambiguous1*>();
|
|
|
|
assert_cannot_catch<Base*, Ambiguous2*>();
|
|
|
|
assert_cannot_catch<Base*, Ambiguous3*>();
|
|
|
|
assert_cannot_catch<Base*, NoPublic1*>();
|
|
|
|
assert_cannot_catch<Base*, NoPublic2*>();
|
|
|
|
}
|
|
|
|
|
|
|
|
void f3() {
|
|
|
|
assert_catches_bp<Base*, Catchable1>();
|
|
|
|
assert_catches_bp<Base*, Catchable2>();
|
|
|
|
assert_catches_bp<Base*, Catchable3>();
|
|
|
|
}
|
|
|
|
|
|
|
|
int main(int, char**) {
|
|
|
|
f1();
|
|
|
|
f2();
|
|
|
|
f3();
|
|
|
|
return 0;
|
|
|
|
}
|