I've got a regular old MacOS/X GUI process, launched by the user double-clicking an icon, and is therefore running with that user's privileges.
What I'd like to do is have this GUI process spawn a child process and communicate with the child process over its stdin/stdout. That's all doable and working.
The trick is that I'd like the child process to be running as root, since the child process will need to do some things that require root access.
I expect that this will require opening a dialog asking the user to enter his administrator password, and that's okay for this use-case.
One way to do it would be to have the parent process run a shell command like this:
/usr/bin/osascript -e 'do shell script "/bin/bash ./my_script.sh" with administrator privileges'
... and have my_script.sh
run the program representing the child process, which I think will work, but is there a more elegant way to accomplish this (i.e. one that doesn't require writing out a shell script to disk somewhere, and yields a more user-friendly password-requester than one that rather mysteriously says "osascript wants to make changes")?
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.