Skip to content

Commit 4bbfa05

Browse files
lheckerDHowett
authored andcommitted
Fix deadlocks due to holding locks across WriteFile calls (#16224)
This fixes a number of bugs introduced in 4370da9, all of which are of the same kind: Holding the terminal lock across `WriteFile` calls into the ConPTY pipe. This is problematic, because the pipe has a tiny buffer size of just 4KiB and ConPTY may respond on its output pipe, before the entire buffer given to `WriteFile` has been emptied. When the ConPTY output thread then tries to acquire the terminal lock to begin parsing the VT output, we get ourselves a proper deadlock (cross process too!). The solution is to tease `Terminal` further apart into code that is thread-safe and code that isn't. Functions like `SendKeyEvent` so far have mixed them into one, because when they get called by `ControlCore` they both, processed the data (not thread-safe as it accesses VT state) and also sent that data back into `ControlCore` through a callback which then indirectly called into the `ConptyConnection` which calls `WriteFile`. Instead, we now return the data that needs to be sent from these functions, and `ControlCore` is free to release the lock and then call into the connection, which may then block indefinitely. ## Validation Steps Performed * Start nvim in WSL * Press `i` to enter the regular Insert mode * Paste 1MB of text * Doesn't deadlock ✅ (cherry picked from commit 71a1a97) Service-Card-Id: 91043521 Service-Version: 1.19
1 parent 2ac5e86 commit 4bbfa05

File tree

12 files changed

+270
-205
lines changed

12 files changed

+270
-205
lines changed

src/cascadia/TerminalControl/ControlCore.cpp

Lines changed: 111 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010

1111
#include <DefaultSettings.h>
1212
#include <unicode.hpp>
13+
#include <utils.hpp>
1314
#include <WinUser.h>
1415
#include <LibraryResources.h>
1516

1617
#include "EventArgs.h"
17-
#include "../../types/inc/GlyphWidth.hpp"
1818
#include "../../buffer/out/search.h"
1919
#include "../../renderer/atlas/AtlasEngine.h"
2020
#include "../../renderer/dx/DxRenderer.hpp"
@@ -443,6 +443,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation
443443
// - <none>
444444
void ControlCore::_sendInputToConnection(std::wstring_view wstr)
445445
{
446+
if (wstr.empty())
447+
{
448+
return;
449+
}
450+
451+
// The connection may call functions like WriteFile() which may block indefinitely.
452+
// It's important we don't hold any mutexes across such calls.
453+
_terminal->_assertUnlocked();
454+
446455
if (_isReadOnly)
447456
{
448457
_raiseReadOnlyWarning();
@@ -492,8 +501,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation
492501
_handleControlC();
493502
}
494503

495-
const auto lock = _terminal->LockForWriting();
496-
return _terminal->SendCharEvent(ch, scanCode, modifiers);
504+
TerminalInput::OutputType out;
505+
{
506+
const auto lock = _terminal->LockForReading();
507+
out = _terminal->SendCharEvent(ch, scanCode, modifiers);
508+
}
509+
if (out)
510+
{
511+
_sendInputToConnection(*out);
512+
return true;
513+
}
514+
return false;
497515
}
498516

499517
void ControlCore::_handleControlC()
@@ -602,46 +620,56 @@ namespace winrt::Microsoft::Terminal::Control::implementation
602620
const ControlKeyStates modifiers,
603621
const bool keyDown)
604622
{
605-
const auto lock = _terminal->LockForWriting();
623+
if (!vkey)
624+
{
625+
return true;
626+
}
606627

607-
// Update the selection, if it's present
608-
// GH#8522, GH#3758 - Only modify the selection on key _down_. If we
609-
// modify on key up, then there's chance that we'll immediately dismiss
610-
// a selection created by an action bound to a keydown.
611-
if (_shouldTryUpdateSelection(vkey) && keyDown)
628+
TerminalInput::OutputType out;
612629
{
613-
// try to update the selection
614-
if (const auto updateSlnParams{ _terminal->ConvertKeyEventToUpdateSelectionParams(modifiers, vkey) })
615-
{
616-
_terminal->UpdateSelection(updateSlnParams->first, updateSlnParams->second, modifiers);
617-
_updateSelectionUI();
618-
return true;
619-
}
630+
const auto lock = _terminal->LockForWriting();
620631

621-
// GH#8791 - don't dismiss selection if Windows key was also pressed as a key-combination.
622-
if (!modifiers.IsWinPressed())
632+
// Update the selection, if it's present
633+
// GH#8522, GH#3758 - Only modify the selection on key _down_. If we
634+
// modify on key up, then there's chance that we'll immediately dismiss
635+
// a selection created by an action bound to a keydown.
636+
if (_shouldTryUpdateSelection(vkey) && keyDown)
623637
{
624-
_terminal->ClearSelection();
625-
_updateSelectionUI();
626-
}
638+
// try to update the selection
639+
if (const auto updateSlnParams{ _terminal->ConvertKeyEventToUpdateSelectionParams(modifiers, vkey) })
640+
{
641+
_terminal->UpdateSelection(updateSlnParams->first, updateSlnParams->second, modifiers);
642+
_updateSelectionUI();
643+
return true;
644+
}
627645

628-
// When there is a selection active, escape should clear it and NOT flow through
629-
// to the terminal. With any other keypress, it should clear the selection AND
630-
// flow through to the terminal.
631-
if (vkey == VK_ESCAPE)
632-
{
633-
return true;
646+
// GH#8791 - don't dismiss selection if Windows key was also pressed as a key-combination.
647+
if (!modifiers.IsWinPressed())
648+
{
649+
_terminal->ClearSelection();
650+
_updateSelectionUI();
651+
}
652+
653+
// When there is a selection active, escape should clear it and NOT flow through
654+
// to the terminal. With any other keypress, it should clear the selection AND
655+
// flow through to the terminal.
656+
if (vkey == VK_ESCAPE)
657+
{
658+
return true;
659+
}
634660
}
635-
}
636661

637-
// If the terminal translated the key, mark the event as handled.
638-
// This will prevent the system from trying to get the character out
639-
// of it and sending us a CharacterReceived event.
640-
return vkey ? _terminal->SendKeyEvent(vkey,
641-
scanCode,
642-
modifiers,
643-
keyDown) :
644-
true;
662+
// If the terminal translated the key, mark the event as handled.
663+
// This will prevent the system from trying to get the character out
664+
// of it and sending us a CharacterReceived event.
665+
out = _terminal->SendKeyEvent(vkey, scanCode, modifiers, keyDown);
666+
}
667+
if (out)
668+
{
669+
_sendInputToConnection(*out);
670+
return true;
671+
}
672+
return false;
645673
}
646674

647675
bool ControlCore::SendMouseEvent(const til::point viewportPos,
@@ -650,8 +678,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation
650678
const short wheelDelta,
651679
const TerminalInput::MouseButtonState state)
652680
{
653-
const auto lock = _terminal->LockForWriting();
654-
return _terminal->SendMouseEvent(viewportPos, uiButton, states, wheelDelta, state);
681+
TerminalInput::OutputType out;
682+
{
683+
const auto lock = _terminal->LockForReading();
684+
out = _terminal->SendMouseEvent(viewportPos, uiButton, states, wheelDelta, state);
685+
}
686+
if (out)
687+
{
688+
_sendInputToConnection(*out);
689+
return true;
690+
}
691+
return false;
655692
}
656693

657694
void ControlCore::UserScrollViewport(const int viewTop)
@@ -1324,8 +1361,19 @@ namespace winrt::Microsoft::Terminal::Control::implementation
13241361
// before sending it over the terminal's connection.
13251362
void ControlCore::PasteText(const winrt::hstring& hstr)
13261363
{
1364+
using namespace ::Microsoft::Console::Utils;
1365+
1366+
auto filtered = FilterStringForPaste(hstr, CarriageReturnNewline | ControlCodes);
1367+
if (BracketedPasteEnabled())
1368+
{
1369+
filtered.insert(0, L"\x1b[200~");
1370+
filtered.append(L"\x1b[201~");
1371+
}
1372+
1373+
// It's important to not hold the terminal lock while calling this function as sending the data may take a long time.
1374+
_sendInputToConnection(filtered);
1375+
13271376
const auto lock = _terminal->LockForWriting();
1328-
_terminal->WritePastedText(hstr);
13291377
_terminal->ClearSelection();
13301378
_updateSelectionUI();
13311379
_terminal->TrySnapOnInput();
@@ -1894,17 +1942,27 @@ namespace winrt::Microsoft::Terminal::Control::implementation
18941942
const auto endPoint = goRight ? clampedClick : cursorPos;
18951943

18961944
const auto delta = _terminal->GetTextBuffer().GetCellDistance(startPoint, endPoint);
1897-
18981945
const WORD key = goRight ? VK_RIGHT : VK_LEFT;
1946+
1947+
std::wstring buffer;
1948+
const auto append = [&](TerminalInput::OutputType&& out) {
1949+
if (out)
1950+
{
1951+
buffer.append(std::move(*out));
1952+
}
1953+
};
1954+
18991955
// Send an up and a down once per cell. This won't
19001956
// accurately handle wide characters, or continuation
19011957
// prompts, or cases where a single escape character in the
19021958
// command (e.g. ^[) takes up two cells.
19031959
for (size_t i = 0u; i < delta; i++)
19041960
{
1905-
_terminal->SendKeyEvent(key, 0, {}, true);
1906-
_terminal->SendKeyEvent(key, 0, {}, false);
1961+
append(_terminal->SendKeyEvent(key, 0, {}, true));
1962+
append(_terminal->SendKeyEvent(key, 0, {}, false));
19071963
}
1964+
1965+
_sendInputToConnection(buffer);
19081966
}
19091967
}
19101968
}
@@ -1915,7 +1973,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
19151973
// - Updates the renderer's representation of the selection as well as the selection marker overlay in TermControl
19161974
void ControlCore::_updateSelectionUI()
19171975
{
1918-
const auto lock = _terminal->LockForWriting();
19191976
_renderer->TriggerSelection();
19201977
// only show the markers if we're doing a keyboard selection or in mark mode
19211978
const bool showMarkers{ _terminal->SelectionMode() >= ::Microsoft::Terminal::Core::Terminal::SelectionInteractionMode::Keyboard };
@@ -2247,14 +2304,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation
22472304

22482305
void ControlCore::_focusChanged(bool focused)
22492306
{
2250-
// GH#13461 - temporarily turn off read-only mode, send the focus event,
2251-
// then turn it back on. Even in focus mode, focus events are fine to
2252-
// send. We don't want to pop a warning every time the control is
2253-
// focused.
2254-
const auto previous = std::exchange(_isReadOnly, false);
2255-
const auto restore = wil::scope_exit([&]() { _isReadOnly = previous; });
2256-
const auto lock = _terminal->LockForWriting();
2257-
_terminal->FocusChanged(focused);
2307+
TerminalInput::OutputType out;
2308+
{
2309+
const auto lock = _terminal->LockForReading();
2310+
out = _terminal->FocusChanged(focused);
2311+
}
2312+
if (out && !out->empty())
2313+
{
2314+
// _sendInputToConnection() asserts that we aren't in focus mode,
2315+
// but window focus events are always fine to send.
2316+
_connection.WriteInput(*out);
2317+
}
22582318
}
22592319

22602320
bool ControlCore::_isBackgroundTransparent()

src/cascadia/TerminalControl/HwndTerminal.cpp

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ void HwndTerminal::RegisterScrollCallback(std::function<void(int, int, int)> cal
272272

273273
void HwndTerminal::_WriteTextToConnection(const std::wstring_view input) noexcept
274274
{
275-
if (!_pfnWriteCallback)
275+
if (input.empty() || !_pfnWriteCallback)
276276
{
277277
return;
278278
}
@@ -758,8 +758,17 @@ try
758758
WI_IsFlagSet(GetKeyState(VK_RBUTTON), KeyPressed)
759759
};
760760

761-
const auto lock = _terminal->LockForWriting();
762-
return _terminal->SendMouseEvent(cursorPosition / fontSize, uMsg, getControlKeyState(), wheelDelta, state);
761+
TerminalInput::OutputType out;
762+
{
763+
const auto lock = _terminal->LockForReading();
764+
out = _terminal->SendMouseEvent(cursorPosition / fontSize, uMsg, getControlKeyState(), wheelDelta, state);
765+
}
766+
if (out)
767+
{
768+
_WriteTextToConnection(*out);
769+
return true;
770+
}
771+
return false;
763772
}
764773
catch (...)
765774
{
@@ -784,8 +793,16 @@ try
784793
{
785794
_uiaProvider->RecordKeyEvent(vkey);
786795
}
787-
const auto lock = _terminal->LockForWriting();
788-
_terminal->SendKeyEvent(vkey, scanCode, modifiers, keyDown);
796+
797+
TerminalInput::OutputType out;
798+
{
799+
const auto lock = _terminal->LockForReading();
800+
out = _terminal->SendKeyEvent(vkey, scanCode, modifiers, keyDown);
801+
}
802+
if (out)
803+
{
804+
_WriteTextToConnection(*out);
805+
}
789806
}
790807
CATCH_LOG();
791808

@@ -797,31 +814,39 @@ try
797814
return;
798815
}
799816

800-
const auto lock = _terminal->LockForWriting();
801-
802-
if (_terminal->IsSelectionActive())
817+
TerminalInput::OutputType out;
803818
{
804-
_ClearSelection();
805-
if (ch == UNICODE_ESC)
819+
const auto lock = _terminal->LockForWriting();
820+
821+
if (_terminal->IsSelectionActive())
806822
{
807-
// ESC should clear any selection before it triggers input.
808-
// Other characters pass through.
823+
_ClearSelection();
824+
if (ch == UNICODE_ESC)
825+
{
826+
// ESC should clear any selection before it triggers input.
827+
// Other characters pass through.
828+
return;
829+
}
830+
}
831+
832+
if (ch == UNICODE_TAB)
833+
{
834+
// TAB was handled as a keydown event (cf. Terminal::SendKeyEvent)
809835
return;
810836
}
811-
}
812837

813-
if (ch == UNICODE_TAB)
814-
{
815-
// TAB was handled as a keydown event (cf. Terminal::SendKeyEvent)
816-
return;
817-
}
838+
auto modifiers = getControlKeyState();
839+
if (WI_IsFlagSet(flags, ENHANCED_KEY))
840+
{
841+
modifiers |= ControlKeyStates::EnhancedKey;
842+
}
818843

819-
auto modifiers = getControlKeyState();
820-
if (WI_IsFlagSet(flags, ENHANCED_KEY))
844+
out = _terminal->SendCharEvent(ch, scanCode, modifiers);
845+
}
846+
if (out)
821847
{
822-
modifiers |= ControlKeyStates::EnhancedKey;
848+
_WriteTextToConnection(*out);
823849
}
824-
_terminal->SendCharEvent(ch, scanCode, modifiers);
825850
}
826851
CATCH_LOG();
827852

src/cascadia/TerminalCore/ITerminalInput.hpp

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,17 @@ namespace Microsoft::Terminal::Core
1616
ITerminalInput& operator=(const ITerminalInput&) = default;
1717
ITerminalInput& operator=(ITerminalInput&&) = default;
1818

19-
virtual bool SendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates states, const bool keyDown) = 0;
20-
virtual bool SendMouseEvent(const til::point viewportPos, const unsigned int uiButton, const ControlKeyStates states, const short wheelDelta, const Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState state) = 0;
21-
virtual bool SendCharEvent(const wchar_t ch, const WORD scanCode, const ControlKeyStates states) = 0;
19+
virtual [[nodiscard]] ::Microsoft::Console::VirtualTerminal::TerminalInput::OutputType SendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates states, const bool keyDown) = 0;
20+
virtual [[nodiscard]] ::Microsoft::Console::VirtualTerminal::TerminalInput::OutputType SendMouseEvent(const til::point viewportPos, const unsigned int uiButton, const ControlKeyStates states, const short wheelDelta, const Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState state) = 0;
21+
virtual [[nodiscard]] ::Microsoft::Console::VirtualTerminal::TerminalInput::OutputType SendCharEvent(const wchar_t ch, const WORD scanCode, const ControlKeyStates states) = 0;
22+
virtual [[nodiscard]] ::Microsoft::Console::VirtualTerminal::TerminalInput::OutputType FocusChanged(const bool focused) = 0;
2223

2324
[[nodiscard]] virtual HRESULT UserResize(const til::size size) noexcept = 0;
2425
virtual void UserScrollViewport(const int viewTop) = 0;
2526
virtual int GetScrollOffset() = 0;
2627

2728
virtual void TrySnapOnInput() = 0;
2829

29-
virtual void FocusChanged(const bool focused) = 0;
30-
3130
protected:
3231
ITerminalInput() = default;
3332
};

0 commit comments

Comments
 (0)