Skip to content

[Workspaces] Tamper-resistant settings via a local service (v6 prototype)#48816

Draft
LegendaryBlair wants to merge 14 commits into
microsoft:mainfrom
LegendaryBlair:workspaces-eop-v6-prototype
Draft

[Workspaces] Tamper-resistant settings via a local service (v6 prototype)#48816
LegendaryBlair wants to merge 14 commits into
microsoft:mainfrom
LegendaryBlair:workspaces-eop-v6-prototype

Conversation

@LegendaryBlair

Copy link
Copy Markdown
Contributor

[Workspaces] Tamper-resistant settings via a local service (v6 prototype)

Draft / prototype. Opened to exercise CI and gather review feedback — not
yet intended to merge. Project/service names are still prototype placeholders.

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 sole
writer
of a protected settings blob under an admin-owned ancestor
(%ProgramData%\Microsoft\PowerToys\SettingsSvc\...). The store node is owned by
a 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 can
read its own settings but cannot write or delete them — closing the
tamper path while preserving normal use.

How it works

  • Service (WorkspacesSettingsService) — generic, namespace-bound settings
    broker exposing a 3-opcode named-pipe protocol (Ping / GetBlob /
    PutBlob). Authenticates callers by install-folder path (per-machine) or by
    Microsoft-pinned signature + matching file version (per-user anchor).
  • Client (WorkspacesSettingsClient) — thin native client; managed callers
    reach it through PTSettingsClient.cs.
  • Managed wiringWorkspacesStorage loads via GetBlob / saves via
    PutBlob, with a defensive parse and a no-service fallback. A once-per-process
    migration backstop imports legacy workspaces.json on first load.
  • Installer — per-machine service install + DACLs, plus deferred CustomActions
    to seed / clean up / per-user-harden the store.

Validation

A 9-step local suite (devtools/verify-prototype.ps1) covers liveness, caller
allow-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.
  • The smoke test is a manual CLI driver (RunVSTest=false), not an automated
    test container.
  • Known follow-ups: final project/service naming, automated CI tests,
    Editor E2E, MSI validation, telemetry.

LegendaryBlair and others added 13 commits June 10, 2026 15:03
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>
@github-actions github-actions Bot added the Product-Workspaces Refers to the Workspaces utility label Jun 23, 2026
@LegendaryBlair LegendaryBlair requested a review from Copilot June 24, 2026 03:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 PTSettingsSvc native 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Product-Workspaces Refers to the Workspaces utility

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants