It turns out that there is at least one more case where the same code is a problem, i.e. what I mention in the commit message. To explain it further, in specific circumstances wined3d_context_gl_wait_command_fence() attempts to acquire a different context from the one that's current at that time and, if that happens after wined3d_device_uninit_3d() has completed, that will end up accessing a NULL device->swapchain[0].
Thinking about it a bit more, that sounds slightly suspicious, actually. In particular, wined3d_device_uninit_3d() is supposed to destroy all contexts, so why are we running context_gl_cleanup() after wined3d_device_uninit_3d() has completed? You also seem to imply we somehow have a command fence submitted on a different context than the one it was allocated from?
You're right, that explanation doesn't seem sound. I guess I'll retract the patch until I find out.