Thread 1 (_do_read) Thread 2 (_aio_thread)
queues aio
ioc->aio_wait()
locks ioc->lock
num_waiting++
finishes aios
ioc->aio_wake
if (num_waiting)
lock ioc->lock (blocks)
num_running == 0, continue
ioc->unlock
ioc->lock taken
do_read destroys ioc
use after free, may block forever
The last bit of the race may vary; the key thing is that thread 2 is
taking a lock that thread 1 can destroy; thread 2 doesn't have it pinned
in memory.
Fix this by simplifying the aio_wake, aio_wait. Since it is mutually
exclusive with a callback completion, we can avoid calling it at all when
a callback in present, and focus on keeping it simple.
Avoid use-after-free by making sure the last decrement happens under
the lock in the aio_wake/wait case.