3 ms·
The syscall writing the data is async, which requires the memory it uses to be available until until it completes. Since this was on the stack here, it means th
by CodesInChaos 5y ago
The syscall writing the data is async, which requires the memory it uses to be available until until it completes. Since this was on the stack here, it means that the function that contains this memory on its stack must not return until IO completes
But the code that was supposed to wait until completion was interrupted by an exception, which unrolled the stack beyond the function owning the relevant memory. So the completing async operation writes into memory owned by somebody else now.
This is a general complication with completion based async IO. You could get the same problems without any interrupts, if you just don't wait for async IO to complete in some circumstances.
- CamperBob2 5y agoMore like a general complication with SEH. One of several reasons why I don't allow it in any code I touch. Anything that obscures the flow of control is a violation of literate programming principles, and exceptions are pretty high on that particular shit list.
- _nalply 5y agoThank you. Now I understand why Rust async is so complicated. It's about lifetimes. It's dangerous to write data asynchronously because... eh... that location might be already invalid. I know this is not about Rust. Forgive me that I mentally connect this story with async Rust.
- CodesInChaos 5y agoThere are two ways how async IO can work: * Readiness based: You use `select` (or preferably its successors) to figure out which file descriptor has data available. Then you synchronously read the data, which completes instantly because there is data available. This is the traditional way on Linux, and also the way Rust's async works. This approach doesn't run into any lifetime issues, because you only need to provide the memory (`&mut [u8]`) to a sync call once data is available. This approach works well from sequential streams, like sockets or pipes, but isn't a natural match for random access file IO. * Completion based: You trigger an IO operation specifying the target memory. The system notifies you when it completes. This approach needs you to keep the memory available until the operation finishes. This is the traditional way on Windows (IO completion ports) and also used by the more recent io_uring on Linux. This one runs into the lifetime issues that caused the bug in the linked article. Thus in Rust it can't safely be used with a `&mut [u8]`, you have to transfer ownership of the buffer to the IO library, so it can keep it alive until the operation completes. The ringbahn crate is an example of this approach in Rust.
- yxhuvud 5y agoGood writeup. One thing to note is that Go, Crystal and other systems based on green threads tend to not run into the problem as they have separate stacks for different execution contexts.
- oshiar53-0 5y agoNote that what you're referring to as "readiness-based I/O" is not asynchronous I/O in the strictiest sense--it's actually non-blocking, synchronous I/O. I find this article helpful in clearing up confusion of asynchronous vs non-blocking I/O: http://blog.omega-prime.co.uk/?p=155 http://blog.omega-prime.co.uk/?p=155