Safe exec(), Avoiding Shell Execution, Closing File Descriptors

 

Be Careful When Executing a Program
Chapter 38.3 โ€” Safe exec(), Avoiding Shell Execution, Closing File Descriptors
โš™๏ธ exec() Safety
๐Ÿšซ No Shell Exec
๐Ÿ“ Close FDs

Why exec() in Privileged Programs is Dangerous

When a privileged program needs to run another program using exec(), system(), or popen(), it must be extremely careful. If the process’s privilege state isn’t fully reset before the exec, the new program inherits those privileges โ€” and may not be designed to handle them safely.

Even if the execed program is a trusted system utility, running it with unexpected root privileges or with open file descriptors to sensitive files creates dangerous, hard-to-predict behavior.

๐Ÿ” Rule 1: Drop Privilege Permanently Before exec()

Before calling exec(), a privileged program must ensure that all user and group IDs are reset to the real (unprivileged) values. The new program should start completely unprivileged and have no ability to regain privilege.

How exec() Interacts with Saved Set-UID

There is one useful behavior: a successful exec() copies the effective user ID to the saved set-user-ID. This means even if you only called seteuid(getuid()) before exec (which normally doesn’t change the saved set-UID), the subsequent exec will copy the now-unprivileged EUID into the saved set-UID of the new program.

Stage Real UID Effective UID Saved Set-UID
SUID-root program started (owner=200, caller=1000) 1000 200 200
After seteuid(getuid()) 1000 1000 200
After successful exec() 1000 1000 1000

The exec copies EUID (now 1000) into saved set-UID, so the new program is fully unprivileged. However, this only works if the exec succeeds. If exec fails, the saved set-UID is unchanged โ€” the process still has privilege=200. The program can then continue and do privileged work, which may be intentional (fallback behavior).

๐Ÿšซ Rule 2: Never exec() a Shell with Privileges

This is one of the most important rules in secure privileged programming: a privileged program must never exec a shell (bash, sh, zsh, etc.) while it still holds privilege.

Shells are general-purpose command interpreters with enormous power. Even if you don’t intend to provide interactive access, the shell can be manipulated through environment variables, startup scripts, argument parsing, and various other mechanisms to run arbitrary commands with your program’s effective UID.

โš ๏ธ Functions That Exec a Shell Indirectly
system() popen() execlp() execvp() execl(“/bin/sh”, …)

All of these are dangerous in privileged programs. system("cmd") literally calls /bin/sh -c cmd. A malicious user controlling PATH or IFS can redirect what gets executed, running arbitrary code as root.

Safe Alternative: Use execve() with Absolute Paths

If you must exec another program, use execve() directly with an absolute path. This bypasses PATH entirely:

/* DANGEROUS: relies on PATH โ€” attacker could put a fake 'ls' in PATH */
execvp("ls", argv);

/* SAFE: absolute path, no PATH lookup, no shell involved */
execve("/bin/ls", argv, envp);

Set-UID Scripts Are Also Unsafe

On systems that honor set-UID bits on scripts, running them is just as dangerous as execing a shell directly. Linux wisely ignores the set-UID/set-GID bits on scripts (shell scripts, Python scripts, etc.) for this reason. Even on systems that do support them, their use should be avoided.

๐Ÿ“ Rule 3: Close Privileged File Descriptors Before exec()

By default, open file descriptors survive across an exec(). If a privileged program opens a file that unprivileged users cannot read (say, /etc/shadow), the resulting file descriptor is a privileged resource. If the program then execs another program without closing that FD, the new program inherits it โ€” and can read the sensitive file.

There are two ways to prevent this:

Method 1: Explicit close()
Call close(fd) for each sensitive file descriptor before exec. Simple and clear but you must track all FDs.
Method 2: FD_CLOEXEC flag
Set the close-on-exec flag with fcntl(fd, F_SETFD, FD_CLOEXEC). The kernel automatically closes this FD on any exec. Best for FDs that should never be inherited.

๐Ÿ’ป Code Example 1: Safe exec() with Privilege Drop and FD Cleanup
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

/*
 * safe_exec(): Safely execute another program from a privileged context.
 * 
 * Steps:
 *  1. Drop all privileges permanently
 *  2. Close sensitive file descriptors  
 *  3. Sanitize environment
 *  4. exec with absolute path
 */
void safe_exec(const char *prog_path, char *const argv[])
{
    uid_t uid = getuid();
    gid_t gid = getgid();

    /* Step 1: Permanently drop ALL privileges */
    /* Drop supplementary groups first (still need EUID=0 for this) */
    if (setgroups(0, NULL) == -1) {
        perror("setgroups");
        exit(EXIT_FAILURE);
    }

    /* Drop group ID */
    if (setresgid(gid, gid, gid) == -1) {
        perror("setresgid");
        exit(EXIT_FAILURE);
    }

    /* Drop user ID last */
    if (setresuid(uid, uid, uid) == -1) {
        perror("setresuid");
        exit(EXIT_FAILURE);
    }

    /* Verify the drop succeeded */
    if (geteuid() != uid || getuid() != uid) {
        fprintf(stderr, "FATAL: privilege drop failed before exec\n");
        exit(EXIT_FAILURE);
    }

    /* Step 2: Close any sensitive FDs above stdin/stdout/stderr */
    /* Simple approach: close all FDs from 3 upward */
    int max_fd = (int)sysconf(_SC_OPEN_MAX);
    for (int fd = 3; fd < max_fd; fd++) {
        close(fd);  /* Ignore errors โ€” FD may not be open */
    }

    /* Step 3: Sanitize critical environment variables */
    /* Set PATH to known-safe value */
    if (setenv("PATH", "/usr/local/bin:/usr/bin:/bin", 1) == -1) {
        perror("setenv PATH");
        exit(EXIT_FAILURE);
    }
    /* Clear IFS โ€” prevents shell word-splitting exploits */
    if (setenv("IFS", "", 1) == -1) {
        perror("setenv IFS");
        exit(EXIT_FAILURE);
    }

    /* Step 4: exec with absolute path (never use execlp/execvp in privileged code) */
    execv(prog_path, argv);

    /* If we get here, exec failed */
    perror("execv");
    exit(EXIT_FAILURE);
}

int main(int argc, char *argv[])
{
    printf("Running as EUID: %d\n", (int)geteuid());

    /* Build the argument list */
    char *args[] = { "/bin/ls", "-la", "/tmp", NULL };

    /* This will drop privilege before executing ls */
    safe_exec("/bin/ls", args);

    /* Never reached */
    return 0;
}

๐Ÿ’ป Code Example 2: Setting FD_CLOEXEC on Sensitive Files
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

/*
 * Demonstrates two ways to prevent FD inheritance across exec():
 *  1. Open with O_CLOEXEC flag (atomic โ€” preferred)
 *  2. Set FD_CLOEXEC on existing FD using fcntl()
 */

int open_private_file(const char *path)
{
    int fd;

    /* Method 1: Open with O_CLOEXEC โ€” atomic, preferred */
    fd = open(path, O_RDONLY | O_CLOEXEC);
    if (fd == -1) {
        perror("open");
        return -1;
    }

    printf("Opened fd=%d for '%s' with O_CLOEXEC\n", fd, path);
    return fd;
}

void set_cloexec_on_existing(int fd)
{
    int flags;

    /* Method 2: Set FD_CLOEXEC on an already-open FD */
    flags = fcntl(fd, F_GETFD);
    if (flags == -1) {
        perror("fcntl F_GETFD");
        return;
    }

    flags |= FD_CLOEXEC;
    if (fcntl(fd, F_SETFD, flags) == -1) {
        perror("fcntl F_SETFD");
        return;
    }

    printf("Set FD_CLOEXEC on fd=%d\n", fd);
}

int main(void)
{
    /* Open a file that should not be inherited by child processes */
    int sensitive_fd = open_private_file("/etc/hostname");
    if (sensitive_fd == -1)
        return 1;

    /* Or set it after opening */
    int another_fd = open("/etc/os-release", O_RDONLY);
    if (another_fd != -1) {
        set_cloexec_on_existing(another_fd);
    }

    printf("\nBoth FDs will be automatically closed on exec()\n");
    printf("No privileged file access leaks to child programs.\n");

    /* These FDs would be closed if we called exec() here */
    close(sensitive_fd);
    if (another_fd != -1) close(another_fd);

    return 0;
}

๐Ÿ“Œ Key Terms

exec() execve() system() popen() FD_CLOEXEC O_CLOEXEC fcntl() setgroups() Absolute Path Shell Exec Attack PATH manipulation IFS attack
๐ŸŽฏ Interview Questions โ€” Safe Program Execution
Q1. Why is calling system() or popen() dangerous in a privileged program?Both functions internally exec /bin/sh -c command. If the process still holds elevated privilege, the shell inherits it. A malicious user can manipulate PATH, IFS, or other shell variables to make the shell execute arbitrary commands with the program’s elevated privileges.

Q2. Why use execve() with an absolute path instead of execvp() in privileged code?execvp() searches PATH to find the program. An attacker can modify PATH to point to a malicious program with the same name. execve() with an absolute path bypasses PATH entirely โ€” the kernel is told exactly what to run.

Q3. What happens to open file descriptors when exec() is called?By default, all open file descriptors survive exec() and are inherited by the new program. If these FDs point to sensitive files (opened with elevated privilege), the new program can access those files even without privilege itself.

Q4. What is FD_CLOEXEC and when should you use it?FD_CLOEXEC is a flag that marks a file descriptor to be automatically closed when exec() is called. It should be set on any FD that the execed program should not inherit, especially FDs opened with elevated privilege. Can be set at open time with O_CLOEXEC or later with fcntl(fd, F_SETFD, FD_CLOEXEC).

Q5. How does exec() interact with the saved set-UID?A successful exec() copies the current effective UID into the saved set-UID of the new process image. So if you call seteuid(getuid()) before exec(), the new program’s saved set-UID will also be the unprivileged UID โ€” it cannot regain privilege. But this only works if exec() succeeds.

Q6. Does Linux allow set-UID scripts (shell scripts with SUID bit)?No. Linux silently ignores the set-UID and set-GID permission bits on interpreter scripts for security reasons. Even on systems that do support them, their use is strongly discouraged because the shell provides too many ways to subvert privilege.

Next: Avoid Exposing Sensitive Information โ†’
Memory erasure, preventing core dumps, mlock()

Continue to Part 4 โ† Part 2

Leave a Reply

Your email address will not be published. Required fields are marked *