LLVM Bugzilla is read-only and represents the historical archive of all LLVM issues filled before November 26, 2021. Use github to submit LLVM bugs

Bug 24792 - exception handling crashes on i386 only when using -Os optimization, maybe stack unwind bug
Summary: exception handling crashes on i386 only when using -Os optimization, maybe st...
Status: RESOLVED FIXED
Alias: None
Product: tools
Classification: Unclassified
Component: opt (show other bugs)
Version: 3.7
Hardware: PC FreeBSD
: P normal
Assignee: Michael Kuperstein
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2015-09-12 01:31 PDT by Don Lewis
Modified: 2015-10-07 02:35 PDT (History)
6 users (show)

See Also:
Fixed By Commit(s):


Attachments
simple test case to reproduce exception handling crash (390 bytes, text/x-c++src)
2015-09-12 01:31 PDT, Don Lewis
Details
Assembly produced by GCC (3.17 KB, application/octet-stream)
2015-09-16 11:54 PDT, Michael Kuperstein
Details

Note You need to log in before you can comment on or make changes to this bug.
Description Don Lewis 2015-09-12 01:31:18 PDT
Created attachment 14870 [details]
simple test case to reproduce exception handling crash

I've seen a number of crashes in the code for OpenOffice whose commonality is exception handling.  This only occur when the code is compiled with -Os optimization and only on i386.  The code operates correctly when compiled with -O2 optimization.  If the code is compiled on amd64, it operates correctly with any optimization level.

Compiling with either of these compiler versions results in crashing executables:

clang version 3.7.0 (tags/RELEASE_370/final)
Target: i386-unknown-freebsd11.0
Thread model: posix

FreeBSD clang version 3.6.1 (tags/RELEASE_361/final 237755) 20150525
Target: i386-unknown-freebsd11.0
Thread model: posix

Clang versions 3.4 and 3.5 do not exhibit this problem.

The attached code fails with this stack trace when compiled with DEBUG undefined:

% c++ -Os clangexceptionbug.cxx 
% gdb a.out
GNU gdb 6.1.1 [FreeBSD]
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-marcel-freebsd"...(no debugging symbols found)...
(gdb) run
Starting program: /home/dl/a.out 
(no debugging symbols found)...(no debugging symbols found)...(no debugging symbols found)...(no debugging symbols found)...(no debugging symbols found)...(no debugging symbols found)...
Program received signal SIGSEGV, Segmentation fault.
0x08048808 in typeinfo for X ()
(gdb) bt
#0  0x08048808 in typeinfo for X ()
#1  0x080487ba in main ()
(gdb) 


With DEBUG defined:

% c++ -Os -DDEBUG clangexceptionbug.cxx
% gdb a.out
GNU gdb 6.1.1 [FreeBSD]
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-marcel-freebsd"...(no debugging symbols found)...
(gdb) run
Starting program: /home/dl/a.out 
(no debugging symbols found)...(no debugging symbols found)...(no debugging symbols found)...(no debugging symbols found)...(no debugging symbols found)...(no debugging symbols found)...X ctor this=0x28612044
caught exception
X dtor this=0x28612044
X dtor this=0xffffdbd8

Program received signal SIGSEGV, Segmentation fault.
0xffffdbf8 in ?? ()
(gdb) bt
#0  0xffffdbf8 in ?? ()
#1  0xffffdbd8 in ?? ()
#2  0x0804879a in _start1 ()
Previous frame inner to this frame (corrupt stack?)
(gdb) 


In the OpenOffice code I saw a case where the value of "this" in a destructor
was offset by 4 bytes from the value of "this" in its matching constructor, but I was unable to produce a simple test case to reproduce that.
Comment 1 Reid Kleckner 2015-09-12 12:01:00 PDT
This is probably the mov -> push call frame conversion that's enabled under -Os.
Comment 2 Michael Kuperstein 2015-09-16 09:25:46 PDT
Yikes.
We generate the following sequence:

.Ltmp0:
        subl    $4, %esp
        pushl   $_ZN1XD2Ev
        pushl   $_ZTI1X
        pushl   %esi
        calll   __cxa_throw
        addl    $16, %esp

The problem is since that _cxa_throw does not return, adjusting esp back to where it should be never happens.

GCC seems to restore the stack pointer once it finishes handling the exception, but I don't really understand EH well enough to make sense of how/where it should be restored.

Reid, David, any pointers?
Comment 3 Reid Kleckner 2015-09-16 10:34:22 PDT
Make sure the DWARF CFI that describes how to unwind the stack is valid at the point of the call. If we really wanted to be rigorous and allow stack unwinding at each PC, we'd insert CFI after each new SP adjustment.
Comment 4 Michael Kuperstein 2015-09-16 11:53:21 PDT
GCC doesn't appear to emit any CFI directives when pushing call parameters - including the parameters to __cxa_throw. 

Attaching the assembly produced by GCC, for reference.
Comment 5 Michael Kuperstein 2015-09-16 11:54:12 PDT
Created attachment 14886 [details]
Assembly produced by GCC
Comment 6 Reid Kleckner 2015-09-16 13:05:42 PDT
GCC is using a frame pointer, so it doesn't have to. Does LLVM use a frame pointer in this case?
Comment 7 Michael Kuperstein 2015-09-16 14:00:14 PDT
No, we don't - and even if we force using an FP, that's still not enough, since by default we try to recover the original SP at function exit by adding to to the stack pointer, not by using the frame pointer.

Forcing both does the right thing for the test case, but:

a) I'm not sure whether changing the FP behavior really should be the fix.
b) In any case, I'm not sure what the right condition to force using an FP would be. Right now, we force FP only for (looking at EH-related stuff) "MMI.callsUnwindInit() || MMI.hasEHFunclets() || MMI.callsEHReturn()".
Comment 8 Reid Kleckner 2015-09-16 15:24:23 PDT
Using the FP would fix the test case, IMO, because it changes the CFI we emit.

Basically, if we're using an FP, the CFI is really simple. It only has to describe where the caller's return address is and where all the CSRs were pushed.

If we're *not* using the FP, we need *really* complicated CFI that describes *every* adjustment to SP (each push and sub). The unwinder is going to be looking for the next return address at some offset relative to ESP instead of EBP in this case, and now ESP is changing dynamically.
Comment 9 Michael Kuperstein 2015-09-16 17:37:11 PDT
I don't understand EH yet, so the below may be total nonsense, but:

1) I think in this simple case the problem isn't the unwinder - the missing stack adjustment is our own. That is, the issue isn't with CFI, it's that we directly generate bad stack adjustments in the landing pad. But we will have CFI issues in more complicated scenarios.

2) In general, that means that for any (non-leaf, but all of this is only relevant for non-leaf anyway) function that's *not* marked nounwind (is this the right condition?), we have to either:
a) Use pushes, setting CFI up correctly for each push.
b) Give up on the push optimization, unless we already have FP for another reason.
c) Force FP.

(a) sounds fairly horrible, and counter-productive.
Out of the two remaining options, I'm not sure whether (b) or (c) are preferable.
Comment 10 David Kreitzer 2015-09-17 10:35:04 PDT
The mechanism that the runtime uses to restore %esp to the proper value following an exception is the GNU_ARGS_SIZE setting. There is an GNU extension to the DWARF call frame information that sets GNU_ARGS_SIZE. Here is an excerpt from the 64-bit ABI:

-----
Outgoing arguments area delta

To maintain the size of the temporarily allocated
outgoing arguments area present on the end of the stack (when using
push instructions), operation GNU_ARGS_SIZE (0x2e) can be used.
This operation takes a single uleb128 argument specifying the current
size. This information is used to adjust the stack frame when jumping into
the exception handler of the function after unwinding the stack frame. Additionally
the CIE Augmentation shall contain an exact specification of the
encoding used. It is recommended to use a PC relative encoding whenever
possible and adjust the size according to the code model used.
-----

You can see that the gcc-generated code is using this.  From your gcc24792.s.  The .cfi_escape is setting GNU_ARGS_SIZE to 16.

-----
.LEHB0:
	.cfi_escape 0x2e,0x10
	call	__fprintf_chk
.LEHE0:
-----

I suspect that if you make sure GNU_ARGS_SIZE gets set correctly, you won't need to force a frame pointer.
Comment 11 Reid Kleckner 2015-09-17 15:13:16 PDT
Thanks for the info! I guess it is a CFI problem, but not the one I thought it was. :)
Comment 12 Michael Kuperstein 2015-09-20 06:15:51 PDT
Thanks, Dave!

It still doesn't make any sense to me, though. The .cfi_escape applies to the outgoing parameters of __fprintf_chk, but I think those get cleaned up once it returns. The problem is with the parameters to __cxa_throw itself - and there's no additional .cfi_escape there. 

What am I missing?
Comment 13 David Kreitzer 2015-09-21 07:53:06 PDT
The GNU_ARGS_SIZE setting remains in effect until changed (in much the same way that the callee-save-register locations remain in effect until changed).

So the GNU_ARGS_SIZE=16 setting prior to the call to __fprintf_chk also applies to the call to __cxa_throw. Note too that for synchronous EH, it is only necessary that the call frame information be accurate at call sites, so there is no need to reset GNU_ARGS_SIZE between the two calls.
Comment 14 Michael Kuperstein 2015-10-07 02:35:57 PDT
Should be fixed in r249522.