#44

server: hold a reference to an async while cancel_process_async cancels it

OpenSirDankwants to merge intofrom

On iOS the wineserver is a thread of the app, so a server crash ends the app. A console program doing async I/O through wininet ran for two minutes and then the app died with SIGSEGV:

EXC_BAD_ACCESS, KERN_INVALID_ADDRESS at 0x0000000000000008
req_cancel_async <- call_req_handler <- thread_poll_event <- main_loop

Disassembling the faulting address in the shipped binary gives:

ldp x9, x8, [x20]       ; x9 = entry->next, x8 = entry->prev
str x8, [x9, #0x8]      ; next->prev = prev    <-- x9 == 0

which is list_remove( &async->process_entry ) on an entry whose next is NULL. A live list entry never has a NULL next; a freed one does.

cancel_process_async() walks process->asyncs and, for each match, calls cancel_async( async ) and then list_remove( &async->process_entry ). cancel_async() can terminate the async, a terminated async can drop its last reference and be destroyed, and async_destroy() does its own list_remove( &async->process_entry ) before the object is freed -- so the removal after the call is the second one, on freed memory. The loop's comment already allows for other asyncs being removed through async_reselect(), which is why it restarts the walk; it does not account for the async it is holding being freed by its own cancel.

The usual Wine idiom: take a reference before the call that can destroy the object, and drop it in the loop that puts the tracked asyncs back, once nothing will touch it again. If this reading were wrong the cost would be one async kept alive a little longer per cancel; what it replaces is a crash.

Nothing here is iOS-specific, but only a server that shares the process's lifetime turns it from a server restart into an app crash.

🤖 Generated with Claude Code