镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Citrix Workspace FMA: remove the runtime-copied updater on uninstall - #57081

Merged
allenhouchins merged 2 commits into
mainfrom
allenhouchins/ecstatic-kowalevski-26c4ec
Oct 31, 2026
Merged

allenhouchins merged 2 commits into
mainfrom
allenhouchins/ecstatic-kowalevski-26c4ec

Conversation

@allenhouchins

Copy link
Copy Markdown
Member

Related issue: Found in #56846 (darwin validator, run 41213361619, shard 5/33)

Problem

The macOS Citrix Workspace uninstall sometimes leaves /Library/Application Support/Citrix Workspace Updater/Citrix Workspace Updater.app (com.citrix.ReceiverUpdater, 26.09.0 / 700026.09.0.17) behind. The darwin validator then reports "App still present after uninstall", because it matches any app named like "Citrix Workspace". It also shows up on real Macs, for example in the osquery-perf software library.

Where the updater comes from

The pkg doesn't install that folder. I read the pkg's TOC, Distribution, PackageInfo, Bom and Scripts with HTTP range requests (1.5 MB of the 387 MB pkg):

  • It has one component, com.citrix.ICAClient. Its payload goes to the staging tree /Library/Citrix Workspace/CitrixWorkspaceInstaller, and the postinstall moves the app into /Applications. The updater exists in the payload only inside the app bundle, at Contents/Frameworks/CitrixWorkspaceApps.framework/Versions/26.09.0.17/Helpers/CitrixWorkspaceApps/Citrix Workspace Updater.app.
  • No payload, receipt or pkg script writes /Library/Application Support/Citrix Workspace Updater. So no pkgutil entry, in the cask or anywhere else, can cover it.
  • For silent installs (installer -pkg, which is how Fleet installs it), the postinstall's configureMsTeamsVDIPlugin writes CWAInstallAUCmd = true to the user's com.citrix.receiver.nomas defaults. Its comment says ReceiverHelper reads that marker, behind a LaunchDarkly kill switch, and installs the Microsoft Teams VDI plugin through the Citrix Workspace updater service.

The failing run's harden-runner events show that sequence, right after the install:

Time (UTC) Process
16:17:41 Install done, /Applications/Citrix Workspace.app detected
16:17:45 Citrix Workspace running
16:17:50 /Library/Application Support/Citrix Workspace Updater/Citrix Workspace Updater.app/.../Citrix Workspace Updater (pid 30319) contacts download.microsoft.com
16:17:52 A second /usr/sbin/installer (pid 30662) runs
16:17:54 Uninstall script starts; another updater process (pid 30683) is running from the same folder
16:18:12 The validator finds the updater there

So the updater is copied out of the app bundle at runtime and run as root, to install an add-on. Whether it happens before the uninstall depends on the feature flag, the network and timing. That's why the same app passed other full-catalog runs the same day.

Change

  • New custom uninstall script, inputs/homebrew/scripts/citrix-workspace-uninstall.sh. It's the current generated script, byte for byte, plus one block after /Applications/Citrix Workspace.app and /Library/Citrix Workspace are removed:
    • It stops any process running from /Library/Application Support/Citrix Workspace Updater/, with TERM, then KILL after 5 seconds. The match is anchored to the start of the command line, so only processes running from that folder match.
    • Then it deletes the folder.
  • At that point, the launchd jobs (including com.citrix.ctxworkspaceupdater and com.citrix.ReceiverUpdaterHelper) are booted out and their plists deleted, ReceiverHelper has been quit, and the app bundle the updater is copied from is gone. Nothing is left that could copy it back.
  • I regenerated the output with the ingester. Only uninstall_script_ref and its script changed: same version (26.09.0), URL, SHA and install script.

Not in this PR: the Teams VDI plugin the updater may have installed by then. I couldn't confirm its pkg ID or install path without capturing it from a real run.

If #56846 merges first, this script's copy of trash() needs the same update, and vice versa.

Checklist for submitter

  • Input data is properly validated, SELECT * is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.
    • The new block uses only a fixed path.
  • Timeouts are implemented and retries are limited to avoid infinite loops
    • It waits at most 5 seconds before KILL.

Testing

  • QA'd all new/changed functionality manually
    • I ran the new block on macOS against fake processes running from a scratch copy of the folder. One exited on TERM, one ignored TERM and was killed after 5 seconds, and one that only named the path as an argument was left running. The folder was removed.
    • go test ./ee/maintained-apps/... ./cmd/maintained-apps/... passes.
    • Not tested: a full install and uninstall on a Mac where the updater actually ran. This PR's darwin validation runs citrix-workspace, but the updater only runs sometimes, so a pass there doesn't prove the fix.

AI

AI: Claude Code (claude-opus-5-5)

Citrix Workspace copies its updater to /Library/Application Support/Citrix
Workspace Updater after a silent install and runs it as root to install
add-ons such as the Teams VDI plugin. No pkg receipt covers that folder, so
the generated uninstall left it behind. A custom uninstall script stops
anything running from it once the app is removed, then deletes it.
@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/citrix-workspace/darwin.json

=== Install Script (no changes) ===
=== Uninstall // 89403d3b -> b077b2e9 ===

--- /tmp/old.vGO4Gx	2026-10-31 18:00:24.922666133 +0000
+++ /tmp/new.vLpBeH	2026-10-31 18:00:24.923666122 +0000
@@ -305,6 +305,19 @@
 forget_pkg 'com.citrix.receiver.bcr'
 sudo rm -rf '/Applications/Citrix Workspace.app'
 sudo rm -rf '/Library/Citrix Workspace'
+# Citrix Workspace copies its updater here at runtime, outside any pkg
+# receipt, and runs it as root to install add-ons such as the Microsoft Teams
+# VDI plugin. Stop it after the app is gone so nothing can copy it back.
+updater_dir='/Library/Application Support/Citrix Workspace Updater'
+if pgrep -f "^$updater_dir/" >/dev/null 2>&1; then
+  sudo pkill -TERM -f "^$updater_dir/" || :
+  for _ in {1..5}; do
+    sleep 1
+    pgrep -f "^$updater_dir/" >/dev/null 2>&1 || break
+  done
+  sudo pkill -KILL -f "^$updater_dir/" || :
+fi
+sudo rm -rf "$updater_dir"
 trash "$LOGGED_IN_USER" '/Library/Logs/Citrix Workspace'
 trash "$LOGGED_IN_USER" '~/Library/Application Support/Citrix Receiver'
 trash "$LOGGED_IN_USER" '~/Library/Application Support/Citrix Workspace'

@allenhouchins
allenhouchins marked this pull request as ready for review October 31, 2026 18:07
Copilot AI balanced review requested due to automatic review settings October 31, 2026 18:07
@allenhouchins
allenhouchins merged commit d5bc53a into main Oct 31, 2026
18 checks passed
@allenhouchins
allenhouchins deleted the allenhouchins/ecstatic-kowalevski-26c4ec branch October 31, 2026 18:07

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.

🟡 Changes recommended

The new updater cleanup lacks automated regression coverage for updater absence, termination and escalation, directory removal, and avoiding unrelated processes.

1 open issue
What changed in this PR

This PR updates Fleet’s Citrix Workspace maintained app to remove the updater copied outside the main app bundle at runtime, addressing leftover software after uninstall.

Changes:

  • Adds updater process cleanup using TERM, up to five seconds of polling, and KILL before deleting its directory.
  • Configures the custom uninstall script and regenerates the output manifest without changing installer metadata or installation behavior.
File Description
ee/​maintained-apps/​outputs/​citrix-workspace/​darwin.json Updates the embedded uninstall script and its reference.
ee/​maintained-apps/​inputs/​homebrew/​scripts/​citrix-workspace-uninstall.sh Adds the custom uninstall script with runtime updater cleanup.
ee/​maintained-apps/​inputs/​homebrew/​citrix-workspace.json Selects the custom uninstall script.

Review effort: Balanced

# receipt, and runs it as root to install add-ons such as the Microsoft Teams
# VDI plugin. Stop it after the app is gone so nothing can copy it back.
updater_dir='/Library/Application Support/Citrix Workspace Updater'
if pgrep -f "^$updater_dir/" >/dev/null 2>&1; then
allenhouchins added a commit that referenced this pull request Nov 1, 2026
…#57086)

Part 1 of 11, split from #56846. That PR changes 992 files, and the
auto-approval only covers PRs under 100 files. The 11 PRs touch disjoint
files, so they merge in any order without conflicts, and together they
match #56846's tree exactly. Merge this one first so the generator
change lands before, or with, the rest of the outputs.

Other parts:
- 2/11: #57087, `aldente` through `captain`
- 3/11: #57088, `capto` through `dia`
- 4/11: #57089, `dialpad` through `free-download-manager`
- 5/11: #57090, `front` through `julia-app`
- 6/11: #57091, `jump-desktop` through `microsoft-excel`
- 7/11: #57092, `microsoft-onenote` through `openboard`
- 8/11: #57093, `opencloud` through `raycast`
- 9/11: #57094, `readest` through `soundsource`
- 10/11: #57095, `sourcetree` through `twingate`
- 11/11: #57096, `typeface` through `zwift`

## Summary

Hardening for the `trash()` helper in generated macOS FMA uninstall
scripts. Fleet runs these scripts as root. `trash()` moved the cask's
`zap` paths into the console user's `~/.Trash` with a root `mv -f`.
Those paths and `~/.Trash` both belong to that user, so root shouldn't
be the one writing there.

- **Paths in the user's home (`~/...`)** are moved to the Trash as the
user, with `sudo -u "$logged_in_user" mv -f`. If the user's move fails,
the script logs `Failed to move <path> to the Trash.` and root doesn't
take over.
- **Other paths** are deleted as root by a new `remove_system_path`
helper instead of being moved into the user's Trash, such as
`/Library/LaunchDaemons/*.plist`, `/Library/PrivilegedHelperTools/*` and
`/Users/Shared/*`. The helper already deleted these at the login window
(#56457).
- **A symlinked parent folder can't redirect a root delete.** `rm -rf`
doesn't follow a symlink at the end of a path, but it does follow one in
a parent folder. For example, a user who creates
`/Users/Shared/TechSmith` as a symlink first could aim Camtasia's
`/Users/Shared/TechSmith/Camtasia` delete at a root-only folder.
`remove_system_path` uses `cd -P` to enter the real parent folder,
checks that it's the folder the path names, and deletes `./<name>` from
there, so a symlink swapped in after the check has no effect. Only
top-level symlinks such as `/tmp` and `/etc` are followed, because users
can't write to `/`. A path that fails the check is left in place and
logged as `Skipped <path>: a folder in its path is a symlink.`
- At the login window, `~` paths are still skipped, and cleanup stays
best-effort: `trash()` never fails the uninstall.

**Behavior change:** system paths listed in a cask's `zap trash:` are
deleted, not moved to the user's Trash. A system path with a symlink in
its parent folders below the top level is skipped.

A shell without Full Disk Access can still move files into `~/.Trash` by
name (only listing it is blocked), so the move as the user doesn't
depend on fleetd's TCC grants.

## Outputs

Across all 11 parts, 974 macOS outputs change. Each changes only its
uninstall script and `uninstall_script_ref`. Version, URL, sha256,
install script and queries are byte-identical to `main`.

This part has the generator, its tests, the 16 custom uninstall scripts,
and 73 outputs:

- **16 custom-script outputs**: each embeds its updated input script.
The custom scripts carry a copy of `trash()`, now the new one plus
`remove_system_path()`, including Citrix Workspace's, added on main in
#57081. `TestCustomUninstallScriptsUseCurrentTrash` keeps both identical
to the generator.
- **20 more frozen outputs**: the ingest job never rewrites these, so
they ship here. (WezTerm is both custom and frozen.)
- **37 generated outputs**, `010-editor` through `alcove`.

Parts 2–11 have the other 901 generated outputs. The outputs were built
and cross-checked against `cmd/maintained-apps` regeneration in #56846.

Because these outputs change, the darwin validator runs on each part's
apps. Expect some failures from apps that had already drifted upstream.
The Check Script Diff job will fail here: its comment is over GitHub's
65,536-character limit at this size. It isn't a required check.

# Checklist for submitter

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## Testing

- [x] Added/updated automated tests
- [ ] QA'd all new/changed functionality manually

`TestUninstallScriptTrash` (renamed from
`TestUninstallScriptTrashWithoutGUIUser`) runs generated `trash` calls
in bash with the user's home under a temp dir. A bare `mv` is stubbed to
fail, so the test fails if root ever moves a file. It checks that:
- home paths really land in `~/.Trash` through `sudo -u`
- system paths are deleted
- a system path under a symlinked parent folder, given directly or
through a glob, is skipped, and the files the symlink points to stay in
place
- a failed user move is reported and nothing is removed as root

The test fails against the old helper, and also fails with the symlink
check in `remove_system_path` turned off.

Also checked:
- `go test ./cmd/maintained-apps/... ./ee/maintained-apps/...` passes on
this branch alone, and on each of parts 2–11 alone
- every custom script passes `bash -n`
- run directly on macOS, `remove_system_path` deletes a normal path,
skips one under a symlinked folder, removes a symlink at the end of the
path without touching its target, and follows `/tmp`

Not yet run as root by fleetd on a real Mac.

## AI

**AI:** Claude Code (claude-opus-5-5)

This branch was previously deployed

1 inactive deployment
Docker Hub — 176b865b Deployed Oct 31, 2026 by allenhouchins via publish #111940
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.

3 participants