Defuse Docker Desktop's install-on-quit updater in macOS FMA install script (#50451)

**Related issue:** Customer reports of failed Docker Desktop updates
from self-service on macOS.

## Details

The reported error is Docker Desktop's own updater speaking, not
Fleet's:

```
failed to back up /Applications/Docker.app before update: renaming (moving) file from /Applications/Docker.app to /Applications/Docker.app.back: rename /Applications/Docker.app /Applications/Docker.app.back: file exists
```

Hosts showing "update available" in self-service are exactly the hosts
where Docker Desktop has already downloaded and staged its **own**
self-update at `~/Library/Application
Support/com.docker.install/in_progress/Docker.app`. When the FMA install
script gracefully quits Docker Desktop, that quit triggers Docker's
install-on-quit updater, which renames `Docker.app` → `Docker.app.back`
and moves the staged copy into place — racing the script's own
`mv`/`rm`/`cp` of `/Applications/Docker.app`. The script previously
cleaned up after this race (leftover `.back` bundle and staged copy);
this PR prevents it instead:

- Remove the entire `com.docker.install` staging directory (staged
bundle + updater state) **before** quitting the app, so the quit can't
trigger Docker's updater. Same whole-directory removal the uninstall's
`post_uninstall_scripts` already does.
- Wait out (bounded, 30s) any updater already in flight before touching
`/Applications/Docker.app`.
- Output regenerated via `go run ./cmd/maintained-apps -slug
docker-desktop/darwin`; version pinned at 4.85.0, installer URL/sha
unchanged, only the install script ref changed.

Hosts already wedged with a stale `Docker.app.back` self-heal: the
script still removes `.back` before copying the new bundle.

## Local validation (macOS arm64, Docker Desktop 4.84.0 running)

- shellcheck and `bash -n` clean; embedded output script matches input
byte-for-byte with correct sha256[:8] ref
- Downloaded the pinned 4.85.0 DMG; sha256 matches the manifest
- Seeded affected-host state (non-empty `/Applications/Docker.app.back`,
staged `com.docker.install/in_progress/Docker.app`) and ran the shipped
script: staging dir removed before quit, running Docker Desktop (VM +
active build) quit gracefully, wait loop did not hang
- Wait loop unit-tested against a live process matching
`com\.docker\.install`: waits until it exits, 30s cap

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [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.
- [x] Timeouts are implemented and retries are limited to avoid infinite
loops

## Testing

- [x] QA'd all new/changed functionality manually
This commit is contained in:
Allen Houchins
2026-08-03 12:47:29 -05:00
committed by GitHub
parent 23a1d4746e
commit 183aa052d2
2 changed files with 15 additions and 13 deletions
@@ -122,18 +122,22 @@ sudo cp -R "$MOUNT_POINT"/* "$TMPDIR"
hdiutil detach "$MOUNT_POINT"
MOUNT_POINT=""
# copy to the applications folder
# Quitting Docker Desktop with a staged self-update triggers its install-on-quit
# updater, which renames Docker.app to Docker.app.back and races this script.
# Remove the staging dir (staged bundle + updater state) first so it can't fire.
sudo rm -rf /Users/*/Library/"Application Support"/com.docker.install
quit_and_track_application 'com.electron.dockerdesktop'
# Wait out any updater already in flight before touching /Applications/Docker.app.
SECONDS=0
while pgrep -f 'com\.docker\.install' >/dev/null 2>&1 && (( SECONDS < 30 )); do
sleep 1
done
if [ -d "$APPDIR/Docker.app" ]; then
sudo mv "$APPDIR/Docker.app" "$TMPDIR/Docker.app.bkp"
fi
# Docker Desktop's own in-app updater leaves stale copies of the old app behind:
# a Docker.app.back bundle alongside Docker.app, and a staged copy at
# ~/Library/Application Support/com.docker.install/in_progress/Docker.app.
# osquery's apps table still picks these up by bundle_identifier, which causes
# Fleet patch policies to report Docker as out of date even after a successful
# upgrade.
# Remove stale self-updater leftovers; osquery's apps table picks them up by
# bundle_identifier and patch policies report Docker as out of date.
sudo rm -rf "$APPDIR/Docker.app.back"
sudo rm -rf /Users/*/Library/"Application Support"/com.docker.install/in_progress/Docker.app
sudo cp -R "$TMPDIR/Docker.app" "$APPDIR"
relaunch_application 'com.electron.dockerdesktop'
mkdir -p /usr/local/cli-plugins
@@ -145,8 +149,6 @@ mkdir -p /usr/local/bin
/bin/ln -h -f -s -- "$APPDIR/Docker.app/Contents/Resources/bin/docker-credential-desktop" "/usr/local/bin/docker-credential-desktop"
/bin/ln -h -f -s -- "$APPDIR/Docker.app/Contents/Resources/bin/docker-credential-ecr-login" "/usr/local/bin/docker-credential-ecr-login"
/bin/ln -h -f -s -- "$APPDIR/Docker.app/Contents/Resources/bin/docker-credential-osxkeychain" "/usr/local/bin/docker-credential-osxkeychain"
# A staged self-update can fire during the quit/relaunch window above and
# recreate the stale copies after the earlier removal, so delete them again
# now that the new bundle is in place.
# Remove stale copies recreated during the quit/relaunch window, if any.
sudo rm -rf "$APPDIR/Docker.app.back"
sudo rm -rf /Users/*/Library/"Application Support"/com.docker.install/in_progress/Docker.app