Linux Capabilities Changing Capabilities Programmatically

 

Linux Capabilities
Chapter 39 โ€” Part 6: Changing Capabilities Programmatically (libcap API)
๐Ÿ”ง libcap API
๐Ÿ’ป Full Program
๐ŸŽฏ Interview Q&A

The libcap API โ€” The Right Way to Manipulate Capabilities

Linux provides two system calls for manipulating process capabilities: capget() and capset(). However, you should almost never call these directly. Instead, use the libcap library API, which provides a higher-level, POSIX 1003.1e-conforming interface that is much safer and more readable.

The libcap API wraps the raw system calls with proper type safety, error handling, and memory management. Using it directly calling capset() is like calling open()/read()/write() at the byte level when you could use FILE* functions โ€” technically possible but unnecessarily error-prone.

39.7 โ€” Rules for Modifying Process Capabilities

The kernel enforces strict rules about which capability modifications a process is allowed to make. A process cannot arbitrarily grant itself capabilities it doesn’t already have. The rules are:

Rule 1 โ€” Inheritable set changes require CAP_SETPCAP or set membership:
If the process does NOT have CAP_SETPCAP in its effective set, then the new inheritable set must be a subset of the union of the existing inheritable AND permitted sets. You can only have in your inheritable set what you already have in inheritable or permitted.
Rule 2 โ€” Inheritable set bounded by bounding set:
The new inheritable set must be a subset of the union of the existing inheritable set AND the capability bounding set. This prevents adding a capability to inheritable that is not in the bounding set.
Rule 3 โ€” Cannot grow the permitted set:
The new permitted set must be a subset of the existing permitted set. A process can only DROP capabilities from its permitted set โ€” it can never add new ones (except through exec()). A capability dropped from permitted is permanently lost.
Rule 4 โ€” Effective set bounded by permitted set:
The new effective set is allowed to contain only capabilities that are also in the new permitted set. You cannot have a capability active (effective) if you don’t have it in your permitted set first.

The libcap API โ€” Key Functions

A typical capability-aware program follows this workflow with libcap:

Step Function Purpose
1 cap_get_proc() Retrieve current process capability sets into a cap_t structure (kernel โ†’ user space)
alt 1a cap_init() Create a new empty cap_t structure (all capabilities cleared in all sets)
2 cap_set_flag() Raise (CAP_SET) or drop (CAP_CLEAR) specific capabilities in the user-space cap_t structure
3 cap_set_proc() Push the modified cap_t structure back to the kernel (user space โ†’ kernel)
4 cap_free() Free memory allocated by libcap functions. ALWAYS required.

Additional useful functions:

Function Purpose
cap_get_flag() Read the current state (CAP_SET or CAP_CLEAR) of a specific capability in a specific set
cap_to_text() Convert a cap_t to a human-readable string (POSIX notation)
cap_from_text() Parse a text string into a cap_t structure
cap_dup() Duplicate a cap_t structure
cap_clear() Clear all capabilities in all sets of a cap_t structure
๐Ÿ“Œ Important: cap_t is an Opaque Pointer
The cap_t type is defined by libcap as an opaque pointer โ€” you must never access the structure it points to directly. Always use the provided API functions. The internal representation is subject to change and is not part of the public API.

๐Ÿ’ป Coding Example โ€” Capability-Aware Password Authentication Program

This is the program from Listing 39-1 in the textbook โ€” a complete, real-world capability-aware program. It demonstrates the proper pattern for capability-aware programming: start with capabilities in the permitted set (not effective), raise them only when needed, use them for the minimum necessary time, then drop all capabilities permanently.

The program authenticates a username and password against the system’s shadow password file. Reading /etc/shadow requires CAP_DAC_READ_SEARCH. Instead of running the entire program as root, we give it only that one capability in its file permitted set.

Setup (run once as root):

$ gcc -o check_password_caps check_password_caps.c -lcap -lcrypt
$ sudo setcap “cap_dac_read_search=p” ./check_password_caps
$ getcap ./check_password_caps
check_password_caps = cap_dac_read_search+p
/*
 * check_password_caps.c  (Based on Listing 39-1 from TLPI)
 *
 * A capability-aware program that authenticates a user against
 * the shadow password database.
 *
 * Security design:
 *   - The binary has CAP_DAC_READ_SEARCH in its file PERMITTED set (not effective).
 *   - On startup, effective set is EMPTY (file effective bit is 0).
 *   - We raise CAP_DAC_READ_SEARCH to effective ONLY to read /etc/shadow.
 *   - After reading shadow file, we drop ALL capabilities permanently.
 *   - The rest of the program runs with zero capabilities.
 *
 * This is the "capability-aware program" pattern described in Section 39.3.4.
 * The file effective bit is 0 (=p means permitted only, not effective).
 *
 * Required file capability:
 *   sudo setcap "cap_dac_read_search=p" ./check_password_caps
 *
 * Compile:
 *   gcc -o check_password_caps check_password_caps.c -lcap -lcrypt
 */

#define _BSD_SOURCE   /* Get getpass() declaration from <unistd.h> */
#define _XOPEN_SOURCE /* Get crypt() declaration */

#include <sys/capability.h>
#include <unistd.h>
#include <limits.h>
#include <pwd.h>
#include <shadow.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

/*
 * modifyCap() - Internal helper: raise or drop one capability in the effective set.
 *
 * @capability: The capability to change (e.g., CAP_DAC_READ_SEARCH)
 * @setting:    CAP_SET to raise, CAP_CLEAR to drop
 *
 * Steps:
 *   1. cap_get_proc()   โ€” get current capability sets
 *   2. cap_set_flag()   โ€” modify the desired capability in the user-space structure
 *   3. cap_set_proc()   โ€” push the modified structure back to the kernel
 *   4. cap_free()       โ€” free the libcap-allocated structure
 *
 * Returns 0 on success, -1 on failure.
 */
static int modifyCap(int capability, int setting)
{
    cap_t caps;
    cap_value_t capList[1];

    /*
     * cap_get_proc() allocates a cap_t structure and fills it with
     * this process's current capability sets from the kernel.
     * MUST be freed with cap_free() when done.
     */
    caps = cap_get_proc();
    if (caps == NULL)
        return -1;

    /*
     * cap_set_flag(cap_t, cap_flag_t, int, cap_value_t[], cap_flag_value_t)
     *
     * Arguments:
     *   caps            โ€” the capability structure to modify
     *   CAP_EFFECTIVE   โ€” which set to modify (we only change effective here)
     *   1               โ€” number of capabilities in capList array
     *   capList         โ€” array of capabilities to change
     *   setting         โ€” CAP_SET (raise) or CAP_CLEAR (drop)
     *
     * This modifies the IN-MEMORY structure only.
     * The kernel is not updated until cap_set_proc() is called.
     */
    capList[0] = capability;
    if (cap_set_flag(caps, CAP_EFFECTIVE, 1, capList, setting) == -1) {
        cap_free(caps);
        return -1;
    }

    /*
     * cap_set_proc() pushes the modified capability structure back to
     * the kernel, actually changing the process's capabilities.
     * This is the step that the kernel checks against the rules in Section 39.7.
     */
    if (cap_set_proc(caps) == -1) {
        cap_free(caps);
        return -1;
    }

    /* Always free the libcap-allocated structure */
    if (cap_free(caps) == -1)
        return -1;

    return 0;
}

/*
 * raiseCap() - Raise a single capability in the effective set.
 * After this call, the capability is ACTIVE and the kernel will allow
 * the corresponding privileged operations.
 */
static int raiseCap(int capability)
{
    return modifyCap(capability, CAP_SET);
}

/*
 * dropAllCaps() - Drop ALL capabilities from ALL three sets.
 *
 * Creates an empty cap_t structure (cap_init() returns a cap_t with
 * all capabilities cleared in all sets), then applies it to the process.
 *
 * After this call, the process has NO capabilities in any set.
 * This is the "nuclear option" for dropping privileges โ€” used when
 * the program no longer needs ANY capabilities for the rest of its life.
 */
static int dropAllCaps(void)
{
    cap_t empty;
    int s;

    /*
     * cap_init() allocates a new cap_t with all capabilities cleared
     * in permitted, effective, and inheritable sets.
     */
    empty = cap_init();
    if (empty == NULL)
        return -1;

    /*
     * cap_set_proc() with an all-zeros structure permanently drops
     * all capabilities. The process now has no capabilities whatsoever.
     */
    s = cap_set_proc(empty);

    cap_free(empty);
    return s;
}

int main(int argc, char *argv[])
{
    char *username, *password, *encrypted, *p;
    struct passwd *pwd;
    struct spwd *spwd;
    int authOk;
    size_t len;
    long lnmax;

    /* ============================================================
     * At program start, effective caps are EMPTY (file effective bit = 0).
     * CAP_DAC_READ_SEARCH is in permitted set (from file permitted set).
     * We have NOT yet raised it to effective โ€” we don't need it yet.
     * ============================================================ */

    lnmax = sysconf(_SC_LOGIN_NAME_MAX);
    if (lnmax == -1)
        lnmax = 256;

    username = malloc(lnmax);
    if (username == NULL) { perror("malloc"); exit(EXIT_FAILURE); }

    printf("Username: ");
    fflush(stdout);

    if (fgets(username, lnmax, stdin) == NULL)
        exit(EXIT_FAILURE);

    len = strlen(username);
    if (username[len - 1] == '\n')
        username[len - 1] = '\0';

    /* Get the public password record (world-readable) โ€” no privilege needed */
    pwd = getpwnam(username);
    if (pwd == NULL) {
        fprintf(stderr, "No such user: %s\n", username);
        exit(EXIT_FAILURE);
    }

    /* ============================================================
     * RAISE CAP_DAC_READ_SEARCH in the effective set.
     *
     * We raise the capability ONLY for the brief window where we
     * need to read /etc/shadow (which is protected: -rw-r----- root shadow).
     *
     * This is the MINIMUM PRIVILEGE principle: hold elevated capabilities
     * for the shortest possible time.
     * ============================================================ */
    if (raiseCap(CAP_DAC_READ_SEARCH) == -1) {
        fprintf(stderr, "raiseCap() failed\n");
        exit(EXIT_FAILURE);
    }

    /* Read the shadow password record โ€” requires CAP_DAC_READ_SEARCH */
    spwd = getspnam(username);
    if (spwd == NULL && errno == EACCES) {
        fprintf(stderr, "No permission to read shadow password file\n");
        exit(EXIT_FAILURE);
    }

    /* ============================================================
     * DROP ALL CAPABILITIES immediately after reading shadow file.
     *
     * We are done with all privileged operations. Drop everything
     * permanently. For the rest of this program, we run with no
     * capabilities at all โ€” just like a normal unprivileged process.
     * ============================================================ */
    if (dropAllCaps() == -1) {
        fprintf(stderr, "dropAllCaps() failed\n");
        exit(EXIT_FAILURE);
    }

    /* Use shadow password if available */
    if (spwd != NULL)
        pwd->pw_passwd = spwd->sp_pwdp;

    /* Get the password (echoing disabled) */
    password = getpass("Password: ");

    /*
     * crypt() hashes the plaintext password using the algorithm and salt
     * stored in the shadow record. We compare the result to the stored hash.
     */
    encrypted = crypt(password, pwd->pw_passwd);

    /* Erase the cleartext password from memory immediately */
    for (p = password; *p != '\0'; )
        *p++ = '\0';

    if (encrypted == NULL) { perror("crypt"); exit(EXIT_FAILURE); }

    authOk = (strcmp(encrypted, pwd->pw_passwd) == 0);

    if (!authOk) {
        printf("Incorrect password\n");
        exit(EXIT_FAILURE);
    }

    printf("Successfully authenticated: UID=%ld\n", (long)pwd->pw_uid);

    /* At this point we run fully authenticated work with zero capabilities */
    exit(EXIT_SUCCESS);
}
๐Ÿ”‘ Security Design Pattern Summary:

File capability set: cap_dac_read_search=p (permitted only, NOT effective)
On startup: Effective = empty. Permitted = {CAP_DAC_READ_SEARCH}.
Before getspnam(): raiseCap(CAP_DAC_READ_SEARCH) โ†’ Effective = {CAP_DAC_READ_SEARCH}
After getspnam(): dropAllCaps() โ†’ Effective = Permitted = Inheritable = empty
Rest of program: Zero capabilities โ€” runs as normal unprivileged process

๐ŸŽฏ Interview Questions
Q1. Why should you use the libcap API instead of calling capget()/capset() directly?

The libcap API provides a higher-level, type-safe, POSIX 1003.1e-conforming interface. The raw system calls have complex data structures with internal version fields that the programmer must manage correctly. libcap handles these internal details, provides proper error reporting, automatically manages memory allocation and freeing, and gives you human-readable text conversion functions. Direct use of capset()/capget() is error-prone and non-portable.

Q2. What is the correct 4-step workflow for modifying a process’s capabilities using libcap?

Step 1: Call cap_get_proc() to retrieve the current capability sets into a cap_t structure. Step 2: Call cap_set_flag() to raise or drop specific capabilities in the desired set(s) within the in-memory structure. Step 3: Call cap_set_proc() to push the modified structure back to the kernel โ€” this is when the change actually takes effect. Step 4: Call cap_free() to release the memory allocated by libcap.

Q3. What is the purpose of cap_init() and how does it differ from cap_get_proc()?

cap_get_proc() retrieves the actual current capability sets of the process from the kernel. cap_init() creates a brand new, empty cap_t structure with all capabilities cleared in all three sets. You use cap_init() when you want to build a capability set from scratch (e.g., when you want to apply a completely empty capability set โ€” like in dropAllCaps()), rather than modifying the existing ones.

Q4. In the password authentication example, why is the file capability set to “=p” (permitted only) rather than “=pe” (permitted and effective)?

The program is capability-aware โ€” it is designed to explicitly manage when capabilities are active. By setting only the permitted bit (=p), the effective set starts empty when the program runs. The program calls raiseCap() to put CAP_DAC_READ_SEARCH in the effective set only for the brief moment it reads the shadow file, then calls dropAllCaps() to permanently remove it. If =pe were used (capability-dumb mode), the capability would be active from the very start โ€” violating the principle of least privilege.

Q5. Can a process ever grant itself a capability that is not in its permitted set? How?

A process cannot directly add to its own permitted set (Rule 3: the new permitted set must be a subset of the existing one). The only way to gain new capabilities in the permitted set is through exec() โ€” specifically by executing a file that has the desired capability in its file permitted set, or by executing a set-user-ID-root program (which triggers the root semantics rules). This is by design: capabilities flow into processes through file execution, not through self-granting.

Q6. What is the cap_flag_t type and what are its valid values?

cap_flag_t is an enumeration type in libcap that identifies which of the three capability sets you are operating on. Its valid values are: CAP_EFFECTIVE (the currently active set), CAP_PERMITTED (the maximum ceiling set), and CAP_INHERITABLE (the set for carrying across exec). It is used as a parameter to cap_set_flag(), cap_get_flag(), and related functions to specify which set to read or modify.

Leave a Reply

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