Skip to content

Commit 863cdd4

Browse files
lheckerDHowett
authored andcommitted
ConPTY: Fix shutdown if killed during startup (#18588)
During startup we relinquish ownership of the console lock to wait for the DA1 response of the hosting terminal. The problem occurs if the hosting terminal disconnects during that time. The broken pipe will cause `VtIo` to send out `CTRL_CLOSE_EVENT` messages, but those won't achieve anything, because the first and only client hasn't even finished connecting yet. What we need to do instead is to return an error code. In order to not use a bunch of booleans to control this behavior, I gave `VtIo` a state enum. This however required restructuring the calling code in order to not have a dozen states. ## Validation Steps Performed * Launch cmd.exe with ConPTY * ...but leave the stdin pipe unbound (which will hang the DA1 request) * Immediately kill the ConPTY session * cmd.exe exits after clicking away the error message ✅ (cherry picked from commit 733a5e7) Service-Card-Id: PVTI_lADOAF3p4s4AxadtzgXjzIg Service-Version: 1.23
1 parent 73721c7 commit 863cdd4

File tree

3 files changed

+105
-126
lines changed

3 files changed

+105
-126
lines changed

src/host/VtIo.cpp

Lines changed: 89 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ using namespace Microsoft::Console::Interactivity;
7979
const HANDLE OutHandle,
8080
_In_opt_ const HANDLE SignalHandle)
8181
{
82-
FAIL_FAST_IF_MSG(_initialized, "Someone attempted to double-_Initialize VtIo");
82+
if (_state != State::Uninitialized)
83+
{
84+
assert(false); // Don't call initialize twice.
85+
return E_UNEXPECTED;
86+
}
8387

8488
_hInput.reset(InHandle);
8589
_hOutput.reset(OutHandle);
@@ -95,47 +99,33 @@ using namespace Microsoft::Console::Interactivity;
9599
}
96100
}
97101

98-
// The only way we're initialized is if the args said we're in conpty mode.
99-
// If the args say so, then at least one of in, out, or signal was specified
100-
_initialized = true;
101-
return S_OK;
102-
}
103-
104-
// Method Description:
105-
// - Create the VtEngine and the VtInputThread for this console.
106-
// MUST BE DONE AFTER CONSOLE IS INITIALIZED, to make sure we've gotten the
107-
// buffer size from the attached client application.
108-
// Arguments:
109-
// - <none>
110-
// Return Value:
111-
// S_OK if we initialized successfully,
112-
// S_FALSE if VtIo hasn't been initialized (or we're not in conpty mode)
113-
// otherwise an appropriate HRESULT indicating failure.
114-
[[nodiscard]] HRESULT VtIo::CreateIoHandlers() noexcept
115-
{
116-
if (!_initialized)
117-
{
118-
return S_FALSE;
119-
}
120-
121-
// SetWindowVisibility uses the console lock to protect access to _pVtRenderEngine.
122-
assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked());
123-
124-
try
102+
// - Create and start the signal thread. The signal thread can be created
103+
// independent of the i/o threads, and doesn't require a client first
104+
// attaching to the console. We need to create it first and foremost,
105+
// because it's possible that a terminal application could
106+
// CreatePseudoConsole, then ClosePseudoConsole without ever attaching a
107+
// client. Should that happen, we still need to exit.
108+
if (IsValidHandle(_hSignal.get()))
125109
{
126-
if (IsValidHandle(_hInput.get()))
110+
try
127111
{
128-
_pVtInputThread = std::make_unique<VtInputThread>(std::move(_hInput), _lookingForCursorPosition);
112+
_pPtySignalInputThread = std::make_unique<PtySignalInputThread>(std::move(_hSignal));
113+
114+
// Start it if it was successfully created.
115+
RETURN_IF_FAILED(_pPtySignalInputThread->Start());
129116
}
117+
CATCH_RETURN();
130118
}
131-
CATCH_RETURN();
132119

120+
// The only way we're initialized is if the args said we're in conpty mode.
121+
// If the args say so, then at least one of in, out, or signal was specified
122+
_state = State::Initialized;
133123
return S_OK;
134124
}
135125

136126
bool VtIo::IsUsingVt() const
137127
{
138-
return _initialized;
128+
return _state != State::Uninitialized;
139129
}
140130

141131
// Routine Description:
@@ -151,50 +141,64 @@ bool VtIo::IsUsingVt() const
151141
[[nodiscard]] HRESULT VtIo::StartIfNeeded()
152142
{
153143
// If we haven't been set up, do nothing (because there's nothing to start)
154-
if (!_initialized)
144+
if (_state != State::Initialized)
155145
{
156146
return S_FALSE;
157147
}
158148

159-
if (_pVtInputThread)
149+
_state = State::Starting;
150+
151+
// SetWindowVisibility uses the console lock to protect access to _pVtRenderEngine.
152+
assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked());
153+
154+
try
160155
{
161-
LOG_IF_FAILED(_pVtInputThread->Start());
156+
if (IsValidHandle(_hInput.get()))
157+
{
158+
_pVtInputThread = std::make_unique<VtInputThread>(std::move(_hInput), _lookingForCursorPosition);
159+
}
162160
}
161+
CATCH_RETURN();
163162

163+
if (_pVtInputThread)
164164
{
165-
Writer writer{ this };
165+
LOG_IF_FAILED(_pVtInputThread->Start());
166166

167-
// MSFT: 15813316
168-
// If the terminal application wants us to inherit the cursor position,
169-
// we're going to emit a VT sequence to ask for the cursor position.
170-
// If we get a response, the InteractDispatch will call SetCursorPosition,
171-
// which will call to our VtIo::SetCursorPosition method.
172-
//
173-
// By sending the request before sending the DA1 one, we can simply
174-
// wait for the DA1 response below and effectively wait for both.
175-
if (_lookingForCursorPosition)
176167
{
177-
writer.WriteUTF8("\x1b[6n"); // Cursor Position Report (DSR CPR)
178-
}
179-
180-
// GH#4999 - Send a sequence to the connected terminal to request
181-
// win32-input-mode from them. This will enable the connected terminal to
182-
// send us full INPUT_RECORDs as input. If the terminal doesn't understand
183-
// this sequence, it'll just ignore it.
184-
writer.WriteUTF8(
185-
"\x1b[c" // DA1 Report (Primary Device Attributes)
186-
"\x1b[?1004h" // Focus Event Mode
187-
"\x1b[?9001h" // Win32 Input Mode
188-
);
168+
Writer writer{ this };
169+
170+
// MSFT: 15813316
171+
// If the terminal application wants us to inherit the cursor position,
172+
// we're going to emit a VT sequence to ask for the cursor position.
173+
// If we get a response, the InteractDispatch will call SetCursorPosition,
174+
// which will call to our VtIo::SetCursorPosition method.
175+
//
176+
// By sending the request before sending the DA1 one, we can simply
177+
// wait for the DA1 response below and effectively wait for both.
178+
if (_lookingForCursorPosition)
179+
{
180+
writer.WriteUTF8("\x1b[6n"); // Cursor Position Report (DSR CPR)
181+
}
189182

190-
writer.Submit();
191-
}
183+
// GH#4999 - Send a sequence to the connected terminal to request
184+
// win32-input-mode from them. This will enable the connected terminal to
185+
// send us full INPUT_RECORDs as input. If the terminal doesn't understand
186+
// this sequence, it'll just ignore it.
187+
writer.WriteUTF8(
188+
"\x1b[c" // DA1 Report (Primary Device Attributes)
189+
"\x1b[?1004h" // Focus Event Mode
190+
"\x1b[?9001h" // Win32 Input Mode
191+
);
192+
193+
writer.Submit();
194+
}
192195

193-
{
194-
// Allow the input thread to momentarily gain the console lock.
195-
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
196-
const auto suspension = gci.SuspendLock();
197-
_deviceAttributes = _pVtInputThread->WaitUntilDA1(3000);
196+
{
197+
// Allow the input thread to momentarily gain the console lock.
198+
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
199+
const auto suspension = gci.SuspendLock();
200+
_deviceAttributes = _pVtInputThread->WaitUntilDA1(3000);
201+
}
198202
}
199203

200204
if (_pPtySignalInputThread)
@@ -208,6 +212,17 @@ bool VtIo::IsUsingVt() const
208212
_pPtySignalInputThread->ConnectConsole();
209213
}
210214

215+
if (_state != State::Starting)
216+
{
217+
// Here's where we _could_ call CloseConsoleProcessState(), but this function
218+
// only gets called once when the first client connects and CONSOLE_INITIALIZED
219+
// is not set yet. The process list may already contain that first client,
220+
// but since it hasn't finished connecting yet, it won't react to a CTRL_CLOSE_EVENT.
221+
// Instead, we return an error here which will abort the connection setup.
222+
return E_FAIL;
223+
}
224+
225+
_state = State::Running;
211226
return S_OK;
212227
}
213228

@@ -244,47 +259,21 @@ void VtIo::CreatePseudoWindow()
244259
}
245260
}
246261

247-
// Method Description:
248-
// - Create and start the signal thread. The signal thread can be created
249-
// independent of the i/o threads, and doesn't require a client first
250-
// attaching to the console. We need to create it first and foremost,
251-
// because it's possible that a terminal application could
252-
// CreatePseudoConsole, then ClosePseudoConsole without ever attaching a
253-
// client. Should that happen, we still need to exit.
254-
// Arguments:
255-
// - <none>
256-
// Return Value:
257-
// - S_FALSE if we're not in VtIo mode,
258-
// S_OK if we succeeded,
259-
// otherwise an appropriate HRESULT indicating failure.
260-
[[nodiscard]] HRESULT VtIo::CreateAndStartSignalThread() noexcept
261-
{
262-
if (!_initialized)
263-
{
264-
return S_FALSE;
265-
}
266-
267-
// If we were passed a signal handle, try to open it and make a signal reading thread.
268-
if (IsValidHandle(_hSignal.get()))
269-
{
270-
try
271-
{
272-
_pPtySignalInputThread = std::make_unique<PtySignalInputThread>(std::move(_hSignal));
273-
274-
// Start it if it was successfully created.
275-
RETURN_IF_FAILED(_pPtySignalInputThread->Start());
276-
}
277-
CATCH_RETURN();
278-
}
279-
280-
return S_OK;
281-
}
282-
283262
void VtIo::SendCloseEvent()
284263
{
285264
LockConsole();
286265
const auto unlock = wil::scope_exit([] { UnlockConsole(); });
287266

267+
// If we're still in the process of starting up, and we're asked to shut down
268+
// (broken pipe), `VtIo::StartIfNeeded()` will handle the cleanup for us.
269+
// This can happen during the call to `WaitUntilDA1`, because we relinquish
270+
// ownership of the console lock.
271+
if (_state == State::Starting)
272+
{
273+
_state = State::StartupFailed;
274+
return;
275+
}
276+
288277
// This function is called when the ConPTY signal pipe is closed (PtySignalInputThread) and when the input
289278
// pipe is closed (VtIo). Usually these two happen at about the same time. This if condition is a bit of
290279
// a premature optimization and prevents us from sending out a CTRL_CLOSE_EVENT right after another.

src/host/VtIo.hpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ namespace Microsoft::Console::VirtualTerminal
5757
static wchar_t SanitizeUCS2(wchar_t ch);
5858

5959
[[nodiscard]] HRESULT Initialize(const ConsoleArguments* const pArgs);
60-
[[nodiscard]] HRESULT CreateAndStartSignalThread() noexcept;
61-
[[nodiscard]] HRESULT CreateIoHandlers() noexcept;
6260

6361
bool IsUsingVt() const;
6462
[[nodiscard]] HRESULT StartIfNeeded();
@@ -69,6 +67,15 @@ namespace Microsoft::Console::VirtualTerminal
6967
void CreatePseudoWindow();
7068

7169
private:
70+
enum class State : uint8_t
71+
{
72+
Uninitialized,
73+
Initialized,
74+
Starting,
75+
StartupFailed,
76+
Running,
77+
};
78+
7279
[[nodiscard]] HRESULT _Initialize(const HANDLE InHandle, const HANDLE OutHandle, _In_opt_ const HANDLE SignalHandle);
7380

7481
void _uncork();
@@ -77,7 +84,7 @@ namespace Microsoft::Console::VirtualTerminal
7784
// After CreateIoHandlers is called, these will be invalid.
7885
wil::unique_hfile _hInput;
7986
wil::unique_hfile _hOutput;
80-
// After CreateAndStartSignalThread is called, this will be invalid.
87+
// After Initialize is called, this will be invalid.
8188
wil::unique_hfile _hSignal;
8289

8390
std::unique_ptr<Microsoft::Console::VtInputThread> _pVtInputThread;
@@ -96,7 +103,7 @@ namespace Microsoft::Console::VirtualTerminal
96103
bool _writerRestoreCursor = false;
97104
bool _writerTainted = false;
98105

99-
bool _initialized = false;
106+
State _state = State::Uninitialized;
100107
bool _lookingForCursorPosition = false;
101108
bool _closeEventSent = false;
102109
int _corked = 0;

src/host/srvinit.cpp

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,6 @@ HRESULT ConsoleCreateIoThread(_In_ HANDLE Server,
381381
// can start, so they're started below, in ConsoleAllocateConsole
382382
auto& gci = g.getConsoleInformation();
383383
RETURN_IF_FAILED(gci.GetVtIo()->Initialize(args));
384-
RETURN_IF_FAILED(gci.GetVtIo()->CreateAndStartSignalThread());
385384

386385
return S_OK;
387386
}
@@ -945,27 +944,11 @@ PWSTR TranslateConsoleTitle(_In_ PCWSTR pwszConsoleTitle, const BOOL fUnexpand,
945944
// We'll need the size of the screen buffer in the vt i/o initialization
946945
if (SUCCEEDED_NTSTATUS(Status))
947946
{
948-
auto hr = gci.GetVtIo()->CreateIoHandlers();
949-
if (hr == S_FALSE)
950-
{
951-
// We're not in VT I/O mode, this is fine.
952-
}
953-
else if (SUCCEEDED(hr))
954-
{
955-
// Actually start the VT I/O threads
956-
hr = gci.GetVtIo()->StartIfNeeded();
957-
// Don't convert S_FALSE to an NTSTATUS - the equivalent NTSTATUS
958-
// is treated as an error
959-
if (hr != S_FALSE)
960-
{
961-
Status = NTSTATUS_FROM_HRESULT(hr);
962-
}
963-
else
964-
{
965-
Status = ERROR_SUCCESS;
966-
}
967-
}
968-
else
947+
// Actually start the VT I/O threads
948+
auto hr = gci.GetVtIo()->StartIfNeeded();
949+
// Don't convert S_FALSE to an NTSTATUS - the equivalent NTSTATUS
950+
// is treated as an error
951+
if (FAILED(hr))
969952
{
970953
Status = NTSTATUS_FROM_HRESULT(hr);
971954
}

0 commit comments

Comments
 (0)