Skip to content

fix: uninstall apps from any drive, and let Cancel stop the leftover search - #3152

Merged
laurentiu021 merged 2 commits into
mainfrom
fix/2783-uninstallers-anywhere-on-the-pc
Nov 5, 2026
Merged

laurentiu021 merged 2 commits into
mainfrom
fix/2783-uninstallers-anywhere-on-the-pc

Conversation

@laurentiu021

Copy link
Copy Markdown
Owner

Closes #2783, with both points from its comment.

What changes for a user

  • Apps on another drive, or installed for one user, can be removed. Uninstaller started an app's own uninstaller only from Program Files, the Windows folder, ProgramData or Local AppData. An app installed on D:\Games, or under Roaming AppData as some per-user installers put theirs, showed "outside trusted directories" and stayed. Uninstalling runs only while SysManager is not elevated, so the uninstaller gets the user's own rights, the same as when Windows Settings runs the same entry. The folder list protected nothing: Local AppData was on it, and a user can write there like anywhere else. Any .exe on this PC's drives is started now.
  • A folder named "Smith & Sons" no longer blocks an uninstall. Any & or ; in the command refused it. The quoted path is the file the shell starts, and no command interpreter reads it, so only the arguments are checked now. On this PC, 1 of 292 uninstall commands has one of those characters.
  • A refusal says what to do. Every refusal ended "Refusing to run for security.", which reads as a warning about the app. Each now starts "Remove it from Windows Settings ▸ Apps." and then says why.
  • Cancel stops the leftover search and the removal. Cancel showed while Uninstaller looked for leftovers and while it removed what was ticked, but neither was given a token, so it stopped nothing. Over a large folder, that could keep "Looking for what the uninstall left behind…" on screen for a long time. Each step now has its own source that Cancel reaches. A stopped search shows what it had found and says so. A stopped removal says what went, and the rest stays ticked on the card.

How

  • UninstallerService.LocalExecutable replaces the folder check. It refuses a path that is not fully qualified, a path whose root is not a drive letter (a share or a \\?\ path, refused before any file lookup, so a network path is never reached), and anything but an .exe. EnsureStandardIntegrity still stops every uninstall while SysManager is elevated, which is why the folder does not matter.
  • ParseUninstallCommand runs the shell metacharacter check on a quoted path's arguments only. Every other form is still checked whole before it is parsed (fix: security hardening batch 2 — cache validation, zip slip, DLL hijack, parser hardening #403). AppAlertService.FoldersOf and LeftoverFinder share the parser, so they now also read the folder of an uninstaller with & in its path.
  • CannotStart(why) builds every refusal, from LocalExecutable, the parser, and the rundll32 and MsiExec checks. The rundll32 DLL keeps its folder rule: rundll32 loads whatever file it is named, and all 5 rundll32 uninstall commands on this PC load from those folders.
  • UninstallerViewModel gives the leftover search and the removal each a new CancellationTokenSource. The search after a cancelled batch still runs for the apps that were removed. ShowLeftoversAsync stops at the cancel and shows what it found, and DescribeFindings says the search was stopped. LeftoverService.RemoveAsync checks the token between items and returns a LeftoverRemoval marked Stopped instead of throwing; before, a throw would have lost the list of what had already gone to the Recycle Bin. The token is not passed to Task.Run, so a removal cancelled before it starts still returns.

Tests

  • Uninstaller: the test runner's own exe, which is outside every old folder, is started. Paths on D: and E: and under Roaming AppData are accepted. A share, a \\?\ path, a .bat, a relative path and a drive-relative path are refused without a file lookup. A missing file says the app may be gone already. Six kinds of refusal through UninstallLocalAsync start with Windows Settings ▸ Apps, and none says "refusing". Quoted paths with & and ; parse, and chaining in the arguments is still refused.
  • Cancel: the search stops at the cancel and the card shows what it found. A cancelled batch still searches. The card's opening line says a stopped search was stopped. The removal stops and keeps the rest on the list. RemoveAsync stops before its next item with the first already recycled, and one cancelled before it starts removes nothing. Both report Stopped.

Red before the fix, by mutation, each file restored byte for byte afterwards:

  • Old folder list: 4 tests fail.
  • Whole-command metacharacter check: 2 fail. Arguments unchecked: 2 fail.
  • Network path accepted: 2 fail.
  • Old refusal wording: all 6 refusal cases fail.
  • Search or removal given no token: each test times out at its 30-second bound.
  • Search sharing the batch's source: 1 fails.
  • RemoveAsync throwing on cancel, or with the token passed to Task.Run: 1 fails each.
  • Stopped text left out: 3 fail for the search, 2 for the removal.

Checks

  • All four projects build with 0 warnings, and dotnet format --verify-no-changes is clean.
  • The unit suite passed twice on the final tree: 10,735 tests, 0 failed, and the one skip that was there before.
  • CHANGELOG (1.136.105), the csproj bump, README (Uninstaller) and ARCHITECTURE (UninstallerService, LeftoverService, UninstallerViewModel) are in this pull request.

…search

Closes #2783.

UninstallerService started a local uninstaller only from Program Files,
the Windows folder, ProgramData or Local AppData. Uninstalling runs only
at standard integrity, so the list protected nothing and refused apps on
another drive or under Roaming AppData. LocalExecutable now takes any
.exe on this PC's drives and still refuses relative and network paths.
Shell metacharacters are checked in a quoted path's arguments only, and
CannotStart words every refusal with Windows Settings > Apps.

The leftover search and removal were given no token, so Cancel stopped
neither. Each gets a source of its own; a stopped search shows what it
found, and RemoveAsync returns what it removed, marked Stopped.
// A source of its own, which Cancel reaches: the batch's may be cancelled already, and measuring a
// large folder can keep the search going for a while. It was given no token, so Cancel showed and
// stopped nothing (#2783).
_cts.Dispose();
Auto-release tags only when the newest entry carries today's UTC date.
@laurentiu021
laurentiu021 merged commit b88c1d9 into main Nov 5, 2026
7 checks passed
@laurentiu021
laurentiu021 deleted the fix/2783-uninstallers-anywhere-on-the-pc branch November 5, 2026 15:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Uninstaller — apps whose uninstaller is on another drive or in Roaming AppData cannot be removed, even without administrator rights

2 participants