terminal/src/host/writeData.cpp
Dustin Howett c0ab9cb5b5 Merged PR 6034984: Fix a crash caused by improper buffer management w/ multiple clients
Until there's a "Wait", there's usually only one API message inflight at
a time.

In our quest for performance, we put that single API message in charge
of its own buffer management: instead of allocating buffers on the heap
and deleting them later (storing pointers to them at the far corners of
the earth), it would instead allocate them from small internal pools (if
possible) and only heap allocate (transparently) if necessary. The
pointers flung to the corners of the earth would be pointers (1) back
into the API_MSG or (2) to a heap block owned by boost::small_vector.

It took us months to realize that those bare pointers were being held by
COOKED_READ and RAW_READ and not actually being updated when the API
message was _copied_ as it was shuffled off to the background to become
a "Wait" message.

It turns out that it's trivially possible to crash the console by
sending two API calls--one that waits and one that completes
immediately--when the waiting message or the "wait completer" has a
bunch of dangling pointers in it. It further turns out that some
accessibility software (like JAWS) attaches directly to the console
session, much like winpty and ConEmu and friends. They're trying to read
out the buffer (API call!) and sometimes there's a shell waiting for
input (API call!). Oops.

In this commit, we fix up the message's internal pointers (in lieu of
giving it a proper copy constructor; see GH-10076) and then tell the
wait completion routine (which is going to be a COOKED_READ, RAW_READ,
DirectRead or WriteData) about the new buffer location.

This is a scoped fix that should be replaced (TODO GH-10076) with a
final one after Ask mode.

Retrieved from https://microsoft.visualstudio.com os.2020 OS official/rs_wdx_dxp_windev eca0875950fd3a9735662474613405e2dc06f485

References GH-10076

Fixes MSFT-33127449
Fixes GH-9692
2021-05-11 16:56:43 +00:00

197 lines
8.8 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "writeData.hpp"
#include "_stream.h"
#include "../types/inc/convert.hpp"
#include "../interactivity/inc/ServiceLocator.hpp"
// Routine Description:
// - Creates a new write data object for used in servicing write console requests
// Arguments:
// - siContext - The output buffer to write text data to
// - pwchContext - The string information that the client application sent us to be written
// - cbContext - Byte count of the string above
// - uiOutputCodepage - When the wait is completed, we *might* have to convert the byte count
// back into a specific codepage if the initial call was an A call.
// We need to remember what output codepage was set at the moment in time
// when the write was delayed as it might change by the time it is serviced.
// Return Value:
// - THROW: Throws if space cannot be allocated to copy the given string
WriteData::WriteData(SCREEN_INFORMATION& siContext,
_In_reads_bytes_(cbContext) wchar_t* const pwchContext,
const size_t cbContext,
const UINT uiOutputCodepage,
const bool requiresVtQuirk) :
IWaitRoutine(ReplyDataType::Write),
_siContext(siContext),
_pwchContext(THROW_IF_NULL_ALLOC(reinterpret_cast<wchar_t*>(new byte[cbContext]))),
_cbContext(cbContext),
_uiOutputCodepage(uiOutputCodepage),
_requiresVtQuirk(requiresVtQuirk),
_fLeadByteCaptured(false),
_fLeadByteConsumed(false),
_cchUtf8Consumed(0)
{
memmove(_pwchContext, pwchContext, _cbContext);
}
// Routine Description:
// - Destroys the write data object
// - Frees the string copy we made on creation
WriteData::~WriteData()
{
if (nullptr != _pwchContext)
{
delete[] _pwchContext;
}
}
// Routine Description:
// - Stores some additional information about lead byte adjustments from the conversion
// in WriteConsoleA before the real WriteConsole processing (always W) is reached
// so we can restore an accurate A byte count at the very end when the wait is serviced.
// Arguments:
// - fLeadByteCaptured - A lead byte was removed from the string before converted it and saved it.
// We need to report to the original caller that we "wrote" the byte
// even though it is held in escrow for the next call because it was
// the last character in the stream.
// - fLeadByteConsumed - We had a lead byte in escrow from the previous call that we stitched onto the
// front of the input string even though the caller didn't write it in this call.
// We need to report the byte count back to the caller without including this byte
// in the calculation as it wasn't a part of what was given in this exact call.
// Return Value:
// - <none>
void WriteData::SetLeadByteAdjustmentStatus(const bool fLeadByteCaptured,
const bool fLeadByteConsumed)
{
_fLeadByteCaptured = fLeadByteCaptured;
_fLeadByteConsumed = fLeadByteConsumed;
}
// Routine Description:
// - For UTF-8 codepages, remembers how many bytes that the UTF-8 parser said it consumed from the input stream.
// This will allow us to give back the correct value after the wait routine Notify services the data later.
// Arguments:
// - cchUtf8Consumed - Count of characters consumed by the UTF-8 parser off the input stream to generate the
// wide character string that is stowed in this object for consumption in the notify routine later.
// Return Value:
// - <none>
void WriteData::SetUtf8ConsumedCharacters(const size_t cchUtf8Consumed)
{
_cchUtf8Consumed = cchUtf8Consumed;
}
// Routine Description:
// - Called back at a later time to resume the writing operation when the output object becomes unblocked.
// Arguments:
// - TerminationReason - if this routine is called because a ctrl-c or ctrl-break was seen, this argument
// contains CtrlC or CtrlBreak. If the owning thread is exiting, it will have ThreadDying. Otherwise 0.
// - fIsUnicode - Input data was in UCS-2 unicode or it needs to be converted with the current Output Codepage
// - pReplyStatus - The status code to return to the client application that originally called the API (before it was queued to wait)
// - pNumBytes - The number of bytes of data that the server/driver will need to transmit back to the client process
// - pControlKeyState - Unused for write operations. Set to 0.
// - pOutputData - not used.
// - true if the wait is done and result buffer/status code can be sent back to the client.
// - false if we need to continue to wait because the output object blocked again
bool WriteData::Notify(const WaitTerminationReason TerminationReason,
const bool fIsUnicode,
_Out_ NTSTATUS* const pReplyStatus,
_Out_ size_t* const pNumBytes,
_Out_ DWORD* const pControlKeyState,
_Out_ void* const /*pOutputData*/)
{
*pNumBytes = _cbContext;
*pControlKeyState = 0;
if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::ThreadDying))
{
*pReplyStatus = STATUS_THREAD_IS_TERMINATING;
return true;
}
// if we get to here, this routine was called by the input
// thread, which grabs the current console lock.
// This routine should be called by a thread owning the same lock on the
// same console as we're reading from.
FAIL_FAST_IF(!(Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()));
std::unique_ptr<WriteData> waiter;
size_t cbContext = _cbContext;
NTSTATUS Status = DoWriteConsole(_pwchContext,
&cbContext,
_siContext,
_requiresVtQuirk,
waiter);
if (Status == CONSOLE_STATUS_WAIT)
{
// an extra waiter will be created by DoWriteConsole, but we're already a waiter so discard it.
waiter.reset();
return false;
}
// There's extra work to do to correct the byte counts if the original call was an A-version call.
// We always process and hold text in the waiter as W-version text, but the A call is expecting
// a byte value in its own codepage of how much we have written in that codepage.
if (!fIsUnicode)
{
if (CP_UTF8 != _uiOutputCodepage)
{
// At this level with WriteConsole, everything is byte counts, so change back to char counts for
// GetALengthFromW to work correctly.
const size_t cchContext = cbContext / sizeof(wchar_t);
// For non-UTF-8 codepages, we need to back convert the amount consumed and then
// correlate that with any lead bytes we may have kept for later or reintroduced
// from previous calls.
size_t cchTextBufferRead = 0;
// Start by counting the number of A bytes we used in printing our W string to the screen.
try
{
cchTextBufferRead = GetALengthFromW(_uiOutputCodepage, { _pwchContext, cchContext });
}
CATCH_LOG();
// If we captured a byte off the string this time around up above, it means we didn't feed
// it into the WriteConsoleW above, and therefore its consumption isn't accounted for
// in the count we just made. Add +1 to compensate.
if (_fLeadByteCaptured)
{
cchTextBufferRead++;
}
// If we consumed an internally-stored lead byte this time around up above, it means that we
// fed a byte into WriteConsoleW that wasn't a part of this particular call's request.
// We need to -1 to compensate and tell the caller the right number of bytes consumed this request.
if (_fLeadByteConsumed)
{
cchTextBufferRead--;
}
cbContext = cchTextBufferRead;
}
else
{
// For UTF-8, we were told exactly how many valid bytes were consumed before we got into the wait state.
// Just give that value back now.
cbContext = _cchUtf8Consumed;
}
}
*pNumBytes = cbContext;
*pReplyStatus = Status;
return true;
}
void WriteData::MigrateUserBuffersOnTransitionToBackgroundWait(const void* /*oldBuffer*/, void* /*newBuffer*/)
{
// WriteData does not hold API message buffers across a wait
}