-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add missing lock guards on Terminal access #15894
Conversation
@@ -43,13 +43,13 @@ try | |||
return DefWindowProc(hwnd, WM_NCCREATE, wParam, lParam); | |||
} | |||
#pragma warning(suppress : 26490) // Win32 APIs can only store void*, have to use reinterpret_cast | |||
auto terminal = reinterpret_cast<HwndTerminal*>(GetWindowLongPtr(hwnd, GWLP_USERDATA)); | |||
const auto publicTerminal = reinterpret_cast<HwndTerminal*>(GetWindowLongPtr(hwnd, GWLP_USERDATA)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I renamed this variable so that it is named identical to all the other code in this file. This helped me review my changeset and to use Ctrl+F & replace.
const auto lock = publicTerminal->_terminal->LockForWriting(); | ||
const auto bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false); | ||
LOG_IF_FAILED(publicTerminal->_CopyTextToSystemClipboard(bufferData, true)); | ||
publicTerminal->_ClearSelection(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This adds the lock call and replaces TerminalClearSelection
with _ClearSelection
so that it isn't acquired again.
publicTerminal->RegisterScrollCallback(callback); | ||
} | ||
CATCH_LOG() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While making the necessary modifications, I noticed that all of the C ABI functions where missing try/catch clauses.
{ | ||
const auto publicTerminal = static_cast<const HwndTerminal*>(terminal); | ||
if (!publicTerminal || !publicTerminal->_terminal || (!publicTerminal->_terminal->IsCursorBlinkingAllowed() && publicTerminal->_terminal->IsCursorVisible())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BlinkCursor()
handles this now.
try | ||
{ | ||
std::wstring text(pData); | ||
_WriteTextToConnection(text); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_WriteTextToConnection
is already noexcept
. No need for the extra function.
_OpenHyperlinkHandlers(*this, winrt::make<OpenHyperlinkEventArgs>(winrt::hstring{ selectedText })); | ||
} | ||
return true; | ||
} | ||
else if (vkey == VK_RETURN && !mods.IsCtrlPressed() && !mods.IsAltPressed()) | ||
{ | ||
// [Shift +] Enter --> copy text | ||
// Don't lock here! CopySelectionToClipboard already locks for you! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...true, but not helpful here. UpdateSelection
modifies the selection, which is concurrently being used by the renderer!
// We're **NOT** taking the lock here unlike _scrollbarChangeHandler because | ||
// we are already under lock (since this usually happens as a result of writing). | ||
// TODO GH#9617: refine locking around pattern tree | ||
_terminal->ClearPatternTree(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This moved into Terminal
. This makes the code more robust (it's also a private function now).
if (!_terminal->IsCursorBlinkingAllowed() && | ||
_terminal->IsCursorVisible()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW this if condition is incorrect. If you flip the early return this old code says:
if (!(!_terminal->IsCursorBlinkingAllowed() && _terminal->IsCursorVisible()))
_terminal->SetCursorOn(!_terminal->IsCursorOn());
which is:
if (_terminal->IsCursorBlinkingAllowed() || !_terminal->IsCursorVisible())
_terminal->SetCursorOn(!_terminal->IsCursorOn());
In other words, this old code would blink the cursor even if it's invisible.
if (_selectionMode != SelectionInteractionMode::Mark) | ||
{ | ||
auto& cursor = _activeBuffer().GetCursor(); | ||
if (cursor.IsBlinkingAllowed() && cursor.IsVisible()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This now matches CursorBlinker.cpp
which doesn't check for cursor.IsPopupShown()
either (the old code here did).
_patternIntervalTree = {}; | ||
_InvalidatePatternTree(oldTree); | ||
_assertLocked(); | ||
if (!_patternIntervalTree.empty()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking if it's empty allows us to skip a potentially expensive operation.
accc78a
to
10ee98c
Compare
f7b287b
to
6a1c8fe
Compare
@@ -832,8 +839,6 @@ void Terminal::ClearSelection() | |||
// - wstring text from buffer. If extended to multiple lines, each line is separated by \r\n | |||
const TextBuffer::TextAndColor Terminal::RetrieveSelectedTextFromBuffer(bool singleLine) | |||
{ | |||
auto lock = LockForReading(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note to self: watch to see where the read lock in RetrieveSelectedText
goes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Into ControlCore
here: https://github.com/microsoft/terminal/blob/dev/lhecker/9617-terminal-race-conditions/src/cascadia/TerminalControl/ControlCore.cpp#L1168
And one more down below. This PR moves all locking outside of Terminal
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks so much for doing this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost feels like _terminal
in ControlCore should like, be behind a mutex at this point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost feels like _terminal
in ControlCore should like, be behind a mutex at this point.
Terminal
is used concurrently by at least 4 threads. The tablebelow lists the class members and the threads that access them
to the best of my knowledge. Where:
winrt::resume_background
)_pfnWriteInput
_pfnWarningBell
_pfnTitleChanged
_pfnCopyToClipboard
_pfnScrollPositionChanged
_pfnCursorPositionChanged
_pfnTaskbarProgressChanged
_pfnShowWindowChanged
_pfnPlayMidiNote
_pfnCompletionsChanged
_renderSettings
_stateMachine
_terminalInput
_title
_startingTitle
_startingTabColor
_defaultCursorShape
_systemMode
_snapOnInput
_altGrAliasing
_suppressApplicationTitle
_trimBlockSelection
_autoMarkPrompts
_taskbarState
_taskbarProgress
_workingDirectory
_fontInfo
_selection
_blockSelection
_wordDelimiters
_multiClickSelectionMode
_selectionMode
_selectionIsTargetingUrl
_selectionEndpoint
_anchorInactiveSelectionEndpoint
_mainBuffer
_altBuffer
_mutableViewport
_scrollbackLines
_detectURLs
_altBufferSize
_deferredResize
_scrollOffset
_patternIntervalTree
_lastKeyEventCodes
_currentPromptState
Only 7 members are specific to one thread and don't require locking.
All other members require some for of locking to be safe for use.
To address the issue this changeset adds
LockForReading/LockForWriting
calls everywhere
_terminal
is accessed inControlCore/HwndTerminal
.Additionally, to ensure these issues don't pop up anymore, it adds to
all
Terminal
functions a debug assertion that the lock is being held.Finally, because this changeset started off rather modest, it contains
changes that I initially made without being aware about the extent of
the issue. It simplifies the access around
_patternIntervalTree
bymaking
_InvalidatePatternTree()
directly use that member.Furthermore, it simplifies
_terminal->SetCursorOn(!IsCursorOn())
toBlinkCursor()
, allowing the code to be shared withHwndTerminal
.Ideally
Terminal
should not be that much of a class so that we don'tneed such coarse locking. Splitting out selection and rendering state
should allow deduplicating code with conhost and use finer locking.
Closes #9617
Validation Steps Performed
I tried to use as many Windows Terminal features as I could and fixed
every occurrence of
_assertLocked()
failures.