Skip to content

Uninstall: reach the signed-in user's Filee, and never report success with Filee left behind - #1120

Merged
KnifeLemon merged 3 commits into
mainfrom
fix/uninstall-leftovers
Nov 4, 2026
Merged

KnifeLemon merged 3 commits into
mainfrom
fix/uninstall-leftovers

Conversation

@KnifeLemon

@KnifeLemon KnifeLemon commented Nov 4, 2026 •

Copy link
Copy Markdown
Owner

Refs #1079. Not Fixes: this covers the startup entry and the false success for certain, but we don't have the reporter's log, so why their Program Files folder survived is not confirmed.

What was wrong

1. The cleanup never ran when Filee was in the tray (every machine-wide uninstall by an administrator). The uninstaller runs elevated and started Filee.exe --uninstall-cleanup with its own rights. SingleInstance uses PipeOptions.CurrentUserOnly; the .NET 10 client (NamedPipeClientStream.ValidateRemotePipeUser) compares the pipe's owner with WindowsIdentity.Owner, and an elevated token's owner is Administrators, not the user. So the tray Filee's pipe refuses the elevated client, --quit never arrives, the mutex wait runs out after 20 s and RunInstallerCommand returns 1 before UninstallCleanup.Run(). The uninstaller only logged the exit code and killed Filee by force. The startup entry, the classic verb, Send To and the engines stayed.

2. A standard user who gets an administrator's account at the prompt. The cleanup ran in the administrator's hive and exited 0, leaving the user's Run value. ProgramsIn could not open the user's processes, so their Filee was neither listed nor ended (Kill swallows the denial too). Its files were moved to the trash folder and it ran on from there, and could still offer updates.

3. The uninstall entry was removed no matter what. A file that could not be deleted or moved was only passed to DeleteAtRestart, whose failure was just logged, then Registry.DeleteKey ran and the card said "Filee is no longer on this computer". uninstall.json was gone by then too, so no retry was possible. If security software refuses the deletes, this gives the reported state: not in Apps, Filee in Program Files, and it starts from the Run key. Those files only go at a real restart, not at a Fast Startup shutdown.

4. A file renamed in place was deleted at restart, but its folder was never scheduled, so an empty folder stayed.

What changed

  • ISetupSystem.RunAsSignedInUser: the cleanup runs with Explorer's token (the existing StartAsShellUser path, now also waiting for the exit code). If that fails it falls back to Setup's rights only when Setup runs as Explorer's account (compared by SID); otherwise it returns -1 rather than a false "clean".
  • If the cleanup didn't finish, it runs again after Filee has been ended. If it still didn't, the Run value is removed from this account's hive when it points into this folder, and the Uninstalled card says so (installer.uninstall.cleanup_failed). A missing Filee.exe is no failure (an earlier try took it), but the Run value is still removed.
  • Only programs uninstall.json lists are ended or can block the uninstall: a folder chosen with /DIR may hold other programs, and they are never touched.
  • The elevated uninstaller holds SeDebugPrivilege only while it looks at or ends programs, so it can reach other accounts' and programs started later don't inherit it. A Filee still running afterwards fails the uninstall before any file goes (installer.uninstall.still_running). So does, machine-wide only, a process named Filee whose path can't be read. It deliberately doesn't test whether Filee.exe is in use: that is also true while Explorer or Apps in Settings has it mapped for its icon.
  • A file that can't be moved or renamed fails the uninstall (installer.uninstall.files_left). The entry, uninstall.json and uninstall.exe stay, and the first-phase uninstaller isn't released, so it can run again. uninstall.json itself is never scheduled or renamed, or the retry after a restart would say Filee isn't installed.
  • A file locked for a moment (a security scan) doesn't count as left: locked files get about 2 seconds, shared across the whole uninstall however many there are, and uninstall.json waits within the same budget.
  • With the trash on another volume, File.Move isn't tried: MOVEFILE_COPY_ALLOWED is documented to succeed with the original still in place when it can't delete it, which would have looked like a removed file (and been copied again on each retry). Not run: this machine has one volume.
  • A file renamed in place is scheduled with its folders. Without administrator rights Windows refuses to schedule it (a per-user install on another volume than %TEMP%): the uninstall still succeeds, since nothing runs that file, and the card says where it stays (installer.uninstall.files_kept).
  • The four new texts are in all 20 language files.

Checked for and not found

Nothing restarts Filee during removal: no RegisterApplicationRestart, watchdog or task. UpdateService only starts Setup when the user asks. The window can't be closed during removal, and the entry was already the last thing removed if removal threw.

Verification

  • dotnet format --verify-no-changes: clean.
  • tests/Filee.Setup.Tests: 230 passed. Tests were also run with the fixes undone: 8 UninstallerTests fail on the old Uninstaller.cs (first commit), and 7 fail with the second commit's five changes undone together (the record.Files filter, keeping uninstall.json, the missing Filee.exe, the renamed file, and the in-use check put back).
  • The owner check in point 1 was read from the IL of System.IO.Pipes.dll 10.0.1. That an elevated token's owner is Administrators is standard Windows behaviour, not something I could run here.
  • A separate program confirmed that a file mapped with LOAD_LIBRARY_AS_IMAGE_RESOURCE refuses a write-open with every share flag, and that the SID comparison with Explorer works without elevation.

Not run: no real install or uninstall, and nothing elevated, so these have only been reasoned through: RunAsShellUser itself, CreateProcessWithTokenW returning a handle we can wait on, and the debug privilege across accounts. Please try once on a test machine before release: machine-wide install, Filee in the tray, uninstall from Settings. %TEMP%\Filee-Uninstall-*.log should show Filee --uninstall-cleanup: exit code 0 without a 20 s gap, and the Run value should be gone. Then the same with a standard user and another account at the prompt.

Not changed

  • Installer.CloseFilee has the same problem as point 1: an elevated Setup's Filee.exe --quit can't reach the tray Filee, so it should wait 20 s and then force-kill it on updates, without Filee saving first. Left out to keep this PR on the uninstall; worth its own change.
  • With another account at the prompt, "delete my settings" still means that account's %APPDATA%.

🤖 Generated with Claude Code

KnifeLemon and others added 3 commits November 4, 2026 22:26
… with Filee left behind

The elevated uninstaller ran "Filee.exe --uninstall-cleanup" with its own rights. The pipe of a Filee without
administrator rights refuses an elevated client (CurrentUserOnly compares the pipe's owner with the token's owner,
which is Administrators when elevated), so that Filee never heard --quit: the command waited 20 s and exited 1 before
cleaning up. Filee was then ended by force, and its startup entry, Explorer menu entries and engines stayed. With
another account at the administrator prompt it cleaned that account's registry and could not even see the user's Filee.

- The cleanup runs as the signed-in user, again once Filee has been ended if it did not finish; the startup entry goes
  anyway if it is in this account's registry, and the Uninstalled card says when the cleanup never finished.
- The elevated uninstaller can see and end other accounts' programs. A Filee that still runs afterwards stops the
  uninstall before anything is deleted, instead of running on from the trash folder.
- A file that can't be removed fails the uninstall and keeps the uninstall entry, uninstall.json and uninstall.exe, so
  it can run again. Before, the entry went regardless, and the card said Filee was gone.
- A file renamed in place is deleted at the next restart together with its folder.

Refs #1079

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review findings on the first commit, each checked against the code:

- Only programs uninstall.json lists are ended or block the uninstall. With a folder the user chose, and the debug
  privilege, the uninstaller could end services and other accounts' programs, and report an unkillable one as Filee.
- A second try after "files left" no longer warns that the cleanup failed: Filee.exe is gone because the first try ran
  it. The startup entry is still removed when it points here.
- uninstall.json is never scheduled or renamed: after the restart we ask for, uninstalling again needs it.
- A file renamed in place that Windows won't delete at the restart (it takes administrator rights) no longer fails the
  uninstall: nothing runs it and a second try wouldn't know its name. The entry goes and the card says where it stays.
- "Still running" no longer comes from opening Filee.exe for writing: that also fails while Explorer or Apps in
  Settings has it mapped for its icon. It looks for a process named Filee whose path can't be read, machine-wide only.
- The signed-in user is compared with Explorer's SID: a name can be the same in a domain and on this PC.
- The debug privilege is on only while looking at or ending programs, so programs started later don't inherit it.
- it.json: the new lines follow the file's indentation.

Refs #1079

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scan holding a file just as it is being removed failed the uninstall on the first try. Locked files now get about
2 seconds before they count as left: eight tries 250 ms apart, shared by the whole uninstall, so the wait stays the same
however many files are locked. uninstall.json waits within the same budget, and is still never scheduled or renamed.

With the trash on another volume File.Move is no longer tried: MOVEFILE_COPY_ALLOWED is documented to succeed with the
original still in place when it can't delete it, which looked like a removed file, and each retry would copy it again.

Refs #1079

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@KnifeLemon
KnifeLemon merged commit da4bf57 into main Nov 4, 2026
9 checks passed
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.

1 participant