https://bugs.winehq.org/show_bug.cgi?id=47843
--- Comment #2 from Brendan Shanks bshanks@codeweavers.com --- I spent some time looking into this issue, it's not related to bcrypt but is actually a race condition caused by some shaky assumptions in the launcher service and how Wine implements the NT thread pool.
There's multiple processes involved: Launcher.exe is the GUI app. RockstarService.exe is the background service, it actually does the downloading and file I/O. They communicate through a named pipe ("\.\pipe\MTLService_Pipe") created by the service. They also write out separate log files: ProgramData/Rockstar Games/Launcher/service_log.txt for the service, and users/steamuser/My\ Documents/Rockstar\ Games/Launcher/launcher.log for the launcher. The service also logs mostly the same output with OutputDebugString().
The problem here is with the service, and it looks like this in service_log.txt (note that there's lots of "Last error" lines printed to this log that really aren't errors and don't matter):
[2019-10-21 15:09:11.255] [ 68] [ FD] Wrote to C:\Program Files\Rockstar Games\Grand Theft Auto San Andreas\models\gta3.img [2019-10-21 15:09:11.256] [ 68] [ 6D] Failed to write to pipe. [2019-10-21 15:09:11.256] [ 68] [ 6D] Last error: 996 (0x3e4): Overlapped I/O incomplete.
[2019-10-21 15:09:11.256] [ 68] [ 6D] Disconnected. [2019-10-21 15:09:11.257] [ 68] [ 6D] Destroying pipe...
The pipe then gets re-created, the whole service is restarted, and this is when the download stops. "Failed to write to pipe" is the error.
The service uses overlapped I/O to write to files, and before WriteFile() is called it uses RegisterWaitForSingleObject() to register a callback to fire when the overlapped I/O event is signaled/finished. The thread calls WriteFile() for the file, it returns quickly, and then WriteFile() is used to write a small message to the pipe (also overlapped). The file write callback just calls GetOverlappedResult(), if it succeeds it does some housekeeping (log messages, unregister wait, close handles) and also writes a small message to the pipe.
The problem is, the same OVERLAPPED structure is used for all writing to the pipe, regardless of what thread is doing the writing. There's also a callback registered for this overlapped event, it calls GetOverlappedResult() to confirm success. Eventually the pipe write from the original thread and from the file write callback happen at almost the same time. The pipe write callback calls GetOverlappedResult() on the shared OVERLAPPED which has been set to pending by the WriteFile() call happening simultaneously in the other thread, GetOverlappedResult() returns FALSE, it logs the "Failed to write to pipe" message, and starts tearing everything down.
Next question: why doesn't this break on Windows? The service calls RegisterWaitForSingleObject() with the WT_EXECUTEINWAITTHREAD flag. An understanding of the Windows thread pool is needed: Windows has a number of "wait threads" which each wait on multiple objects. When an object is signaled, normally the wait thread queues the callback to be executed by another thread, and then goes back to waiting. But when the WT_EXECUTEINWAITTHREAD flag is used, the wait thread itself calls the callback. This has the effect of serializing all callbacks being waited on by that wait thread. In this case, the launcher doesn't have many waits, so all the waits are handled by a single wait thread, and all callbacks are serialized. This behavior is certainly not guaranteed by Windows though, and shouldn't be depended upon.
In contrast, Wine's implementation spawns a new thread for each wait, and then calls the callback from that thread. The callbacks aren't serialized, and end up racing and causing this error.
Making Wine's thread pool implementation match Windows would be a big task. For now I tried a hacky solution of having each thread enter a process-wide critical section before calling the callback when WT_EXECUTEINWAITTHREAD is supplied, and it seems to solve the issue (I can download GTA:SA with no errors). I'll work on getting this upstream (at least into Proton). Also I've attached a test app which reproduces the problem.