Tuesday, August 01, 2006

 

Playing with process contracts in Solaris 10

Even though I know the general details about how process contracts work in Solaris 10, and had seen the list of functions that are used in manipulating process contracts, I hadn't actually tried playing with the API before last week. I started playing with it last week because I wanted to re-implement Sun's process contract handling in sshd so that I could submit it as a patch to OpenSSH.

I first went looking for documentation on the API. There are obviously the man pages for contract(4), libcontract(3CONTRACT), and all of the library calls, but I couldn't find much more than that aside from some threads on the smf-discuss@opensolaris.org forum. The man pages contain all you need, it's just a little bit harder to dig out the information than it would be in different documentation.

I posed some questions about process contracts and then tried to answer them programmatically. The first question I asked was, how do I determine what process contract I belong to? In retrospect, this may not even be a useful question, but it was the first one to come to mind. I have yet to figure this one out, though. I spent an hour or two trying to figure it out, but I eventually moved on, as it was tangential to what I really wanted to accomplish.

The next obvious question was, how do I create a new process contract? This is fairly simple: you open a new process contract template, set the terms of the contract, and activate the template. Once you've done this, a fork() will create a child in a new process contract. Here's an example:

int
pre_fork_activate_template()
{
        int             tmpl_fd;

        if ((tmpl_fd = open64("/system/contract/process/template", O_RDWR)) == -1) {
                perror("Can't open /system/contract/process/template");
                return -1;
        }
        if (ct_pr_tmpl_set_fatal(tmpl_fd, CT_PR_EV_HWERR|CT_PR_EV_SIGNAL) != 0){
                perror("Can't set process contract fatal events");
                return -1;
        }
        if (ct_tmpl_set_critical(tmpl_fd, CT_PR_EV_HWERR) != 0) {
                perror("Can't set process contract critical events");
                return -1;
        }
        if (ct_tmpl_activate(tmpl_fd) != 0) {
                perror("Can't activate process contract template");
                return -1;
        }

        return tmpl_fd;
}


(Of course, there's a potentially bad failure mode in the above code if it's a long-running daemon, as you could start leaking file descriptors if the open64() succeeds but any of the following function calls fail.)

And after this, a fork() will create a child in a new process contract. The parent will be the holder of this contract. This may not be what you want to do, as contracts aren't destroyed automatically when a child exits. This means that the parent could still be holding the contract long after the child is dead. For a short-lived process, this isn't a problem, but a long-running daemon with this behavior would be accumulating dead process contracts that count against certain limits (as discussed in this thread.) One way of handling this is to keep track of all the process contracts and reap them when they're no longer useful, or you could simply abandon the contract immediately. I've done the latter in this bit of code:

void
post_fork_contract_processing(int tmpl_fd,int pid)
{
        char            ctl_path[PATH_MAX];
        ctid_t          ctid;
        ct_stathdl_t    stathdl;
        int             ctl_fd;
        int             pathlen;
        int             stat_fd;

        /*
         * First clear the active template.
         */
        if (ct_tmpl_clear(tmpl_fd) != 0) {
                perror("Parent can't clear active template");
                return;
        }
        close(tmpl_fd);

        /*
         * If the fork didn't succeed (pid < 0), or if we're the child
         * (pid == 0), we have nothing more to do.
         */
        if (pid <= 0) {
                return;
        }

        /*
         * Now abandon the contract we've created.  This involves the
         * following steps:
         * - Get the contract id (ct_status_read(), ct_status_get_id())
         * - Get an fd for the ctl file for this contract
         *   (/system/contract/process//ctl)
         * - Abandon the contract (ct_ctl_abandon(fd))
         */
        if ((stat_fd = open64(CTFS_ROOT "/process/latest", O_RDONLY)) == -1) {
                perror("Parent can't open latest");
                return;
        }
        if (ct_status_read(stat_fd, CTD_COMMON, &stathdl) != 0) {
                perror("Parent can't read contract status");
                return;
        }
        if ((ctid = ct_status_get_id(stathdl)) < 0) {
                perror("ct_status_get_id() failed");
                ct_status_free(stathdl);
                return;
        }
        ct_status_free(stathdl);
        close(stat_fd);

        pathlen = snprintf(ctl_path, PATH_MAX, CTFS_ROOT "/process/%ld/ctl",ctid);
        if (pathlen > PATH_MAX) {
                fprintf(stderr,"Contract ctl file path exceeds maximum path length\n");
                return;
        }
        if ((ctl_fd = open64(ctl_path, O_WRONLY)) < 0) {
                perror("Parent couldn't open control file for child contract");
                return;
        }
        if (ct_ctl_abandon(ctl_fd) < 0) {
                perror("Parent couldn't abandon contract");
        }
        close(ctl_fd);
}


Note that getting the control file for the process contract for the child process involves getting the id of that contract so that we can construct the path for it under /system/contract/process.

And then (for completeness), I used this code to test things:

main()
{
        int             tmpl_fd;
        pid_t           pid;

        if ((tmpl_fd = pre_fork_activate_template()) < 0) {
                exit(1);
        }
        /*
         * Now that we've set the active template, fork a process to see
         * a new contract created.
         */
        if ((pid = fork()) < 0) {
                perror("Can't fork");
        }
        post_fork_contract_processing(tmpl_fd,pid);

        sleep(60);
}

Comments:
Have you found a way to modify process contracts _without_ having to write a custom binary? It seems that Sun would want to have a way to modify an existing contract without this level of complexity...
 
Actually, Sun does provide a way to start a new process in a new contract, ctrun. I described this here.
 
Post a Comment



<< Home

This page is powered by Blogger. Isn't yours?