[Workspaces] Tamper-resistant settings via a local service (v6 prototype)#48816
Draft
LegendaryBlair wants to merge 14 commits into
Draft
[Workspaces] Tamper-resistant settings via a local service (v6 prototype)#48816LegendaryBlair wants to merge 14 commits into
LegendaryBlair wants to merge 14 commits into
Conversation
Drops every v5 launcher-gating element (Authenticode / MS publisher /
ProgramFiles path / empty-args triple-AND) and tackles the EoP from the
other end: same-user malware can no longer modify workspaces.json
because the file lives under a DACL that only the new PTWorkspacesSvc
service can write.
Components:
- WorkspacesSettingsService/ native C++ service exe (SCM + --console)
using NT SERVICE\PTWorkspacesSvc virtual
account; protocol/Protocol.h holds the
tiny wire format (5 opcodes).
- WorkspacesSettingsClient/ native static lib used by Editor /
SnapshotTool / smoke test.
- WorkspacesCsharpLibrary/
SettingsService/ managed mirror: client, paths, one-shot
legacy migration helper.
- WorkspacesSettingsService.wxs ServiceInstall (virtual account, demand
start, restart-on-failure) +
ServiceControl + CreateFolder with
protected DACL on the data root.
Caller authentication (no signatures, by design):
1. ImpersonateNamedPipeClient -> token must be a real interactive user
(rejects SYSTEM / SERVICE / Anonymous).
2. GetNamedPipeClientProcessId -> caller image path must be under the
MSI-recorded PT install folder.
3. Image filename must match a tiny allow-list (Editor / SnapshotTool /
runner). The launcher is *not* on the list; it only reads, and
reads via the user's R+X grant on the file's DACL.
End-to-end verified on dev box:
- Smoke test from arbitrary folder -> AuthRejected (allow-list works).
- Smoke test renamed + located under a PT_DEV_INSTALL_FOLDER override ->
Ping=Ok, GetSettings=Ok (empty for first-time user).
- PutSettings completes against the service exe but errors on the DACL
apply because the NT SERVICE\PTWorkspacesSvc account only exists after
CreateService runs -- confirms the DACL is in fact being applied
against the right principal.
See Workspaces-EoP-Fix/Design-v6-Prototype-Notes.md for the threat model,
wire protocol, reproduction steps, and the list of known gaps before
this can graduate from prototype.
The service runs as NT SERVICE\PTWorkspacesSvc (a virtual account, not a member of Authenticated Users). The default DACL on a user-mode process only grants PROCESS_QUERY_LIMITED_INFORMATION to the process owner, SYSTEM, Administrators and Authenticated Users — so the previous code that called OpenProcess after RevertToSelf failed with ERROR_ACCESS_DENIED for every caller and the auth check rejected even legitimate clients. Fix: keep impersonating the caller while OpenProcess() + the image-name read happen, so the access check is performed against the user's own token, which naturally has access to its own processes. Revert to the service identity immediately after — all subsequent file IO still runs as the service account, preserving the DACL-based EoP protection. Also adds: - TESTING.md: step-by-step manual test recipe - setup-ptworkspacessvc.ps1: idempotent admin setup script that registers the virtual-account service, creates the protected ProgramData folder, applies the PROTECTED DACL and starts the service. End-to-end verified on a dev box: smoke test from a non-allow-listed location returns AuthRejected, smoke test renamed to Editor under an allowed install folder returns Ok for Ping/Get/Put, and a non-elevated user attempting Set-Content directly against the data file is denied by the OS DACL (the core EoP fix verification). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes the custom-install-path bypass of the path-based caller auth.
PowerToys MSI lets users browse to an arbitrary install directory
(WIXUI_INSTALLDIR -> INSTALLFOLDER, PTInstallDirDlg.wxs). If they pick
a folder under a user-writable parent (e.g. D:\Tools\PowerToys
inheriting Authenticated Users:Modify) or run a per-user MSI under
%LocalAppData%, same-user malware can drop a file named
PowerToys.WorkspacesEditor.exe there and pass both the path-prefix and
basename-allow-list checks.
Defense added to the service-side auth pipeline:
Paths::IsFolderAdminOnlyWritable(folder)
GetNamedSecurityInfoW on the install folder, walk DACL ACEs,
reject if any non-admin/system/TrustedInstaller trustee has
FILE_ADD_FILE | FILE_ADD_SUBDIRECTORY | FILE_WRITE_DATA |
FILE_APPEND_DATA | FILE_DELETE_CHILD | DELETE | WRITE_DAC |
WRITE_OWNER | GENERIC_WRITE | GENERIC_ALL.
Wired into AuthenticateCaller as step 3a, after path-prefix and before
basename allow-list. CREATOR OWNER is whitelisted (only matters for
newly-created children).
Companion MSI requirements (documented in design notes, not yet
implemented in this prototype): apply PROTECTED admin-only DACL to
INSTALLFOLDER at install time, and explicitly grant the virtual
service account (RX) on INSTALLFOLDER so it can read the DACL to
validate it. Verified on the dev box that without that grant
GetNamedSecurityInfoW returns ERROR_ACCESS_DENIED.
Per-user MSI installs are unsupported by v6 by design (the install
folder is fundamentally user-writable). Recommended MSI gate:
<Condition>NOT ALLUSERS=""</Condition>.
Smoke-tested on dev box:
# folder = SYSTEM:F, Administrators:F, NT SERVICE\ALL SERVICES:RX
Ping -> Ok OK accepted
# folder + FAREAST\bozhang:F (attacker-writable)
Ping -> AuthRejected OK rejected by 3a
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…binding
Implements the v6 final design (see Workspaces-EoP-Fix/Design-v6-Final.md):
Service boundary is now tightened to the minimum. The service knows
authentication, a namespace allow-list, and how to read/write opaque byte
blobs - nothing else. All business logic (JSON shape, schema version,
legacy migration, sensitive-field stripping) moves into the caller.
Protocol slimmed:
* Drop GetSchemaVersion / MigrateFromLegacy opcodes
* Drop JsonInvalid / SchemaUnsupported / Internal / kCurrentSchemaVersion
* Drop kIdleShutdownSeconds (unused)
* Add NamespaceUnknown / NotFound status codes
* Rename GetSettings/PutSettings -> GetBlob/PutBlob (payload-agnostic)
* Max payload tightened to 1 MiB (was 8 MiB)
Service identity & naming:
* NT SERVICE\PTWorkspacesSvc -> NT SERVICE\PTSettingsSvc
* \\.\pipe\PTWorkspacesSvc -> \\.\pipe\PTSettingsSvc
* Exe TargetName -> PowerToys.PTSettingsSvc.exe
* Namespace WorkspacesSvc:: -> PTSettingsSvc::
Caller authentication:
* kAllowedCallerExeNames[] flat list replaced with typed CallerBinding[]
table mapping each allow-listed exe basename to a namespace id.
Adding a new module is a one-row change with no service code changes.
* AuthenticateCaller now returns the matched binding in CallerIdentity
so the dispatch layer knows which namespace to operate on.
* Bindings table defensively validated against IsValidNamespaceId before
being used as a directory name.
Storage layout (was Workspaces-specific, now generic):
* %ProgramData%\Microsoft\PowerToys\Workspaces\<sid>\workspaces.json
-> %ProgramData%\Microsoft\PowerToys\SettingsSvc\<ns>\<sid>\blob.bin
* Service lazily creates the <ns>\ intermediate folder.
* Per-user <sid>\ folder gets PROTECTED DACL (svc:F, admin:F,
specific-user:RX) - user A cannot read user B's blob.
Client lib:
* WorkspacesSvcClient -> PTSettingsClient
* Ping/GetBlob/PutBlob, payload as std::vector<uint8_t> (opaque bytes)
Smoke test:
* Updated for new API (ping/get [<output-file>]/put <input-file>)
Local validation tooling:
* setup-ptsettingssvc.ps1: registers service, sets up PROTECTED data
root, sets up admin-only fake install folder with allow-listed exe.
* verify-prototype.ps1: 7-step end-to-end check (liveness, basename
rejection, path-prefix rejection, install-folder DACL hardness,
round-trip, NotFound semantics, per-user DACL). All 7 pass.
Smoke test verified on dev box: 7/7 PASS.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the per-user caller-authentication anchor from the latest design (Design-v6-Final.md §7 / §15 microsoft#5 option d). AuthenticateCaller is now a single pipeline branched on the install-folder DACL: * path trusted (image under an admin-only-writable install folder) -> accept on the PATH anchor (per-machine, unchanged), else * fall back to the BINARY-IDENTITY anchor: the image must be Microsoft-signed AND its file version must equal the service's own version (per-user installs in user-writable %LocalAppData%). New CallerVerify.{h,cpp}: * VerifyMicrosoftSignature - WinVerifyTrust (chains to a machine root, runs in the service context) + signer leaf subject == Microsoft. * GetBinaryVersion / GetServiceOwnVersion - VS_FIXEDFILEINFO compare. The signature makes the version field tamper-proof (re-stamp breaks the signature; an old signed binary has an older version -> downgrade defeated). Version comparison alone would be forgeable, so it is only ever used paired with the signature. Links wintrust/crypt32/version. Validation: verify-prototype.ps1 still 7/7. Step 4 (user-writable install folder) now exercises the signature-branch REJECT path (the unsigned smoke test fails the signature check); step 1 confirms the path branch still accepts. Positive signature-accept needs a real MS-signed binary with a matching version resource (not available in the dev prototype). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the stale managed client and wires the C# library to the
current service protocol (Design-v6-Final.md §10).
* PTSettingsClient.cs (replaces WorkspacesSvcClient.cs): correct pipe
name PTSettingsSvc, 1 MiB cap, opcodes Ping/GetBlob/PutBlob, status
bands mapped to a coarse Result { Ok, NotFound, AuthRejected,
Unavailable, Protocol, IoError }. Opaque bytes, no JSON awareness.
* WorkspacesStorage: Load() now reads via GetBlob with defensive parse
(malformed/empty -> empty list, never throws), distinguishes NotFound
(service up, no blob) from Unavailable (no service -> last-resort
legacy-file read). Adds Save() via PutBlob with the same no-service
fallback. JSON shape/serialisation stays caller-side.
* WorkspacesMigration: rewritten for the slim protocol — no more
MigrateFromLegacy opcode; runner reads the legacy file once and
PutBlobs it, idempotent via a %LocalAppData% sentinel.
* SettingsPaths: aligned to the SettingsSvc\Workspaces\<sid>\blob.bin
layout; keeps the legacy %LocalAppData% path for migration/fallback.
Also fixes the StyleCop header/format violations that were failing the
WorkspacesCsharpLibrary build. Library builds clean (0 errors).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Updates the orphaned service fragment to the current design and wires it
into the build + per-machine feature (Design-v6-Final.md §6/§9/§11).
Installer validation is deferred (per direction) — authored, not yet
MSI-validated.
WorkspacesSettingsService.wxs:
* PTWorkspacesSvc -> PTSettingsSvc, exe PowerToys.PTSettingsSvc.exe,
DisplayName 'PowerToys Settings Service', Start=auto (§6).
* Data root %ProgramData%\Microsoft\PowerToys\SettingsSvc with a
PROTECTED DACL: svc:F, Administrators:F, SYSTEM:F, AuthUsers:RX (§9).
* New HardenInstallFolderDacl component: applies the admin-only-writable
INSTALLFOLDER DACL (SYSTEM/Admins/TrustedInstaller:F, Users:RX) and
grants NT SERVICE\PTSettingsSvc:RX so the service can read the DACL
during caller auth (§8/§11).
Product.wxs: reference PTSettingsServiceComponentGroup in CoreFeature,
guarded by NOT PerUser (per-user hardening via one-time elevation is a
documented follow-up, §15 microsoft#5 d).
wixproj: compile WorkspacesSettingsService.wxs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds two checks to verify-prototype.ps1 so the owner / lock-down claims
are part of the automated suite:
8. Owner of the store nodes is a non-user trusted principal
(SYSTEM / Administrators / NT SERVICE\PTSettingsSvc) — never the
logged-in user. Confirms the load-bearing 'owner != user' invariant
(a user-owner could rewrite its own DACL via WRITE_DAC).
9. A Medium-IL (non-elevated) SAFER user token gets BOTH write and
delete rejected on blob.bin — proving the lock holds against
same-user tampering and deletion.
SaferModify.cs is the helper (impersonates a SAFER NormalUser token and
attempts write+delete); the script auto-compiles it from source if the
exe is absent, so the suite stays self-contained. All 9 steps pass on
the dev box.
Finding folded into the design: the prototype owns service-created nodes
as the service account (not SYSTEM); verified to still hold the lock, so
Design-v6-Final.md \u00a79 now accepts SYSTEM / Administrators / the service
account as valid non-user owners.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…kstop
Implements the remaining installer + migration work (Design-v6-Final.md
§10/§11). The seeding/cleanup/harden LOGIC is PowerShell (the CA payload)
and is validated live on the dev box; the WiX wiring is authored (MSI
validation deferred).
CustomActions/ (new, run as SYSTEM):
* Seed-PtSettingsStore.ps1 — per-machine install-time seeding: enumerate
HKLM ProfileList, and for each user with a legacy %LocalAppData%
workspaces.json create a protected blob (owner=SYSTEM, PROTECTED DACL
svc:F/admin:F/system:F/user:RX). Idempotent. Direct SYSTEM write — no
service round-trip, no migration opcode.
* Remove-PtSettingsStore.ps1 — uninstall cleanup: stop+delete the service
and RECURSIVELY delete the SettingsSvc tree (the runtime-created
<SID>\blob.bin nodes aren't MSI-tracked; only SYSTEM/elevated can).
* Harden-PtSettingsPerUser.ps1 — per-user lazy elevation: register the
service if absent, create the protected store, migrate THIS user.
Validated live: seed (user can't write/delete, service can read,
idempotent), cleanup (SYSTEM removes the protected tree the user could
not), harden (migrates + locks this user). verify-prototype.ps1 still 9/9.
WorkspacesSettingsService.wxs:
* Install the three scripts next to the service binary.
* PtSeedStore CA (deferred, no-impersonate) after InstallFiles / NOT Installed.
* PtCleanupStore CA (deferred, no-impersonate) before RemoveFiles / REMOVE=ALL.
WorkspaceService.cs:
* EnsureMigrationBackstop() — once-per-process, idempotent call to
WorkspacesMigration.Run() before the first Load (the straggler backstop;
primary seeding is install-time). Source-compatible; full-app build
deferred (native NuGet restore needed).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The env-var install-folder override is a dev convenience for the smoke test; it must not ship. Wrap it in #ifdef _DEBUG so Release builds rely solely on the MSI-written HKLM InstallFolder value. Debug builds (used by verify-prototype.ps1) keep it; suite still 9/9. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Tests - Register WorkspacesSettingsService.vcxproj and WorkspacesSettingsClient.vcxproj in PowerToys.slnx under /modules/Workspaces/ (shipping product folder). - Register WorkspacesSvcSmokeTest.vcxproj under /modules/Workspaces/Tests/ so the CLI driver builds but is excluded from the shipping product. - Add Debug/Release|ARM64 configurations to the client and smoke-test vcxproj so they match the solution platforms (service already had all four). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
microsoft#5 Version resource: - Add WorkspacesSettingsService.rc embedding a Win32 VERSIONINFO sourced from the central PowerToys version (common/version/version.h). A native exe has no managed assembly version; the Win32 FileVersion is the canonical value the per-user signature+version trust anchor (CallerVerify.cpp / VS_FIXEDFILEINFO) compares. - Wire the .rc into WorkspacesSettingsService.vcxproj (ResourceCompile). microsoft#4 Dev scaffolding: - Move setup-ptsettingssvc.ps1, verify-prototype.ps1, SaferModify.cs into src/modules/Workspaces/WorkspacesSettingsService/devtools/ (out of the repo root, clearly marked dev-only, never shipped). - Derive RepoRoot from script location instead of a hardcoded D:\ path. - Remove superseded setup-ptworkspacessvc.ps1. - Add devtools/.gitignore (ignore compiled *.exe helpers) and devtools/README.md. - Drop throwaway demo binaries from the repo root. Validated: service builds with embedded FileVersion/ProductVersion; relocated 9-step suite still passes 9/9. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…fy.cs - Smoke test is a manual CLI driver, not a VSTest container; set RunVSTest=false so the CI /t:Build;Test pass builds but does not try to execute it as a test. - Add the standard MIT license header to devtools/SaferModify.cs. Verified: all three native projects clean-rebuild with zero warnings under the inherited CI props (Level4 base, TreatWarningAsError=true, Spectre, SDLCheck); smoke test /t:Test is now a no-op. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces a prototype “tamper-resistant settings” architecture for Workspaces by adding a local Windows service (PTSettingsSvc) as the privileged writer for a per-user settings blob under %ProgramData%\Microsoft\PowerToys\SettingsSvc\..., accessed via a small named-pipe protocol and client libraries. It also adds prototype validation scripts and initial installer wiring (per-machine) plus managed migration/backstop logic.
Changes:
- Add
PTSettingsSvcnative service + pipe protocol (Ping/GetBlob/PutBlob), caller authentication, and protected on-disk storage under ProgramData. - Add native + managed clients, plus managed migration/backstop and updated Workspaces storage integration.
- Add installer fragments and PowerShell CustomActions to install the service, create/harden the store, seed legacy data, and remove on uninstall; add devtools validation scripts.
Reviewed changes
Copilot reviewed 41 out of 41 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| TESTING.md | Prototype one-click setup/smoke-test instructions. |
| src/modules/Workspaces/WorkspacesSettingsService/WorkspacesSettingsService.vcxproj | New native service project definition. |
| src/modules/Workspaces/WorkspacesSettingsService/WorkspacesSettingsService.rc | Service version resource used for signature+version anchoring. |
| src/modules/Workspaces/WorkspacesSettingsService/WorkspacesSettingsService.cpp | Service entrypoint, SCM wiring, and console-mode support. |
| src/modules/Workspaces/WorkspacesSettingsService/smoketest/WorkspacesSvcSmokeTest.vcxproj | Smoke-test CLI project (opted out of VSTest). |
| src/modules/Workspaces/WorkspacesSettingsService/smoketest/SmokeTest.cpp | CLI driver for ping/get/put against the service. |
| src/modules/Workspaces/WorkspacesSettingsService/protocol/Protocol.h | Shared wire protocol constants/opcodes/status codes. |
| src/modules/Workspaces/WorkspacesSettingsService/PipeServer.h | Service pipe server interface. |
| src/modules/Workspaces/WorkspacesSettingsService/PipeServer.cpp | Pipe creation, request framing, auth gating, blob read/write dispatch. |
| src/modules/Workspaces/WorkspacesSettingsService/Paths.h | ProgramData store path helpers + install-folder hardening check contract. |
| src/modules/Workspaces/WorkspacesSettingsService/Paths.cpp | Path resolution, SID conversion, and install-folder DACL hardness evaluation. |
| src/modules/Workspaces/WorkspacesSettingsService/FileGuard.h | Storage helpers for DACL hardening + atomic write + bounded reads. |
| src/modules/Workspaces/WorkspacesSettingsService/FileGuard.cpp | DACL application for per-user folders, atomic replace, and file read helper. |
| src/modules/Workspaces/WorkspacesSettingsService/devtools/verify-prototype.ps1 | 9-step end-to-end local verification suite for the prototype. |
| src/modules/Workspaces/WorkspacesSettingsService/devtools/setup-ptsettingssvc.ps1 | Elevated local setup script (service registration + ACLs + fake install folder). |
| src/modules/Workspaces/WorkspacesSettingsService/devtools/SaferModify.cs | Helper to test Medium-IL write/delete rejection using SAFER token. |
| src/modules/Workspaces/WorkspacesSettingsService/devtools/README.md | Devtools usage documentation. |
| src/modules/Workspaces/WorkspacesSettingsService/devtools/.gitignore | Ignore compiled helper executables. |
| src/modules/Workspaces/WorkspacesSettingsService/CallerVerify.h | Signature + version trust-anchor API contract. |
| src/modules/Workspaces/WorkspacesSettingsService/CallerVerify.cpp | WinVerifyTrust + signer subject check + version extraction helpers. |
| src/modules/Workspaces/WorkspacesSettingsService/CallerAuth.h | Caller identity/authentication API contract. |
| src/modules/Workspaces/WorkspacesSettingsService/CallerAuth.cpp | Pipe-client impersonation + SID extraction + path/signature trust + binding lookup. |
| src/modules/Workspaces/WorkspacesSettingsService/Bindings.h | Allow-list/binding table API (exe basename → namespace). |
| src/modules/Workspaces/WorkspacesSettingsService/Bindings.cpp | Initial Workspaces bindings (multiple EXEs + runner) and namespace-id validation. |
| src/modules/Workspaces/WorkspacesSettingsClient/WorkspacesSettingsClient.vcxproj | New native static-lib client project. |
| src/modules/Workspaces/WorkspacesSettingsClient/PTSettingsClient.h | Native client public API + result mapping contract. |
| src/modules/Workspaces/WorkspacesSettingsClient/PTSettingsClient.cpp | Native pipe client implementation and framing. |
| src/modules/Workspaces/WorkspacesLib/WorkspacesData.cpp | Updates native Workspaces file-path helpers for v6 prototype. |
| src/modules/Workspaces/WorkspacesCsharpLibrary/Utils/FolderUtils.cs | Updates managed Workspaces data folder path logic for v6 prototype. |
| src/modules/Workspaces/WorkspacesCsharpLibrary/SettingsService/WorkspacesMigration.cs | One-shot legacy migration logic via PTSettingsSvc. |
| src/modules/Workspaces/WorkspacesCsharpLibrary/SettingsService/SettingsPaths.cs | Centralized v6 vs legacy path resolution (ProgramData SettingsSvc vs LocalAppData). |
| src/modules/Workspaces/WorkspacesCsharpLibrary/SettingsService/PTSettingsClient.cs | Managed named-pipe client mirroring the wire protocol. |
| src/modules/Workspaces/WorkspacesCsharpLibrary/Data/WorkspacesStorage.cs | Moves storage load/save onto PTSettingsSvc with fallback to legacy file. |
| src/modules/Workspaces/Workspaces.ModuleServices/WorkspaceService.cs | Adds a once-per-process migration backstop before reading workspaces. |
| PowerToys.slnx | Adds new service/client/smoketest projects to solution. |
| installer/PowerToysSetupVNext/WorkspacesSettingsService.wxs | New WiX fragment: service install + ProgramData store + install-folder hardening + CustomActions. |
| installer/PowerToysSetupVNext/Product.wxs | Conditionally includes the service component group for per-machine builds. |
| installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj | Includes the new WiX fragment in the build. |
| installer/PowerToysSetupVNext/CustomActions/Seed-PtSettingsStore.ps1 | Per-machine seeding: legacy → protected blob for all profiles. |
| installer/PowerToysSetupVNext/CustomActions/Remove-PtSettingsStore.ps1 | Uninstall cleanup: remove service and/or ProgramData store tree. |
| installer/PowerToysSetupVNext/CustomActions/Harden-PtSettingsPerUser.ps1 | One-time elevated per-user hardening/migration path (prototype). |
Comment on lines
+22
to
+26
| // v6: settings folder is %ProgramData%\Microsoft\PowerToys\Workspaces\<sid>. | ||
| // The launcher reads directly from this path (DACL grants R+X to the | ||
| // owning user). Writers (Editor / SnapshotTool) must go through the | ||
| // PTWorkspacesSvc named pipe — see WorkspacesSettingsClient. | ||
| std::wstring GetServiceManagedUserFolder() |
Comment on lines
+24
to
+32
| // v6: settings live in the service-managed per-user folder under | ||
| // %ProgramData% (ACL'd so only PTWorkspacesSvc can write). Callers | ||
| // that just want to *read* (Launcher, Editor's initial load) can still | ||
| // use this path directly — the user has Read+Execute via the DACL. | ||
| // Writers must round-trip through WorkspacesSvcClient.PutSettings. | ||
| public static string DataFolder() | ||
| { | ||
| return SettingsPaths.CurrentUserFolder(); | ||
| } |
Comment on lines
+56
to
+60
| ReportStatus(SERVICE_STOP_PENDING, 5000); | ||
| if (g_stopEvent) | ||
| { | ||
| SetEvent(g_stopEvent); | ||
| } |
|
|
||
| try | ||
| { | ||
| New-ProtectedDir -path $nsRoot -userSid $sid # namespace root (re-asserts each time; cheap) |
Comment on lines
+63
to
+67
| foreach ($d in @($nsRoot, (Join-Path $nsRoot $UserSid))) | ||
| { | ||
| if (-not (Test-Path $d)) { New-Item -ItemType Directory -Force $d | Out-Null } | ||
| Set-ProtectedAcl -path $d -isDir $true -sid $UserSid | ||
| } |
Comment on lines
+12
to
+18
| // Creates `folder` if it doesn't exist and applies the DACL that locks | ||
| // the directory to: | ||
| // * the service account — Full Control | ||
| // * BUILTIN\Administrators — Read & Execute (audit/backup) | ||
| // * the user whose SID is passed in — Read & Execute (Launcher needs to read) | ||
| // * Everyone else — denied (DACL is protected, no inherit) | ||
| HRESULT EnsureUserFolder(const std::wstring& folder, |
Comment on lines
+21
to
+25
| // Atomically replaces `targetFile` with `bytes`. Internally writes to | ||
| // a sibling .tmp and uses ReplaceFileW so a crash during write never | ||
| // leaves the file in a half-written state. Re-asserts the directory's | ||
| // protective DACL after the write in case something has tampered with it. | ||
| HRESULT WriteFileAtomically(const std::wstring& targetFile, |
The service exe ships in its own subfolder (WorkspacesSettingsService\\), so it was not covered by the flat-named entries in ESRPSigning_core.json and would ship unsigned. Add a path-qualified entry mirroring the existing Tools\\PowerToys.BugReportTool.exe pattern so ESRP signs it. Validated: build 150557866 (Signed YAML Release Build) is green; this only adds one file to the sign list and cannot regress that build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[Workspaces] Tamper-resistant settings via a local service (v6 prototype)
Summary
Workspaces settings are today stored at a user-writable path
(
%LocalAppData%\Microsoft\PowerToys\Workspaces\workspaces.json). A same-user,non-elevated process can tamper with that file and influence what later runs
elevated, which is an Elevation-of-Privilege vector.
This change makes a small local Windows service (
PTSettingsSvc) the solewriter of a protected settings blob under an admin-owned ancestor
(
%ProgramData%\Microsoft\PowerToys\SettingsSvc\...). The store node is owned bya non-user principal (SYSTEM/Administrators/service account) with a PROTECTED
DACL (
svc:F,admin:F,system:F,user:RX), so a Medium-IL user token canread its own settings but cannot write or delete them — closing the
tamper path while preserving normal use.
How it works
WorkspacesSettingsService) — generic, namespace-bound settingsbroker exposing a 3-opcode named-pipe protocol (
Ping/GetBlob/PutBlob). Authenticates callers by install-folder path (per-machine) or byMicrosoft-pinned signature + matching file version (per-user anchor).
WorkspacesSettingsClient) — thin native client; managed callersreach it through
PTSettingsClient.cs.WorkspacesStorageloads viaGetBlob/ saves viaPutBlob, with a defensive parse and a no-service fallback. A once-per-processmigration backstop imports legacy
workspaces.jsonon first load.to seed / clean up / per-user-harden the store.
Validation
A 9-step local suite (
devtools/verify-prototype.ps1) covers liveness, callerallow-list, path-prefix, install-folder DACL hardness, round-trip, NotFound,
per-user DACL, non-user ownership, and non-elevated write+delete rejection.
Current status: 9 / 9 pass.
Notes for reviewers
_DEBUG-only dev override (PT_DEV_INSTALL_FOLDER) is gated out of Release.devtools/contains developer-only validation tooling; it is not shipped.RunVSTest=false), not an automatedtest container.
Editor E2E, MSI validation, telemetry.