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

Make macOS FMA app copies readable by all users - #56605

Closed
allenhouchins wants to merge 1 commit into
mainfrom
allenhouchins/fma-macos-app-permissions
Closed

allenhouchins wants to merge 1 commit into
mainfrom
allenhouchins/fma-macos-app-permissions

Conversation

@allenhouchins

Copy link
Copy Markdown
Member

Related issue: Follow-up to #56532. The Sourcetree-only fix is #56604.

Summary

Fleet runs macOS FMA install scripts as root. The generated copy step, sudo cp -R "$TMPDIR/X.app" "$APPDIR", keeps the archive's permissions. When a vendor archive stores owner-only permissions, only root can read the installed app. Fleet reports it as installed, because osquery runs as root, but end users can't open it. Homebrew never hits this because the user who installs owns the files.

What changes

  • The generated copy is now if ! sudo cp -R "$TMPDIR/X.app" "$APPDIR" || ! sudo chmod -R go+rX "$APPDIR/X.app"; then <rollback>. go+rX adds read permission for group and others. It adds execute only on folders and on files that are already executable. It never removes permissions or adds write. If the chmod fails, the script removes the copy, restores the previous version and exits 1, like a failed copy. An app users can't open counts as a failed install.
  • 11 custom install scripts contain the generated copy block word for word and get the same change: Claude, Cycling '74 Max, Firefox (also used by Firefox ESR), Firefox Developer Edition, Firefox Nightly, GitHub Desktop, Homebrew, Microsoft Edge, Steam, Webex and Wispr Flow. The other custom install scripts copy in their own way and are unchanged.
  • 838 macOS outputs are updated: every output that uses the generated copy, including 9 frozen ones.

Which apps are affected today

I read the file list inside 294 of the 299 zip-based macOS FMAs. For the other 5, the download host doesn't allow partial downloads or returns 404.

DMG installs use the same copy step. I didn't scan DMGs.

How the outputs were regenerated

A full ingest would pull in newer upstream versions and skips frozen outputs. Instead, I made the same one-line substitution in each darwin.json and recomputed the sha256[:8] ref. The script only rewrote files whose unchanged JSON re-encoded byte for byte, so formatting matches Go's encoder. For seven samples, the real ingester produced identical files: Sourcetree, OpenRCT2, Screen Studio, Fastmail, Elgato Game Capture HD, YACReader (two apps) and 010 Editor (a DMG).

Merge order

#56604 adds a Sourcetree custom install script. If it merges first, I'll rebase this PR and remove sourcetree-install.sh and the input's install_script_path, because the generated script does the same thing.

Checklist for submitter

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
    See Changes files for more information.

  • 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 chmod uses the same escaped app path as the existing mv, cp and rm lines.

Testing

  • Added/updated automated tests

    TestInstallScriptAppReadableByUsers runs the generated script on an owner-only bundle and checks the installed permissions. It also checks that a failed chmod restores the previous app. Both cases fail without this change.

  • QA'd all new/changed functionality manually

Done locally, without root:

  • Ran the generated Sourcetree script against the real 4.2.19 zip, with $APPDIR pointed at a temp folder. It exits 0. Every file and folder in the installed bundle is readable by others (symlinks aside), codesign --verify --deep --strict passes and spctl -a accepts it.
  • chmod -R doesn't follow symlinks out of the bundle and doesn't fail on broken links.

Not yet done: an install as root on a real host. CI's darwin validator installs every changed app as root. Like #56457, that run may also surface failures that already exist on main and are unrelated to this change.

AI

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

Fleet installs as root, and `cp -R` keeps the archive's modes, so a vendor
archive with owner-only modes (Sourcetree) left the bundle readable only by
root. The generated copy now runs `chmod -R go+rX` on the app and rolls back
like a failed copy if that fails.

Applied to the 11 custom install scripts that carry the generated copy block,
and to every macOS output that uses it.
allenhouchins added a commit that referenced this pull request Oct 26, 2026
…56618)

**Related issue:** Follow-up to #56532. Sourcetree is fixed on its own
in #56604.

## Summary

Fleet runs macOS FMA install scripts as root, and `cp -R` keeps the
vendor zip's permissions. The OpenRCT2 and Screen Studio zips each store
one file as `-rw-------`. Installed under `/Applications`, that file is
readable only by root. Fleet still reports the app as installed, because
osquery runs as root.

This adds a custom install script for each app. Each one is the current
generated script plus `sudo chmod -R go+rX "$APPDIR/<App>.app"` after
the copy, the same as the Sourcetree fix in #56604. If the chmod fails,
the script removes the copy, restores the previous version and exits 1,
like a failed copy.

### Effect of the owner-only file

To test this, I removed read access to each file for a regular user
before the app's first launch, then launched the app.

- **OpenRCT2 0.5.6, `Contents/Info.plist`:** the app launches.
LaunchServices registers it with no bundle ID, name or version.
`osascript -e 'application id "io.openrct2.OpenRCT2" is running'`, `tell
… to quit` and `open -b io.openrct2.OpenRCT2` all fail, so the FMA's
quit-before-update and relaunch steps don't find the app.
- **Screen Studio 4.1.0-5005,
`Contents/Resources/app.asar.unpacked/bin/arm64/tf-restormer.safetensors`:**
the app launches. The file is the TF-Restormer model that the bundled
`screen-studio-speech` helper loads for its speech-restoration audio
step. A user-run helper can't open it. I couldn't trigger that render
outside the app to see the failure end to end.

I tested two other apps the same way and saw no effect, so they're left
unchanged:
- **Fastmail:** `embedded.provisionprofile` is owner-only. The profile
check at launch still passes.
- **Elgato Game Capture HD:** `CodeResources` is `-rw-r-----` and is
readable by admins through the `admin` group.

The generator-wide version of this fix (#56605) is closed for now.

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [ ] QA'd all new/changed functionality manually

Done locally, without root:
- Ran each new script against the real vendor zip (SHA-256 matches the
output), with `$APPDIR` pointed at a temp folder and `sudo` removed.
Both exit 0. Every file and folder in both installed bundles is readable
by others (symlinks aside). Screen Studio passes `codesign --verify
--deep --strict`. The OpenRCT2 release isn't signed at all, so there's
no signature to check.
- Regenerated the outputs with `go run ./cmd/maintained-apps --slug
<app>/darwin`. The version, URL and SHA-256 are unchanged. Only the
install script and its ref change.

Not yet done: an install as root on a real host. CI's darwin validator
installs both apps as root.

## AI

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


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved Homebrew installations for OpenRCT2 and Screen Studio. The
apps are closed before updates and reopened afterward if they were
running.
* Installations now handle failed updates more safely by restoring the
existing app when replacement fails. Installed apps are also accessible
to non-root users.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
allenhouchins added a commit that referenced this pull request Nov 6, 2026
**Related issue:** Resolves #57915

## Summary

Fleet runs macOS FMA install scripts as root, and `cp -R` keeps the
source bundle's permissions. The Novabench 6.2.1 DMG stores
`Novabench.app` owner-only: all 54 folders in the bundle are
`drwx------`, and `Contents/MacOS/Novabench`, `Info.plist`, both
`CodeResources` files and the bundled `com.novabench.service.pkg` are
`-rwx------` or `-rw-------`. Installed under `/Applications`, the
bundle is readable only by root, so users can't open the app. Fleet
still reports it as installed, because osquery runs as root.

This adds a custom install script for Novabench. It's the current
generated script plus `sudo chmod -R go+rX "$APPDIR/Novabench.app"`
after the copy, the same as #56604 (Sourcetree) and #56618 (OpenRCT2,
Screen Studio). If the chmod fails, the script removes the copy,
restores the previous version and exits 1, like a failed copy.

This fixes only Novabench. The generator-wide version (#56605) is
closed.

# 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

- [ ] QA'd all new/changed functionality manually

Done locally, without root:
- Checked the bundle's permissions in the cask's DMG
(`https://novabench.com/files/novabench.dmg`, 6.2.1, mounted read-only).
- Ran the new script against that DMG with `$APPDIR` pointed at a temp
folder and `sudo` and `osascript` stubbed. It exits 0. Every folder and
file in the installed bundle is readable by others, and the bundle
passes `codesign --verify --deep --strict`.
- Ran it again over an existing install with `chmod` made to fail. It
exits 1 and restores the previous version.
- Regenerated the output with `go run ./cmd/maintained-apps
--slug=novabench/darwin`. The version, URL and SHA-256 are unchanged.
Only the install script and its ref change.
- `go test ./ee/maintained-apps/... ./cmd/maintained-apps/...` passes.

Not yet done: an install as root on a real host. CI's darwin validator
installs the app as root.

## AI

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


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Bug Fixes**
* Novabench installation now attempts to close the app before updating
it and relaunches it afterward if it was running.
* If installation fails, the previous app is restored when available,
rather than leaving a partial installation.
* Installed app permissions allow non-administrator users to access and
run Novabench.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

This branch was previously deployed

1 inactive deployment
Docker Hub — c07807dd Deployed Oct 26, 2026 by allenhouchins via publish #110910
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