Team BBL
Previous Page Next Page

8.13. system Function

It is convenient to execute a command string from within a program. For example, assume that we want to put a time-and-date stamp into a certain file. We could use the functions we describe in Section 6.10 to do this: call time to get the current calendar time, then call localtime to convert it to a broken-down time, and then call strftime to format the result, and write the results to the file. It is much easier, however, to say

system("date > file");

ISO C defines the system function, but its operation is strongly system dependent. POSIX.1 includes the system interface, expanding on the ISO C definition to describe its behavior in a POSIX environment.

#include <stdlib.h>

int system(const char *cmdstring);

Returns: (see below)


If cmdstring is a null pointer, system returns nonzero only if a command processor is available. This feature determines whether the system function is supported on a given operating system. Under the UNIX System, system is always available.

Because system is implemented by calling fork, exec, and waitpid, there are three types of return values.

  1. If either the fork fails or waitpid returns an error other than EINTR, system returns 1 with errno set to indicate the error.

  2. If the exec fails, implying that the shell can't be executed, the return value is as if the shell had executed exit(127).

  3. Otherwise, all three functionsfork, exec, and waitpidsucceed, and the return value from system is the termination status of the shell, in the format specified for waitpid.

    Some older implementations of system returned an error (EINTR) if waitpid was interrupted by a caught signal. Because there is no cleanup strategy that an application can use to recover from this type of error, POSIX later added the requirement that system not return an error in this case. (We discuss interrupted system calls in Section 10.5.)

Figure 8.22 shows an implementation of the system function. The one feature that it doesn't handle is signals. We'll update this function with signal handling in Section 10.18.

Figure 8.22. The system function, without signal handling
#include    <sys/wait.h>
#include    <errno.h>
#include    <unistd.h>

int
system(const char *cmdstring)    /* version without signal handling */
{
    pid_t   pid;
    int     status;

    if (cmdstring == NULL)
        return(1);      /* always a command processor with UNIX */

    if ((pid = fork()) < 0) {
        status = -1;    /* probably out of processes */
    } else if (pid == 0) {              /* child */
        execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
        _exit(127);     /* execl error */
    } else {                             /* parent */
        while (waitpid(pid, &status, 0) < 0) {
            if (errno != EINTR) {
                status = -1; /* error other than EINTR from waitpid() */
                break;
            }
        }
    }

    return(status);
}

The shell's -c option tells it to take the next command-line argumentcmdstring, in this caseas its command input instead of reading from standard input or from a given file. The shell parses this null-terminated C string and breaks it up into separate command-line arguments for the command. The actual command string that is passed to the shell can contain any valid shell commands. For example, input and output redirection using < and > can be used.

If we didn't use the shell to execute the command, but tried to execute the command ourself, it would be more difficult. First, we would want to call execlp instead of execl, to use the PATH variable, like the shell. We would also have to break up the null-terminated C string into separate command-line arguments for the call to execlp. Finally, we wouldn't be able to use any of the shell metacharacters.

Note that we call _exit instead of exit. We do this to prevent any standard I/O buffers, which would have been copied from the parent to the child across the fork, from being flushed in the child.

We can test this version of system with the program shown in Figure 8.23. (The pr_exit function was defined in Figure 8.5.)

Figure 8.23. Calling the system function
#include "apue.h"
#include <sys/wait.h>

int
main(void)
{
    int      status;

    if ((status = system("date")) < 0)
        err_sys("system() error");
    pr_exit(status);

   if ((status = system("nosuchcommand")) < 0)
       err_sys("system() error");
   pr_exit(status);

   if ((status = system("who; exit 44")) < 0)
       err_sys("system() error");
   pr_exit(status);

   exit(0);
}

Running the program in Figure 8.23 gives us

   $ ./a.out
   Sun Mar 21 18:41:32 EST 2004
   normal termination, exit status = 0     for date
   sh: nosuchcommand: command not found
   normal termination, exit status = 127   for nosuchcommand
   sar      :0       Mar 18 19:45
   sar      pts/0    Mar 18 19:45 (:0)
   sar      pts/1    Mar 18 19:45 (:0)
   sar      pts/2    Mar 18 19:45 (:0)
   sar      pts/3    Mar 18 19:45 (:0)
   normal termination, exit status = 44   for exit

The advantage in using system, instead of using fork and exec directly, is that system does all the required error handling and (in our next version of this function in Section 10.18) all the required signal handling.

Earlier systems, including SVR3.2 and 4.3BSD, didn't have the waitpid function available. Instead, the parent waited for the child, using a statement such as

   while ((lastpid = wait(&status)) != pid && lastpid != -1)
    ;

A problem occurs if the process that calls system has spawned its own children before calling system. Because the while statement above keeps looping until the child that was generated by system terminates, if any children of the process terminate before the process identified by pid, then the process ID and termination status of these other children are discarded by the while statement. Indeed, this inability to wait for a specific child is one of the reasons given in the POSIX.1 Rationale for including the waitpid function. We'll see in Section 15.3 that the same problem occurs with the popen and pclose functions, if the system doesn't provide a waitpid function.

Set-User-ID Programs

What happens if we call system from a set-user-ID program? Doing so is a security hole and should never be done. Figure 8.24 shows a simple program that just calls system for its command-line argument.

Figure 8.24. Execute the command-line argument using system
#include "apue.h"

int
main(int argc, char *argv[])
{
    int     status;

    if (argc < 2)
        err_quit("command-line argument required");

    if ((status = system(argv[1])) < 0)
        err_sys("system() error");
    pr_exit(status);

    exit(0);
}

We'll compile this program into the executable file tsys.

Figure 8.25 shows another simple program that prints its real and effective user IDs.

Figure 8.25. Print real and effective user IDs
#include "apue.h"

int
main(void)
{
    printf("real uid = %d, effective uid = %d\n", getuid(), geteuid());
    exit(0);
}

We'll compile this program into the executable file printuids. Running both programs gives us the following:

   $ tsys printuids                          normal execution, no special privileges
   real uid = 205, effective uid = 205
   normal termination, exit status = 0
   $ su                                      become superuser
   Password:                                 enter superuser password
   # chown root tsys                         change owner
   # chmod u+s tsys                          make set-user-ID
   # ls -l tsys                              verify file's permissions and owner
   -rwsrwxr-x 1 root       16361 Mar 16 16:59 tsys
   # exit                                    leave superuser shell
   $ tsys printuids
   real uid = 205, effective uid = 0         oops, this is a security hole
   normal termination, exit status = 0

The superuser permissions that we gave the tsys program are retained across the fork and exec that are done by system.

When /bin/sh is bash version 2, the previous example doesn't work, because bash will reset the effective user ID to the real user ID when they don't match.

If it is running with special permissionseither set-user-ID or set-group-IDand wants to spawn another process, a process should use fork and exec directly, being certain to change back to normal permissions after the fork, before calling exec. The system function should never be used from a set-user-ID or a set-group-ID program.

One reason for this admonition is that system invokes the shell to parse the command string, and the shell uses its IFS variable as the input field separator. Older versions of the shell didn't reset this variable to a normal set of characters when invoked. This allowed a malicious user to set IFS before system was called, causing system to execute a different program.

    Team BBL
    Previous Page Next Page