How to and When to uninstall a privileged helper

I had an application that needs to do very frequent privileged work.I used to achieve these work by AuthorizationExecuteWithPrivileges(). And I would like to follow the recommended way by using SMJobBless with XPC (a privileged helper over XPC communication). However, I got 2 questions about the unstalling the privileged helper (helper for short below).

I noticed that SMJobRemove had been marked as deprecated and there is no other API as replacement of it now. So I would like to understand what is the supposed or recommended approach to unstall the helper now?


More background of this question:

My application can be installed by a simple drag from with the dmg packege. And the uninstall process is also just by draging to the Trash.

The first time the app starts, it will try to bless the helper with asking the authentication/authorization from a user. Subsequently, it will not bother users next time startup or when communicating with the helper for privileged works.

The reason that I need to uninstall the helper is for the helper version compatibility. There could be multiple versions of the application with potential different version of helpers co-existing in a user's system. So an idea is that each application with an exactly version number will install and communicate with a helper with a specified version as well (i.e. application 1.0.0 <-> helper-100 / application 2.0.1 <-> helper-201 ...). Therefore, to avoid leaking such a number of helpers in the disk when the applications are removed, I would like to unstall (remove) the helper[ver_num] when a specific version of applications are removed.


Furthermore, when to trigger the uninstall process?

My current idea is that when the first time the application starts, it will register the folder path where the application is in in the helper (over XPC) plist. The helper will then watch the folder change over the paths (if there are multi applications with that version exists). Once all the applications of that version with different paths registered in helper are removed, the helper will fork a subprocess to uninstall the helper (by SMJobRemove??).

If the above thinking are not on a correct direction, could you have any suggestion about these 2 questions?

Accepted Reply

I noticed that SMJobRemove had been marked as deprecated and there is no other API as replacement of it now.

SMJobRemove
is not the logical opposite of
SMJobBless
:
  • SMJobBless
    copies the privileged helper tool, creates a launchd property list file in
    /Library/LaunchDaemons/
    , and loads the job into
    launchd
  • SMJobRemove
    unloads the job from
    launchd

As such,

SMJobRemove
is the logical opposite of
SMJobSubmit
.

There is, alas, no logical opposite of

SMJobBless
)-:

Furthermore, when to trigger the uninstall process?

There isn’t a good way to do this automatically.

My current idea is that when the first time the application starts, it will register the folder path where the application is in in the helper (over XPC) plist.

This approach is unlikely to yield a reliable solution. There are just too many ways that things can go wrong. For example, what if the user launches your app off a removable drive and then removes the drive? You can’t work out whether that’s a permanent or temporary removal, and thus whether your privileged helper tool should self immolate or not.

I recommend that you offer the user the option of manually uninstalling the privileged helper tool. Users who care will do the manual uninstall. Users who don’t care won’t notice the leftover helper.

For this to be acceptable you have to ensure that any leftover helpers have a minimal impact on the system. If they do all the usual launchd daemon things (launch on demand, exit on pressure), the cost to the user should be negligible.

Obviously the above is less than ideal. It would be nice if macOS had a better mechanism for escalating privileges in a coherent and controlled manner. Please do file an enhancement request describing your requirements in this space, then post the bug number here, just for the record.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Replies

I noticed that SMJobRemove had been marked as deprecated and there is no other API as replacement of it now.

SMJobRemove
is not the logical opposite of
SMJobBless
:
  • SMJobBless
    copies the privileged helper tool, creates a launchd property list file in
    /Library/LaunchDaemons/
    , and loads the job into
    launchd
  • SMJobRemove
    unloads the job from
    launchd

As such,

SMJobRemove
is the logical opposite of
SMJobSubmit
.

There is, alas, no logical opposite of

SMJobBless
)-:

Furthermore, when to trigger the uninstall process?

There isn’t a good way to do this automatically.

My current idea is that when the first time the application starts, it will register the folder path where the application is in in the helper (over XPC) plist.

This approach is unlikely to yield a reliable solution. There are just too many ways that things can go wrong. For example, what if the user launches your app off a removable drive and then removes the drive? You can’t work out whether that’s a permanent or temporary removal, and thus whether your privileged helper tool should self immolate or not.

I recommend that you offer the user the option of manually uninstalling the privileged helper tool. Users who care will do the manual uninstall. Users who don’t care won’t notice the leftover helper.

For this to be acceptable you have to ensure that any leftover helpers have a minimal impact on the system. If they do all the usual launchd daemon things (launch on demand, exit on pressure), the cost to the user should be negligible.

Obviously the above is less than ideal. It would be nice if macOS had a better mechanism for escalating privileges in a coherent and controlled manner. Please do file an enhancement request describing your requirements in this space, then post the bug number here, just for the record.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thank you for the suggestions. And here is the Bug number 29038737.

"This approach is unlikely to yield a reliable solution. There are just too many ways that things can go wrong. "

This is also what I am quite concerned about the idea. The removable disk example is a very good one that can be considered as one of the cases that the helper is unexpectedly removed. For these cases, I can have the application check and re-install (re-bless) the helper with prompt the auth at the startup though this bothers the user for the auth again. Are there any other side effects about this idea? The most important thing I was worried about is that whether the solution could have any undefined behavior since it is not a standard approach.

Are there any other side effects about this idea?

My example was just that, an example. I can’t think of any other specific cases of where you’ll see problems but I still strongly recommend you adopt the simplest possible solution (explicit uninstall by the user) rather than the complex scheme you propose.

I’ve worked with a bunch of developers who’ve created

SMJobBless
-based solutions and my experience is that, while things work really well for the vast majority of users, a small fraction of users will have mysterious issues. Your complex scheme for daemon self immolation is likely to cause more problems like this, and make it harder to debug those problems.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thank you for the advice! I now understand it and will keep that in mind.

I recommend that you offer the user the option of manually uninstalling the privileged helper tool.


What would be a practical and acceptable way to do this in a GUI application?


I know that the uninstalling task itself is trivial, i.e.:


#! /bin/sh

sudo launchctl unload /Library/LaunchDaemons/com.foo.bar.plist
sudo rm /Library/LaunchDaemons/com.foo.bar.plist
sudo rm /Library/PrivilegedHelperTools/com.foo.bar


However, how should I call this code, without resorting to tell the user she must open the terminal?


Specific problems I'm having:


  1. Most solutions I can think of require the use of AuthorizationExecuteWithPrivileges, however this function is deprecated. Should I still use it for the uninstaller?
  2. When executing launchctl unload programmatically with AuthorizationExecuteWithPrivileges (either by launching a script or via NSTask), I can't use sudo. But for the life of me, I can't figure out how to use launchctl to unload a system daemon without sudo. I tried to use the -D and -S options, but I obviously did that wrong.
  3. Any solutions that make use of sudo (like ugly AppleScript hacks) only work with the current user is an administrator.
  4. Uninstalling the tool from within the tool itself (to avoid AuthorizationExecuteWithPrivileges) seems to pose a chicken-and-egg problem: launchctl unload needs the plist to be present, but presumbably exists the process before the plist can be removed.

Uninstalling the tool from within the tool itself (…) seems to pose a chicken-and-egg problem …

The trick here is to use

launchctl remove
.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Quinn, We have a related issue and are currently using AuthorizationExecuteWithPrivileges.


In our case, we have a separate Uninstall app, which is really just a UI that calls into a script-only Installer package that does the actual uninstalling.


i.e our app wants to do:


sudo installer -pkg <subpath of uninstall package> -target <path to volume software is installed on>


What I am trying to avoid is having to have the Unintsller UI have to bless a helper tool, which in turn just runs the above (via NSTask I guess). The less things that can go wrong with the uninstaller, the better.


(Conceptually this is just another version of "Calling a Privileged Installer" from the Authorization Services Programming Guide, and does not seem to have been superceded by anything in "EvenBetterAuthorizationSample", which is for when your application needs ongoing access to privileged operations. An uninstaller is not 'ongoing'.)


BTW - Keeping the actual uninstalling as a pkg simplifies enterprise-wide uninstalling. i.e admins can extract the pkg from the Uninstaller app and push it to all relevant macs)

My take on this is that, once you start going down the privileged helper tool path, you really need to fully commit. The launchd daemon installed by your app has to take care of three privileged operations:

  • Update

  • Uninstall

  • Whatever privileged stuff you were originally trying to do

All of these should be guarded by authorisation rights, so that they have a default authorisation policy that site admins can customise.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hi Quinn,

In summary, are these the right steps for uninstallation?

#!/bin/sh

sudo launchctl remove /Library/LaunchDaemons/com.foo.bar.plist
sudo rm /Library/LaunchDaemons/com.foo.bar.plist
sudo rm /Library/PrivilegedHelperTools/com.foo.bar

I have a few questions about it:

  1. I see that launchctl remove is an synchronous variant of unload. Is there a possibility that the privileged helper gets killed before the "rm" steps? Or will launchd wait for the process to exit on its own accord?
  2. I would prefer to avoid using a script (I'm worried that it's a point of vulnerability if I get the permissions/signing wrong on it), so I planning to use the Process and FileManager APIs to achieve the same thing, right from my helper process. Is there any danger posed by a program deleting its own executable from the file system? I assume it keeps the inode open (is that the right terminology?) and won't be an issue, but could you please confirm that?
  3. Out of curiosity, what would happen if you deleted the plist and executable without first unloading or removing it with launchctl?

Thanks

are these the right steps for uninstallation?

I do this is in this order:

  1. Remove property list.

  2. Remove helper tool.

  3. Remove the job.

I see that launchctl remove is an synchronous variant of unload.

That’s not quite right. The unload command requires a property list whereas the remove command works on a job label, which is important because I’ve already removed the property list when I invoke this.

Is there any danger posed by a program deleting its own executable from the file system?

No.

Out of curiosity, what would happen if you deleted the plist and executable without first unloading or removing it with launchctl?

Probably nothing good (-:

The problem with deleting the launchd property list is that the job is still loaded but there’s nothing tracking that. On the plus side, this state will be cleared when you restart [1].

Removing the executable causes an obvious problem. If the job stops for any reason, launchd may end up trying to start it again, and that’ll fail if the executable is missing.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] It’s possible that the system might have some details about the job cached but the one case where I know that happens, the disabled state, is irrelevant to this discussion because you know the job is enabled.

I do this is in this order:

  1. Remove property list.
  2. Remove helper tool.
  3. Remove the job.

Hi @quinn, are you sure we need to 'Remove the job' here. I followed the steps you captured here to remove my 'helper tool' using 'sudo launchctl remove /Library/LaunchDaemons/com.my***.Installutil.plist' and I had to and running SMJobBless() didnt work again. I even rebooted my macbook which didnt help. SMJobBless was not giving any error and no errors were captured in console logs. Console log was showing 'backgroundtaskmanagementd' did the job normally:

2024-01-18 21:51:33.286201+0530 0x607f8    Default     0xa7a28              474    0    backgroundtaskmanagementd: [com.apple.backgroundtaskmanagement:main] effectiveItemDisposition: appURL=(null), type=legacy daemon, url=file:///Library/LaunchDaemons/com.my***.Installutil.plist, config={
    BTMConfigArguments =     (     
        "/Library/PrivilegedHelperTools/com.my***.Installutil"
    );    
    BTMConfigBundleIdentifiers =     (     
    );    
    BTMConfigExecutablePath = "/Library/PrivilegedHelperTools/com.my***.Installutil";
    BTMConfigLabel = "com.my***.Installutil";
}

After spending 2 days I thought to doubt the step captured here. To recover the setup I had to call SMJobBless() API which didnt start the helper tool, but copied the plist and helper too, to its location, then I had to run 'sudo launchctl load -w -F /Library/LaunchDaemons/com.my***.Installutil.plist' separately and then it started to work.

I am capturing the steps below for others so that they dont run into same problem:

How to uninstall a helper tool:

  1. Remove property list.
  2. Remove helper tool.

Note dont run 'launchctl remove' if you wise to reinstall the helpertool again. I still have to try 'SMJobRemove' API and check if that removes the job from the launchctl list. else @quinn can guide how to remove the job from launch list, otherwise as pointed by quinn earlier the next reboot will remove the entery from the list.