Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

uninstall does not work on macOS 11.1+ #313

Closed
Sergong opened this issue Jan 17, 2021 · 23 comments · Fixed by #599
Closed

uninstall does not work on macOS 11.1+ #313

Sergong opened this issue Jan 17, 2021 · 23 comments · Fixed by #599

Comments

@Sergong
Copy link

Sergong commented Jan 17, 2021

Your Environment

  • mas version: 1.7.1
  • macOS version: 11.1

mas Install Method

  • brew install mas (homebrew-core)

Describe the Bug

mas uninstall [id] complains it needs root permissions to uninstall but sudo mas uninstall [id] says the app is not installed.

To Reproduce

Steps to reproduce the behavior:

  1. Install an app; mas install [id]
  2. Uninstall the app with sudo mas uninstall [id]

Expected Behavior

Moves the app to the Trash Bin

Actual Behavior

It will say: Error: not installed

Screenshots, Terminal Output

image

Additional Context

None

@endlesslycurious
Copy link

I'm seeing the same issue on MAS 1.7.1 on macOS 11.2

@kabirbg
Copy link

kabirbg commented Feb 21, 2021

Same as @endlesslycurious; at least for the second part it seems like MAS is engineered so that when run as root with sudo (or su) it would look for app store installations by the root user, which would (normally) be none, as opposed to when run with normal user privileges. And macOS probably doesn't allow uninstallation of an app without root password at all... interesting quandary, not sure how you might fix it.

@philsherry
Copy link

Works with sudo -s if that helps anyone, but things end up in root's trash:

==> App moved to trash: /private/var/root/.Trash/Telegram.app

@baggiponte
Copy link

Same issue here, my mas' version is 1.8.2 and macOS is 11.4.

@Stooovie
Copy link

Same in BS 11.6 and mas 1.8.3. Uninstall not working, says "not installed".

@JanOwiesniak
Copy link

same issue on:

  • macOS 12.0.1 (21A559)
  • mas 1.8.5
mas uninstall 408981434 
Warning: Apps installed from the Mac App Store require root permission to remove.
Error: Unable to move app to trash.
Error: Uninstall failed
sudo mas uninstall 408981434
Error: Not installed
sudo -s mas uninstall 408981434
Error: Not installed
mas list
408981434   iMovie      (10.2.5)

@carlfugate
Copy link

Same issue

  • MacOS 11.5.2 (M1 Mac)
  • MAS 1.8.6

Screen Shot 2022-01-16 at 1 49 16 PM

@Drallas
Copy link

Drallas commented Feb 3, 2022

Same issue for me:

mas uninstall 897118787
Error: Not installed

Any new on what's causing this?

sudo -s doesn't sove it!

@whilestevego
Copy link

MacOS 12.2.1 and still not working

image

@njjerrysmith
Copy link

Also an issue for me on MAS 1.8.6 for Mac 12.4

@yandongxu
Copy link

Same issue for me:

mas uninstall 897118787
Error: Not installed

Any new on what's causing this?

sudo -s doesn't sove it!

Same here!

@tarikkavaz
Copy link

Macos 12.6.4 (21G511)
Same here

@carlfugate
Copy link

I'm not a Swift programmer but I'm guessing something in this function has changed?
https://github.com/mas-cli/mas/blob/main/Sources/MasKit/Commands/Uninstall.swift

public func run(_ options: Options) -> Result<Void, MASError> {
let appId = UInt64(options.appId)

    guard let product = appLibrary.installedApp(forId: appId) else {
        return .failure(.notInstalled)
    }

jscheytt added a commit to jscheytt/dotfiles that referenced this issue Apr 14, 2023
Still won't work because of
mas-cli/mas#313
but maybe it is fixed somehow in the future ...
@claudiodekker
Copy link

claudiodekker commented Jul 20, 2023

It's a permission issue related to macOS's security sandbox : When you use sudo, the command is ran with root privileges and in the root environment, but doesn't have access to your user-specific App Store data. This is why the mas command is unable to see the installed apps and uninstall them. You can verify this by running mas list (will show your apps) and sudo mas list (will be empty).

To fix this, I'm afraid we'll need something like a Privileged Helper Tool (https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Articles/AccessControl.html#//apple_ref/doc/uid/TP40007244-SW5). A working example of this can be found under "EvenBetterAuthorizationSample".

Unfortunately I'm not experienced with Swift, otherwise I'd look into this, but that's basically what's going on.

(related?: #417)

@BitGrub
Copy link

BitGrub commented Mar 17, 2024

So mas hasn't had an uninstall function in 3 years?

@jacoboneill
Copy link

Still having the same issue

  • MAS Version: 1.8.6
  • Mac OS: 13.6.6

@adriangalilea
Copy link

So mas hasn't had an uninstall function in 3 years?

You could have fixed it all along yet here we are 🙃

Unfortunately I'm not experienced with Swift, otherwise I'd look into this, but that's basically what's going on.

I don't think this is the only issue, you can run things with root as your regular user with sudo -u <username> mas list and you'll see it works just fine yet uninstall still fails.

I'm wondering what's the issue, I haven't inspected the code, but can't we just sudo rm -rf "/Applications/<name_of_the_app>.app" and then optionally something like find ~/Library -name "*<name_of_the_app>*" -print0 | xargs -0 sudo rm -rf ?

What am I missing?

@rgoldberg rgoldberg changed the title 🐛 [BUG] mas uninstall does not work on macOS 11.1 uninstall does not work on macOS 11.1+ Sep 14, 2024
@rgoldberg
Copy link
Contributor

rgoldberg commented Sep 15, 2024

The problem is occurring because an earlier part of the code must be run by a user whose associated Apple ID was used to install the app that you want to uninstall (uninstall can be rewritten to skip this part), while a later part must be run as root (thus we cannot avoid needing root access). What follows is a description of what is currently done, why it causes a problem, various solutions to avoid the problem, and my preferred solution:

The folders & files on the file system for apps installed from the App Store (i.e. App Store apps) are owned by the root user, instead of by the user who installed them. An app is uninstalled by trashing its app folder, but that can only be done by root. Thus, no matter what, mas uninstall must have root privileges. The easiest way is to require that the user run sudo mas uninstall <app id>. This ensures that users know that we don’t do anything with their password, and allows the use of /etc/sudoers to allow running without manual password input. Since users shouldn’t be logged in as root, I’ll just talk about sudo from here on out.

The other option is to allow mas uninstall <app id> to be run without sudo, but have it directly ask the user for their password so it can then run sudo in another process or call an equivalent privilege elevation function from Swift. The problems with this are:

  • users must then trust us with their password & we must safeguard their password
  • /etc/sudoers can’t be used (IIUC), so uninstall wouldn’t work well in scripts
  • we must add in the ability to interactively accept input from the user
  • we must either run a separate process to invoke sudo with the password or learn how to elevate privileges in Swift without spawning another process, which is supposedly very difficult

Thus, I suggest requiring being called with sudo.

To trash the app, we need to know the path to the app folder on the file system (e.g., /Applications/Xcode.app). Since we only have the app id, we use it to lookup the app’s path from an app list provided by an Apple private framework, which is only populated with apps installed for the Apple ID associated with the current user. The list lookup also limits us to uninstalling apps only for the Apple ID associated with the current user.

sudo mas uninstall <app id> currently fails, obviously, because the current user is therefore root; root is not associated with any Apple ID., so the app list is empty, so we cannot find the app folder path to trash.

If we don’t care about preventing users from uninstalling apps from other Apple IDs, we can try to find the app path without using the list lookup. That would avoid needing mas (or parts thereof) being run as a user with an associated Apple ID that was used to install the app.

Unfortunately, the app ID does not seem to be deterministically findable from the files for an App Store app. The plists I’ve seen under App Store app folders do not include the app ID. Recursively greppjng for the app ID in an app’s files sometime shows it in a binary executable, but sometimes not. One extended attribute (com.apple.appstore.store_cohort) of some App Store app folders includes the app ID (as the value for pgid), but that extended attribute does not include the app ID for some other App Store app folders.

The bundle ID (which should be unique for each app), however, seems to be in Bundle identifier in each app’s Contents/Info.plist (I cannot verify it's there for all, but it's there for all I've seen). So, we could lookup app info (including the bundle ID) from the Apple web API using the app ID, then find the app folder path by searching through the plists for the bundle ID. All as root so we don’t need to switch to any other user.

If we do the above, we should allow the bundle ID to be passed as a command line argument (i.e. mas uninstall <bundle id>) instead of the app ID, so the call to the App Store would be unnecessary in that case. Moreover, we should allow bundle IDs to be used anywhere app IDs are allowed on the command line for already installed apps (e.g., mas upgrade <bundle id>) or to install new apps if the Apple endpoints can take bundle IDs instead of app IDs.

Or we could still use the app list provided by the Apple private framework to get the app path by switching from root to the user that ran sudo, either by switching users within Swift itself just for that one lookup, or by calling sudo -u $SUDO_USER mas path <app id> in a spawned process (mas path <app id> is a new command, but maybe it could be handled by some other new command or by an existing one). I don’t know how hard it will be to implement either of those options. They also might not be able to work, because maybe switching users only switches the “UNIX” user without starting the service or whatever that populates the installed App Store app list.

So, my best guess is:

  • require that uninstall be called as root (normally via sudo)
  • /etc/sudoers can be setup to not require manual password input, thereby enabling automation
  • allow uninstalling apps from any Apple ID
  • allow bundle ID to be provided instead of app ID (this would allow uninstalling of any app, not just App Store apps, unless we added a check to ensure it was from the App Store)
  • if an app ID is provided, lookup the bundle ID, presumably by performing a lookup from the App Store online API
  • use bundle ID to find app folder path from Contents/Info.plist
  • if more than one Contents/Info.plist is found with a matching bundle ID, output an error listing all matching app folder paths, but don’t uninstall any of them (unless a new --all option was specified). You can then uninstall whichever you want manually using the displayed app folder path to trash the app, or you can uninstall all apps by rerunning, this time with --all.

Do people agree or disagree with this plan?

@jacoboneill
Copy link

@rgoldberg I've got to agree your solution seems the most elegant for the problem. Obviously an update to the docs and man pages would need to be done 😄

@rgoldberg
Copy link
Contributor

@jacoboneill Thanks.

I assume I would start just fixing uninstall to work with app ID, then later I'd implement bundle IDs as command line arguments for all appropriate commands at once, just so we could get uninstall fixed a bit faster.

I'm working on some other simpler issues first. I am also hoping to get in touch with some other project members before making larger changes like this, but I might work on this if I don't hear back from others soon.

@rgoldberg
Copy link
Contributor

@chris-araman provided some details about elevated-privilege solutions here: #216 (comment)

@xav-ie
Copy link

xav-ie commented Oct 4, 2024

Hello, I wanted to share my workaround that others may use in the meantime. It is not without issues, but it works for me very well! I am trying to bootstrap my new laptop, and just wanted to declare exactly which mas apps should be installed. Part of doing that requires removing ones that are not declared.

Expand for workaround script
  1. what is currently installed?
❯ mas list | awk '{print $1}'
682658836
408981434
1501592214 # twingate
409201541
409183694
6446206067 # slack
409203825

^ In my case, twingate and slack are installed on purpose. The rest are apple defaults I don't want (Garage Band, Pages, etc.)

  1. By tacking on the mas apps that should stay installed, we can filter with
    uniq -u and get the ones that should not be installed like this:
❯ (mas list | awk '{print $1}'; \ 
   echo -e "6446206067\n1501592214") | sort | uniq -u
408981434
409183694
409201541
409203825
682658836
  1. a. sudo mas uninstall each id
❯ (mas list | awk '{print $1}'; \
   echo -e "6446206067\n1501592214") | sort | uniq -u \
  | xargs -I {} sudo mas uninstall {}
Error: Not installed # x5

whoops! #313
mas should be able to uninstall but it looks like there is some intricate
permissions issues.

  1. b. workaround using manual method
    Get the bundleId of each of the applications to uninstall
❯ (mas list | awk '{print $1}'; \
   echo -e "6446206067\n1501592214") | sort | uniq -u \ 
  | xargs -I {}  curl -s -X GET "https://itunes.apple.com/lookup?id={}" \
  | jq -r '.results[0].bundleId'
com.apple.iMovieApp
com.apple.iWork.Keynote
com.apple.iWork.Pages
com.apple.iWork.Numbers
com.apple.garageband10
  1. Use these bundleIds returns to look up their location on the computer
❯ (mas list | awk '{print $1}'; \
   echo -e "6446206067\n1501592214") | sort | uniq -u \
  | xargs -I {}  curl -s -X GET "https://itunes.apple.com/lookup?id={}" \
  | jq -r '.results[0].bundleId' \
  | xargs -I {} mdfind "kMDItemCFBundleIdentifier == '{}'"
/Applications/iMovie.app
/Applications/Keynote.app
/Applications/Pages.app
/Applications/Numbers.app
/Applications/GarageBand.app

^ the benefit of using mdfind is that is sidesteps the issue in mas-cli
as it only searches locations available to the current user. This means,
as long as permissions are set up correctly so that you cannot see
another user's home directory, then their ~/Applications/ will never
show up here! We could also apply filtering here to be extra safe, but
uncessary. Especially so since I don't plan on having multiple users ever.

...Oooooof. But if you have a Cask installed with brew, that would also show
up in this list. I don't use brew, but you would need to somehow list
its install locations and remove those from this list, since those could
not have been made by mas.

  1. uninstall 🎉
❯ (mas list | awk '{print $1}'; \
   echo -e "6446206067\n1501592214") | sort | uniq -u \
  | xargs -I {}  curl -s -X GET "https://itunes.apple.com/lookup?id={}" \
  | jq -r '.results[0].bundleId' \
  | xargs -I {} mdfind "kMDItemCFBundleIdentifier == '{}'" \
  | xargs -I {} sudo rm -rf {}
  1. Bonus: use GNU Parallel to increase uninstall speed
    Having to download each, then parse each, then remove each sequentially
    is slow and unnecessary. Using GNU Parallel, we can greatly increase the
    speed of this to be nearly instantaneous.
❯ (mas list | awk '{print $1}'; \ 
   echo -e "6446206067\n1501592214" ) | sort | uniq -u \
  | parallel -j $(nproc) '
  # Fetch the bundleId using iTunes API
  bundleId=$(curl -s -X GET "https://itunes.apple.com/lookup?id={}" \ 
           | jq -r ".results[0].bundleId");
                                                                          
  # Find the application path using mdfind
  appPath=$(mdfind "kMDItemCFBundleIdentifier == \"$bundleId\"");
                                                                          
  # Uninstall the app if found
  if [ -n "$appPath" ]; then
    echo "Uninstalling $appPath...";
    sudo rm -rf "$appPath";
                                                                          
    # Optionally clean up support files
    sudo rm -rf ~/Library/Preferences/"$bundleId".plist;
    sudo rm -rf ~/Library/Caches/"$bundleId";
    sudo rm -rf ~/Library/Application\ Support/"$bundleId";
  else
    echo "App not found for ID {}";
  fi
'

Thank you, @rgoldberg, for providing the detailed write up of how to do this. I never would have thought of this. Also, I feel guilty not mentioning bots wrote most the GNU Parallel code

@rgoldberg rgoldberg self-assigned this Oct 6, 2024
@rgoldberg
Copy link
Contributor

@xav-i.e. Thanks.

To simplify your workaround, you can use kMDItemAppStoreAdamID instead of kMDItemCFBundleIdentifier, so you can skip the bundleId steps.

I'll use NSMetadataQuery to access this information from Swift.

rgoldberg added a commit to rgoldberg/mas that referenced this issue Oct 26, 2024
rgoldberg added a commit to rgoldberg/mas that referenced this issue Oct 26, 2024
because multiple installed apps can have the same app ID or app name.

Partial mas-cli#313

Signed-off-by: Ross Goldberg <[email protected]>
rgoldberg added a commit to rgoldberg/mas that referenced this issue Oct 26, 2024
rgoldberg added a commit to rgoldberg/mas that referenced this issue Oct 26, 2024
Improve errors.

Simplify code.

Partial mas-cli#313

Signed-off-by: Ross Goldberg <[email protected]>
rgoldberg added a commit to rgoldberg/mas that referenced this issue Oct 26, 2024
Partial mas-cli#313

Signed-off-by: Ross Goldberg <[email protected]>
rgoldberg added a commit to rgoldberg/mas that referenced this issue Oct 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.