This is an archive of the discontinued LLVM Phabricator instance.

[WinEHPrepare] Fix accidental truncation of EH funclets with GNUstep ObjC runtime
AbandonedPublic

Authored by sgraenitz on Apr 29 2022, 9:01 AM.

Details

Summary

Unwinding ObjC code involves calls to intrinsics like llvm.obj.retain and llvm.objc.destroyWeak. Exception handling on Windows is based on funclets and WinEHPrepare only allows calls to nounwind intrinsics. This works just fine, except for ObjC nounwind intrinsics, because these are lowered into regular function calls in an earlier stage already (i.e. PreISelIntrinsicLoweringPass). Thus, WinEHPrepare accidentally drops them as implausible instructions and silently truncates certain funclets. Eventually, this causes crashes during unwinding on Windows with the GNUstep ObjC runtime [1].

This patch is a first attempt to fix the issue: PreISelIntrinsicLoweringPass passes on nounwind attributes to lowered call sites and WinEHPrepare gets a little less conservative by allowing any nounwind calls. It fixes the issue in GNUstep libobjc2 [1] and related projects. I will work on a test case during the next weeks and I understand if this is not immediately ready to land upstream. For the moment, I am looking for feedback to avoid unintended side effects.

Thanks for all input!

[1] https://github.com/gnustep/libobjc2/issues/222

Diff Detail

Event Timeline

sgraenitz created this revision.Apr 29 2022, 9:01 AM
Herald added a project: Restricted Project. · View Herald TranscriptApr 29 2022, 9:01 AM
Herald added a subscriber: hiraditya. · View Herald Transcript
sgraenitz requested review of this revision.Apr 29 2022, 9:01 AM
Herald added a project: Restricted Project. · View Herald TranscriptApr 29 2022, 9:01 AM
efriedma added a subscriber: efriedma.
efriedma added inline comments.
llvm/lib/CodeGen/PreISelIntrinsicLowering.cpp
113

Are all these functions actually nounwind? Destroying objects can call into user code.

I think the original issue and these changes might also be related to these frontend crashes when using @try/catch with @finally in Objective-C code on Windows:

https://github.com/llvm/llvm-project/issues/43828
https://github.com/llvm/llvm-project/issues/51899

I will try to verify this next week.

There is a second attempt here, that I am more uncertain about: https://github.com/weliveindetail/llvm-project/commit/93e830eb1b9280d08e3f082b746a8d1343d413ac

However, the behavior of CodeGenFunction::getBundlesForFunclet() appears related. It works, because all the relevant ObjC ARC calls will have the "funclet" entry in their bundle list. I had to extend the respective WinEHPrepare condition, because sometimes funclet pads had disappeared (via optimizations I assume?).

I don't fully understand the intended behavior of the existing code and so I can't say which one might be closer to a correct solution (or whether they are both far away).

llvm/lib/CodeGen/PreISelIntrinsicLowering.cpp
113

You mean all intrinsics lowered pre-ISel? Not sure, so far we observed the following ones to trigger truncation and from the IR I looked at they all were all declared nounwind:
objc_destroyWeak, objc_release, objc_retain, objc_retainAutoreleasedReturnValue, objc_storeStrong

Yes destruction likely calls back into user code. In C++ throwing from destructors is undefined behavior. Is that the case in ObjC as well?

rnk added a comment.Apr 29 2022, 8:14 PM

Which pass inserts the calls to these intrinsics? Can that pass be responsible for calculating funclet colors and adding the operand bundles to the intrinsics? That would match what we do for other instrumentation passes (ASan, PGO). The pre isel lowering pass can simply carry over the funclet operand bundle if present. I think there is a utility for carrying over that and similar operand bundles.

llvm/lib/CodeGen/PreISelIntrinsicLowering.cpp
113

I think if the original intrinsic call is nounwind, it is safe to transfer that onto the lowered replacement call regardless of how it interacts with WinEH, other transforms will have already optimized assuming this instruction doesn't throw exceptions.

llvm/lib/CodeGen/WinEHPrepare.cpp
969

I'd rather keep this restricted to intrinsics if possible. The funclet bundle operands exist to make sure that we don't have to clone common blocks with any interesting EH state. They prevent mid level passes from doing things like tail merging, or merging identical regions of IR.

The design is somewhat complicated because it tries to defend against all possible legal IR transforms, not just the ones in tree. Unfortunately, the design makes it really hard for any pass after the frontend IR generation to correctly insert call instructions, and that probably includes ARC.

sgraenitz abandoned this revision.May 2 2022, 5:14 AM
sgraenitz marked 3 inline comments as done.

Thanks for your comments Reid, this was really helpful!

Which pass inserts the calls to these intrinsics? Can that pass be responsible for calculating funclet colors and adding the operand bundles to the intrinsics?

Yes, my second attempt was a basic sketch for that, but I wasn't sure it's going in the right direction. With your explanation it makes a lot more sense now.

So, I did some polishing and set up a new review here: D124762

llvm/lib/CodeGen/WinEHPrepare.cpp
969

Ok, my patch in the new review avoids it and uses bundle operands instead.

ahatanak added inline comments.
llvm/lib/CodeGen/PreISelIntrinsicLowering.cpp
113

Retain and release methods under ARC aren't allowed to raise exceptions, so I assume llvm.objc.retain and llvm.objc.release don't raise exceptions either.

https://clang.llvm.org/docs/AutomaticReferenceCounting.html#retain-count-semantics

I guess the other methods are marked as nounwind for similar reasons.