libobjc2 icon indicating copy to clipboard operation
libobjc2 copied to clipboard

_objc_unexpected_exception hook not implemented on Windows

Open triplef opened this issue 2 years ago • 6 comments

We’d like to use the _objc_unexpected_exception hook in order to send unhandled exceptions to our crash reporting including the exception reason for context. While this hook is handled for other platforms in eh_personality.c, the Windows implementation in eh_win32_msvc.cc seems to be lacking support for this.

Would there be a way to implement this in the runtime, or do we need to do what WinObjC is doing and use SetUnhandledExceptionFilter() in our code to install a handler like this?

triplef avatar Dec 16 '21 14:12 triplef

You can't expose a global variable from a DLL on Windows, only functions. We should expose an objc_setUnexpectedHandler or similar (I think the Apple runtime has one of these?) that sets it (and do the same for all of the other hooks).

davidchisnall avatar Dec 16 '21 14:12 davidchisnall

Good point. The Apple runtime has objc_setUncaughtExceptionHandler(), and GNUstep Base also checks for that already.

Would you be able to give an outline of what support for this would look like in the runtime? Unfortunately I’m not familiar with SEH but I’d like to try to add this. As it looks like RtlRaiseException() doesn’t return, would we have to install our own unhandled exception filter in the runtime and basically do what WinObjC is doing to get the NSException instance, or is there a better way to do this?

triplef avatar Dec 16 '21 15:12 triplef

Ah, sorry, yes I was answering the wrong question. SEH is a bit different from DWARF EH in terms of model. With DWARF, you do a two-phase unwind that first finds a handler, then walks the stack again running cleanups until it reaches the handler. SEH does a one-phase walk where each stack frame runs some code in a funclet (which runs on top of the stack but has access to the stack frame that defined it) that does cleanup / catch logic in a single pass. This means that you can't just throw the exception and get an error from the first-stage unwinder if there isn't a catch, you have to register a callback to be called when the unwinder reaches a point where the exception isn't caught. This is documented here:

https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setunhandledexceptionfilter

The one in WinObjC is approximately the right shape, it just needs to call the callback function.

davidchisnall avatar Dec 16 '21 15:12 davidchisnall

Thanks for the explanation! While testing this I ran into a related issue with the information stored in EXCEPTION_RECORD.ExceptionInformation (probably no one has used this on 64-bit so far as WinObjC is 32-bit only).

On 64-bit, all addresses in the _ThrowInfo object are stored in 32-bit fields relative to EXCEPTION_RECORD.ExceptionInformation[3], which in our implementation contains the address of this variable defined at the top of objc_exception_throw(): https://github.com/gnustep/libobjc2/blob/b32ee7787d8a624e96952bbd8f34259c19e34427/eh_win32_msvc.cc#L106-L109

I found that the exceptTypes variable, which is allocated on the stack further down in the function, for me is 503 bytes before &x, which causes IMAGE_RELATIVE() to return a negative address. Since everything is assumed to be unsigned here and in the client code (i.e. WinObjC) this results in invalid addresses.

Are local variables allocated further on the top of the stack than _alloca() memory, and is there something else we could use as the image relative base address?

@DHowett I think you originally wrote this code, would be great to get your thoughts on this.

triplef avatar Dec 17 '21 10:12 triplef

They're relative to something on the stack because the exception object itself is also allocated on the stack. We can't (or, at least, don't) construct the Objective-C type descriptors statically, we construct them in the throw function. This is fine in the Windows model because the funclets all run on top of the stack and the stack frame containing the throw isn't destroyed until after the catch. I don't think it actually matters that this is negative - unsigned arithmetic wraps, so that should also be fine, but there may be some casts that break?

davidchisnall avatar Dec 17 '21 21:12 davidchisnall

Ah yes good catch – the WinObjC code was using ptrdiff_t for the base address, which is signed. Changing that to size_t makes it work. I’ll try to put together a PR.

triplef avatar Dec 22 '21 13:12 triplef