Using Elevated Privileges to Run System Functions

I’m posting this in the hopes that some time in the future this code will help someone with some struggles I have just been through. So if someone can benefit from this - great.

In OS X, in order to perform certain system level functions, you need utilize Apple’s Authorization Services API. This ends up elevating the user rights and allows execution of sudo or root commands. Using the AuthorizationMBS classes from MonkeyBread, this is fairly easily accomplished. Without too much effort you can do things like an an IP address to one of your NICs. Or you can copy files into places where authentication is needed. There are other actions though where the AuthorizationMBS class is not enough. You actually have to run code as root. One example of such an action is things using the launchctl command.

In my case, I wanted to take advantage of using OS X’s built in TFTP server to send firmware files to a device that communicates with my software. The TFTPd daemon is not normally running by default. You could start it by calling tftpd directly, but that is not the preferred way of doing it. Apple’s man documentation specifically says to use launchctl to control the startup and shutdown of the TFTP server. I want to detail for the community what I have done with TFTP as an example so that others may some day follow in my footsteps through this minefield.

Before I offer up my code and what I have done, I wish to thank several community members for their assistance: @Norman Palardy, @jim mckay, @Eduardo Gutierrez de Oliveira and @Tim Jones. Without their help, I would not have been able to get to where I am.

So let’s get started:

1.) Modify the plist file:

OS X launchctl services start up based on the contents of their plist file. These files are normally stored in /System/Library/LaunchDaemons. In order to properly run your service, you will need to edit the plist file prior to starting the service. Instead of actually modifying the copy of my plist file in question (tftp.plist), I decided to create an entirely new plist file in my temporary directory that had all the parameters that I wanted/needed. So here is my code for that:

Sub SetUpOSXPlist()
  #If TargetMacOS Then
    Dim f as FolderItem = App.ExecutableFile.Parent.Parent.Child("Resources")  ' This is the path to where I will keep my firmware files.  
                                                                                                                                           ' The TFTP root directory will be here
    If f = Nil Then Return
    Dim TempPath as FolderItem = SpecialFolder.Temporary
    Dim f1 as FolderItem = SpecialFolder.Temporary.Child("tftp.plist")
    If f1 = Nil Then Return
    Dim bs as BinaryStream = BinaryStream.Create(f1,true)
    bs.write("<?xml version=""1.0"" encoding=""UTF-8""?>"+EndOfLine.Unix)
    bs.Write("<!DOCTYPE plist PUBLIC ""-//Apple//DTD PLIST 1.0//EN"" """">"+EndOfLine.Unix)
    bs.write("<plist version=""1.0"">"+EndOfLine.Unix)
    bs.write("<string>"+f.nativepath+"</string>"+EndOfLine.Unix)     ' Set the root directory here
End Sub

2.) Stop the service in case it exists - We don’t necessarily know if another program or process has started the service in question. So we want to stop it first.

3.) Make a backup of the existing plist file.

4.) Copy the plist file we created to the /System/Library/LaunchDaemons folder

These 3 steps are carried out in the following code which is in the open event of my window where I need these functions. In the code below, there is a call to RunAuthShellOSX. This is a method that calls the AuthorizationMBS classes. It is documented in the MBS examples and will not be documented here.

    Dim f as FolderItem = App.ExecutableFile.Parent.Parent.Child("Resources")  ' Where I keep all my firmware files and helper apps
    If f <> Nil and f.Exists Then
      // Set up plistFile
      Dim ftmp as FolderItem = SpecialFolder.Temporary.child("tftp.plist")
      If ftmp = Nil or Not ftmp.Exists Then Return
      Dim MyScript as String
      Dim MyParams as String =""
// The commands in MyScript below are the shell commands I want to execute using elevated permissions.  I first set the permissions for my resources folder
// so that the TFTP service can serve them.  Maybe this is not a good idea to change permissions inside the app bundle, but it works.
// I then unload the TFTP service using launchctl.  This stops the service.  This launchctl command CAN be called in normal elevated privileges. 
// I then have commands to rename the existing tftp.plist file to tftp.bak.  And then copy in the new plist file to the /System/Library/LaunchDaemons folder
      MyScript = "#! /bin/sh"+endofline.unix+endofline.unix+"chmod 777 "+f.shellpath+EndOfLine.UNIX+ _
      "launchctl unload -F /System/Library/LaunchDaemons/tftp.plist"+endofline.unix+ _
      "mv -f  /System/Library/LaunchDaemons/tftp.plist /System/Library/LaunchDaemons/tftp.bak"+EndOfLine.UNIX+_
      "cp "+ftmp.nativepath+" /System/Library/LaunchDaemons/tftp.plist"+EndOfLine.UNIX

// Run the script      
      If RunAuthShellOSX(MyScript,MyParams) Then      ' RunAuthShellOSX is a method that uses the AuthorizationMBS method.
        MyScript = "#! /bin/sh"+endofline.unix+endofline.unix+f.ShellPath+"/TFTPStartHelper True"
        MyParams = ""

        If RunAuthShellOSX(MyScript,MyParams) Then
          System.DebugLog("TFTP Server Started")
        End IF
      End If
    End If

Now after I run the first script in the AuthorizationMBS methods, there’s a line there referencing TFTPStartHelper. This is where the extra elevated privileges come into play. You cannot just be authorized and start a launchctl service. You have to actually be running as root. So TFTPStartHelper is a helper app that I wrote that has its permissions and setuid bit set to that of root. So the app actually runs as root and carries out its commands as root. Thanks to @jim mckay for this idea. This app is a small console app that is copied into the resources folder of my app bundle during a post compile copy step. The code for this app is below. After compiling the app, run the following commands on from a terminal window:

sudo chown root /path/to/help
sudo chgrp wheel  /path/to/help
sudo chmod 6755 /path/to/help

/path/to/help is the path to your compiled helper application. This is a one time thing and should set things up so the helper runs as root. Then set up a post compile copy step to put the helper where you need it.

See next post for more as the forum won’t let me post a longer post…

OK, here is the helper app code. I have a argument parameter I use that can be True or False from the command line. True starts the service while false stops it.

Function Run(args() as String) As Integer
  Dim s as new shell
// Set up the setuid method in code just in case our efforts previously in the terminal after compilation were not enough.
// This is what I would call "belt and suspenders" to make 100% sure we are at root level.

// Thanks to Eduardo for this
  soft declare function setuid lib "/usr/lib/libc.dylib" (uid as Integer) as Integer
  dim setuid_result as Integer = setuid(0)
  if setuid_result = -1 then
    system.debuglog "Couldn't raise root setuid"
    return 1
  end if
// Now process our commands in a shell object
    If args(1) = "True" Then
      s.Execute("launchctl load -F /System/Library/LaunchDaemons/tftp.plist")  // Load the daemon.  - no need for sudo as we are already there.
      s.Execute("launchctl start")  // Start the service
      s.Execute("launchctl stop")  // stop the service
      s.Execute("launchctl unload -F /System/Library/LaunchDaemons/tftp.plist")  // unload the daemon
    End If
    // No argument was entered when the app was called.  Assume shut down the services then.
    s.Execute("launchctl stop")
    s.Execute(" launchctl unload -F /System/Library/LaunchDaemons/tftp.plist")   
  End Try
End Function

A key thing to note here is to NOT use SUDO in the commands in the helper app. Sudo expects that there be a user interaction terminal to get the root password and you don’t have that - so it will fail. It is not needed anyhow, since the helper app is already running as root.

So there you go. So the code here in our open event describe above, runs the helper app:

    MyScript = "#! /bin/sh"+endofline.unix+endofline.unix+f.ShellPath+"/TFTPStartHelper True"
        MyParams = ""

        If RunAuthShellOSX(MyScript,MyParams) Then
          System.DebugLog("TFTP Server Started")
        End IF

So now you are running the helper app as root using Authorization Services in OS X. It works quite nicely.

Now, finally when you are done and want to quit everything and set it back to normal, you need to reverse the process. Stop the service just loaded, copy back the original plist files, etc. Here’s what I have in my Window.Close event:

    Dim f as FolderItem = App.ExecutableFile.Parent.Parent.Child("Resources")

// Script to call the helper app to stop the services
    Dim MyScript as String = "#! /bin/sh"+endofline.unix+endofline.unix+f.ShellPath+"/TFTPStartHelper False"
    Dim MyParams as String =""
    If RunAuthShellOSX(MyScript,MyParams) Then
      System.DebugLog "TFTP Server Stopped"
// Now we want to delete the existing tftp.plist file as it was the one we created.
      MyScript = "#! /bin/sh"+endofline+endofline+"rm /System/Library/LaunchDaemons/tftp.plist"
      If RunAuthShellOSX(MyScript,MyParams) Then
// Now we want to rename the backup as it is the original plist file for the service
        MyScript = "#! /bin/sh"+endofline+endofline+"mv /System/Library/LaunchDaemons/tftp.bak /System/Library/LaunchDaemons/tftp.plist"
        If RunAuthShellOSX(MyScript,MyParams) Then
          System.DebugLog "Done"
        End If
      End If
    End If

There. I’ve just helped you through a minefield. My code isn’t necessarily complete. I need to add better error handling for operations that may fail or NIL folder items paths. I just finished all this very early this morning ! But this works quite well and is functioning solidly.

Please feel free to add comments. I hope someone, someday finds this useful