You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Windows ConPTY backend data race in ptyHandles can crash in remove_pty_baton #921
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;
staticboolremove_pty_baton(int id) {
auto it = std::remove_if(ptyHandles.begin(), ptyHandles.end(), [id](constauto& ptyHandle) {
return ptyHandle->id == id;
});
if (it != ptyHandles.end()) {
ptyHandles.erase(it);
returntrue;
}
returnfalse;
}
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.
Should we lock the shared list and/or switch to something like: std::shared_ptr<pty_baton> possibly?
This can be addressed with an exclusive access to the global, using shared_ptr will just make the lifetime harder to reason for no advantage in this case.
There was also concern where asserts were showing up in release builds and I think its from:
Thats something the addon inherits from Node.js build configuration
where it looks like you're passing -O2 to request optimizations, but there's no -DNDEBUG to control <assert.h>'s behavior.
Separately, I don't really know the format of this weird file, but it looks like it might be sending -fstack-protector-strong to MSVC, which will be ignored:
but there's no -DNDEBUG to control <assert.h>'s behavior.
What I mean is when building the addon a hidden details is that, we inherit a base configuration from Node.js/Electron https://github.com/nodejs/node/blob/bf7e79c264eef9bd0c743588aa6a158a26d90103/common.gypi#L180-L275 the binary into which the addon gets loaded which sets the minimum configuration/expectation (/MT or /MD for CRT) etc. What we have in the addon gyp are additions that were only required because of binskim policies. Ideally an addon doesn't need any special release configuration tweaks. We can define NDEBUG in the addon gyp, but I don't do it since the binary itself doesn't build with it and to avoid possible ODR.
Separately, I don't really know the format of this weird file, but it looks like it might be sending -fstack-protector-strong to MSVC, which will be ignored:
Activity
anthonykim1 commentedon May 12, 2026
Robo (@deepak1556) Should we lock the shared list and/or switch to something like:
std::shared_ptr<pty_baton>possibly?There was also concern where asserts were showing up in release builds and I think its from:
node-pty/src/win/conpty.cc
Line 106 in 21b81ac
deepak1556 commentedon May 13, 2026
This can be addressed with an exclusive access to the global, using shared_ptr will just make the lifetime harder to reason for no advantage in this case.
Thats something the addon inherits from Node.js build configuration
deepak1556 commentedon May 13, 2026
Thanks for capturing this issue Stephen Toub (@stephentoub)
StephanTLavavej commentedon May 13, 2026
Anthony Kim (@anthonykim1)
Robo (@deepak1556)
Are you sure it's not controlled by this repo's build configuration? I saw
node-pty/binding.gyp
Lines 7 to 32 in fa83ecf
where it looks like you're passing
-O2to request optimizations, but there's no-DNDEBUGto control<assert.h>'s behavior.Separately, I don't really know the format of this weird file, but it looks like it might be sending
-fstack-protector-strongto MSVC, which will be ignored:deepak1556 commentedon May 14, 2026
What I mean is when building the addon a hidden details is that, we inherit a base configuration from Node.js/Electron https://github.com/nodejs/node/blob/bf7e79c264eef9bd0c743588aa6a158a26d90103/common.gypi#L180-L275 the binary into which the addon gets loaded which sets the minimum configuration/expectation (/MT or /MD for CRT) etc. What we have in the addon gyp are additions that were only required because of binskim policies. Ideally an addon doesn't need any special release configuration tweaks. We can define
NDEBUGin the addon gyp, but I don't do it since the binary itself doesn't build with it and to avoid possible ODR.we don't, the condition block if-else works like this, refs https://chromium.googlesource.com/external/gyp/+show/md-pages/docs/UserDocumentation.md#564
Upgrade node-pty to 1.2.0-beta.15 for the ConPTY handle race fix
fix: pin node-pty 1.2.0-beta.15 to stop a Windows access violation
ci: build and test prebuilds on Node 24.22.1, as upstream CI does
Refresh 0.15.0 onto upstream node-pty 3d2987d and fix the prebuild ma…