The Windows ConPTY backend appears to have a data race around the global ptyHandles vector in src/win/conpty.cc.
ptyHandles is a process-global std::vector<std::unique_ptr<pty_baton>>. It is accessed from multiple threads without synchronization:
- the JS/main thread inserts into it in
PtyStartProcess
- the JS/main thread reads from it via
get_pty_baton in connect, resize, clear, and kill
- each PTY process gets a native watcher thread from
SetupExitCallback
- each watcher thread calls
remove_pty_baton(baton->id) when its child process exits
The problematic code is:
static std::vector<std::unique_ptr<pty_baton>> ptyHandles;
static bool remove_pty_baton(int id) {
auto it = std::remove_if(ptyHandles.begin(), ptyHandles.end(), [id](const auto& ptyHandle) {
return ptyHandle->id == id;
});
if (it != ptyHandles.end()) {
ptyHandles.erase(it);
return true;
}
return false;
}
Because multiple SetupExitCallback watcher threads can call remove_pty_baton concurrently, two std::remove_if calls can race while moving/erasing unique_ptr elements in the same vector. One thread can observe an element that has been moved from by another thread and then dereference a null unique_ptr in the predicate (ptyHandle->id). There are also races between get_pty_baton and removal/erase.
A crash dump from a release build showed two native threads simultaneously inside remove_pty_baton, both from SetupExitCallback watcher lambdas. The faulting thread crashed at the predicate dereference with rax == 0:
conpty!remove_pty_baton::<lambda>::operator()+0x3
cmp dword ptr [rax], ebx
Exception code: 0xc0000005
Attempt to read from address 0000000000000000
This is consistent with ptyHandle being a moved-from/null std::unique_ptr during concurrent std::remove_if.
The Windows ConPTY backend appears to have a data race around the global
ptyHandlesvector insrc/win/conpty.cc.ptyHandlesis a process-globalstd::vector<std::unique_ptr<pty_baton>>. It is accessed from multiple threads without synchronization:PtyStartProcessget_pty_batoninconnect,resize,clear, andkillSetupExitCallbackremove_pty_baton(baton->id)when its child process exitsThe problematic code is:
Because multiple
SetupExitCallbackwatcher threads can callremove_pty_batonconcurrently, twostd::remove_ifcalls can race while moving/erasingunique_ptrelements in the same vector. One thread can observe an element that has been moved from by another thread and then dereference a nullunique_ptrin the predicate (ptyHandle->id). There are also races betweenget_pty_batonand removal/erase.A crash dump from a release build showed two native threads simultaneously inside
remove_pty_baton, both fromSetupExitCallbackwatcher lambdas. The faulting thread crashed at the predicate dereference withrax == 0:This is consistent with
ptyHandlebeing a moved-from/nullstd::unique_ptrduring concurrentstd::remove_if.