Pitfalls in File Operations and File I/O

 

Pitfalls in File Operations and File I/O
Chapter 38.7 โ€” umask, /tmp Hazards, O_EXCL, fchown(), mkstemp()
๐Ÿ“‚ umask
๐Ÿ” O_EXCL
๐Ÿ—‚๏ธ mkstemp()

Why File Operations are a Security Minefield

File operations seem simple โ€” open a file, write some data, close it. But for privileged programs, every file operation is a potential attack vector. The permissions on a newly created file, where it’s created, how it’s named, and the order of permission-setting operations can all be exploited by a malicious user if not handled carefully.

๐Ÿ”ง Rule 1: Set a Restrictive umask

The umask is a process-wide mask that removes permission bits from newly created files. It is inherited by child processes and survives exec(). A privileged program should always set a restrictive umask to ensure no file is accidentally created as world-writable or world-readable.

If a root-owned file is world-writable, any user can modify it โ€” completely bypassing the privilege model. A malicious user could wait for the privileged program to create such a file and immediately write malicious content to it.

umask value Permissions removed Resulting file mode (if open with 0666) Security level
0000 Nothing removed rw-rw-rw- (world-writable!) Dangerous
0022 Group and other write rw-r–r– Acceptable
0077 All group and other access rw——- (owner only) Best for sensitive files
0177 All except owner read+write rw——- (effective) Secure
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main(void)
{
    /* Set restrictive umask at program startup.
     * This means any file created will be at most rw------- (0600)
     * even if open() is called with mode 0666. */
    umask(0177);

    /* Create a file โ€” permissions will be 0600 (rw-------) */
    int fd = open("/tmp/secure_file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    /* Actual permissions = 0666 & ~0177 = 0600 = rw------- */
    printf("File created. Run: ls -la /tmp/secure_file.txt\n");
    close(fd);

    return 0;
}

๐Ÿ” Rule 2: Use O_EXCL to Guarantee You’re the Creator

When a privileged program creates a file and the security depends on being the sole creator (not opening an attacker-planted file), use O_EXCL with O_CREAT. The combination O_CREAT | O_EXCL is atomic โ€” if the file already exists, open() fails with EEXIST. There is no TOCTTOU window.

Without O_EXCL, an attacker could pre-create the file (as a symlink to /etc/passwd), and your program would open it instead of creating a new file.

#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>

/*
 * Creates a file exclusively โ€” fails if it already exists.
 * This is an atomic check-and-create, preventing TOCTTOU.
 */
int create_exclusively(const char *path)
{
    int fd;

    /* O_CREAT | O_EXCL: atomically create and open.
     * Fails with EEXIST if file already exists. */
    fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0600);
    if (fd == -1) {
        if (errno == EEXIST) {
            fprintf(stderr, "File already exists: %s (possible attack!)\n", path);
        } else {
            perror("open");
        }
        return -1;
    }

    printf("Created new file: %s (fd=%d)\n", path, fd);
    return fd;
}

int main(void)
{
    int fd = create_exclusively("/tmp/my_config_1234.tmp");
    if (fd != -1) {
        /* We are the guaranteed creator of this file */
        write(fd, "secure config data\n", 19);
        close(fd);
    }
    return 0;
}

โš ๏ธ Rule 3: Avoid /tmp โ€” Use mkstemp() If You Must

/tmp is a world-writable directory โ€” any user can create files there. When a privileged program creates a predictable filename in /tmp, an attacker can pre-create that file (as a symlink or regular file) before the privileged program does. This is called a symlink attack.

If you absolutely must use a temporary file:

Use mkstemp()
Creates a file with a random, unpredictable name. Returns an open FD. Atomically creates and opens.
Use a Private Directory
Create a directory owned by root (mode 0700) and create temp files inside it. Attacker can’t pre-create files there.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(void)
{
    char template[] = "/tmp/myprog_XXXXXX";

    /* mkstemp() replaces XXXXXX with a random string.
     * It creates the file atomically and returns an open FD.
     * File permissions are 0600 โ€” only owner can read/write. */
    int fd = mkstemp(template);
    if (fd == -1) {
        perror("mkstemp");
        return 1;
    }

    printf("Created temp file: %s (fd=%d)\n", template, fd);

    /* Optional: unlink the file immediately so it disappears
     * when closed โ€” the data is accessible only through the FD. */
    unlink(template);
    printf("File unlinked (will disappear on close, data still accessible via fd)\n");

    /* Write to it via FD */
    const char *data = "temporary sensitive data\n";
    write(fd, data, strlen(data));

    close(fd);
    printf("Temp file closed and gone.\n");
    return 0;
}

๐Ÿ‘ค Rule 4: Use fchown()/fchmod() on the FD, Not the Path

If a set-UID-root program creates a file that initially needs to be owned by root but will later be handed to another user, the ownership transfer must be done carefully.

The correct sequence: create the file as non-world-writable first, then change ownership with fchown() (on the FD), then set final permissions with fchmod() (on the FD). Using chown(path, ...) instead of fchown(fd, ...) creates a TOCTTOU window on the path.

#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>

/*
 * Creates a file as root, then transfers ownership to another user.
 * Uses FD-based calls (fchown, fchmod) to avoid TOCTTOU.
 */
int create_and_transfer(const char *path, uid_t target_uid, gid_t target_gid)
{
    int fd;

    /* Step 1: Create file โ€” initially NOT world-writable.
     * Use 0600 (owner read-write only) while we still own it. */
    fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0600);
    if (fd == -1) {
        perror("open");
        return -1;
    }

    /* Step 2: Write initial content */
    write(fd, "config data for user\n", 21);

    /* Step 3: Transfer ownership using FD (not path) โ€” no TOCTTOU */
    if (fchown(fd, target_uid, target_gid) == -1) {
        perror("fchown");
        close(fd);
        unlink(path);
        return -1;
    }

    /* Step 4: Set final permissions using FD (not path) */
    if (fchmod(fd, 0644) == -1) {
        perror("fchmod");
        close(fd);
        unlink(path);
        return -1;
    }

    printf("File %s created and transferred to UID=%d GID=%d\n",
           path, (int)target_uid, (int)target_gid);
    close(fd);
    return 0;
}

int main(void)
{
    /* This would be run as root in a real scenario */
    create_and_transfer("/tmp/user_config.conf", 1000, 1000);
    return 0;
}

๐Ÿ“Œ Key Terms

umask O_EXCL O_CREAT mkstemp() fchown() fchmod() Symlink Attack /tmp Hazards World-Writable Atomic Create Predictable Filename
๐ŸŽฏ Interview Questions โ€” File Operations
Q1. Why should a privileged program set a restrictive umask?The umask removes permission bits from newly created files. Without a restrictive umask, a root-created file might be world-writable, allowing any user to modify it. Setting umask(0177) ensures all created files are accessible only to the owner.

Q2. What does O_CREAT | O_EXCL guarantee?The combination is atomic โ€” it creates a new file only if it doesn’t already exist. If the file exists (perhaps planted by an attacker), open() fails with EEXIST. This prevents symlink attacks and guarantees the program is the sole creator of the file.

Q3. Why is creating predictable filenames in /tmp dangerous?/tmp is world-writable. An attacker can pre-create a file (or symlink) with the name your program will use. When your privileged program creates the file, it may end up operating on the attacker’s file โ€” or following the symlink to a sensitive system file.

Q4. What does mkstemp() do and why is it safer than generating temp filenames yourself?mkstemp() atomically creates a file with a randomly generated, unpredictable name and returns an open file descriptor. Because the name is random and the creation is atomic, an attacker cannot predict or pre-create the file.

Q5. Why use fchown(fd) instead of chown(path) when changing file ownership?chown(path) looks up the path at call time โ€” an attacker can replace the file with a symlink between the file creation and the chown() call. fchown(fd) operates on an already-open file descriptor pinned to a specific inode, eliminating this TOCTTOU window.

Next: Don’t Trust Inputs โ†’
Environment variables, user input validation, runtime assumptions

Continue to Part 8 โ† Part 6

Leave a Reply

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