Debugging memory corruption: who the hell writes "2" into my stack? (2016)
unity.com
unity.com
It seems like the select() was within its rights to have passed a stack allocated buffer to be written asynchronously by the kernel since it, presumably, knew it couldn't encounter any exceptions. But injecting one has broken that assumption.
If the select() implementation had returned normally with an error or was expecting then I'd assume this wouldn't have happened.
https://learn.microsoft.com/en-us/windows/win32/api/winsock2...
https://learn.microsoft.com/en-us/windows/win32/api/synchapi...
If they had implemented this to use a C++ exception to return control to C++, they should have encountered this stack corruption issue immediately upon implementing this, rather than have a customer some point in the future hit it before they did.
My read of this is that they had the callback function do something and someone eventually got it to throw an exception. This is undefined behavior because there is no correct way to unwind a C stack frame. However, that is not obvious, especially if you test it since if the C function does nothing special such that it needs no special clean up, everything should be fine. However, WaitForSingleObjectEx() does something extremely special. Skipping that by unwinding the stack bit them.
I filed bugs against both GCC and LLVM requesting warnings to protect people from doing this:
I think this is the rare direction that more langs should follow Rust in that 'clever' code can be more easily quarantined for scrutiny via unsafe.
Cortisol actually reduces the effectiveness of working memory. Write your code like someone looking at it is already having a bad day, because not only are odds very good they will be, but that’s the costliest time for them to be looking at it. Probability x consequences.
Grug brain is maybe good for 1:1 interactions or over coffee with the people you vent to or are vented to.
Speaking of which, I once did consulting for a company that insisted that they had hit a bug in ZFS because there were backtraces showing ZFS. Using KASAN, I found that the bug was in fact in another third party kernel module that they used that appeared to be the result of a hobbyist project on github. It had a string function write 1 past the end of a memory allocation, which often would corrupt memory allocations used in ZFS' AVL trees. ZFS' AVL tree code did not like this, so ZFS appeared in backtraces.
That said, the bug described by the unity blog is really an undefined behavior bug rather than a memory corruption bug. Doing an exception in a C++ callback function called by a C function is undefined since C does not support stack unwinding. If the C function does not need any special cleanup when stack unwinding is done, it will probably work fine, but in this particular case, the function needed very special cleanup and invoking undefined behavior bit them.
Sure, but I’ve rarely seen quality comparable to the Linux kernel in a commercial context.
> I am going to guess that Sony Ericsson had a ton of use after free bugs and array out of bounds bugs, as that is the main way that developers can step on each others' code when it shares a process space.
Mostly yes. But there were some interesting ones too. I debugged one that turned out to be a stack overflow, for example. There was basically this giant function with ridiculously many local variables. When that function called another function the stack pointer was bumped far out of the allotted stack space and the called function overwrote stuff when writing to local variables.
> What is despair? I have known it—hear my song. Despair is when you’re debugging a kernel driver and you look at a memory dump and you see that a pointer has a value of 7. THERE IS NO HARDWARE ARCHITECTURE THAT IS ALIGNED ON 7. Furthermore, 7 IS TOO SMALL AND ONLY EVIL CODE WOULD TRY TO ACCESS SMALL NUMBER MEMORY. Misaligned, small-number memory accesses have stolen decades from my life.
All James Mickens' USENIX articles are fun (for a very specific subset of computer scientist - the kind that would comment on this thread). https://mickens.seas.harvard.edu/wisdom-james-mickens
x86 page table entries can't express "user read only, kernel read/write"
Linux for example maintains a big writable linear mapping of all RAM at all times (on 64-bit), you can corrupt read-only user pages through it all day and never fault. Code running in the kernel generlly uses virtual addresses from that mapping.
The kernel has no reason to be trying to bypass page protections; what if you asked it to write to a shared read-only page?
The try-catch dance you're describing is only necessary for accessing the userspace mapping (because it might fault). Kernel code which dereferences kernel pointers doesn't do that.
Pages don't get remapped into userspace: userspace gets an additional aliased mapping with restricted permissions. The kernel's writable mapping still exists. There's nothing to "bypass", what permissions userspace applies its user mapping of the same physical page has no effect on the kernel mapping.
:-)
#define NULL ((void *)0)
As of C99, C also has uintptr_t, which lets you treat pointers as integers.A bit tangential, but I've been crying about the insane Unity project build times for years now, and about how they've taken zero steps to fix them and are instead trying their hardest to sell you cloud builds. Glad to see them having to suffer through what they're inflicting on us for once!
Regardless, very good writeup, and yet another reason to never ever under any conditions use exceptions.
UNIX system calls never do this. The kernel won't keep references to pointers you pass them and write to them later. It just isn't in the DNA. The only exceptions I can think of would be clone(), which is abstracted by the POSIX threads runtime, and Windows-inspired non-standard event i/o system calls like epoll.
I mean, this is because the UNIX model was based on readiness rather than completion. Which is slower. Hence the newer I/O models.
That's what i/o completion ports are working around.
They solve a problem UNIX doesn't have.
https://www.man7.org/linux/man-pages/man7/aio.7.html
That said, it would be insane to pass it pointers to the stack, as the only safe way to make that work would be to do `aio_suspend()` or busy wait and you might as well just use a synchronous read() function in that case.
Wait but read() wouldn't allow overlapping operations. Whereas if you suspend or busy wait you can do that for multiple operations executing concurrently.
Also if the buffer is from a caller frame then you could also return safely no?
Valgrind flagged an "invalid write", which I eventually hunted down as a fencepost error in a dependency which overwrote their allocated stack array by one byte. I recall that it wrote "1" rather than "2", though, haha.
> Lesson learnt, folks: do not throw exceptions out of asynchronous procedures if you’re inside a system call!
The author's debugging skills are impressive and significantly better than mine, but I find this an unsatisfying takeaway. I yearn for a systemic approach to either prevent such issues altogether or to make them less difficult to troubleshoot. The general solution is to move away from C/C++ to memory safe languages whenever possible, but such choices are of course not always realistic.
With my project, I started running most of the test suite under Valgrind periodically. That took took half an hour to finish rather than a few seconds, but it caught many similar memory corruption issues over the next few years.
MSAN, ASAN, and UBSAN are great tools that have saved me a lot of time and headaches, but they don't catch everything that the compiler of a memory safe language can, at least not today.
Rust programs use unsafe a lot in practice. One of the greatest difficulties I've had in supporting Rust with Cosmopolitan Libc is that Rust libraries all try to be clever by using raw assembly system calls rather than using libc. So our Rust binaries will break mysteriously when I run them on other OSes. Everyone who does AI or scientific computing with Rust, if you profile their programs, I guarantee you 99% of the time it's going to be inside C/C++ code. If better C/C++ tools can give us memory safety, then how much difference does it really make if it's baked into the language syntax. Rust can't prove everything at compile time.
Some of the Rust programs I've used like Alacrity will runtime panic all the time. Because the language doesn't actually save you. What saves you is smart people spending 5+ years hammering out all the bugs. That's why old tools we depend on every day like GNU programs never crash and their memory bugs are rare enough to be newsworthy. The Rust community has a reputation for toxic behavior that raises questions about its the reliability of its governance. Rust evangelizes its ideas by attacking other languages and socially ostracizing the developers who use them. Software development is the process of manipulating memory, so do you really want to be relinquishing control over your memory to these kinds of people?
> Dependency soup.
Exactly why I don't use it. I don't really feel like including the source for the entire toolchain as part of my project and building it all myself. At least if I write standards conforming C++ there are multiple compiler implementations that can all handle it. I also have a reasonable expectation that a few decades from now I will be able to `apt get somecompiler` and the code will still just work (aside from any API changes at the OS level, for which compatibility shims will almost certainly exist).
If I can't build something starting from a repo in a network isolated environment then I want absolutely nothing to do with it. (Emscripten I am looking at you. I will not be downloading sketchy binary blobs from cloud storage to "build from source" that is not a source build that is binary distribution you liars.)
More tools in the C/C++ realm are always welcome but I've yet to see more than 50% of projects I've worked on be able to successfully use ASAN(assuming you've got the time to burn to configure them and all their dependencies properly). I've used ASAN, CBMC and other tools to good effect but find Rust more productive overall.
I’m sorry, this is coming from Justine “the magic syscall numbers are my god given right to use” Tunney?
So using raw system calls is a distraction for them.
I encountered the issue when our (not Ubuntu, not 24.04) LTS upstream backported security fixes that included the mmap changes without updating universe to include a clang version with the fixes. Any developers diligent enough to update and run sanitisers locally started seeing intermittent crashes.
https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html#...
What would catch this would be a C++ compiler warning. I filed bug reports against both GCC and LLVM asking for one:
This just feels important to point out because this feature is 15 years old and still commonly misunderstood, and each time people are wanting the same thing (actual compile-time prevention of `throw`) which it is not.
Edit: OK I finally just went and tried it on godbolt.org. C++17 GCC, Clang, and MSVC all give 1 warning on this code for `bar` and that's all.
void canthrow() {
throw 42;
}
void foo() noexcept {
canthrow();
}
void bar() noexcept {
throw 42;
}https://godbolt.org/z/rjPfYjnzf
https://godbolt.org/z/14ocshsE5
I filed bugs against both GCC and LLVM requesting warnings:
It would also help if the APC docs documented that APCs must not throw.
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=118263
https://github.com/llvm/llvm-project/issues/121427
Perhaps the C++ standards committee should specify that doing this should cause a compiler failure, rather than a warning.
Pointers to C++ functions that can throw exceptions should not be passed to C functions as callback pointers. Executing the exception in the callback context is undefined behavior, since C does not support stack unwinding.
Presumably, any language that has exception handling would have an issue on Windows when doing select() in one thread and QueueUserAPC() in another thread to interrupt it with a callback function that throws an exception. What happens then depends on how the stack unwinding works.
No programming language can avoid this because they need to use the underlying operating system's system calls in order to function. They also need to use the C functions in ntdll to do system calls since Microsoft does not support doing system calls outside of calling the corresponding functions in ntdll and friends.
The problem boils down to usage of stack memory after the memory is given to somebody else.
While this isn't incorrect in this case the problem seems to be caused by stack unwinding without the consent of lower frames rather than a willful bug where the callee forgets about the ownership.
There is no "called back" in this case. The APC was executed by the sleep and corrupted the stack by unwinding across the C winsock code without any cleanup. It never returned.
The user-mode enters an "alertable" wait which allows an asynchronous procedure (APC) to interrupt it and execute code. Instead of returning the APC causes an exception, unwinds the stack across the APC delivery and ends up executing some random code instead of returning to the winapi code that called wait(alertable: true) in a loop. So the code that was supposed to be synchronous because of while(!completed) wait(); suddenly is broken out of the loop without actually being completed.
> Exceptions (where lower frames matter) hid the control flow here, but that's one way to reach this situation (early return is another way, as shown by Raymond Chen's post).
This isn't just hiding the control flow here. It's control flow that shouldn't have existed in the first place. It walks across the boundary of the windows APC dispatcher. Unity folks needed to go out of their way to make this "work" in the first place because using c++ exceptions and standard library threads this wouldn't work.
I'm actually not sure if Linux would handle this in the same way or what. Plausibly it sees the same leaf page tables as user space, trips a fault, and doesn't scribble the pages anyway. Maybe Windows translates the user-provided virtual address to a physical address (or other kernel mapping that happens to have write permission) upon registration.
Of course, it's not uncommon to find unsafe Rust code that misbehaves badly when something panics, which is yet another of its hazards that I wish were better documented.
In this case, I'd put down the lesson as "exceptions in C++ are very dangerous if you're coming out of C code", since they can cause undocumented and unexpected behavior that ultimately led to the use-after-return issue here.
[0] https://blog.rust-lang.org/2024/09/05/Rust-1.81.0.html#abort...
It can also result in logic errors if objects are used after their methods panic, but such usage is generally not expected to work in the first place.
Without exceptions the possible control flow is entirely explicit. It would have at least been obvious that cleanup wasn't properly handled in the select function for all cases.
An exception was effectively injected from outside of the code via low level shenanigans. That's not "exceptions bad" that's "low level monkeying with program control flow can blow up in your face".
It’s probably feasible to use them if you draw a tight boundary around the exception-using code, use RAII without fail inside that boundary to ensure everything cleans up properly, and make sure all code that might be called from the outside has try/catch blocks. (And, obviously, don’t trigger async calls to throw from the middle of someone else’s function!).
I find it a lot easier to avoid them entirely. Error handling is a little more annoying, but it’s worth it.
I had a few questions I asked ChatGPT to understand better: https://chatgpt.com/share/677411f9-b8a0-8013-8724-8cdff8dc4d...
Very interesting insights about low level programming in general
This is an absolutely standard trick that is known as "the self-pipe trick". I believe DJB created and named it. It is used for turning APIs not based on file handles/descriptors into events for an event loop based on file handles/descriptors, especially for turning signals into events for select(2)/poll(2)/epoll/kqueue/...
> Also that pattern is a bit too sweet as it can be a nice way to create condition variables or queues, the problem being one is now paying syscall overhead.
Sure but you don't need to, and even if you did, the system call overhead is probably not the hill you're dying on, or if it is then you want io_uring or similar.
Especially when I still have English set as the second priority language.
*This could, in theory, be caught by fancy hardware strategies like capabilities. But those are somewhat more esoteric.
Anyway, it is a shame that the compiler does not issue a warning when you do this. I filed bug reports with both GCC and LLVM requesting that they issue warnings, which should be able to avoid this mess if the compilers issue them and developers heed them:
To put it another way, you can design a safe wrapper around this in Rust, but you can't in C++.
They can't. Rust can't verify the safety of the called code once you cross the language boundary. Handing out the pointer is inherently unsafe.
In the user space FFI case at least you might be able to switch to an implementation written in the same (memory safe) language that you are already using. Not so for a syscall.
The problem was that the C API exposed by the kernel did not encode lifetime requirements, so they were accidentally violated. Rust APIs (including ones that wrap C interfaces) can encode lifetime requirements, so you get compile time errors if you screw it up.
I don't think you can win this argument by saying "but you have to use `unsafe` to write the Rust wrapper". That's obviously unavoidable.
As others have mentioned, this same issue could happen in Rust until very recently. As of Rust 1.81.0, Rust will abort instead of unwinding C stack frames:
https://blog.rust-lang.org/2024/09/05/Rust-1.81.0.html#abort...
That avoids this issue in Rust. As for avoiding it in C++ code, I have filed bugs against both GCC and LLVM requesting warnings:
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=118263
https://github.com/llvm/llvm-project/issues/121427
Once the compilers begin emitting warnings, this should not be an issue anymore as long as developers heed the warnings.
Correctness of the Rust wrapper can't be checked by the compiler, just like correctness of C headers is unchecked, and it just has to match the actual underlying ABI.
The task of making a safe API wrapper can be relatively simple, because you don't have to take into consideration safety an application as a whole, you only need to translate requirements of individual APIs to Rust's safety requirements, function by function. In this case you would need to be aware that the function call may unwind, so whether someone making a dedicated safe API for it would think of it or not, is only a speculation.
If those specifications were written down clearly enough then this dev wouldn't have needed to spend 5 days debugging this since he spent a significant amount of time reading the documentation to find any errors they are making that is mentioned in the documentation.
And don't say that they can actually just read the rust code and check that since well, I can't read low level rust code and how any of the annotations ca interact with each other.
A single line of rust code could easily need several paragraphs of written documentation so that someone not familier with what rust is specifying will actually understand what that entails.
This is part of why Rust is difficult, you have to nail down the specification and a small change to the specification causes broad changes to the codebase. The same might need to happen in C, but many times it doesn't.
In C you can have a function that returns a pointer, and get no information how long that pointer is valid for, what is responsible for freeing it, whether it's safe to use from another thread. That's not only an obstacle for making a safe Rust API for it, that's also a problem for C programmers who don't want to just wing it and hope it won't crash.
The benefit of safe wrappers is that as a downstream user you don't need to manually check their requirements. They're encoded in the type system that the compiler checks for you. If it compiles, it's safe by Rust's definition. The safety rules are universal for all of Rust, which also makes it easier to understand the requirements, because they're not custom for each library or syscall. The wrappers boil it down to Rust's references, lifetimes, guards, markers, etc. that work the same everywhere.
> That Linux drama was due to "nontechnical nonsense" of maintainers refusing to document their APIs requirements.
> If those specifications were written down clearly enough then this dev wouldn't have needed to spend 5 days debugging...
The exception caused the stack frame to disappear before the OS kernel was done with it. Presumably, the timeout would have been properly handled had the stack not been unwound by the exception. If it had not, that would be a bug in Windows.
There is a conceptually simple solution to this issue, which is to have the C++ compiler issue a warning when a programmer does this. I filed bug reports against both GCC and LLVM asking for one:
This sort of stuff is why any productivity arguments for C++ over Rust are bullshit. Sure you spend a little more time writing lifetime annotations, but in return you avoid spending 5 days debugging one memory corruption bug.
https://blog.rust-lang.org/2024/09/05/Rust-1.81.0.html#abort...
This could only be made safe by the OS somehow imposing safety mechanisms on the binary level, or by wrapping all OS APIs into APIs of the safe language, where the wrappers have to take care to ensure both the guarantees implied by the language and the assumptions made by the OS APIs. (Writing the OS itself in a memory-safe language isn’t sufficient, for one because it very likely will still require some amount of “unsafe” code, and furthermore because one would still want to allow applications written in a different language, which even if it also is memory-safe, would need memory-correct wrappers/adapters.)
This is similar to the distinction between memory-safe languages like Rust where the safety is established primarily on the source level, not on the binary level, and memory-safe runtimes like the CLR (.NET) and the JVM.
To my knowledge the kernel and win32 is in fact written in C++ and only the interface has C linkage and follows C norms.
So this error occurred going C++ > C > C++ never mind languages with different memory protection mechanisms like Rust > C > C++.
Of course, you can write C++ without exception safety too, but “C++ as a better C” and exception-safe C++ are effectively like two different languages.
The article doesn't say it was a C++ exception. Could've been a SEH exception.
It would involve looking at the function pointer in QueueUserAPC and making sure the function being called doesn't mess with the stack frame being executed on.
This function will run in the context of the called thread, in that thread's stack. NOT in the calling thread.
It's a weird execution mode where you're allowed to hijack a blocked thread and run some code in its context.
Don't know enough about Rust or the like to say if that's something that could be done in the language with attributes/annotations for a function, but it seems plausible.
I filed bugs against both GCC and LLVM requesting warnings:
https://blog.rust-lang.org/2024/09/05/Rust-1.81.0.html#abort...
You have to explicitly opt into unwinding like this now otherwise the program will abort.
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=118263
https://github.com/llvm/llvm-project/issues/121427
If/when they are implemented, they will become errors with -Werror.
> Whenever an exception is thrown and the search for a handler ([except.handle]) encounters the outermost block of a function with a non-throwing exception specification, the function std :: terminate is invoked ([except.terminate])
https://timsong-cpp.github.io/cppwp/n4950/except.spec#5If it is not, then there is a bug in the C++ implementation.
> Another possibility is to avoid it in the first place by not allowing C++ function pointers that are not marked noexcept to be passed to C functions.
That said, my personal preference is to use C instead of C++, which avoids the issues of exceptions breaking kernel expectations entirely.
1) The top level of an async routine should have a handler that catches all exceptions and dies if it catches one.
2) If you have a resource you have a cleanup routine for it.
So I'd say it's more about the basic expectations of a function called from C, which includes a million other trivial things like "don't write beyond the bounds of buffers you're given" and "don't clobber your caller's stack frame" and "don't spawn another thread just to write to output pointers after your function returns" (not that any of these is the issue here).
You have to know how exactly asynchronous callbacks registered with the kernel get called, how their stack frames get setup, how kernel writes to local variables within a stack frame of a user thread, how stack frames are adjusted when a blocking system call returns to user space and finally, how and when exceptions (in any language) mess up the above when they implement a different flow of control than that expected by the above "async callback kernel api architecture". All of these are at play here and once you put them together you understand the scenario.
The replay system would also be unlikely to pose a problem. Replay systems usually just encode and replay the side effects, so there is no need to "reimplement" the operations. So, if you did some wacky system call, but all it did is write 0x2 to a memory location, M, you effectively just record: "at time T we issued a system call that wrote 0x2 to M". Then, when you get to simulated time T in the replay, you do not reissue the wacky system call, you just write 0x2 to M and call it a day.
The primary complexity is actually in creating a consistent timeline with respect to parallel asynchronous writes. Record-Replay systems like rr usually just serialize multithreaded execution during recording to avoid such problems. You could also do so by just serializing the executing thread and the parallel asynchronous write by stopping execution of the thread while the write occurs.
Again, not really sure if that would be possible in Windows, but there is nothing particularly mechanically hard about doing this. It is just a question of whether it matches the abstractions and hooks Windows uses and supports.
As such, for an asynchronous kernel write you'd want to set up the kernel to never mutate recordee memory, instead having it modify recorder-local memory, which the recorder can then copy over to the real process whenever, and get to record when it happens while at it (see https://github.com/rr-debugger/rr/issues/2613). But such can introduce large delays, thereby changing execution characteristics (if not make things appear to happen in a different order than the kernel would, if done improperly). And you still need the recording system to have accurately implemented the forwarding of whatever edge-case of the asynchronous operation you hit.
And, if done as just that, you'd still hit the problem encountered in the article of it looking like unrelated code changes the memory (whereas with synchronous syscalls you'd at least see the mutation happening on a syscall instruction). So you'd want some extra injected recordee instruction(s) to present separation of recordee actions from asynchronous kernel ones. As a sibling comment notes, rr as-is doesn't handle any asynchronous kernel write cases (though it's certainly not entirely impossible to).
Maybe Windows TTD records async NtDeviceIoControlFile acculately, maybe it doesn't; I don't know.
The time travel debugging available with WinDbg should be able to wind back to the point of corruption - that'd probably have taken a few days off the initial realisation that an async change to the stack was causing the problem.
There'd still be another reasoning step required to understand why that happened - but you would be able to step back in time e.g. to when this buffer was previously used on the stack to see how select () was submitting it to the kernel.
In fact, a data breakpoint / watchpoint could likely have taken you back from the corruption to the previous valid use, which may have been the missing piece.
In the Unix world we have this too, and it's called signals, but every documentation about signals is sure to say "in a signal handler, almost nothing is safe!". You aren't supposed to call printf() in a signal handler. Throwing exceptions is unthinkable.
I skimmed the linked QueueUserAPC() documentation page and it says none of this. Exceptions aren't the handgrenade here (though sure, they're nasty) — QueueUserAPC() is.
This is more like select() sometimes calling a user-supplied function in addition to checking for I/O.
[1] https://learn.microsoft.com/en-us/windows/win32/sync/asynchr...
> A thread enters an alertable state when it calls the SleepEx, SignalObjectAndWait, MsgWaitForMultipleObjectsEx, WaitForMultipleObjectsEx, or WaitForSingleObjectEx function.
So this is a lot less like Unix signals. It only really works if the thread you're doing the async procedure call to is one that's likely to use those.
So APCs are safe enough -- a lot safer than Unix signal handlers.
Now, MSFT does NOT document that APCs can't throw, but since the functions that enter alertable states (hence which can call APCs) are extern "C" functions, it follows that APCs cannot throw.
So part of the problem here is that the PAPCFUNC function type is not noexcept, therefore the compiler can't stop you from using a function that is not noexcept as an APC, thus APCs can -but must not!- throw exceptions.
The issue is more about the actual code that calls alertable waits not expecting exceptions that will unwind which will most likely be all of winapi code because in it exception == crash.
That being said, I filed bugs against both GCC and LLVM requesting warnings, which is the next best thing:
What ended up happening is that if the GC ran inside the callback then the objects the native code could see could move, and so the next block of code was smashing the heap by writing to the wrong spots. All the small inputs finished before a GC was called and looked fine but larger ones went into undefined behavior. So dumb.
That auto-translation is something else
Gotta love programmers out of context
When the Kernel does the memory write, user-mode memory debuggers don't see it happen.
Then obviously, cancelling an operation is always tricky business with lifetime due to asynchronicity. My approach is to always design my APIs with synchronous cancel semantics, which is sometimes tricky to implement. Many common libraries don't do it right.
Windows allows some synchronous calls to be interrupted by another thread to run an APC if the called thread is in an "alertable wait" state. The interrupted thread then returns to the blocking call, so the pointers in the call are expected to be valid.
Edit 2: I should clarify that the thread returns to the blocking call, which then exits with WAIT_IO_COMPLETION status. So you have to retry it again. but the stack context is expected to be safe.
APC is an "Asynchronous procedure call", which is asynchronous to the calling thread in that it may or may not get run. Edit: May or may not run a future time.
(https://learn.microsoft.com/en-us/windows/win32/sync/asynchr...)
There are very limited things you are supposed to do in an APC, but these are poorly documented and need one to think carefully about what is happening when a thread is executing in a stack frame and you interrupt it with this horrorshow.
Win32 API is a plethora of footguns. For the uninitiated it can be like playing Minesweeper with code. Or like that scene in Galaxy Quest where the hammers are coming at you at random times as you try to cross a hallway.
A lot of it was designed by people who, I think, would call one stupid for holding it wrong.
I suppose it's a relic of the late 80s and 90s when you crawled on broken glass because there was no other way to get to the other side.
You learn a lot of the underlying systems this way, but these days people need to get shit done and move on with their lives.
Us olds are left behind staring at nostalgically at our mangled feet while we yell at people to get off our lawns.
One must not throw a C++ exception across stack frames that don't participate in C++ stack unwinding, whether it's a Win32 APC, another Win32 callback, a POSIX signal or `qsort` (for the people that believe qsort still has a place in this decade). How the Win32 API is designed is absolutely irrelevant for the bug in this code.
From that point on I am sure he knew to do that. What's obvious to you can also just be your experience.