-
Notifications
You must be signed in to change notification settings - Fork 8.4k
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
Fix deadlocks due to holding locks across WriteFile calls #16224
Fix deadlocks due to holding locks across WriteFile calls #16224
Conversation
@@ -10,11 +10,11 @@ | |||
|
|||
#include <DefaultSettings.h> | |||
#include <unicode.hpp> | |||
#include <utils.hpp> |
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.
The issue was reported here: #9232 (comment)
This PR changes a lot of whitespace indentation, so use this for review: https://github.com/microsoft/terminal/pull/16224/files?w=1
const auto lock = _terminal->LockForWriting(); | ||
if (!vkey) | ||
{ | ||
return true; |
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.
why not false?
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.
The previous code did it:
return vkey ? _terminal->SendKeyEvent(vkey,
scanCode,
modifiers,
keyDown) :
true;
…inal-race-conditions-fixup
59cb7b6
to
454e36d
Compare
@DHowett @zadjii-msft FYI this also needs backporting to 1.19. It can cause WT to deadlock under certain circumstances. |
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.
conceptually I get it. Haven't read it all but looks fine
_sendInputToConnection(*out); | ||
return true; | ||
} | ||
return false; |
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.
🔖
} | ||
|
||
// It's important to not hold the terminal lock while calling this function as sending the data may take a long time. | ||
_sendInputToConnection(filtered); |
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.
The above block is 1:1 what used to be the Terminal::WritePastedText
but now it's outside the lock. It's inlined because Terminal
otherwise asserts that it's locked when BracketedPasteEnabled
gets called.
return _terminal->SendCharEvent(ch, scanCode, modifiers); | ||
TerminalInput::OutputType out; | ||
{ | ||
const auto lock = _terminal->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.
Reading? SendCharEvent
manipulates state.
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.
Wait, SendKeyEvent manipulates state and SendCharEvent only reads it?
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.
They both manipulate state and I should've used LockForWriting
here. I think this is fine though, as this is almost assuredly not the only place where this is incorrect. I've been considering to just replace it with Lock()
or to finally wrap Terminal
entirely into a til::mutex<Terminal>
kind of struct (but with a fair mutex).
Should I do that?
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 wouldn't necessarily do either of those for the backport version of this PR. I can see the ways in which the til::mutex
-like wrapper would be great, though... what do you feel is the right answer?
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 think the wrapped mutex class has significant safety benefits, because you cannot forget to lock something. But it wouldn't have prevented the bug this PR addresses. I'm fairly confident that I've fixed all cases where we forgot to lock Terminal
right now so I don't think it would have any additional benefit for a backport either.
…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 ✅
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
As reported here: #16224 (comment) The underlying `WriteFile` call may block indefinitely and we shouldn't hold the terminal lock during that period.
This fixes a number of bugs introduced in 4370da9, all of which are of
the same kind: Holding the terminal lock across
WriteFile
calls intothe 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 ConPTYoutput 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 isthread-safe and code that isn't. Functions like
SendKeyEvent
so farhave 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 callbackwhich then indirectly called into the
ConptyConnection
which callsWriteFile
. Instead, we now return the data that needs to be sent fromthese functions, and
ControlCore
is free to release the lock andthen call into the connection, which may then block indefinitely.
Validation Steps Performed
i
to enter the regular Insert mode