Skip to content
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

The combination of -fno-rtti -fexceptions is very brittle #66117

Open
ldionne opened this issue Sep 12, 2023 · 2 comments
Open

The combination of -fno-rtti -fexceptions is very brittle #66117

ldionne opened this issue Sep 12, 2023 · 2 comments
Labels
c++ clang Clang issues not falling into any other category

Comments

@ldionne
Copy link
Member

ldionne commented Sep 12, 2023

The combination of -fno-rtti and -fexceptions is not supported very well, and we've seen several issues related to that internally at Apple. This bug report captures an analysis of this issue I did years ago to try to make it visible to the larger community.

Current behaviour

When both these options are specified, we don't generate RTTI for types, except when a type is thrown, at which point we generate some minimal RTTI in the TU where it is thrown (I am not sure whether the "minimal RTTI is exactly the same as "normal RTTI"). Also note that we don't generate RTTI even in the place where a key function exists, and similarly we don't assume that other TUs have a definition of the RTTI when a key function exists.

Problem with the current approach

Let's say some TU a.cpp built with -fno-rtti -fexceptions calls a function (defined in some other TU) that can throw a type E, and tries to catch E. Then, let's say that function is defined in some other TU b.cpp built with -frtti -fexceptions throws a type E. Let's also assume that another TU c.cpp built with -frtti -fexceptions defines the RTTI for E (through a key function, for example). The problem here is that in a.cpp, we'll generate minimal RTTI for E, and in b.cpp we'll use the normal RTTI assumed to be in c.cpp (because there's a key function). Since the two RTTIs don't get de-duplicated, we get a type identity mismatch in a.cpp and b.cpp, and the exception isn't caught.

Here's a minimal working example:

#/usr/bin/env bash

cat <<EOF > a.cpp
struct E { virtual ~E(); };
extern void f();
int main() {
    try {
        f();
    } catch (E const&) { // tries catching E with RTTI from a.o

    }
}
EOF

cat <<EOF > b.cpp
struct E { virtual ~E(); };
extern void f() { throw E{}; } // throws E with RTTI from c.o
EOF

cat <<EOF > c.cpp
struct E { virtual ~E(); };
E::~E() { } // key function
EOF

clang++ a.cpp -fno-rtti -fexceptions -std=c++11 -c -o a.o
clang++ b.cpp -frtti -fexceptions -std=c++11 -c -o b.o
clang++ c.cpp -frtti -fexceptions -std=c++11 -c -o c.o
clang++ b.o c.o -shared -o b.dylib
clang++ a.o b.dylib -o a.exe
nm a.o b.o c.o a.exe b.dylib | c++filt
./a.exe

To map this example to reality, imagine that E is something like std::exception (or a derived class), that b.dylib is libc++.dylib, and that a.exe is a user program built with -fno-rtti -fexceptions. It becomes clear why people are having problems with the feature.

I once discussed this issue with @dexonsmith and we had discussed this potential solution:

  • Add an attribute generate_rtti (name TBD)
  • When you’d normally generate RTTI for a type and currently suppress the RTTI generation based on whether -fno-rtti is present, instead check whether the type has the attribute and still generate full RTTI if it has it.
  • When you try to throw a type with -fno-rtti -fexceptions, you get:
    • a warning if the type doesn’t have the attribute, and minimal RTTI gets generated (like today, for backwards compatibility)
    • the normal behaviour (like when -fno-rtti is not specified) if the type has the attribute. In particular, this means that no RTTI is generated if we can tell it’s somewhere else (e.g. when there’s a key function), and full RTTI (that the linker can dedupe) is generated when we can’t tell for sure.

rdar://58055046

@ldionne ldionne added the clang Clang issues not falling into any other category label Sep 12, 2023
@dexonsmith
Copy link
Collaborator

Thanks for summarizing. The proposed generate_rtti attribute still feels right to me, especially for libraries like libc++.

@Endilll Endilll added c++ and removed new issue labels Sep 15, 2023
@llvmbot
Copy link
Collaborator

llvmbot commented Sep 15, 2023

@llvm/issue-subscribers-c-1

The combination of `-fno-rtti` and `-fexceptions` is not supported very well, and we've seen several issues related to that internally at Apple. This bug report captures an analysis of this issue I did years ago to try to make it visible to the larger community.

Current behaviour

When both these options are specified, we don't generate RTTI for types, except when a type is thrown, at which point we generate some minimal RTTI in the TU where it is thrown (I am not sure whether the "minimal RTTI is exactly the same as "normal RTTI"). Also note that we don't generate RTTI even in the place where a key function exists, and similarly we don't assume that other TUs have a definition of the RTTI when a key function exists.

Problem with the current approach

Let's say some TU a.cpp built with -fno-rtti -fexceptions calls a function (defined in some other TU) that can throw a type E, and tries to catch E. Then, let's say that function is defined in some other TU b.cpp built with -frtti -fexceptions throws a type E. Let's also assume that another TU c.cpp built with -frtti -fexceptions defines the RTTI for E (through a key function, for example). The problem here is that in a.cpp, we'll generate minimal RTTI for E, and in b.cpp we'll use the normal RTTI assumed to be in c.cpp (because there's a key function). Since the two RTTIs don't get de-duplicated, we get a type identity mismatch in a.cpp and b.cpp, and the exception isn't caught.

Here's a minimal working example:

#/usr/bin/env bash

cat <<EOF > a.cpp
struct E { virtual ~E(); };
extern void f();
int main() {
    try {
        f();
    } catch (E const&) { // tries catching E with RTTI from a.o

    }
}
EOF

cat <<EOF > b.cpp
struct E { virtual ~E(); };
extern void f() { throw E{}; } // throws E with RTTI from c.o
EOF

cat <<EOF > c.cpp
struct E { virtual ~E(); };
E::~E() { } // key function
EOF

clang++ a.cpp -fno-rtti -fexceptions -std=c++11 -c -o a.o
clang++ b.cpp -frtti -fexceptions -std=c++11 -c -o b.o
clang++ c.cpp -frtti -fexceptions -std=c++11 -c -o c.o
clang++ b.o c.o -shared -o b.dylib
clang++ a.o b.dylib -o a.exe
nm a.o b.o c.o a.exe b.dylib | c++filt
./a.exe

To map this example to reality, imagine that E is something like std::exception (or a derived class), that b.dylib is libc++.dylib, and that a.exe is a user program built with -fno-rtti -fexceptions. It becomes clear why people are having problems with the feature.

I once discussed this issue with @dexonsmith and we had discussed this potential solution:

  • Add an attribute generate_rtti (name TBD)
  • When you’d normally generate RTTI for a type and currently suppress the RTTI generation based on whether -fno-rtti is present, instead check whether the type has the attribute and still generate full RTTI if it has it.
  • When you try to throw a type with -fno-rtti -fexceptions, you get:
    • a warning if the type doesn’t have the attribute, and minimal RTTI gets generated (like today, for backwards compatibility)
    • the normal behaviour (like when -fno-rtti is not specified) if the type has the attribute. In particular, this means that no RTTI is generated if we can tell it’s somewhere else (e.g. when there’s a key function), and full RTTI (that the linker can dedupe) is generated when we can’t tell for sure.

rdar://58055046

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ clang Clang issues not falling into any other category
Projects
None yet
Development

No branches or pull requests

4 participants