Linux Capabilities Part 3: Process & File Capability Sets

 

Linux Capabilities
Chapter 39 โ€” Part 3: Process & File Capability Sets
๐Ÿ“˜ Theory Deep Dive
๐Ÿ’ป Coding Example
๐ŸŽฏ Interview Q&A

The Three-Set Architecture

The Linux capabilities system does not work with a single capability set. Instead, both processes and files each have three separate capability sets. Understanding the purpose and relationship between these six sets (3 per process, 3 per file) is the most important conceptual foundation for working with Linux capabilities.

Think of these sets as a series of gates or filters that control which capabilities a process can actually use at any given moment. They give very fine-grained control: a capability can be allowed but currently inactive, allowed to pass across exec, or permanently restricted.

39.3.1 โ€” Process Capability Sets

The kernel maintains three capability sets for every process (technically, per-thread). Each set is stored as a bitmask โ€” one bit per capability. The three sets are:

Set Name Symbol Purpose & Role
Permitted P(permitted) The maximum ceiling of capabilities a process may ever use. A process can only raise a capability in its effective set if that capability is already in the permitted set. If a capability is dropped from the permitted set, it is permanently gone โ€” it cannot be reacquired (unless the process execs a file that grants it back).
Effective P(effective) The currently active capabilities โ€” the ones the kernel actually checks during privilege verification. This is the set that matters when a system call requires a capability. A process can temporarily disable a capability by removing it from the effective set (while keeping it in the permitted set), and later re-enable it.
Inheritable P(inheritable) Capabilities that may be passed across an exec() call. When a process calls exec(), the inheritable set is used (along with the file’s inheritable set) to determine which capabilities the new program gets in its permitted set. This allows a process to selectively preserve specific capabilities across exec().
๐Ÿ“Œ Analogy โ€” The Hotel Key Card System:
Think of the permitted set as all the keys stored in the hotel safe in your room. The effective set is the subset of keys you actually have in your pocket right now (ready to use). You can take a key from the safe and put it in your pocket (raise to effective), or put it back (drop from effective). But if you throw a key in the bin (drop from permitted), you can never get it back.

The relationship between permitted and effective is exactly analogous to the relationship between the saved set-user-ID and the effective user ID in a set-user-ID-root program:

  • Dropping a capability from effective only = temporarily dropping effective UID 0 (but keeping it in saved set-user-ID)
  • Dropping a capability from both effective and permitted = permanently dropping superuser privileges (setting both effective UID and saved set-user-ID to nonzero)

Important implementation details:

  • Capability sets are per-thread attributes. In a multithreaded program, each thread can have different capabilities.
  • A child process created by fork() inherits copies of all three capability sets from its parent.
  • The /proc/PID/status file shows CapInh, CapPrm, CapEff, and CapBnd as 64-bit hex values.
  • Before Linux 2.6.25, capability sets were 32-bit. They were expanded to 64-bit in 2.6.25 to accommodate additional capabilities.

39.3.2 โ€” File Capability Sets

Just like processes, executable files can have capability sets attached to them. These are stored as a security extended attribute named security.capability on the file. File capabilities were introduced in Linux 2.6.24.

When a process calls exec(), the kernel combines the process’s current capability sets with the file’s capability sets to compute the new capability sets for the process running the program.

File Set Name Symbol What It Does During exec()
Permitted F(permitted) Formerly called “forced”. These capabilities are forced into the process’s new permitted set regardless of what the process already had. This is how a file grants capabilities to a process โ€” it says “any process that execs me will get at least these capabilities in its permitted set.”
Effective F(effective) This is NOT a full set โ€” it is a single bit (on or off). If the bit is 1, then all capabilities that end up in the process’s new permitted set are also automatically placed into the effective set. If the bit is 0, the new effective set starts empty (the program must manually raise capabilities).
Inheritable F(inheritable) Formerly called “allowed”. This is ANDed against the process’s inheritable set to determine which capabilities from the process’s inheritable set are also added to the new permitted set. This is the file’s way of saying: “I allow these inheritable capabilities from the process to come through.”
๐Ÿ”‘ Why the File Effective Set is Just One Bit:
The design choice of making the file effective set a single bit reflects two use cases:

  • Capability-dumb programs (old programs unaware of capabilities): They don’t know to raise capabilities in their effective set. For these programs, the file effective bit should be 1, so that permitted capabilities automatically become effective โ€” the program just works.
  • Capability-aware programs (written to use capabilities properly): These programs will call libcap functions to raise and drop capabilities as needed. For them, the file effective bit should be 0, so the effective set starts empty and the program explicitly raises only what it needs โ€” implementing the principle of least privilege at runtime.

39.3.6 โ€” Assigning and Viewing File Capabilities from the Shell

The setcap(8) and getcap(8) commands from the libcap package are the tools for managing file capabilities. Understanding the notation they use is important.

# View capabilities on an executable
$ getcap /bin/ping
ping = cap_net_raw+ep

# Assign CAP_SYS_TIME to permitted(p) and effective(e) sets of a file
$ sudo setcap “cap_sys_time=pe” ./mydate

# Assign CAP_DAC_READ_SEARCH to permitted only (program is capability-aware)
$ sudo setcap “cap_dac_read_search=p” ./myprogram

# Remove all file capabilities from a file
$ sudo setcap -r ./myprogram

# Assign multiple capabilities at once
$ sudo setcap “cap_net_bind_service,cap_net_raw=ep” ./myserver

The notation format is: capability_name(s) = flags

Flag Letter Meaning
p Add to file Permitted set
e Set the file Effective bit to 1
i Add to file Inheritable set
๐Ÿ“Œ Real-world Example from the PDF:
The date(1) program normally cannot change the system time as a non-root user. By giving it CAP_SYS_TIME in both permitted and effective sets:
sudo setcap "cap_sys_time=pe" ./date
Now any unprivileged user can run this copy of date and change the system time. The capability is in the file’s permitted set (so it enters the process’s permitted set on exec) and the effective bit is set (so it’s immediately in the effective set too).

Overview: All Six Capability Sets

Here is a side-by-side comparison of all six sets (3 process + 3 file):

Set Belongs To Size Primary Role
Permitted Process 64-bit mask Maximum capabilities the process can ever use
Effective Process 64-bit mask Currently active capabilities checked by the kernel
Inheritable Process 64-bit mask Capabilities to possibly carry across exec()
Permitted File 64-bit mask Capabilities forced into process permitted set on exec()
Effective File 1 bit only If set, all new permitted caps also become effective
Inheritable File 64-bit mask Mask applied to process inheritable set to grant caps on exec()

๐Ÿ’ป Coding Example โ€” Displaying All Three Process Capability Sets

This program uses libcap to display each of the three capability sets for the calling process, both as a human-readable string and by iterating over all known capabilities to show their individual status. It demonstrates how capability-aware programs inspect their own capability state at startup.

/*
 * show_all_cap_sets.c
 *
 * Displays all three capability sets of the calling process:
 *   - Permitted
 *   - Effective
 *   - Inheritable
 *
 * Shows both the compact text form and a per-capability breakdown.
 *
 * Compile:
 *   gcc -o show_all_cap_sets show_all_cap_sets.c -lcap
 *
 * Test as root:
 *   sudo ./show_all_cap_sets
 *
 * Test with specific caps assigned to file:
 *   sudo setcap "cap_net_bind_service,cap_sys_time=p" ./show_all_cap_sets
 *   ./show_all_cap_sets
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/capability.h>
#include <unistd.h>

/*
 * print_set_status() - For a given cap_t, print the status of every
 * known capability in the specified set (Permitted, Effective, or Inheritable).
 *
 * We iterate capability values from 0 up to CAP_LAST_CAP.
 * CAP_LAST_CAP is a macro defined by libcap for the highest-numbered capability.
 * cap_is_supported() lets us skip capabilities not supported by this kernel.
 */
static void print_set_status(cap_t caps, cap_flag_t which_set, const char *set_name)
{
    int cap_num;
    cap_flag_value_t value;
    const char *cap_name_str;

    printf("\n  --- %s Set ---\n", set_name);
    printf("  %-35s %s\n", "Capability", "Status");
    printf("  %-35s %s\n", "----------", "------");

    /*
     * CAP_LAST_CAP is the integer value of the highest-numbered capability.
     * We iterate from 0 to CAP_LAST_CAP inclusive.
     *
     * cap_to_name() converts a capability number to its symbolic string,
     * e.g., cap_to_name(13) might return "cap_net_bind_service".
     * It allocates memory โ€” we must cap_free() it.
     */
    for (cap_num = 0; cap_num <= CAP_LAST_CAP; cap_num++) {

        /* Get the string name for this capability number */
        cap_name_str = cap_to_name(cap_num);
        if (cap_name_str == NULL)
            continue;  /* Unknown capability โ€” skip */

        /* Get the flag value for this capability in this set */
        if (cap_get_flag(caps, (cap_value_t)cap_num, which_set, &value) == -1) {
            cap_free((void *)cap_name_str);
            continue;
        }

        /* Only print capabilities that are SET (to keep output concise) */
        if (value == CAP_SET) {
            printf("  %-35s SET\n", cap_name_str);
        }

        cap_free((void *)cap_name_str);
    }
}

int main(void)
{
    cap_t caps;
    char *text_form;

    printf("=== Process Capability Sets Viewer ===\n");
    printf("PID=%d  UID=%d  EUID=%d\n",
           (int)getpid(), (int)getuid(), (int)geteuid());

    /*
     * cap_get_proc() retrieves the calling process's capability sets
     * from the kernel and stores them in a newly allocated cap_t structure.
     *
     * The cap_t type is an opaque pointer โ€” never access it directly.
     * Always use the libcap API functions.
     */
    caps = cap_get_proc();
    if (caps == NULL) {
        perror("cap_get_proc");
        exit(EXIT_FAILURE);
    }

    /*
     * cap_to_text() converts the entire cap_t structure to a compact
     * human-readable string using the POSIX 1003.1e notation.
     *
     * Examples:
     *   ""                   โ€” no capabilities at all
     *   "=ep"                โ€” all capabilities in effective and permitted
     *   "cap_net_raw+ep"     โ€” only CAP_NET_RAW in effective and permitted
     *   "cap_sys_time+p"     โ€” CAP_SYS_TIME in permitted only
     */
    text_form = cap_to_text(caps, NULL);
    if (text_form != NULL) {
        printf("\nCompact representation: [%s]\n", text_form);
        cap_free(text_form);
    }

    /*
     * Now print which capabilities are set in each of the three sets.
     * We call print_set_status() three times, once per set.
     *
     * CAP_PERMITTED    โ€” the maximum ceiling (what the process may have)
     * CAP_EFFECTIVE    โ€” currently active (what the kernel checks)
     * CAP_INHERITABLE  โ€” may be passed across exec()
     */
    print_set_status(caps, CAP_PERMITTED,   "Permitted");
    print_set_status(caps, CAP_EFFECTIVE,   "Effective");
    print_set_status(caps, CAP_INHERITABLE, "Inheritable");

    if (getuid() != 0) {
        printf("\n  (All sets empty for an unprivileged process with no file capabilities)\n");
        printf("  Try: sudo ./show_all_cap_sets\n");
        printf("  Or:  sudo setcap \"cap_net_bind_service+ep\" ./show_all_cap_sets\n");
    }

    /*
     * ALWAYS free the cap_t structure. cap_free() handles the internal
     * memory correctly. Do NOT use free() on a cap_t.
     */
    cap_free(caps);

    printf("\n=== Done ===\n");
    return 0;
}
Sample output when run as root:

Compact representation: [=ep]

  --- Permitted Set ---
  cap_chown                           SET
  cap_dac_override                    SET
  cap_dac_read_search                 SET
  cap_fowner                          SET
  cap_net_bind_service                SET
  cap_sys_time                        SET
  ... (all ~40 capabilities listed as SET)

  --- Effective Set ---
  (same as Permitted โ€” all SET)

  --- Inheritable Set ---
  (empty by default even for root)

๐ŸŽฏ Interview Questions
Q1. What are the three process capability sets and what is the role of each?

Permitted: The maximum ceiling โ€” a superset of what the process can ever have in effective or inheritable sets. Once a capability is dropped from permitted, it is permanently gone.
Effective: The currently active set โ€” what the kernel actually checks when the process attempts a privileged operation. A process can add/remove capabilities here as long as they remain in permitted.
Inheritable: Capabilities that may be carried across an exec() call, subject to masking by the file’s inheritable set.

Q2. Why is the file effective capability set a single bit rather than a full bitmask?

The file effective bit handles two different types of programs. For capability-dumb programs (old programs that don’t know about capabilities), the bit is set to 1 so that all newly acquired permitted capabilities automatically become effective โ€” the program works without modification. For capability-aware programs, the bit is 0 so the effective set starts empty after exec, and the program explicitly raises only the capabilities it needs at the right time, following the principle of least privilege.

Q3. What happens to a child process’s capabilities when fork() is called?

A child process created by fork() inherits exact copies of all three capability sets (permitted, effective, and inheritable) from its parent. The child starts with exactly the same capabilities as the parent at the moment fork() was called.

Q4. Where are file capabilities stored on the filesystem?

File capabilities are stored in a security extended attribute named security.capability on the executable file. Extended attributes (xattrs) are stored in the file’s i-node metadata. The CAP_SETFCAP capability is required to modify this attribute. You can view and set file capabilities using getcap(8) and setcap(8).

Q5. What is the former name of the file permitted and inheritable capability sets, and why is this informative?

The file permitted set was formerly called “forced” โ€” these capabilities are forced into the process’s permitted set during exec, regardless of what the process already had.
The file inheritable set was formerly called “allowed” โ€” these are the capabilities the file allows to pass through from the process’s inheritable set into the new permitted set during exec. The old names directly describe the mechanism and are still useful for understanding the behavior.

Q6. How does temporarily disabling a capability in the effective set relate to the concept of set-user-ID programs?

The relationship is a direct analogy. In a set-user-ID-root program, you can temporarily drop root by calling seteuid() to a non-zero UID while keeping 0 in the saved set-user-ID (so you can restore it later). With capabilities, temporarily dropping a capability from the effective set (while keeping it in the permitted set) achieves the same goal at the granularity of a single capability. Permanently dropping from both effective AND permitted is analogous to permanently dropping root by setting both effective and saved set-user-ID to non-zero.

Leave a Reply

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