Unable to use "launchctl asuser" in a fast user switching loginwindow

Background

Alright, so there's a lot of voodoo and undocumented stuff going on here but hopefully somebody can help me out. I've reverse engineered how stuff might work based on:

https://opensource.apple.com/source/launchd/launchd-442.21/support/launchctl.c.auto.html

https://developer.apple.com/library/archive/technotes/tn2083/_index.html#//apple_ref/doc/uid/DTS10003794-CH1-SUBSECTION10

I've got a launchdaemon running that spawns another process in the /dev/console bootstrap context in order to act as a remote desktop server. What I'm trying to accomplish here, is to run one of my processes as root in the current gui bootstrap context which is attached to the console.

There are several guesswork states in MacOS (11.6, M1) that I've discovered. When you boot a machine, the loginwindow process is run in the bootstrap context of 88 (_windowserver). This makes sense because this process is created by WindowServer. The current console UID is discoverable by running:

echo "show State:/Users/ConsoleUser" | scutil

You can also introspect loginwindow using launchctl procinfo and friends.

Note that, this is before any login has ever happened on the machine.

In this state I can do anything in the gui bootstrap context by running this from the launchdaemon: launchctl asuser 88 myprogram

In my case, I'm taking a screenshot using AppKit/CoreGraphics and checking some permissions.

Once a user logs in, that loginwindow gets blessed by the OS and ownership is transferred to the logged in user. If you lock the machine, you're still in the same bootstrap context and everything works as expected. You can also log out and log into another user and everything works as I expect it to in terms of who controls loginwindow.

However, as soon as you hit the "Switch user" button from the lock screen the following happens:

  1. A new loginwindow is spawned with the bootstrap context of root (UID of 0)
  2. launchctl asuser 0 myprog seems not to properly execute within the bootstrap context of root.

My guess is that: 1 is a bug(?), the fast user switching bootstrap context should probably run as 88 rather than 0.

A "fix" is running pkill loginwindow which nukes all gui sessions and restarts one loginwindow running in the bootstrap context of 88. This is of course not an acceptable solution.

Doing the same thing using launchctl bootstrap gui/0 doesn't work either. I understand that the concept of "bootstrap gui/0" and "asuser 0" sounds nonsensical and it probably is. I'm just trying to find a working solution here.

Is there a more proper way of being able run as root in the bootstrap context of a logged in/not yet logged in loginwindow?

In case anyone is curious, I'm porting this to MacOS: https://fleetdeck.io

asuser is unlikely to be helpful here; it’s a legacy command that doesn’t understand modern launchd constructs.

I've got a launch daemon running that spawns another process in the /dev/console bootstrap context in order to act as a remote desktop server.

The best way to achieve this goal — and this is the approach used by the built-in Screen Sharing feature and by most third-party screen sharing products — is to have two launchd jobs:

  • A launchd daemon to handle your networking and general state management

  • A launchd agent to do GUI work on the daemons behalf

The agent should have LimitLoadToSessionType set to Aqua and LoginWindow. The system will then start an instance of this in each GUI login session. That can use IPC (preferably XPC) to connect to your daemon, tell it about the state of the GUI login session it’s loaded in, and also perform work on the daemon’s behalf.

In the specific case of the pre-login context, you’ll find that the agent is started as root when the login window context starts up and then, when the user logs in, that instance is terminated and a new one is started as that user.

Your key references here should be:

Share and Enjoy

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

Thanks for the quick answer, Quinn!

The agent should have LimitLoadToSessionType set to Aqua and LoginWindow. The system will then start an instance of this in each GUI login session. That can use IPC (preferably XPC) to connect to your daemon, tell it about the state of the GUI login session it’s loaded in, and also perform work on the daemon’s behalf.

Is it possible to restrict the launchd agent to run in the current gui session attached the console? I'd prefer a single instance of the launchd agent to run in the gui session attached to the console, and no other launchd agents running in suspended gui sessions.

It would be preferable for me to have the daemon manually control the lifecycle of the launchd agent since sometimes we need to kill it during an update. Is the only known method compatible with all types of gui sessions to use a launchd config with RunAtLoad + LimitLoadToSessionType set to LoginWindow? Or could the launchd agent be configured as disabled and then manually loaded by the daemon using launchctl? For instance, using launchctl load? If so, I could terminate it manually and start it from the daemon as needed.

One hack I could do (which I'm not going to do), is to use LimitLoadToSessionType set to LoginWindow and have launchd launch the agent when nobody is logged in and use launchctl asuser when /dev/console is non-root. This would give me a 1:1 in terms of # of daemons:# of agents and the agent would always be running in an active gui session.

Is it possible to restrict the launchd agent to run in the current gui session attached the console?

No. But you can have each agent detect whether it’s running on the console and quiesce if it’s not.

and no other launchd agents running in suspended gui sessions.

You mention of “suspended” GUI login sessions suggests that you’ve missing an important wrinkle here: macOS supports multiple GUI login sessions being simultaneously active via Screen Sharing. These sessions are not on the console but in no way are they ‘suspended’.

sometimes we need to kill it during an update.

You can solve this within the architecture I’ve described: Have the daemon update the agent on disk and then send a command to the agent to tell it to terminate. When launchd relaunches the agent, it’ll be running the new code.

IMPORTANT When you update the code on disk, follow the rules described in Updating Mac Software.

It would be preferable for me to have the daemon manually control the lifecycle of the launchd agent

This is not how macOS works and if you attempt to do this you will have problems, either now or in the future.

macOS is an open system and so you can hack around with this as much as you like. However, the architecture I’ve described here is the only one that DTS supports. And for good reason. I’ve been helping developers with this issue since we introduced fast user switching back in Mac OS X (as it was known then) 10.3. The most significant change came in 10.5, when we added the launchd session hierarchical described in TN2083. My experience is that folks who don’t strictly follow this architecture always come a cropper at some point.

Share and Enjoy

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

Thanks for the info Quinn.

That article about Updating Mac Software makes a lot of sense and explains situations I've had when overwriting signed executables executables and having to reboot to fix runtime crashes. In our update logic, we extract a new .app to a temporary folder and then atomically replace the bundle in /Application using a rename operation of the temporary folder<>/Applications folder. It seems to work so far and I believe that it complies with those guidelines. When writing this I was primarily concerned with the update being transactional but it does make sense that the OS has some kind of filesystem cache tied to code signing verification that needs to be accounted for.

You mention of “suspended” GUI login sessions suggests that you’ve missing an important wrinkle here: macOS supports multiple GUI login sessions being simultaneously active via Screen Sharing. These sessions are not on the console but in no way are they ‘suspended’.

Interesting. I knew that screengrabs worked in non console gui sessions but I didn't know that input/interactivity worked. This is really great. Will experiment with this. I guess in theory you could have a ton of thin clients connected to one mac then :)

The architecture that you've described is the correct way to do this, I agree. In the long term we will massage the architecture towards splitting up the processes by network / gui. In the short term we'll restrict our use of launchctl commands and stick to one daemon plist and one agent plist with XPC communication between the daemon<>agents.

Unable to use "launchctl asuser" in a fast user switching loginwindow
 
 
Q