OpenSolaris

You are not signed in. Sign in or register.

There have been quite a few posts, articles, and resources made available discussing the new process privilege model introduced the Solaris 10 operating system. The majority of them have focused on describing what this new privilege model is, why it is useful and how it can be used through mechanisms such as Solaris RBAC, SMF, and ppriv(1). With the launch of OpenSolaris, I feel the urge to discuss privileges from the view of a software developer (not that I am one anymore, but please indulge me a bit). If you are interested in this kind of thing, you can find more information about this in the Solaris 10 product documentation.

Let's look at a how a developer could modify a program to be privilege aware. In particular, for our example, let's take a set-id program and configure it such that it:

  1. drops any privileges that it will never need;
  2. enables the remaining privileges exactly when it needs them;
  3. relinquishes the use of privileges when they are no longer needed

Sounds pretty straight forward, right? Essentially, we just want to ensure that a program is only running with those privileges that it needs, and it activates those privileges only when it needs them. To illustrate this concept, let's take a look at ping.c . This program was converted to use Solaris privileges by Casper Dik way back in 2003. I chose to use it for this example because:

  1. the ping(1M) source code is simple and straightforward to read and understand, and
  2. all of the changes needed to make it privilege aware were contained in one file

There is nothing special about ping with respect to privileges and the same techniques described below could be applied to other programs whether they are set-id or not.

So, let's begin this tale back in February of 2003 before the ping command was made privilege aware. In those days, ping was a set-uid root program that controlled its use of privilege using seteuid(2). When the program started as root, it quickly set its effective UID to the UID of the calling user to run with less privilege. When it came time to execute a privileged operation, the code issued another seteuid call that reset its EUID to root so that the privileged operation could succeed.

With the introduction of process privileges in Solaris 10, this model was no longer needed. Rather than executing code as EUID 0, specific privileges are used to define the types of privileged operations that are be permitted. This is a huge step forward as privilege aware programs will now run only with the privileges that they need (exactly when they need them). So, enough of the fluff, let's get to the code! Note that all of the following code examples were taken from ping.c .

The first thing that you will notice that if you want to configure a program to be privilege aware, you will need to include a new header that will declare functions and define constants used by the privilege functions described below:

     72    #include <priv_utils.h>

With the header out of the way, we move into main where the first thing that we do is drop all of the privileges that we do not need:

    247         /*
    248          * This program needs the net_icmpaccess privileges.  We'll fail
    249          * on the socket call and report the error there when we have
    250          * insufficient privileges.
    251          */
    252         (void) __init_suid_priv(PU_CLEARLIMITSET, PRIV_NET_ICMPACCESS,
    253             (char *)NULL);

Remember that ping is still set-uid root. This means that when it is started, it will have the privileges that have been assigned to root which by default is all. The purpose of the __init_suid_priv function is to do the following (described in priv_utils.h):

     48 /*
     49  * Should be run at the start of a set-uid root program;
     50  * if the effective uid == 0 and the real uid != 0,
     51  * the specified privileges X are assigned as follows:
     52  *
     53  * P = I + X + B (B added insofar allowable from L)
     54  * E = I
     55  * (i.e., the requested privileges are dormant, not active)
     56  * Then resets all uids to the invoking uid; no-op if euid == uid == 0.
     57  *
     58  * flags: PU_LIMITPRIVS, PU_CLEARLIMITSET, PU_CLEARINHERITABLE
     59  *
     60  * Caches the required privileges for use by __priv_bracket().
     61  *
     62  */

So this function (as used in the ping.c code) will reset the real and effective UID of the process to that of the calling user so that it is no longer running as root. Further, it will clear the limit set of the process which means that any children spawned by this process will themselves have no privileges. Lastly, this function will also add the net_icmpaccess privilege to the process' permitted set so that it can be enabled and used when necessary. The process' four privilege sets (effective, inheritable, permitted and limit) will look like this:

# ppriv -S `pgrep ping`
14527: ping localhost
flags = PRIV_AWARE
E: basic
I: basic
P: basic,net_icmpaccess
L: none

According to ppriv, the net_icmpaccess privilege is used to allow a process to send and receive ICMP packets:

$ ppriv -lv net_icmpaccess
net_icmpaccess
Allows a process to send and receive ICMP packets.

So rather than having the potential to access all of root's power, a ping process will now run as the calling user's UID with a single (non-basic) privilege that allows the sending or receiving ICMP packets. Sounds great, but we are not done yet! In priv_utils.h, you will find more instruction as to how to proceed:

     65 /*
     66  * After calling __init_suid_priv we can __priv_bracket(PRIV_ON) and
     67  * __priv_bracket(PRIV_OFF) and __priv_relinquish to get rid of the
     68  * privileges forever.
     69  */

Recall that before ping was made privilege aware, it used seteuid to control when its privileges were in effect. This was necessary for the program to only run in a privileged capacity when it needs to execute privileged operations. In the new model, the process leverages a capability called privilege bracketing which controls when a privilege (defined in the process' permitted set) is made effective. To see this capability in action, take a look at the following code which appears in the setup_socket function:

   1196         /* now we need the net_icmpaccess privilege */
   1197         (void) __priv_bracket(PRIV_ON);
   1198
   1199         recv_sock = socket(family, SOCK_RAW,
   1200             (family == AF_INET) ? IPPROTO_ICMP : IPPROTO_ICMPV6);
   1201
   1202         if (recv_sock < 0) {
   1203                 Fprintf(stderr, "%s: socket %s\n", progname, strerror(errno));
   1204                 exit(EXIT_FAILURE);
   1205         }
   1206
   1207         /* revert to non-privileged user after opening sockets */
   1208         (void) __priv_bracket(PRIV_OFF);

As you can see, the __priv_bracket function is used around the privileged operation (in this case the socket(2) call) to control whether or not the instructions are executed with privilege. This is one form of privilege bracketing that enables and disables all of the privileges cached by the __init_suid_priv function called earlier in the program (and described above). There are other privilege manipulation functions available to allow more fine grained control if needed.

Our final step is to relinquish the privileges when we are sure that we will no longer need them:

    602         __priv_relinquish();

The __priv_relinquish function is called after the setup_socket function has completed in main. Since the program will no longer need the net_raw_icmpaccess privilege (the only non-basic privilege available to the process), it can now be safely dropped. The __priv_relinquish function is used for just this purpose. Note that once a privilege is relinquished, it is gone. If you think that you may need the privilege later in the program, you should simply disable its use (removing it from the process' effective set) until you need it again.

So, now that we have described this process, it is time to talk about one gotcha.

During the development of this HOWTO, I was reminded by Darren Moffat that the privilege functions and header file described above are private to Solaris (and more specifically the ON [OS and Networking] consolidation). So, the approach described above will work just fine if you are modifying set-id programs in ON such as atq, atrm, traceroute, lpstat, and the like.

What if you are developing programs for another consolidation or something that is entirely external to OpenSolaris? Can you still implement privilege bracketing?

Absolutely! In addition to the ON private header files and functions discussed above, there are also a set of public header files and functions that can also be used. For the sake of comparison, let's now discuss how we can adapt the privilege-aware version of ping.c discussed above to use the public privilege manipulation header files and functions to accomplish the same result - namely making the program privilege-aware and implementing bracketing around privileged operations.

Without further ado, let's just dive back into the code. By way of convention, I will be showing only the changes that need be implemented to convert the ping.c program to use the new public functions.

The first thing that you will notice is that a different header file is used. If you want to use the public interfaces, then you should be sure to include priv.h and not priv_utils.h:

72c72
< #include <priv_utils.h>
---
> #include <priv.h>

Next, we move to the section of the code that configured the privilege sets at the start of the program. This was done in order to drop any privileges that were not needed and disable those that were left (and not needed right now). This was originally accomplished using the __init_suid_priv function which provided a convenient wrapper for the functionality described below. Rather than a single line, we need to do a little more work. To make the code easier to follow, a new function, setup_privs, was created to do the initial privilege operations. This handles the majority of the work originally done by __init_suid_priv. This code is fairly well documented, so I will not go into too much detail as to what it does.

225a226
> static priv_set_t *setup_privs(void);
227a229,298
>  * setup_privs()
>  */
> priv_set_t *
> setup_privs(void)
> {
>     priv_set_t *pPrivSet = NULL;
>     priv_set_t *lPrivSet = NULL;
>
>     /*
>      * Start with the 'basic' privilege set and then remove any
>      * of the 'basic' privileges that will not be needed by this
>      * process.  The 'net_icmpaccess' privilege will be added
>      * since we know that we will need it for the permitted set.
>      */
>
>     if ((pPrivSet = priv_str_to_set("basic", ",", NULL)) == NULL) {
>             perror("priv_str_to_set");
>             return (NULL);
>     }
>
>     /*
>      * Let's clear all of the privileges we know we will not
>      * need from the 'basic' set.
>      */
>
>     (void) priv_delset(pPrivSet, PRIV_FILE_LINK_ANY);
>     (void) priv_delset(pPrivSet, PRIV_PROC_EXEC);
>     (void) priv_delset(pPrivSet, PRIV_PROC_FORK);
>     (void) priv_delset(pPrivSet, PRIV_PROC_INFO);
>     (void) priv_delset(pPrivSet, PRIV_PROC_SESSION);
>
>     /* Next add the known required privilege, 'net_icmpaccess' */
>
>     (void) priv_addset(pPrivSet, PRIV_NET_ICMPACCESS);
>
>     /* Set the permitted privilege set. */
>
>     if (setppriv(PRIV_SET, PRIV_PERMITTED, pPrivSet) != 0) {
>             perror("setppriv(PRIV_SET, PRIV_PERMITTED)");
>             return (NULL);
>     }
>
>     /* Ensure that the 'net_icmpaccess' privilege is off by default. */
>
>     if (priv_set(PRIV_OFF, PRIV_EFFECTIVE, PRIV_NET_ICMPACCESS,
>             NULL) != 0) {
>             perror("priv_set(PRIV_OFF, PRIV_EFFECTIVE)");
>             return (NULL);
>     }
>
>     /* Clear the limit set. */
>
>     if ((lPrivSet = priv_allocset()) == NULL) {
>             perror("priv_allocset");
>             return (NULL);
>     }
>
>     priv_emptyset(lPrivSet);
>
>     if (setppriv(PRIV_SET, PRIV_LIMIT, lPrivSet) != 0) {
>             perror("setppriv(PRIV_SET, PRIV_LIMIT)");
>             return (NULL);
>     }
>
>     priv_freeset(lPrivSet);
>
>     return (pPrivSet);
> }
>
> /*

So, why do we go through the process of starting with the basic set of privileges and removing them all? Why not just start out with no privileges and simply the code? The answer lies in the fact that the basic privilege set is not intended to be static. Over time additional non-administrative privileges may be added. If you started with simply no privileges, then you may find that your code will fail as it will need a privilege for an operation that previously did not require one. In essence, this is a way of future-proofing your code so that it can better adapt to changes in Solaris down the road.

So at this point, we have defined a function that will help minic most of the behavior that had been done by __init_suid_priv. Let's take a look at how this function is used:

243a315
>     priv_set_t *privSet = NULL;
252,253d323
<     (void) __init_suid_priv(PU_CLEARLIMITSET, PRIV_NET_ICMPACCESS,
<         (char *)NULL);
254a325,337
>     if ((privSet = setup_privs()) == NULL) {
>             exit(EXIT_FAILURE);
>     }
>
>     /*
>      * Reset the real and effective UIDs for this process.
>      */
>
>     if (setreuid(getuid(), getuid()) != 0) {
>             perror("setreuid");
>             exit(EXIT_FAILURE);
>     }
>

As you can see, the __init_suid_priv call has been replaced by calls to both setup_privs and setreuid(2) functions. This is a fairly simple replacement since all of the "hard work" was done in the setup_privs function. We capture the privilege set parameter, privSet, as we will need it later in the code when it comes time to relinquish our privileges. Let's continue sequentially down the code to see where other replacements are needed. Frankly, the hard part is over. The rest of the changes needed to complete the conversion from private to public privilege manipulation functions are trivial. Let's take a look at the next replacement:

602c685,687
<     __priv_relinquish();
---
>     /*
>      * Clear the permitted set of the 'net_icmpaccess' privilege.
>      */
603a689,696
>     (void) priv_delset(privSet, PRIV_NET_ICMPACCESS);
>
>     if (setppriv(PRIV_SET, PRIV_PERMITTED, privSet) != 0) {
>             perror("setppriv(PRIV_PERMITTED)");
>             exit(EXIT_FAILURE);
>     }
>     priv_freeset(privSet);
>

In this case, we are replacing the __priv_relinquish function with a call to setppriv(2). Before we can do this however, we need to remove the net_icmpaccess privilege from privSet using the priv_delset function. Remember that privSet (returned to us from setup_privs earlier in the code) contains the privileges from the basic set that we had not already dropped as well as the net_icmpaccess privilege. By removing the net_icmpaccess privilege, setppriv will set to the process' permitted privilege set to the non-dropped basic privileges (which in this particular case is none although as Solaris continues to evolve and new non-administrative privileges are added, this may change).

Moving along to the next section of replacement code, we come to where the bracketing of the net_icmpaccess privilege is enforced. In this case, the call to __priv_bracket is replaced with a call to priv_set(3C) function. priv_set is called with the PRIV_ON parameter which enables the net_icmpaccess privilege for the process' effective privilege set.

1197c1290,1293
<     (void) __priv_bracket(PRIV_ON);
---
>     if (priv_set(PRIV_ON, PRIV_EFFECTIVE, PRIV_NET_ICMPACCESS, NULL) != 0) {
>             perror("priv_set(PRIV_ON, PRIV_EFFECTIVE)");
>             exit(EXIT_FAILURE);
>     }

Similarly, the companion instance of __priv_bracket is replaced with another call to priv_set once the privileged operations are complete to remove the net_icmpaccess privilege (from the process' effective privilege set) - therefore completing the bracketing of privilege.

1208c1304,1308
<     (void) __priv_bracket(PRIV_OFF);
---
>     if (priv_set(PRIV_OFF, PRIV_EFFECTIVE, PRIV_NET_ICMPACCESS,
>             NULL) != 0) {
>             perror("priv_set(PRIV_OFF, PRIV_EFFECTIVE)");
>             exit(EXIT_FAILURE);
>     }

That is all there is to it. As you can tell, there is a bit more work in the initial setup of a process' privilege sets, but once that is complete, the use of the public privilege manipulation functions is straightforward. I hope that this helps to illustrate how you can privilege enable your code whether you choose to use either method described above.

Last, but certainly not least, I would like to offer my sincere thanks to Casper, Joep and Darren for their help with this article. You guys are the best!