Skip to content

Commit fa83ecf

Browse files
authored
fix(win): use-after-free from unsynchronized access to ptyhandles (#922)
The windows backend keeps a global vector of `ptyHandles`. Since the cleanup of the baton was moved into the per-pty watcher thread (commit 17062cd): - The watcher thread called `remove_pty_baton` without any lock, racing with other watchers and with JS-thread reads. - `emplace_back` in `startProcess` could reallocate the vector while another thread was iterating it.
1 parent 21b81ac commit fa83ecf

2 files changed

Lines changed: 87 additions & 28 deletions

File tree

src/win/conpty.cc

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#include <node_api.h>
1515
#include <assert.h>
1616
#include <Shlwapi.h> // PathCombine, PathIsRelative
17+
#include <atomic>
18+
#include <mutex>
1719
#include <sstream>
1820
#include <iostream>
1921
#include <string>
@@ -50,9 +52,12 @@ struct pty_baton {
5052
};
5153

5254
static std::vector<std::unique_ptr<pty_baton>> ptyHandles;
53-
static volatile LONG ptyCounter;
55+
static std::mutex g_ptyHandlesMutex;
56+
static std::atomic<int> ptyCounter{0};
5457

55-
static pty_baton* get_pty_baton(int id) {
58+
// The leading scoped-lock parameter encodes the precondition that the caller
59+
// holds g_ptyHandlesMutex.
60+
static pty_baton* get_pty_baton(const std::lock_guard<std::mutex>&, int id) {
5661
auto it = std::find_if(ptyHandles.begin(), ptyHandles.end(), [id](const auto& ptyHandle) {
5762
return ptyHandle->id == id;
5863
});
@@ -62,17 +67,6 @@ static pty_baton* get_pty_baton(int id) {
6267
return nullptr;
6368
}
6469

65-
static bool remove_pty_baton(int id) {
66-
auto it = std::remove_if(ptyHandles.begin(), ptyHandles.end(), [id](const auto& ptyHandle) {
67-
return ptyHandle->id == id;
68-
});
69-
if (it != ptyHandles.end()) {
70-
ptyHandles.erase(it);
71-
return true;
72-
}
73-
return false;
74-
}
75-
7670
struct ExitEvent {
7771
int exit_code = 0;
7872
};
@@ -99,11 +93,15 @@ void SetupExitCallback(Napi::Env env, Napi::Function cb, pty_baton* baton) {
9993
ExitEvent *exit_event = new ExitEvent;
10094
// Wait for process to complete.
10195
WaitForSingleObject(baton->hShell, INFINITE);
102-
// Get process exit code.
103-
GetExitCodeProcess(baton->hShell, (LPDWORD)(&exit_event->exit_code));
104-
// Clean up handles
105-
CloseHandle(baton->hShell);
106-
assert(remove_pty_baton(baton->id));
96+
{
97+
std::lock_guard<std::mutex> lock(g_ptyHandlesMutex);
98+
GetExitCodeProcess(baton->hShell, (LPDWORD)(&exit_event->exit_code));
99+
CloseHandle(baton->hShell);
100+
const int id = baton->id;
101+
std::erase_if(ptyHandles, [id](const auto& ptyHandle) {
102+
return ptyHandle->id == id;
103+
});
104+
}
107105

108106
auto status = tsfn.BlockingCall(exit_event, callback); // In main thread
109107
switch (status) {
@@ -298,10 +296,13 @@ static Napi::Value PtyStartProcess(const Napi::CallbackInfo& info) {
298296

299297
if (SUCCEEDED(hr)) {
300298
// We were able to instantiate a conpty
301-
const int ptyId = InterlockedIncrement(&ptyCounter);
299+
const int ptyId = ++ptyCounter;
302300
marshal.Set("pty", Napi::Number::New(env, ptyId));
303-
ptyHandles.emplace_back(
304-
std::make_unique<pty_baton>(ptyId, hIn, hOut, hpc));
301+
{
302+
std::lock_guard<std::mutex> lock(g_ptyHandlesMutex);
303+
ptyHandles.emplace_back(
304+
std::make_unique<pty_baton>(ptyId, hIn, hOut, hpc));
305+
}
305306
} else {
306307
throw Napi::Error::New(env, "Cannot launch conpty");
307308
}
@@ -349,10 +350,13 @@ static Napi::Value PtyConnect(const Napi::CallbackInfo& info) {
349350
const bool useConptyDll = info[4].As<Napi::Boolean>().Value();
350351
Napi::Function exitCallback = info[5].As<Napi::Function>();
351352

352-
// Fetch pty handle from ID and start process
353-
pty_baton* handle = get_pty_baton(id);
354-
if (!handle) {
355-
throw Napi::Error::New(env, "Invalid pty handle");
353+
pty_baton* handle;
354+
{
355+
std::lock_guard<std::mutex> lock(g_ptyHandlesMutex);
356+
handle = get_pty_baton(lock, id);
357+
if (!handle) {
358+
throw Napi::Error::New(env, "Invalid pty handle");
359+
}
356360
}
357361

358362
// Prepare command line
@@ -471,7 +475,8 @@ static Napi::Value PtyResize(const Napi::CallbackInfo& info) {
471475
SHORT rows = static_cast<SHORT>(info[2].As<Napi::Number>().Uint32Value());
472476
const bool useConptyDll = info[3].As<Napi::Boolean>().Value();
473477

474-
const pty_baton* handle = get_pty_baton(id);
478+
std::lock_guard<std::mutex> lock(g_ptyHandlesMutex);
479+
const pty_baton* handle = get_pty_baton(lock, id);
475480

476481
if (handle != nullptr) {
477482
HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
@@ -512,7 +517,8 @@ static Napi::Value PtyClear(const Napi::CallbackInfo& info) {
512517
return env.Undefined();
513518
}
514519

515-
const pty_baton* handle = get_pty_baton(id);
520+
std::lock_guard<std::mutex> lock(g_ptyHandlesMutex);
521+
const pty_baton* handle = get_pty_baton(lock, id);
516522

517523
if (handle != nullptr) {
518524
HANDLE hLibrary = LoadConptyDll(info, useConptyDll);
@@ -543,7 +549,8 @@ static Napi::Value PtyKill(const Napi::CallbackInfo& info) {
543549
int id = info[0].As<Napi::Number>().Int32Value();
544550
const bool useConptyDll = info[1].As<Napi::Boolean>().Value();
545551

546-
const pty_baton* handle = get_pty_baton(id);
552+
std::lock_guard<std::mutex> lock(g_ptyHandlesMutex);
553+
const pty_baton* handle = get_pty_baton(lock, id);
547554

548555
if (handle != nullptr) {
549556
HANDLE hLibrary = LoadConptyDll(info, useConptyDll);

src/windowsTerminal.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,58 @@ if (process.platform === 'win32') {
268268
});
269269
});
270270
});
271+
272+
describe('Regression for #921', () => {
273+
it('should not crash with concurrent kills while resizing/clearing', function (done) {
274+
this.timeout(60000);
275+
const N = 30;
276+
const terms: WindowsTerminal[] = [];
277+
let ready = 0;
278+
let exited = 0;
279+
let spamInterval: NodeJS.Timeout | undefined;
280+
const cleanup = (err?: Error): void => {
281+
if (spamInterval) {
282+
clearInterval(spamInterval);
283+
spamInterval = undefined;
284+
}
285+
done(err);
286+
};
287+
const startRace = (): void => {
288+
spamInterval = setInterval(() => {
289+
for (const t of terms) {
290+
try {
291+
t.resize(80 + Math.floor(Math.random() * 40), 24 + Math.floor(Math.random() * 20));
292+
} catch (e) { /* already exited */ }
293+
try {
294+
t.clear();
295+
} catch (e) { /* already exited */ }
296+
}
297+
}, 1);
298+
for (const t of terms) {
299+
try { t.kill(); } catch (e) { /* */ }
300+
}
301+
};
302+
for (let i = 0; i < N; i++) {
303+
const t = new WindowsTerminal('cmd.exe', [], { useConptyDll });
304+
terms.push(t);
305+
let readied = false;
306+
t.on('data', () => {
307+
if (readied) return;
308+
readied = true;
309+
ready++;
310+
if (ready === N) {
311+
startRace();
312+
}
313+
});
314+
t.on('exit', () => {
315+
exited++;
316+
if (exited === N) {
317+
cleanup();
318+
}
319+
});
320+
}
321+
});
322+
});
271323
});
272324
});
273325
}

0 commit comments

Comments
 (0)