Skip to content
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

Merged
merged 3 commits into from
Nov 8, 2023

Conversation

lhecker
Copy link
Member

@lhecker lhecker commented Oct 24, 2023

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 ✅

@lhecker lhecker added Area-Input Related to input processing (key presses, mouse, etc.) Issue-Bug It either shouldn't be doing this or needs an investigation. Product-Terminal The new Windows Terminal. Priority-1 A description (P1) labels Oct 24, 2023
@@ -10,11 +10,11 @@

#include <DefaultSettings.h>
#include <unicode.hpp>
#include <utils.hpp>
Copy link
Member Author

@lhecker lhecker Oct 24, 2023

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not false?

Copy link
Member Author

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;

@lhecker lhecker force-pushed the dev/lhecker/9617-terminal-race-conditions-fixup branch from 59cb7b6 to 454e36d Compare November 1, 2023 22:17
@lhecker
Copy link
Member Author

lhecker commented Nov 7, 2023

@DHowett @zadjii-msft FYI this also needs backporting to 1.19. It can cause WT to deadlock under certain circumstances.

Copy link
Member

@zadjii-msft zadjii-msft left a 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;
Copy link
Member

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);
Copy link
Member Author

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.

@zadjii-msft zadjii-msft merged commit 71a1a97 into main Nov 8, 2023
17 checks passed
@zadjii-msft zadjii-msft deleted the dev/lhecker/9617-terminal-race-conditions-fixup branch November 8, 2023 16:28
return _terminal->SendCharEvent(ch, scanCode, modifiers);
TerminalInput::OutputType out;
{
const auto lock = _terminal->LockForReading();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading? SendCharEvent manipulates state.

Copy link
Member

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?

Copy link
Member Author

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?

Copy link
Member

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?

Copy link
Member Author

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.

src/cascadia/TerminalControl/ControlCore.cpp Show resolved Hide resolved
radu-cernatescu pushed a commit to radu-cernatescu/terminal that referenced this pull request Nov 8, 2023
…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 ✅
DHowett pushed a commit that referenced this pull request Nov 13, 2023
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
github-merge-queue bot pushed a commit that referenced this pull request May 15, 2024
As reported here:
#16224 (comment)

The underlying `WriteFile` call may block indefinitely and
we shouldn't hold the terminal lock during that period.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Input Related to input processing (key presses, mouse, etc.) Issue-Bug It either shouldn't be doing this or needs an investigation. Priority-1 A description (P1) Product-Terminal The new Windows Terminal.
Projects
Development

Successfully merging this pull request may close these issues.

5 participants