镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Windows ConPTY backend data race in ptyHandles can crash in remove_pty_baton #921

Closed

Description

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.

Activity

  1. anthonykim1 commented on May 12, 2026

    @anthonykim1
    Contributor

    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:

    assert(remove_pty_baton(baton->id));

  2. added
    bugIssue identified by VS Code Team member as probable bug
    on May 13, 2026
  3. added this to the 1.121.0 milestone on May 13, 2026
  4. deepak1556 commented on May 13, 2026

    @deepak1556
    Contributor

    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

  5. deepak1556 commented on May 13, 2026

    @deepak1556
    Contributor

    Thanks for capturing this issue Stephen Toub (@stephentoub)

  6. StephanTLavavej commented on May 13, 2026

    @StephanTLavavej
    Member

    Anthony Kim (@anthonykim1)

    There was also concern where asserts were showing up in release builds

    Robo (@deepak1556)

    Thats something the addon inherits from Node.js build configuration

    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

    ['OS=="win"', {
    'msvs_configuration_attributes': {
    'SpectreMitigation': 'Spectre'
    },
    'msvs_settings': {
    'VCCLCompilerTool': {
    'AdditionalOptions': [
    '/guard:cf',
    '/sdl',
    '/W3',
    '/we4146',
    '/we4244',
    '/we4267',
    '/ZH:SHA_256'
    ]
    },
    'VCLinkerTool': {
    'AdditionalOptions': [
    '/DYNAMICBASE',
    '/guard:cf'
    ]
    }
    },
    }, {
    'cflags': ['-O2', '-fstack-protector-strong'],
    }],

    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:

    C:\Temp>cl /EHsc /nologo /W4 -O2 -fstack-protector-strong meow.cpp
    cl : Command line warning D9002 : ignoring unknown option '-fstack-protector-strong'
    meow.cpp
    
    C:\Temp>
    
  7. deepak1556 commented on May 14, 2026

    @deepak1556
    Contributor

    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:

    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

    'conditions': [
          [<condition>, {
              true
          }, {
             false
         }]
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugIssue identified by VS Code Team member as probable bugverifiedVerification succeeded

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions