I was very tangentially involved with a use-after-close IO descriptor fault recently, and I came to realize that descriptor indexes are typically allocated by the smallest available index, Previously, I had erroneously believed that the indexes were sequentially allocated by an ever-increasing counter, until wrap-around occurs.
Obviously, the smallest available index allocation strategy makes use-after-close faults rather more significant, because in a server application that is constantly closing and allocating descriptors, it makes it rather more likely that an erroneous operation will actually have a tangible effect, and not simply return an EINVAL or similar errno.
So in my low-level linux-inotify binding, I felt it wise to add protection against use-after-close faults. The approach I'm currently investigating is the obvious one: to (conceptually) represent the inotify socket by a "MVar (Maybe Fd)" value.
However, if there's no file system events to read from the socket, we want to call threadWaitRead, So somewhere there's some code that's essentially this:
readMVar inotifyfd >>= maybe (throwIO ...) threadWaitRead
So the problem is, what happens when the thread executing this snippet yields in between the readMVar and the threadWaitRead? It would then be possible for another thread (or two) to close the inotify descriptor, and then allocate another descriptor with the same index. The first thread would then end up blocking until the new descriptor becomes readable, after which the thread would re-read the MVar and throw an exception that the descriptor has been closed.
This is a _relatively_ benign effect, but still, it does prevent the waiting thread from throwing an exception at the earliest possible moment, and there are several particularly unlucky scenarios I can think of where such a thread could be waiting on the wrong descriptor for a very long time. Not to mention that this is a whole family of race conditions, which I haven't really explored the other possible manifestations of which might not be as benign.
Somebody did also point me to the new threadWaitReadSTM primitives, but this doesn't cover this use case although the original proposal (properly implemented, of course) would:
In any case, my linux-inotify binding is available on github; the current main branch is unreleased and also has code to attempt to make reading from a descriptor a thread-safe operation. However I'm not too enthusiastic about this unreleased code at the moment, and am considering throwing it out. However, I'd still very much like to add protection against use-after-close faults, and I'd certainly prefer being able to concurrently call both close and read on the descriptor with complete safety.
Best,
Leon