Separate pruning of elevated/unelevated session buffers (#19546)

Previously, launching an unelevated session after an elevated one would
delete the latter's persisted buffers, and vice versa of course. Also,
elevated buffers didn't have an ACL forbidding access to unelevated
users. That's also fixed now.

Closes #19526

## Validation Steps Performed
* Unelevated/elevated WT doesn't erase each other's buffers 
* Old buffers named `buffer_` are renamed to `elevated_` if needed 
This commit is contained in:
Leonard Hecker
2025-11-20 20:49:14 +01:00
committed by GitHub
parent 2537ea7df8
commit 81cdb07646
20 changed files with 211 additions and 103 deletions

View File

@@ -1139,6 +1139,7 @@ NOYIELD
NOZORDER
NPFS
nrcs
NRNW
NSTATUS
ntapi
ntdef

View File

@@ -2628,11 +2628,8 @@ void TextBuffer::_AppendRTFText(std::string& contentBuilder, const std::wstring_
}
}
void TextBuffer::SerializeToPath(const wchar_t* destination) const
void TextBuffer::SerializeTo(HANDLE handle) const
{
const wil::unique_handle file{ CreateFileW(destination, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) };
THROW_LAST_ERROR_IF(!file);
static constexpr size_t writeThreshold = 32 * 1024;
std::wstring buffer;
buffer.reserve(writeThreshold + writeThreshold / 2);
@@ -2661,7 +2658,7 @@ void TextBuffer::SerializeToPath(const wchar_t* destination) const
{
const auto fileSize = gsl::narrow<DWORD>(buffer.size() * sizeof(wchar_t));
DWORD bytesWritten = 0;
THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), buffer.data(), fileSize, &bytesWritten, nullptr));
THROW_IF_WIN32_BOOL_FALSE(WriteFile(handle, buffer.data(), fileSize, &bytesWritten, nullptr));
THROW_WIN32_IF_MSG(ERROR_WRITE_FAULT, bytesWritten != fileSize, "failed to write");
buffer.clear();
}

View File

@@ -288,7 +288,7 @@ public:
const bool isIntenseBold,
std::function<std::tuple<COLORREF, COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors) const noexcept;
void SerializeToPath(const wchar_t* destination) const;
void SerializeTo(HANDLE handle) const;
struct PositionInformation
{

View File

@@ -8,8 +8,7 @@ namespace TerminalApp
None,
Content,
MovePane,
PersistLayout,
PersistAll
Persist,
};
runtimeclass BellEventArgs

View File

@@ -2218,7 +2218,7 @@ namespace winrt::TerminalApp::implementation
}
}
void TerminalPage::PersistState(bool serializeBuffer)
void TerminalPage::PersistState()
{
// This method may be called for a window even if it hasn't had a tab yet or lost all of them.
// We shouldn't persist such windows.
@@ -2233,7 +2233,7 @@ namespace winrt::TerminalApp::implementation
for (auto tab : _tabs)
{
auto t = winrt::get_self<implementation::Tab>(tab);
auto tabActions = t->BuildStartupActions(serializeBuffer ? BuildStartupKind::PersistAll : BuildStartupKind::PersistLayout);
auto tabActions = t->BuildStartupActions(BuildStartupKind::Persist);
actions.insert(actions.end(), std::make_move_iterator(tabActions.begin()), std::make_move_iterator(tabActions.end()));
}
@@ -2319,6 +2319,29 @@ namespace winrt::TerminalApp::implementation
CloseWindowRequested.raise(*this, nullptr);
}
std::vector<IPaneContent> TerminalPage::Panes() const
{
std::vector<IPaneContent> panes;
for (const auto tab : _tabs)
{
const auto impl = _GetTabImpl(tab);
if (!impl)
{
continue;
}
impl->GetRootPane()->WalkTree([&](auto&& pane) {
if (auto content = pane->GetContent())
{
panes.push_back(std::move(content));
}
});
}
return panes;
}
// Method Description:
// - Move the viewport of the terminal of the currently focused tab up or
// down a number of lines.
@@ -3527,9 +3550,12 @@ namespace winrt::TerminalApp::implementation
if (hasSessionId)
{
using namespace std::string_view_literals;
const auto settingsDir = CascadiaSettings::SettingsDirectory();
const auto idStr = Utils::GuidToPlainString(sessionId);
const auto path = fmt::format(FMT_COMPILE(L"{}\\buffer_{}.txt"), settingsDir, idStr);
const auto admin = IsRunningElevated();
const auto filenamePrefix = admin ? L"elevated_"sv : L"buffer_"sv;
const auto path = fmt::format(FMT_COMPILE(L"{}\\{}{}.txt"), settingsDir, filenamePrefix, sessionId);
control.RestoreFromPath(path);
}

View File

@@ -122,7 +122,8 @@ namespace winrt::TerminalApp::implementation
safe_void_coroutine RequestQuit();
safe_void_coroutine CloseWindow();
void PersistState(bool serializeBuffer);
void PersistState();
std::vector<IPaneContent> Panes() const;
void ToggleFocusMode();
void ToggleFullscreen();

View File

@@ -140,22 +140,16 @@ namespace winrt::TerminalApp::implementation
// "attach existing" rather than a "create"
args.ContentId(_control.ContentId());
break;
case BuildStartupKind::PersistAll:
case BuildStartupKind::Persist:
{
const auto connection = _control.Connection();
const auto id = connection ? connection.SessionId() : winrt::guid{};
if (id != winrt::guid{})
{
const auto settingsDir = CascadiaSettings::SettingsDirectory();
const auto idStr = ::Microsoft::Console::Utils::GuidToPlainString(id);
const auto path = fmt::format(FMT_COMPILE(L"{}\\buffer_{}.txt"), settingsDir, idStr);
_control.PersistToPath(path);
args.SessionId(id);
}
break;
}
case BuildStartupKind::PersistLayout:
default:
break;
}

View File

@@ -253,11 +253,11 @@ namespace winrt::TerminalApp::implementation
AppLogic::Current()->NotifyRootInitialized();
}
void TerminalWindow::PersistState(bool serializeBuffer)
void TerminalWindow::PersistState()
{
if (_root)
{
_root->PersistState(serializeBuffer);
_root->PersistState();
}
}
@@ -830,6 +830,11 @@ namespace winrt::TerminalApp::implementation
return _root.as<winrt::Windows::UI::Xaml::Controls::Control>();
}
winrt::Windows::Foundation::Collections::IVector<IPaneContent> TerminalWindow::Panes() const
{
return winrt::single_threaded_vector(_root->Panes());
}
// Method Description:
// - Gets the title of the currently focused terminal control. If there
// isn't a control selected for any reason, returns "Terminal"

View File

@@ -71,7 +71,7 @@ namespace winrt::TerminalApp::implementation
void Create();
void PersistState(bool serializeBuffer);
void PersistState();
void UpdateSettings(winrt::TerminalApp::SettingsLoadEventArgs args);
@@ -111,6 +111,7 @@ namespace winrt::TerminalApp::implementation
float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const;
Windows::UI::Xaml::UIElement GetRoot() noexcept;
winrt::Windows::Foundation::Collections::IVector<IPaneContent> Panes() const;
hstring Title();
void TitlebarClicked();

View File

@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import "IPaneContent.idl";
import "TerminalPage.idl";
import "ShortcutActionDispatch.idl";
@@ -59,9 +60,10 @@ namespace TerminalApp
Boolean ShouldImmediatelyHandoffToElevated();
void HandoffToElevated();
void PersistState(Boolean serializeBuffer);
void PersistState();
Windows.UI.Xaml.UIElement GetRoot();
IVector<IPaneContent> Panes { get; };
String Title { get; };
Boolean FocusMode { get; };

View File

@@ -1815,15 +1815,43 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_closeConnection();
}
void ControlCore::PersistToPath(const wchar_t* path) const
void ControlCore::PersistTo(HANDLE handle) const
{
const auto lock = _terminal->LockForReading();
_terminal->SerializeMainBuffer(path);
_terminal->SerializeMainBuffer(handle);
}
void ControlCore::RestoreFromPath(const wchar_t* path) const
{
const wil::unique_handle file{ CreateFileW(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, nullptr) };
wil::unique_handle file{ CreateFileW(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, nullptr) };
// This block of code exists temporarily to fix buffer dumps that were
// previously persisted as "buffer_" but really should be named "elevated_".
// If loading the properly named file fails, retry with the old name.
if (!file)
{
static constexpr std::wstring_view needle{ L"\\elevated_" };
// Check if the path contains "\elevated_", indicating that we're in an elevated session.
const std::wstring_view pathView{ path };
const auto idx = pathView.find(needle);
if (idx != std::wstring_view::npos)
{
// If so, try to open the file with "\buffer_" instead, which is what we previously used.
std::wstring altPath{ pathView };
altPath.replace(idx, needle.size(), L"\\buffer_");
file.reset(CreateFileW(altPath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, nullptr));
// If the alternate file is found, move it to the correct location.
if (file)
{
LOG_IF_WIN32_BOOL_FALSE(MoveFileW(altPath.c_str(), path));
}
}
}
if (!file)
{
return;

View File

@@ -153,7 +153,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void ColorSelection(const Control::SelectionColor& fg, const Control::SelectionColor& bg, Core::MatchMode matchMode);
void Close();
void PersistToPath(const wchar_t* path) const;
void PersistTo(HANDLE handle) const;
void RestoreFromPath(const wchar_t* path) const;
void ClearQuickFix();

View File

@@ -2578,7 +2578,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_restorePath = std::move(path);
}
void TermControl::PersistToPath(const winrt::hstring& path) const
void TermControl::PersistTo(int64_t handle) const
{
// Don't persist us if we weren't ever initialized. In that case, we
// never got an initial size, never instantiated a buffer, and didn't
@@ -2590,7 +2590,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// file then.
if (_initializedTerminal)
{
winrt::get_self<ControlCore>(_core)->PersistToPath(path.c_str());
winrt::get_self<ControlCore>(_core)->PersistTo(reinterpret_cast<HANDLE>(handle));
}
}

View File

@@ -69,7 +69,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
bool SwitchSelectionEndpoint();
bool ExpandSelectionToWord();
void RestoreFromPath(winrt::hstring path);
void PersistToPath(const winrt::hstring& path) const;
void PersistTo(int64_t handle) const;
void OpenCWD();
void Close();
Windows::Foundation::Size CharacterDimensions() const;

View File

@@ -106,7 +106,7 @@ namespace Microsoft.Terminal.Control
Boolean ExpandSelectionToWord();
void ClearBuffer(ClearBufferType clearType);
void RestoreFromPath(String path);
void PersistToPath(String path);
void PersistTo(Int64 handle);
void OpenCWD();
void Close();
Windows.Foundation.Size CharacterDimensions { get; };

View File

@@ -1523,9 +1523,9 @@ std::wstring Terminal::CurrentCommand() const
return _activeBuffer().CurrentCommand();
}
void Terminal::SerializeMainBuffer(const wchar_t* destination) const
void Terminal::SerializeMainBuffer(HANDLE handle) const
{
_mainBuffer->SerializeToPath(destination);
_mainBuffer->SerializeTo(handle);
}
void Terminal::ColorSelection(const TextAttribute& attr, winrt::Microsoft::Terminal::Core::MatchMode matchMode)

View File

@@ -127,7 +127,7 @@ public:
std::wstring CurrentCommand() const;
void SerializeMainBuffer(const wchar_t* destination) const;
void SerializeMainBuffer(HANDLE handle) const;
#pragma region ITerminalApi
// These methods are defined in TerminalApi.cpp

View File

@@ -10,6 +10,8 @@
#include <WtExeUtils.h>
#include <til/hash.h>
#include <wil/token_helpers.h>
#include <winrt/TerminalApp.h>
#include <sddl.h>
#include "AppHost.h"
#include "resource.h"
@@ -1038,12 +1040,12 @@ void WindowEmperor::_setupSessionPersistence(bool enabled)
}
_persistStateTimer.Interval(std::chrono::minutes(5));
_persistStateTimer.Tick([&](auto&&, auto&&) {
_persistState(ApplicationState::SharedInstance(), false);
_persistState(ApplicationState::SharedInstance());
});
_persistStateTimer.Start();
}
void WindowEmperor::_persistState(const ApplicationState& state, bool serializeBuffer) const
void WindowEmperor::_persistState(const ApplicationState& state) const
{
// Calling an `ApplicationState` setter triggers a write to state.json.
// With this if condition we avoid an unnecessary write when persistence is disabled.
@@ -1052,11 +1054,11 @@ void WindowEmperor::_persistState(const ApplicationState& state, bool serializeB
state.PersistedWindowLayouts(nullptr);
}
if (_app.Logic().Settings().GlobalSettings().ShouldUsePersistedLayout())
if (_app.Logic().Settings().GlobalSettings().FirstWindowPreference() != FirstWindowPreference::DefaultProfile)
{
for (const auto& w : _windows)
{
w->Logic().PersistState(serializeBuffer);
w->Logic().PersistState();
}
}
@@ -1066,6 +1068,8 @@ void WindowEmperor::_persistState(const ApplicationState& state, bool serializeB
void WindowEmperor::_finalizeSessionPersistence() const
{
using namespace std::string_view_literals;
if (_skipPersistence)
{
// We received WM_ENDSESSION and persisted the state.
@@ -1075,78 +1079,106 @@ void WindowEmperor::_finalizeSessionPersistence() const
const auto state = ApplicationState::SharedInstance();
_persistState(state, _app.Logic().Settings().GlobalSettings().FirstWindowPreference() == FirstWindowPreference::PersistedLayoutAndContent);
_persistState(state);
const auto firstWindowPreference = _app.Logic().Settings().GlobalSettings().FirstWindowPreference();
const auto wantsPersistence = firstWindowPreference != FirstWindowPreference::DefaultProfile;
// If we neither need a cleanup nor want to persist, we can exit early.
if (!_needsPersistenceCleanup && !wantsPersistence)
{
return;
}
const std::filesystem::path settingsDirectory{ std::wstring_view{ CascadiaSettings::SettingsDirectory() } };
const auto admin = _app.Logic().IsRunningElevated();
const auto filenamePrefix = admin ? L"elevated_"sv : L"buffer_"sv;
const auto persistBuffers = firstWindowPreference == FirstWindowPreference::PersistedLayoutAndContent;
std::unordered_set<std::wstring, til::transparent_hstring_hash, til::transparent_hstring_equal_to> bufferFilenames;
if (persistBuffers)
{
// If the app is running elevated, we create files with a mandatory
// integrity label (ML) of "High" (HI) for reading and writing (NR, NW).
wil::unique_hlocal_security_descriptor sd;
SECURITY_ATTRIBUTES sa{};
if (admin)
{
unsigned long cb;
THROW_IF_WIN32_BOOL_FALSE(ConvertStringSecurityDescriptorToSecurityDescriptorW(L"S:(ML;;NRNW;;;HI)", SDDL_REVISION_1, wil::out_param_ptr<PSECURITY_DESCRIPTOR*>(sd), &cb));
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = sd.get();
}
// Persist all terminal buffers to "buffer_{guid}.txt" files.
// We remember the filenames so that we can clean up old ones later.
for (const auto& w : _windows)
{
const auto panes = w->Logic().Panes();
for (const auto pane : panes)
{
try
{
const auto term = pane.try_as<winrt::TerminalApp::ITerminalPaneContent>();
if (!term)
{
continue;
}
const auto control = term.GetTermControl();
if (!control)
{
continue;
}
const auto connection = control.Connection();
if (!connection)
{
continue;
}
const auto sessionId = connection.SessionId();
if (sessionId == winrt::guid{})
{
continue;
}
auto filename = fmt::format(FMT_COMPILE(L"{}{}.txt"), filenamePrefix, sessionId);
const auto path = settingsDirectory / filename;
if (wil::unique_hfile file{ CreateFileW(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_DELETE, &sa, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr) })
{
control.PersistTo(reinterpret_cast<int64_t>(file.get()));
bufferFilenames.emplace(std::move(filename));
}
}
CATCH_LOG();
}
}
}
// Now remove the "buffer_{guid}.txt" files that shouldn't be there.
if (_needsPersistenceCleanup)
{
// Get the "buffer_{guid}.txt" files that we expect to be there
std::unordered_set<winrt::guid> sessionIds;
if (const auto layouts = state.PersistedWindowLayouts())
const auto filter = fmt::format(FMT_COMPILE(L"{}\\{}*"), settingsDirectory.native(), filenamePrefix);
// This could also use std::filesystem::directory_iterator.
// I was just slightly bothered by how it doesn't have a O(1) .filename()
// function, even though the underlying Win32 APIs provide it for free.
// Both work fine.
WIN32_FIND_DATAW ffd;
const wil::unique_hfind handle{ FindFirstFileExW(filter.c_str(), FindExInfoBasic, &ffd, FindExSearchNameMatch, nullptr, FIND_FIRST_EX_LARGE_FETCH) };
if (!handle)
{
for (const auto& windowLayout : layouts)
{
for (const auto& actionAndArgs : windowLayout.TabLayout())
{
const auto args = actionAndArgs.Args();
NewTerminalArgs terminalArgs{ nullptr };
if (const auto tabArgs = args.try_as<NewTabArgs>())
{
terminalArgs = tabArgs.ContentArgs().try_as<NewTerminalArgs>();
}
else if (const auto paneArgs = args.try_as<SplitPaneArgs>())
{
terminalArgs = paneArgs.ContentArgs().try_as<NewTerminalArgs>();
}
if (terminalArgs)
{
sessionIds.emplace(terminalArgs.SessionId());
}
}
}
return;
}
// Remove the "buffer_{guid}.txt" files that shouldn't be there
// e.g. "buffer_FD40D746-163E-444C-B9B2-6A3EA2B26722.txt"
do
{
const std::filesystem::path settingsDirectory{ std::wstring_view{ CascadiaSettings::SettingsDirectory() } };
const auto filter = settingsDirectory / L"buffer_*";
WIN32_FIND_DATAW ffd;
// This could also use std::filesystem::directory_iterator.
// I was just slightly bothered by how it doesn't have a O(1) .filename()
// function, even though the underlying Win32 APIs provide it for free.
// Both work fine.
const wil::unique_hfind handle{ FindFirstFileExW(filter.c_str(), FindExInfoBasic, &ffd, FindExSearchNameMatch, nullptr, FIND_FIRST_EX_LARGE_FETCH) };
if (!handle)
const auto nameLen = wcsnlen_s(&ffd.cFileName[0], ARRAYSIZE(ffd.cFileName));
const std::wstring_view filename{ &ffd.cFileName[0], nameLen };
if (!bufferFilenames.contains(filename))
{
return;
std::filesystem::remove(settingsDirectory / filename);
}
do
{
const auto nameLen = wcsnlen_s(&ffd.cFileName[0], ARRAYSIZE(ffd.cFileName));
const std::wstring_view name{ &ffd.cFileName[0], nameLen };
if (nameLen != 47)
{
continue;
}
wchar_t guidStr[39];
guidStr[0] = L'{';
memcpy(&guidStr[1], name.data() + 7, 36 * sizeof(wchar_t));
guidStr[37] = L'}';
guidStr[38] = L'\0';
const auto id = Utils::GuidFromString(&guidStr[0]);
if (!sessionIds.contains(id))
{
std::filesystem::remove(settingsDirectory / name);
}
} while (FindNextFileW(handle.get(), &ffd));
}
} while (FindNextFileW(handle.get(), &ffd));
}
}

View File

@@ -65,7 +65,7 @@ private:
void _unregisterHotKey(int index) noexcept;
void _setupGlobalHotkeys();
void _setupSessionPersistence(bool enabled);
void _persistState(const winrt::Microsoft::Terminal::Settings::Model::ApplicationState& state, bool serializeBuffer) const;
void _persistState(const winrt::Microsoft::Terminal::Settings::Model::ApplicationState& state) const;
void _finalizeSessionPersistence() const;
void _checkWindowsForNotificationIcon();

View File

@@ -74,6 +74,28 @@ struct fmt::formatter<winrt::hstring, wchar_t> : fmt::formatter<fmt::wstring_vie
}
};
template<>
struct fmt::formatter<winrt::guid, wchar_t> : fmt::formatter<fmt::wstring_view, wchar_t>
{
auto format(const winrt::guid& value, auto& ctx) const
{
return fmt::format_to(
ctx.out(),
L"{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
value.Data1,
value.Data2,
value.Data3,
value.Data4[0],
value.Data4[1],
value.Data4[2],
value.Data4[3],
value.Data4[4],
value.Data4[5],
value.Data4[6],
value.Data4[7]);
}
};
// This is a helper macro for both declaring the signature of an event, and
// defining the body. Winrt events need a method for adding a callback to the
// event and removing the callback. This macro will both declare the method