Repository: go
Issue: windows: SEH and VEH don't play well together
Labels: OS-Windows,NeedsInvestigation

On Windows, there are two types of exception handling: structured exception handling (SEH) and vectored exception handling (VEH).

MSVC generates binaries with SEH, in which individual functions have exception handlers attached. On 64bit, these are stored in a table in the .pdata section of the PE. When a function throws an exception (say, due to an illegal instruction or a divide by zero), the SEH for it, if any, is called.

Go's runtime uses VEH, in which the entire runtime has a series of handlers that VEH jumps through until it reaches a termination point.

When an exception is thrown, ntdll executes VEH before SEH. This is reasonable behavior, but poses a problem for our use of VEH.

Recent MSVC toolchains generate arm64 DLLs that probe for ARMv8.1 atomics by testing some instructions, and using SEH to trap a potential illegal instruction exception indicating that those instructions aren't there. ARM64 DLLs generated by recent MSVC toolchains do this unconditionally during initialization.

When the Go runtime loads one of these recent DLLs (using LoadLibrary like normal), its VEH handler is called for the illegal instruction exception generated by the probing. The Go VEH handler then decides it's fatal, and quits. That's a big problem.

I see a few solutions to this:

1) We special case illegal instruction exceptions on arm64 that are thrown from outside Go's .text section, as I've done on https://go-review.googlesource.com/c/go/+/340070 . This works fine (and I'm shipping it now) and seems like the least invasive change. This could however, mask other illegal instruction exceptions that are thrown from outside Go's .text section that aren't subsequently caught and handled by SEH, in which case the binary will terminate without spitting out a stacktrace. That seems like an unlikely and insignificant edge case, however.

2) We always allow our VEH to fall through to SEH (or to nothing) if the exception comes from outside Go's .text section. This would work, but would also mean that legitimate crashes caused by library functions would not result in a Go stacktrace being printed. I sort of like this solution, but I can imagine why others might not. And those stacktraces are probably pretty useful. This would mean deleting the `TestRaiseException` unit test we have in runtime/.

3) We allow VEH to fall through to SEH if the exception comes from outside Go's .text section and there does exist an SEH for that function. This would be a more general case of (1), and perhaps a very good solution. I can probably do the reverse engineering necessary to implement this, though it's not easy. However, if an SEH then decided not to continue execution but to crash instead, we wouldn't print a stacktrace. It remains to be seen where that edge case would be hit.

4) We make our VEH call an SEH directly if it exists, and only crash with a stacktrace if the SEH called doesn't handle it. This is very tricky to do, but would probably give us the best of all worlds.

5) We do (2), but then install a SEH (using `RtlAddFunctionTable`) for the IP of syscall.Syscall that prints a stacktrace, so that we do ultimately get the unhandled exceptions that VEH passes on due to (2). This seems simple-ish, though I suspect we would miss stacktraces caused by threads created by non-Go code.

Before I go further in thinking about this, I thought we ought to discuss all this first. https://go-review.googlesource.com/c/go/+/340070 works as a stopgap solution for now, anyhow.

CC @rsc @ianlancetaylor @aclements @cherrymui @bradfitz @alexbrainman @jstarks