Skip to content

fix(windows): remove the bundled installer and uninstaller (#466) - #481

Merged
PathGao merged 1 commit into
masterfrom
fix/strip-installer
Aug 6, 2026
Merged

fix(windows): remove the bundled installer and uninstaller (#466)#481
PathGao merged 1 commit into
masterfrom
fix/strip-installer

Conversation

@PathGao

@PathGao PathGao commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Closes #466. Implements the route you picked in #466 (comment) — strip the install/uninstall code and let NSIS own it.

What the scanner reacts to

The uninstall path in setup.rs wrote a batch file to %TEMP%, wrote a .vbs that ran it with a hidden window and then deleted itself, and launched the pair through wscript.exe. Hidden script → self-delete → launch an executable is close to the definition of Trojan:Win32/Wacatac.B!ml, which scores behaviour rather than matching a signature. Each property had a reason (the batch waits on the PID rather than the image name so a portable copy survives an uninstall, #358), but the shape is what gets scored — so the only thing that clears it is removing the shape.

The install half was already dead code

get_app_mode entered installer mode only on a filename containing installer or an explicit --install, and #355 stopped publishing any installer-named artifact. Nothing shipped could reach install_app, create_shortcut, check_install_status or Installer.svelte. Removing them changes no observable behaviour for anyone on 2.7.0.

The uninstall half needed a migration — and it turned out not to need a Windows box

I asked you to verify on Windows whether the updater's /UPDATE run still writes the registry entry. That question doesn't arise. Reading the bundler template at the version that built 2.7.0 (tauri-cli-v2.9.6):

Section Install = lines 614..717
  line 682   WriteRegStr SHCTX "${UNINSTKEY}" "UninstallString" "$INSTDIR\uninstall.exe"
  occurrences of $UpdateMode inside 614..717:  0

The write isn't behind a mode check at all — the $UpdateMode guards in that file sit in Section WebView2 and Section Uninstall. So it lands whether or not /UPDATE is passed.

Both sides also address the same key: the custom installer used APP_NAME (setup.rs:47) and the template uses ${PRODUCTNAME} (:58), and both are Markpad.

One gap is left, and the hook closes it. Section Install writes through SHCTX, so an old install that chose the other hive keeps a second Add/Remove Programs row pointing at a command the binary no longer answers. MARKPAD_DROP_LEGACY_UNINSTALL_ENTRY checks both hives and deletes the key only when the value ends in --uninstall — a tail only the custom installer ever wrote, so it cannot match an entry this installer owns. DeleteRegKey under HKLM without elevation is a silent no-op, which is why there is also a backstop in the binary: a stray --uninstall hands off to uninstall.exe beside it and exits, and starts normally if there is none.

Also

Verification

npm run check 646 files 0 errors · npm test 728/728 · cargo test 125/125.

The Rust count is down 32 from master, which is exactly the number of #[test] inside the deleted setup.rs — the tests went with the code they covered, and nothing else moved.

What I could not verify here, stated plainly:

  • forward_legacy_uninstall is #[cfg(target_os = "windows")], so a macOS cargo check does not compile it. I type-checked it separately against x86_64-pc-windows-msvc. It has not been run.
  • The NSIS macro cannot be compiled on macOS at all. makensis runs in the Windows build-test leg, so a malformed macro fails there — please treat that leg as the real check on the hook.
  • Nobody has run an actual 2.6.x → 2.7.1 upgrade and read the registry afterwards. The argument above is from the template source, not from an observation.

🤖 Generated with Claude Code

VirusTotal scores the shipped binary `Trojan:Win32/Wacatac.B!ml`, and #334
already identified the cause: the uninstall path writes a batch file to %TEMP%,
writes a .vbs that runs it with a hidden window and then deletes itself, and
launches the pair through wscript.exe. That sequence is close to the definition
of the behaviour classifier that flags it. It is a false positive, but the
pattern it matches is really there, so the only way to clear it is to remove
the pattern.

The install half was already unreachable. `get_app_mode` entered installer mode
only on a filename containing `installer` or an explicit `--install`, and #355
stopped publishing any installer-named artifact, so nothing shipped could reach
`install_app`, `create_shortcut`, `check_install_status` or Installer.svelte.

The uninstall half was reachable, through `UninstallString = "…\Markpad.exe"
--uninstall` written by custom installs from 2.6.x. Two things now cover those:

  * NSIS rewrites that value. `Section Install` in the bundler template writes
    `UninstallString` to `$INSTDIR\uninstall.exe` with no `$UpdateMode` guard
    anywhere in the section, so it lands on a plain install and on an updater
    run alike. Both sides address the same key -- the custom installer used
    APP_NAME and the template uses ${PRODUCTNAME}, which are both "Markpad".
  * A hook drops the stale entry when the two disagree on a hive. Section
    Install writes through SHCTX, so an old install that chose the other hive
    keeps its row in Add/Remove Programs. The hook matches on the `--uninstall`
    tail, which only the custom installer ever wrote, and deletes just that key.

`forward_legacy_uninstall` is the backstop for the case both miss: a stray
`--uninstall` hands off to uninstall.exe beside the binary and exits. If there
is nothing to hand off to it starts normally, because a window the user did not
ask for is still a better answer than a click that does nothing.

Signing would not have helped. A certificate addresses SmartScreen reputation,
not a behaviour classifier's score.

Drops mslnk and chrono, which setup.rs was the only user of.

Verified: `npm run check` 0 errors, `npm test` 728/728, `cargo test` 125/125
(down 32, the count of `#[test]` inside the deleted setup.rs). The Windows-only
`forward_legacy_uninstall` is not built by a macOS `cargo check`, so it was type
-checked separately against x86_64-pc-windows-msvc. The NSIS macro cannot be
compiled here at all; makensis runs in the Windows build-test leg, which is
where a malformed macro would surface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PathGao
PathGao merged commit 7aa2aa9 into master Aug 6, 2026
4 checks passed
@PathGao
PathGao deleted the fix/strip-installer branch August 6, 2026 02:36
PathGao added a commit that referenced this pull request Aug 7, 2026
* fix(windows): remove dead installer code that survives in the binary

The code that triggered Trojan:Win32/Wacatac.B!ml was removed in #481,
but three things that still look like an installer to a behaviour
classifier were left in the binary:

- forward_legacy_uninstall() — answered --uninstall from pre-2.7 custom
  installs.  The NSIS template writes UninstallString = uninstall.exe
  unconditionally, and the auto-updater passes /UPDATE which runs Section
  Install, so every install that has ever updated through 2.7.1 already
  has the corrected registry entry.  The hooks.nsi macro also drops
  stray --uninstall entries from both hives as a backstop.

- is_installer_mode detection — checked for --install or 'installer' in
  the exe name.  No shipped artifact has triggered either since #355
  stopped publishing an installer-named file, and #481 removed
  Installer.svelte, install_app, and the invoke_handler registrations
  that were the only consumers of the resulting window label.

- The 'installer' window label in capabilities/default.json — window
  that no code can create any more.

None of these do anything observable today, but a scanner that reads the
file rather than tracing reachability still sees them.  Removing them
shrinks the binary by the 55 lines of Rust that remained from the old
custom installer.

Addresses the setup.exe detection reported in #466 (comment).

* chore(deps): bump mermaid 11.16.0 -> 11.16.1 to fix npm audit

npm audit reports 5 moderate vulnerabilities in mermaid <= 11.16.0:
- GHSA-c4c3-pg64-4m4v (prototype pollution)
- GHSA-6x64-9x62-f2gx (CSS injection)
- GHSA-3rrr-jr9j-h3q3 (prototype pollution)
- GHSA-2v8p-3f2j-5mp7 (infinite loop DoS)
- GHSA-rhh3-jpg6-66xh (DoS)

All fixed in 11.16.1.

---------

Co-authored-by: PathGao <PathGao@users.noreply.github.com>
PathGao added a commit that referenced this pull request Aug 7, 2026
Installer.svelte was removed in #481.  These keys are never referenced
by any t() call outside i18n.ts itself.

Removes 302 lines (27 English keys × 25 locales, minus languages that
already lacked the translations).  English key count drops from 335 to
308.

Co-authored-by: PathGao <PathGao@users.noreply.github.com>
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.

Windows portable release flagged by Microsoft VirusTotal heuristic detection

1 participant