Skip to content

Instantly share code, notes, and snippets.

@bradfa
Last active December 2, 2025 13:29
Show Gist options
  • Select an option

  • Save bradfa/af4b6b7aaca1011d16fcde9bce22f9cb to your computer and use it in GitHub Desktop.

Select an option

Save bradfa/af4b6b7aaca1011d16fcde9bce22f9cb to your computer and use it in GitHub Desktop.
Linux kernel landlock feature

Landlock: Linux Kernel Sandbox Access Control

Landlock is a Linux Security Module (LSM) that enables unprivileged processes to create security sandboxes by restricting filesystem and network access. This document explains how Landlock works across kernel versions 6.1, 6.6, and 6.12, highlighting the evolution of features and practical examples for C application development.

Architecture Overview

Core Principles

  • Unprivileged Access Control: Available to any process without special privileges
  • Deny-by-Default: Only explicitly allowed actions are permitted
  • Compositional: Multiple layers of restrictions can be stacked
  • Path-Based: All permissions are tied to filesystem paths and hierarchies
  • Immutable: Once enforced, restrictions can only be added, never removed

Key Components

  1. Ruleset: A collection of access control rules for specific filesystem operations
  2. Domain: A read-only ruleset bound to a process thread (stored in task credentials)
  3. Layer: Each enforcement operation creates a new layer in the domain
  4. Object: Kernel objects (inodes) that rules are tied to
  5. Rule: Associates access permissions with a specific filesystem object

Syscall Interface

Landlock provides three system calls, available through linux/landlock.h:

1. landlock_create_ruleset()

int landlock_create_ruleset(const struct landlock_ruleset_attr *attr,
                           size_t size, uint32_t flags);

Purpose: Creates a new ruleset and returns a file descriptor for it.

Key Parameters:

  • attr: NULL or pointer to landlock_ruleset_attr structure
  • size: Size of the landlock_ruleset_attr structure
  • flags: Currently only LANDLOCK_CREATE_RULESET_VERSION

Features:

  • When called with attr=NULL, size=0, flags=LANDLOCK_CREATE_RULESET_VERSION, returns the highest supported ABI version (2 in Linux 6.1)
  • Otherwise creates a ruleset that handles specific filesystem access rights

2. landlock_add_rule()

int landlock_add_rule(int ruleset_fd, enum landlock_rule_type rule_type,
                     const void *rule_attr, uint32_t flags);

Purpose: Adds rules to an existing ruleset.

Key Parameters:

  • ruleset_fd: File descriptor returned by landlock_create_ruleset()
  • rule_type: Currently only LANDLOCK_RULE_PATH_BENEATH
  • rule_attr: Pointer to rule-specific structure
  • flags: Must be 0

3. landlock_restrict_self()

int landlock_restrict_self(int ruleset_fd, uint32_t flags);

Purpose: Enforces a ruleset on the current thread.

Requirements:

  • The process must have no_new_privs enabled via prctl(PR_SET_NO_NEW_PRIVS, 1)
  • OR have CAP_SYS_ADMIN capability in its namespace

Filesystem Access Rights

In Linux 6.1, Landlock only supports filesystem restrictions (no network controls):

File Operations

  • LANDLOCK_ACCESS_FS_EXECUTE: Execute a file
  • LANDLOCK_ACCESS_FS_WRITE_FILE: Open a file with write access
  • LANDLOCK_ACCESS_FS_READ_FILE: Open a file with read access

Directory Operations

  • LANDLOCK_ACCESS_FS_READ_DIR: Open a directory or list its content
  • LANDLOCK_ACCESS_FS_REMOVE_DIR: Remove an empty directory or rename it
  • LANDLOCK_ACCESS_FS_REMOVE_FILE: Unlink or rename a file

Creation Operations

  • LANDLOCK_ACCESS_FS_MAKE_CHAR: Create/link a character device
  • LANDLOCK_ACCESS_FS_MAKE_DIR: Create/link a directory
  • LANDLOCK_ACCESS_FS_MAKE_REG: Create/link a regular file
  • LANDLOCK_ACCESS_FS_MAKE_SOCK: Create/link a UNIX domain socket
  • LANDLOCK_ACCESS_FS_MAKE_FIFO: Create/link a named pipe
  • LANDLOCK_ACCESS_FS_MAKE_BLOCK: Create/link a block device
  • LANDLOCK_ACCESS_FS_MAKE_SYM: Create/link a symbolic link

Special Operations (ABI v2+)

  • LANDLOCK_ACCESS_FS_REFER: Link/rename between different directories

C Application Development

Step 1: Define Syscall Wrappers

Most glibc versions don't include Landlock syscalls, so you need wrappers:

#include <unistd.h>
#include <sys/syscall.h>
#include <linux/landlock.h>

static inline int landlock_create_ruleset(
    const struct landlock_ruleset_attr *attr, size_t size, uint32_t flags) {
    return syscall(__NR_landlock_create_ruleset, attr, size, flags);
}

static inline int landlock_add_rule(int ruleset_fd,
    enum landlock_rule_type rule_type, const void *rule_attr, uint32_t flags) {
    return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr, flags);
}

static inline int landlock_restrict_self(int ruleset_fd, uint32_t flags) {
    return syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
}

Step 2: Check Landlock Support

int check_landlock_support(void) {
    int abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);

    if (abi < 0) {
        if (errno == ENOSYS) {
            fprintf(stderr, "Landlock not supported by kernel\n");
        } else if (errno == EOPNOTSUPP) {
            fprintf(stderr, "Landlock disabled at boot time\n");
        }
        return -1;
    }

    printf("Landlock ABI version: %d\n", abi);
    return abi;
}

Step 3: Create a Ruleset

Purpose: Define what types of actions we want to control, not where they apply.

This creates an empty ruleset that knows about specific access rights but doesn't enforce anything yet. Think of it as defining the "language" of restrictions we can use later.

int create_file_ruleset(void) {
    struct landlock_ruleset_attr ruleset_attr = {
        // Handle all filesystem access rights - this defines WHAT we can control
        .handled_access_fs =
            LANDLOCK_ACCESS_FS_EXECUTE |
            LANDLOCK_ACCESS_FS_WRITE_FILE |
            LANDLOCK_ACCESS_FS_READ_FILE |
            LANDLOCK_ACCESS_FS_READ_DIR |
            LANDLOCK_ACCESS_FS_REMOVE_DIR |
            LANDLOCK_ACCESS_FS_REMOVE_FILE |
            LANDLOCK_ACCESS_FS_MAKE_CHAR |
            LANDLOCK_ACCESS_FS_MAKE_DIR |
            LANDLOCK_ACCESS_FS_MAKE_REG |
            LANDLOCK_ACCESS_FS_MAKE_SOCK |
            LANDLOCK_ACCESS_FS_MAKE_FIFO |
            LANDLOCK_ACCESS_FS_MAKE_BLOCK |
            LANDLOCK_ACCESS_FS_MAKE_SYM |
            LANDLOCK_ACCESS_FS_REFER
    };

    int abi = check_landlock_support();
    if (abi < 0) return -1;

    // Remove FS_REFER for ABI < 2
    if (abi < 2) {
        ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER;
    }

    return landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
}

Important: The ruleset creation defines the capabilities but does not enforce any restrictions. It's like creating security policy templates that can be applied to specific locations.

Critical Distinction: Handled vs Allowed Access Rights

Handled Access Rights (.handled_access_fs)
  • Purpose: Define what types of operations the ruleset knows about and can control
  • Behavior: If not handled, the operation always succeeds (bypasses Landlock entirely)
  • Example: If you don't handle LANDLOCK_ACCESS_FS_WRITE_FILE, then file write operations bypass Landlock
Allowed Access Rights (.allowed_access in path rules)
  • Purpose: Define what specific permissions are granted to specific paths
  • Behavior: If not explicitly allowed, the operation is blocked - this is deny-by-default
  • Example: If you only grant access to /tmp, then /home access is automatically denied

Landlock's Deny-by-Default Security Model

Yes, by default everything is blocked unless explicitly allowed. This is a fundamental security property of Landlock.

How Deny-by-Default Works
int demonstrate_deny_by_default(void) {
    int ruleset_fd = create_file_ruleset();

    // Only explicitly grant access to /tmp
    add_readwrite_path(ruleset_fd, "/tmp");
    // NO access granted to /home, /var, /root, etc.

    // Add basic system read access for executables
    add_readonly_path(ruleset_fd, "/usr");
    add_readonly_path(ruleset_fd, "/bin");

    enforce_sandbox(ruleset_fd);
    close(ruleset_fd);

    // Test access:

    // SUCCEEDS - explicitly allowed
    FILE *f1 = fopen("/tmp/test.txt", "w");
    printf("Access to /tmp: %s\n", f1 ? "ALLOWED" : "DENIED");

    // SUCCEEDS - explicitly allowed
    FILE *f2 = fopen("/usr/bin/ls", "r");
    printf("Access to /usr/bin: %s\n", f2 ? "ALLOWED" : "DENIED");

    // DENIED - not explicitly granted
    FILE *f3 = fopen("/home/user/config.txt", "r");
    printf("Access to /home: %s\n", f3 ? "ALLOWED" : "DENIED");

    // DENIED - not explicitly granted
    FILE *f4 = fopen("/var/log/syslog", "r");
    printf("Access to /var/log: %s\n", f4 ? "ALLOWED" : "DENIED");

    if (f1) fclose(f1);
    if (f2) fclose(f2);
    // f3 and f4 are NULL, no need to close

    return 0;
}

Common Pitfall: Incomplete Access Right Handling

// DANGEROUS - Incomplete handling
struct landlock_ruleset_attr ruleset_attr = {
    // Forgetting to handle REMOVE operations
    .handled_access_fs =
        LANDLOCK_ACCESS_FS_READ_FILE |
        LANDLOCK_ACCESS_FS_WRITE_FILE
        // Missing: REMOVE_FILE, REMOVE_DIR
};

int ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
add_readwrite_path(ruleset_fd, "/tmp");

// PROBLEM: Process can delete files ANYWHERE because REMOVE_FILE not handled!
unlink("/etc/passwd");  // SUCCEEDS - bypasses Landlock entirely!
Correct Comprehensive Handling

Always handle a comprehensive set of access rights and grant permissions selectively:

struct landlock_ruleset_attr ruleset_attr = {
    .handled_access_fs =
        // File operations
        LANDLOCK_ACCESS_FS_EXECUTE |
        LANDLOCK_ACCESS_FS_WRITE_FILE |
        LANDLOCK_ACCESS_FS_READ_FILE |

        // Directory operations
        LANDLOCK_ACCESS_FS_READ_DIR |

        // Modification operations
        LANDLOCK_ACCESS_FS_REMOVE_DIR |
        LANDLOCK_ACCESS_FS_REMOVE_FILE |
        LANDLOCK_ACCESS_FS_MAKE_DIR |
        LANDLOCK_ACCESS_FS_MAKE_REG |
        LANDLOCK_ACCESS_FS_MAKE_SYM |

        // Version-specific additions
        #if defined(LANDLOCK_ACCESS_FS_TRUNCATE)
        LANDLOCK_ACCESS_FS_TRUNCATE |
        #endif
        #if defined(LANDLOCK_ACCESS_FS_IOCTL_DEV)
        LANDLOCK_ACCESS_FS_IOCTL_DEV |
        #endif
        #if defined(LANDLOCK_ACCESS_FS_REFER)
        LANDLOCK_ACCESS_FS_REFER |
        #endif
        0
};
Security Benefits of Deny-by-Default
  1. Explicit Auditing: You can see exactly what's allowed by reading the code
  2. No Accidental Permissions: Forgotten paths don't accidentally get access
  3. Secure by Default: Applications must explicitly request each permission
  4. Predictable Security: Unknown paths are automatically protected
Practical Rule of Thumb
  • Handle all operations you care about in handled_access_fs
  • Only grant permissions to paths that absolutely need them
  • Assume everything else is blocked by default
  • Test access to restricted paths to verify denial works correctly

This deny-by-default approach makes Landlock extremely secure - you don't need to explicitly block dangerous paths, you only need to explicitly allow the safe ones.

Step 4: Add Access Rules

Purpose: Define WHERE those actions are allowed or denied by applying the ruleset capabilities to specific paths.

This is where we speak the "language" defined in the ruleset creation, telling Landlock exactly which directories and files get which permissions.

int add_readonly_path(int ruleset_fd, const char *path) {
    struct landlock_path_beneath_attr path_beneath = {
        .allowed_access =
            LANDLOCK_ACCESS_FS_EXECUTE |
            LANDLOCK_ACCESS_FS_READ_FILE |
            LANDLOCK_ACCESS_FS_READ_DIR
    };

    path_beneath.parent_fd = open(path, O_PATH | O_CLOEXEC);
    if (path_beneath.parent_fd < 0) {
        perror("Failed to open path");
        return -1;
    }

    int ret = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
                               &path_beneath, 0);
    close(path_beneath.parent_fd);
    return ret;
}

int add_readwrite_path(int ruleset_fd, const char *path) {
    struct landlock_path_beneath_attr path_beneath = {
        .allowed_access =
            LANDLOCK_ACCESS_FS_EXECUTE |
            LANDLOCK_ACCESS_FS_WRITE_FILE |
            LANDLOCK_ACCESS_FS_READ_FILE |
            LANDLOCK_ACCESS_FS_READ_DIR |
            LANDLOCK_ACCESS_FS_REMOVE_DIR |
            LANDLOCK_ACCESS_FS_REMOVE_FILE |
            LANDLOCK_ACCESS_FS_MAKE_CHAR |
            LANDLOCK_ACCESS_FS_MAKE_DIR |
            LANDLOCK_ACCESS_FS_MAKE_REG |
            LANDLOCK_ACCESS_FS_MAKE_SOCK |
            LANDLOCK_ACCESS_FS_MAKE_FIFO |
            LANDLOCK_ACCESS_FS_MAKE_BLOCK |
            LANDLOCK_ACCESS_FS_MAKE_SYM
    };

    path_beneath.parent_fd = open(path, O_PATH | O_CLOEXEC);
    if (path_beneath.parent_fd < 0) {
        perror("Failed to open path");
        return -1;
    }

    int ret = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
                               &path_beneath, 0);
    close(path_beneath.parent_fd);
    return ret;
}

Step 5: Enforce the Ruleset

Purpose: Apply the complete ruleset to the current process and its children.

This is the final step that activates all the previously defined rules. Once enforced, the sandbox is active for the current process and will be inherited by all child processes.

int enforce_sandbox(int ruleset_fd) {
    // Prevent privilege escalation
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
        perror("Failed to set no_new_privs");
        return -1;
    }

    // Enforce Landlock restrictions
    if (landlock_restrict_self(ruleset_fd, 0)) {
        perror("Failed to enforce Landlock");
        return -1;
    }

    printf("Landlock sandbox enforced\n");
    return 0;
}

Recommended Security Pattern: Root-Read-Then-Grant-Selective

The most secure and maintainable approach is to start with a restrictive base and then grant specific permissions where needed:

Why This Pattern Works

  1. Fail-closed security: Everything denied by default
  2. Explicit permissions: Only what's specifically allowed works
  3. Least privilege: Grant minimum necessary access
  4. Maintainable: Clear visibility of what gets which permissions

Implementation Example

// Helper to get current working directory
int add_current_directory_permissions(int ruleset_fd) {
    char cwd[PATH_MAX];
    if (getcwd(cwd, sizeof(cwd)) == NULL) {
        perror("getcwd failed");
        return -1;
    }

    // Grant read/write access to current directory
    return add_readwrite_path(ruleset_fd, cwd);
}

// Full restrictive sandbox using the pattern
int create_minimum_privilege_sandbox(void) {
    int ruleset_fd = create_file_ruleset();

    // Start with root read-only (denied-by-default)
    add_readonly_path(ruleset_fd, "/");      // Base filesystem access

    // Grant specific additional permissions selectively
    add_current_directory_permissions(ruleset_fd);  // Current working directory
    add_readwrite_path(ruleset_fd, "/tmp");         // Temp files
    add_readwrite_path(ruleset_fd, "/var/log/myapp"); // App-specific logs

    // Essential system read-only access
    add_readonly_path(ruleset_fd, "/usr");
    add_readonly_path(ruleset_fd, "/bin");
    add_readonly_path(ruleset_fd, "/lib");
    add_readonly_path(ruleset_fd, "/etc");

    enforce_sandbox(ruleset_fd);
    close(ruleset_fd);
    return 0;
}

Directory Hierarchy Benefits

Landlock's path-based approach means permissions apply hierarchically:

// This gives read access to everything under /usr
add_readonly_path(ruleset_fd, "/usr");

// So /usr/bin, /usr/lib, /usr/share all get read access
// This includes sub-subdirectories like /usr/local/bin

Application-Specific Examples

Web Server Sandbox

int create_web_server_sandbox(void) {
    int ruleset_fd = create_file_ruleset();

    // Start with restrictive base (read-only root)
    add_readonly_path(ruleset_fd, "/");

    // Grant specific additional permissions
    add_readonly_path(ruleset_fd, "/var/www/html");   // Web content
    add_readwrite_path(ruleset_fd, "/var/log/apache2");  // Logs
    add_readwrite_path(ruleset_fd, "/tmp");              // Temp files
    add_readwrite_path(ruleset_fd, "/var/www/uploads");  // User uploads
    add_current_directory_permissions(ruleset_fd);        // Working directory

    enforce_sandbox(ruleset_fd);
    close(ruleset_fd);
    return 0;
}

Desktop Application Sandbox

int create_desktop_app_sandbox(void) {
    int ruleset_fd = create_file_ruleset();

    // Restrictive base
    add_readonly_path(ruleset_fd, "/");

    // Application-specific permissions
    add_readwrite_path(ruleset_fd, getenv("HOME"));    // User's home directory
    add_readwrite_path(ruleset_fd, "/tmp");            // Temporary files
    add_readonly_path(ruleset_fd, "/usr/share/app");   // Application resources
    add_readonly_path(ruleset_fd, "/usr/lib/app");     // Application libraries

    enforce_sandbox(ruleset_fd);
    close(ruleset_fd);
    return 0;
}

Alternative: Minimal Approach

For highly security-sensitive applications, some prefer the opposite approach - only allow what's explicitly needed:

int create_minimal_sandbox(void) {
    int ruleset_fd = create_file_ruleset();

    // No base permissions - only the bare minimum
    add_readonly_path(ruleset_fd, "/bin/sh");      // For system() calls
    add_readonly_path(ruleset_fd, "/lib");        // Essential libraries
    add_readwrite_path(ruleset_fd, cwd_path);     // Working directory only
    add_readwrite_path(ruleset_fd, "/tmp");       // Maybe temp access

    enforce_sandbox(ruleset_fd);
    close(ruleset_fd);
    return 0;
}

Complete Example

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <linux/landlock.h>

/* Syscall wrappers from Step 1 */

int main(void) {
    int ruleset_fd;

    // Check support
    if (check_landlock_support() < 0) {
        return 1;
    }

    // Create ruleset
    ruleset_fd = create_file_ruleset();
    if (ruleset_fd < 0) {
        perror("Failed to create ruleset");
        return 1;
    }

    // Add readonly access to system directories
    if (add_readonly_path(ruleset_fd, "/usr") ||
        add_readonly_path(ruleset_fd, "/bin") ||
        add_readonly_path(ruleset_fd, "/lib") ||
        add_readonly_path(ruleset_fd, "/etc")) {
        perror("Failed to add readonly paths");
        close(ruleset_fd);
        return 1;
    }

    // Add readwrite access to temp directory
    if (add_readwrite_path(ruleset_fd, "/tmp")) {
        perror("Failed to add readwrite path");
        close(ruleset_fd);
        return 1;
    }

    // Enforce sandbox
    if (enforce_sandbox(ruleset_fd)) {
        close(ruleset_fd);
        return 1;
    }

    close(ruleset_fd);

    // Test the sandbox
    printf("Sandbox active. Testing access:\n");

    // This should work
    printf("Opening /etc/passwd: ");
    FILE *f = fopen("/etc/passwd", "r");
    printf("%s\n", f ? "OK" : "DENIED");
    if (f) fclose(f);

    // This should be denied
    printf("Creating /tmp/test_file: ");
    FILE *f2 = fopen("/tmp/test_file", "w");
    printf("%s\n", f2 ? "OK" : "DENIED");
    if (f2) fclose(f2);

    // This should be denied
    printf("Opening /root/.bashrc: ");
    FILE *f3 = fopen("/root/.bashrc", "r");
    printf("%s\n", f3 ? "OK" : "DENIED");
    if (f3) fclose(f3);

    return 0;
}

Practical Usage Patterns

Basic Read-Only Sandbox

// Create a minimal sandbox that only allows reading essential paths
int create_readonly_sandbox(void) {
    int ruleset_fd = create_file_ruleset();
    if (ruleset_fd < 0) return -1;

    // Only add basic read-only paths
    add_readonly_path(ruleset_fd, "/usr");
    add_readonly_path(ruleset_fd, "/bin");
    add_readonly_path(ruleset_fd, "/lib");
    add_readonly_path(ruleset_fd, "/lib64");
    add_readonly_path(ruleset_fd, "/etc");

    // No write access to any path

    enforce_sandbox(ruleset_fd);
    close(ruleset_fd);
    return 0;
}

Temporary Directory Sandbox

// Allow access only to a specific temporary directory
int create_tmp_sandbox(const char *tmp_dir) {
    int ruleset_fd = create_file_ruleset();
    if (ruleset_fd < 0) return -1;

    // Full access to specified temp directory only
    add_readwrite_path(ruleset_fd, tmp_dir);

    enforce_sandbox(ruleset_fd);
    close(ruleset_fd);
    return 0;
}

Configuration-Based Sandboxing

// Helper to parse colon-separated paths from environment
int add_paths_from_env(int ruleset_fd, const char *env_var, uint64_t access) {
    char *paths = getenv(env_var);
    if (!paths) return 0;

    char *paths_copy = strdup(paths);
    char *path = strtok(paths_copy, ":");

    while (path) {
        struct landlock_path_beneath_attr path_beneath = {
            .allowed_access = access,
            .parent_fd = open(path, O_PATH | O_CLOEXEC)
        };

        if (path_beneath.parent_fd >= 0) {
            landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
                            &path_beneath, 0);
            close(path_beneath.parent_fd);
        }

        path = strtok(NULL, ":");
    }

    free(paths_copy);
    return 0;
}

Important Notes

ABI Compatibility

// Always check and adapt to the available ABI version
int abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
if (abi >= 2) {
    // Can use LANDLOCK_ACCESS_FS_REFER
} else {
    // Must avoid FS_REFER features
}

Enforcement Limits

  • Maximum 16 layers of stacked rulesets
  • Ruleset modifications are tracked with reference counting
  • Landlock cannot be disabled once enforced on a process
  • Children inherit parent's Landlock domain

Network Restrictions

Important: In Linux 6.1, Landlock does not implement network restrictions. The framework is designed for future extension, but network port binding controls are not available in this kernel version.

Kernel Configuration

Landlock requires:

  • CONFIG_SECURITY_LANDLOCK=y in kernel configuration
  • CONFIG_LSM=landlock,[...] to enable at boot time
  • OR lsm=landlock,[...] kernel boot parameter to enable dynamically

Error Handling

Common error codes:

  • ENOSYS: Landlock not supported by kernel
  • EOPNOTSUPP: Landlock disabled at boot time
  • EACCES: Access denied by Landlock rules
  • EXDEV: Privilege escalation attempt (e.g., linking to less restrictive path)
  • E2BIG: Too many rule layers (max 16)
  • EPERM: Cannot enforce (missing no_new_privs or CAP_SYS_ADMIN)

This provides a comprehensive foundation for using Landlock in Linux 6.1 applications to create secure filesystem sandboxes.

Landlock Evolution: Version Comparison

Landlock has evolved significantly from Linux 6.1 to 6.12, with major new features added across different ABI versions:

ABI Version Timeline

  • Linux 6.1: ABI v2 (Filesystem access with LANDLOCK_ACCESS_FS_REFER)
  • Linux 6.6: ABI v3 (Added LANDLOCK_ACCESS_FS_TRUNCATE)
  • Linux 6.12: ABI v6 (Network restrictions, IPC scoping, device ioctl control)

Kernel Version Feature Matrix

Feature Linux 6.1 (ABI v2) Linux 6.6 (ABI v3) Linux 6.12 (ABI v6)
Filesystem access Yes Yes Yes
File operations (read/write/execute) Yes Yes Yes
Directory operations Yes Yes Yes
File creation operations Yes Yes Yes
LANDLOCK_ACCESS_FS_REFER Yes Yes Yes
LANDLOCK_ACCESS_FS_TRUNCATE No Yes Yes
LANDLOCK_ACCESS_FS_IOCTL_DEV No No Yes
Network port restrictions No No Yes
TCP bind/connect control No No Yes
IPC scoping No No Yes
Abstract UNIX socket scoping No No Yes
Signal scoping No No Yes

New Features in Linux 6.6 (ABI v3)

File Truncation Control

Linux 6.6 introduced LANDLOCK_ACCESS_FS_TRUNCATE (ABI v3), providing fine-grained control over file truncation operations:

#define LANDLOCK_ACCESS_FS_TRUNCATE (1ULL << 14)

Controlled Operations:

  • truncate(2) and ftruncate(2) system calls
  • creat(2) system call (which can truncate existing files)
  • open(2) with O_TRUNC flag

Enhanced File Operations Support:

// Updated file access rights for ABI v3+
access_rights |= LANDLOCK_ACCESS_FS_TRUNCATE;

// Example: Allow file creation without truncation
struct landlock_path_beneath_attr path_readwrite = {
    .allowed_access =
        LANDLOCK_ACCESS_FS_READ_FILE |
        LANDLOCK_ACCESS_FS_WRITE_FILE |
        LANDLOCK_ACCESS_FS_MAKE_REG |
        LANDLOCK_ACCESS_FS_MAKE_DIR |
        // Explicitly NOT including LANDLOCK_ACCESS_FS_TRUNCATE
        0
};

// Example: Allow file creation with truncation
struct landlock_path_beneath_attr path_full_write = {
    .allowed_access =
        LANDLOCK_ACCESS_FS_READ_FILE |
        LANDLOCK_ACCESS_FS_WRITE_FILE |
        LANDLOCK_ACCESS_FS_TRUNCATE |  // Allow overwriting
        LANDLOCK_ACCESS_FS_MAKE_REG |
        LANDLOCK_ACCESS_FS_MAKE_DIR
};

ABI Compatibility:

int abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
if (abi < 3) {
    printf("File truncation control not available (ABI %d)\n", abi);
    // Fall back to more restrictive policies
    ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_TRUNCATE;
}

New Features in Linux 6.12 (ABI v6)

Network Port Restrictions

Linux 6.12 introduced comprehensive network port control (ABI v4+), allowing restriction of TCP socket operations:

New Structures and Types

// Extended ruleset attributes
struct landlock_ruleset_attr {
    __u64 handled_access_fs;     // Filesystem rights
    __u64 handled_access_net;    // Network rights (NEW in v4)
    __u64 scoped;                // IPC scoping (NEW in v5)
};

// New rule type for network ports
enum landlock_rule_type {
    LANDLOCK_RULE_PATH_BENEATH = 1,
    LANDLOCK_RULE_NET_PORT,      // NEW in v4
};

// Network port rule structure
struct landlock_net_port_attr {
    __u64 allowed_access;        // Network access rights
    __u64 port;                  // Port number (host endianness)
};

Network Access Rights

#define LANDLOCK_ACCESS_NET_BIND_TCP    (1ULL << 0)  // Bind TCP socket
#define LANDLOCK_ACCESS_NET_CONNECT_TCP (1ULL << 1)  // Connect TCP socket

Network Ruleset Creation Example

int create_network_ruleset(void) {
    struct landlock_ruleset_attr ruleset_attr = {
        .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE |
                           LANDLOCK_ACCESS_FS_READ_DIR,
        .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
                            LANDLOCK_ACCESS_NET_CONNECT_TCP,
        // No IPC scoping in this example
    };

    // Check ABI version for network support
    int abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
    if (abi < 4) {
        fprintf(stderr, "Network restrictions not available (ABI %d)\n", abi);
        return -1;
    }

    return landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
}

// Add network port rules
int add_network_port_rule(int ruleset_fd, uint16_t port, uint64_t access) {
    struct landlock_net_port_attr net_rule = {
        .allowed_access = access,
        .port = port
    };

    return landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, &net_rule, 0);
}

// Example: Allow binding to port 8080 only
int setup_web_server_sandbox(void) {
    int ruleset_fd = create_network_ruleset();
    if (ruleset_fd < 0) return -1;

    // Allow binding to specific web server ports
    add_network_port_rule(ruleset_fd, 80, LANDLOCK_ACCESS_NET_BIND_TCP);   // HTTP
    add_network_port_rule(ruleset_fd, 8080, LANDLOCK_ACCESS_NET_BIND_TCP); // Alt HTTP

    // Allow outgoing connections to any port (for updates/external APIs)
    for (uint16_t port = 80; port <= 90; port++) {
        add_network_port_rule(ruleset_fd, port, LANDLOCK_ACCESS_NET_CONNECT_TCP);
    }

    enforce_sandbox(ruleset_fd);
    close(ruleset_fd);
    return 0;
}

Network Port Zero Handling

Special handling for port 0 (ephemeral port range):

// Allow binding to ephemeral ports (port 0 automatically maps to range)
add_network_port_rule(ruleset_fd, 0, LANDLOCK_ACCESS_NET_BIND_TCP);

Device IOCTL Control (ABI v5)

Linux 6.12 introduced device ioctl restrictions with LANDLOCK_ACCESS_FS_IOCTL_DEV:

#define LANDLOCK_ACCESS_FS_IOCTL_DEV (1ULL << 15)

Controlled Operations:

  • ioctl(2) calls on character and block devices
  • Exclusions: Common safe ioctl calls continue to work:
    • File descriptor operations (FIOCLEX, FIONCLEX)
    • File description operations (FIONBIO, FIOASYNC)
    • Filesystem operations (FIFREEZE, FITHAW, FIGETBSZ)
    • Safe device operations (FS_IOC_FIEMAP, FICLONE, etc.)
// Example: Allow device access except for ioctls
struct landlock_path_beneath_attr device_path = {
    .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE |
                     LANDLOCK_ACCESS_FS_WRITE_FILE
    // Explicitly NOT including LANDLOCK_ACCESS_FS_IOCTL_DEV
};

// Example: Full device access including ioctls
struct landlock_path_beneath_attr full_device_access = {
    .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE |
                     LANDLOCK_ACCESS_FS_WRITE_FILE |
                     LANDLOCK_ACCESS_FS_IOCTL_DEV  // Allow ioctl commands
};

IPC Scoping (ABI v5)

Linux 6.12 introduced IPC scoping to restrict inter-process communication:

Scope Flags

#define LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET (1ULL << 0)  // Restrict abstract UNIX sockets
#define LANDLOCK_SCOPE_SIGNAL                (1ULL << 1)  // Restrict signal sending

Scoping Usage

int create_scoped_ruleset(void) {
    struct landlock_ruleset_attr ruleset_attr = {
        .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
        .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL
    };

    int abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
    if (abi < 5) {
        fprintf(stderr, "IPC scoping not available (ABI %d)\n", abi);
        return -1;
    }

    return landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
}

// Example: Create strict isolation sandbox
int create_iso_sandbox(void) {
    int ruleset_fd = create_scoped_ruleset();
    if (ruleset_fd < 0) return -1;

    // Add minimal filesystem access
    add_readonly_path(ruleset_fd, "/bin");
    add_readonly_path(ruleset_fd, "/lib");
    add_readonly_path(ruleset_fd, "/usr");
    add_readwrite_path(ruleset_fd, "/tmp");

    // Full scoping: no external IPC or signals
    struct landlock_ruleset_attr scoped_attr = {
        .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL
    };

    enforce_sandbox(ruleset_fd);
    close(ruleset_fd);
    return 0;
}

Scoping Behavior

  • Abstract UNIX Socket Scoping: Processes can only connect to abstract sockets created by processes in the same Landlock domain
  • Signal Scoping: Processes can only send signals to processes in the same domain
  • No Exceptions: Scoping is absolute - no rules can allow exceptions to scoping restrictions
  • Inheritance: Scoping applies to inherited sockets but cannot disconnect already established connections

Advanced Cross-Version Examples

Version-Aware Sandbox Creation

int create_maximal_sandbox(void) {
    struct landlock_ruleset_attr ruleset_attr = {0};
    int abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);

    if (abi < 0) {
        fprintf(stderr, "Landlock not supported\n");
        return -1;
    }

    // Filesystem rights (available in all versions)
    ruleset_attr.handled_access_fs =
        LANDLOCK_ACCESS_FS_EXECUTE |
        LANDLOCK_ACCESS_FS_WRITE_FILE |
        LANDLOCK_ACCESS_FS_READ_FILE |
        LANDLOCK_ACCESS_FS_READ_DIR |
        LANDLOCK_ACCESS_FS_REMOVE_DIR |
        LANDLOCK_ACCESS_FS_REMOVE_FILE |
        LANDLOCK_ACCESS_FS_MAKE_CHAR |
        LANDLOCK_ACCESS_FS_MAKE_DIR |
        LANDLOCK_ACCESS_FS_MAKE_REG |
        LANDLOCK_ACCESS_FS_MAKE_SOCK |
        LANDLOCK_ACCESS_FS_MAKE_FIFO |
        LANDLOCK_ACCESS_FS_MAKE_BLOCK |
        LANDLOCK_ACCESS_FS_MAKE_SYM;

    // Add version-specific features
    if (abi >= 2) {
        ruleset_attr.handled_access_fs |= LANDLOCK_ACCESS_FS_REFER;
    }

    if (abi >= 3) {
        ruleset_attr.handled_access_fs |= LANDLOCK_ACCESS_FS_TRUNCATE;
    }

    if (abi >= 5) {
        ruleset_attr.handled_access_fs |= LANDLOCK_ACCESS_FS_IOCTL_DEV;
        // Enable IPC scoping
        ruleset_attr.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL;
    }

    if (abi >= 4) {
        // Add network support
        ruleset_attr.handled_access_net =
            LANDLOCK_ACCESS_NET_BIND_TCP |
            LANDLOCK_ACCESS_NET_CONNECT_TCP;
    }

    return landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
}

Network-Enabled Application (v6.12+)

int create_networked_app_sandbox(void) {
    int abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
    if (abi < 4) {
        fprintf(stderr, "Network sandboxing requires ABI 4+, got %d\n", abi);
        return create_basic_file_sandbox(); // Fallback to filesystem only
    }

    // Create ruleset with both filesystem and network restrictions
    struct landlock_ruleset_attr ruleset_attr = {
        .handled_access_fs =
            LANDLOCK_ACCESS_FS_READ_FILE |
            LANDLOCK_ACCESS_FS_READ_DIR |
            LANDLOCK_ACCESS_FS_WRITE_FILE |
            LANDLOCK_ACCESS_FS_TRUNCATE,  // v3+

        .handled_access_net =
            LANDLOCK_ACCESS_NET_BIND_TCP |
            LANDLOCK_ACCESS_NET_CONNECT_TCP,

        .scoped = (abi >= 5) ? LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET : 0
    };

    int ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
    if (ruleset_fd < 0) return -1;

    // Add filesystem rules
    add_readonly_path(ruleset_fd, "/usr");
    add_readonly_path(ruleset_fd, "/lib");
    add_readwrite_path(ruleset_fd, "/tmp");
    add_readwrite_path(ruleset_fd, "/var/log");

    // Add network rules - allow specific server ports
    add_network_port_rule(ruleset_fd, 80, LANDLOCK_ACCESS_NET_BIND_TCP);   // HTTP
    add_network_port_rule(ruleset_fd, 443, LANDLOCK_ACCESS_NET_BIND_TCP);  // HTTPS

    // Allow outgoing connections to specific services
    uint16_t allowed_out_ports[] = {53, 80, 443, 587, 993}; // DNS, HTTP/S, SMTP, IMAPS
    for (int i = 0; i < sizeof(allowed_out_ports)/sizeof(uint16_t); i++) {
        add_network_port_rule(ruleset_fd, allowed_out_ports[i],
                             LANDLOCK_ACCESS_NET_CONNECT_TCP);
    }

    enforce_sandbox(ruleset_fd);
    close(ruleset_fd);
    return 0;
}

Migration Guide

From Linux 6.1 to 6.6

  1. Update ABI version check:
// Old way
if (abi == 2) { ... }

// New way
if (abi >= 3) {
    // Can use LANDLOCK_ACCESS_FS_TRUNCATE
}
  1. Add truncation control:
// For applications that need file overwriting
ruleset_attr.handled_access_fs |= LANDLOCK_ACCESS_FS_TRUNCATE;

From Linux 6.6 to 6.12

  1. Extended ruleset structure:
struct landlock_ruleset_attr ruleset_attr = {
    .handled_access_fs = fs_rights,
    .handled_access_net = net_rights,  // NEW
    .scoped = scope_flags             // NEW
};
  1. Add network support:
if (abi >= 4) {
    // Use network features
    add_network_port_rule(ruleset_fd, 80, LANDLOCK_ACCESS_NET_BIND_TCP);
}
  1. Consider IPC scoping:
if (abi >= 5) {
    // Add isolation features
    ruleset_attr.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET;
}

This comprehensive coverage shows how Landlock has evolved from a filesystem-only sandbox in Linux 6.1 to a full-featured security framework in Linux 6.12 with network controls, device restrictions, and IPC isolation.

Process Inheritance and Child Process Security

Automatic Inheritance of Landlock Restrictions

Landlock restrictions are automatically inherited by child processes through both fork() and exec() system calls. This inheritance is a fundamental security property that ensures containment is maintained across the entire process tree.

Inheritance Behavior

After fork():

  • Child inherits the exact same Landlock domain as the parent
  • All filesystem, network, and IPC restrictions are inherited
  • The complete layer stack of rulesets is preserved

After exec():

  • Landlock domain is preserved across the exec operation
  • The new program inherits all restrictions
  • This behavior differs from some other security mechanisms that might reset on exec

Inheritance Example

int main(void) {
    // Parent creates and enforces Landlock restrictions
    int ruleset_fd = create_file_ruleset();

    add_readonly_path(ruleset_fd, "/usr");
    add_readwrite_path(ruleset_fd, "/tmp");

    enforce_sandbox(ruleset_fd);
    close(ruleset_fd);

    printf("Parent: Landlock sandbox active\n");

    // Test parent access
    FILE *f = fopen("/etc/passwd", "r");
    printf("Parent accessing /etc/passwd: %s\n", f ? "ALLOWED" : "DENIED");
    if (f) fclose(f);

    pid_t pid = fork();
    if (pid == 0) {
        // Child process
        printf("Child: Inherits parent's Landlock restrictions\n");

        // Child has same restrictions
        FILE *f_child = fopen("/etc/passwd", "r");
        printf("Child accessing /etc/passwd: %s\n", f_child ? "ALLOWED" : "DENIED");
        if (f_child) fclose(f_child);

        // Child cannot escape sandbox by exec
        execl("/bin/ls", "ls", "/root", NULL);
        perror("exec failed");
        exit(1);
    } else if (pid > 0) {
        // Parent waits for child
        int status;
        waitpid(pid, &status, 0);
        printf("Parent: Child finished\n");
    }

    return 0;
}

system() Function and Landlock

The system() function involves both fork() and exec(), and importantly, the spawned shell process and its commands are fully subject to the parent's Landlock restrictions.

How system() Works with Landlock

  1. system() forks a child process
  2. Child inherits parent's Landlock domain
  3. Child calls exec() to run shell (usually /bin/sh)
  4. Shell inherits Landlock domain
  5. Shell executes command, which also inherits the domain

Practical system() Usage

int main(void) {
    // Create Landlock sandbox
    int ruleset_fd = create_file_ruleset();
    add_readonly_path(ruleset_fd, "/usr");
    add_readonly_path(ruleset_fd, "/bin");  // Needed for system() calls
    add_readwrite_path(ruleset_fd, "/tmp");

    enforce_sandbox(ruleset_fd);
    close(ruleset_fd);

    printf("Parent: Landlock sandbox active\n");

    // This will fail due to filesystem restrictions
    int result = system("ls /root");  // Likely denied
    printf("system() ls /root returned: %d\n", result);

    // This will work because /tmp is allowed
    result = system("ls /tmp");  // Should succeed
    printf("system() ls /tmp returned: %d\n", result);

    // Shell redirection also respects Landlock
    result = system("echo 'test' > /tmp/file");  // Works
    result = system("echo 'test' > /root/file"); // Denied

    return 0;
}

Security Implications of Inheritance

No Privilege Escalation

The inheritance mechanism prevents privilege escalation:

  • Sandbox processes cannot spawn non-sandboxed children
  • execve() of SUID binaries preserves the sandbox when PR_SET_NO_NEW_PRIVS is set
  • Children can add more restrictive layers but cannot remove existing ones

Command Injection Mitigation

Landlock can help mitigate command injection vulnerabilities:

// Dangerous user input
char *user_input = "cat /etc/shadow";  // Malicious command

// This will be blocked if /etc/shadow is restricted
system(user_input);

Dependency Requirements

When using system(), ensure access to:

  • Shell executable (usually /bin/sh)
  • Required libraries and interpreters
  • Any programs executed within shell commands
// Ensure shell and dependencies are accessible
add_readonly_path(ruleset_fd, "/bin/sh");
add_readonly_path(ruleset_fd, "/lib");  // Shared libraries

Thread Inheritance Behavior

  • All threads created after Landlock enforcement inherit the restrictions
  • Sibling threads created before enforcement do not automatically inherit
  • This is different from fork/exec behavior

Alternative Process Management

For more control, applications can use direct fork()/exec():

int safer_exec_with_landlock() {
    pid_t pid = fork();
    if (pid == 0) {
        // Child process - inherits Landlock automatically

        // Execute command directly (no shell interpretation)
        execl("/bin/ls", "ls", "/tmp", NULL);
        perror("execl failed");
        exit(1);
    } else if (pid > 0) {
        int status;
        waitpid(pid, &status, 0);
        return status;
    }
    return -1;
}

Inheritance Security Benefits

  1. Reliable containment: Parents can trust children remain sandboxed
  2. Predictable security: Inheritance rules are consistent and well-defined
  3. Attack surface reduction: Compromised children cannot bypass parent policies
  4. Application simplicity: Developers only need to enforce security once

Important Considerations

  • Design intention: Inheritance is a core security feature, not an implementation detail
  • No escape methods: There are no legitimate ways for children to bypass inherited restrictions
  • Layer preservation: All enforcement layers are preserved in child processes
  • Consistency across APIs: Both fork()/exec() and system() respect inheritance

This inheritance model makes Landlock particularly suitable for:

  • Container runtimes managing container processes
  • System services spawning worker processes
  • Security applications that need to control child process behavior
  • Any scenario where process tree security must be guaranteed

This comprehensive coverage shows how Landlock has evolved from a filesystem-only sandbox in Linux 6.1 to a full-featured security framework in Linux 6.12 with network controls, device restrictions, IPC isolation, and robust process inheritance security.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment