I was able to finally able to get it working, by studying the source code to cocoasudo as mentioned by n.m.
The tricky part was realizing that AuthorizationExecuteWithPrivileges gives the child process an euid ("effective user ID") of 0/root, so it can do things that require root access, but it doesn't change the child process's uid ("real user ID"); that remains set to the user ID of the parent process, so it isn't really seen by the rest of the system as a root-process.
That detail caused a number of things in my shell-script to not work correctly when launched in this manner; for one thing, when executing the script using /bin/bash
, even though whoami
indicated it was being executed as root, actions that required root access (such as copying a file to a root-only-writeable directory) would still fail. Curiously, /bin/sh
did not seem to suffer from that problem.
The other major stumbling block was that my script needed to call launchctl load -w com.mycompany.myproduct.plist
to start up a System LaunchDaemon, but launchctl
bases its decision on whether to install a service as user-specific vs system-wide based on the Real User ID, so my service would always get installed as a user-specific service (running without root access, and only launched when the current user logs in) rather than as a system-wide service running as root.
After lots of guesswork and googling around, I was directed to the "Hints and Tips" section at the bottom of Apple Tech Note TN2083, which states:
IMPORTANT: For this to work properly, you must run launchctl as root
(both its EUID and RUID must be 0). This ensures that launchctl
communicates with the primary instance of launchd.
... and since there is (AFAIK) no system-supplied command-line tool to call setuid(0)
for me, I had to write my own little script-launcher tool in C:
int main(int argc, char ** argv)
{
if (argc >= 2)
{
if (setuid(0) == 0) // make us really root, not just "effectively root"
{
system(argv[1]); // then run the specified script file
return 0;
}
else perror("setuid");
}
else printf("Usage: setuid_script_launcher <path_to_script>\n");
return 10;
}
That was way more work than I think it should have been to figure out, but it's all working now. I'm describing it here to spare the next person some pain.