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.
- 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
- Ruleset: A collection of access control rules for specific filesystem operations
- Domain: A read-only ruleset bound to a process thread (stored in task credentials)
- Layer: Each enforcement operation creates a new layer in the domain
- Object: Kernel objects (inodes) that rules are tied to
- Rule: Associates access permissions with a specific filesystem object
Landlock provides three system calls, available through linux/landlock.h:
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 tolandlock_ruleset_attrstructuresize: Size of thelandlock_ruleset_attrstructureflags: Currently onlyLANDLOCK_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
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 bylandlock_create_ruleset()rule_type: Currently onlyLANDLOCK_RULE_PATH_BENEATHrule_attr: Pointer to rule-specific structureflags: Must be 0
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_privsenabled viaprctl(PR_SET_NO_NEW_PRIVS, 1) - OR have
CAP_SYS_ADMINcapability in its namespace
In Linux 6.1, Landlock only supports filesystem restrictions (no network controls):
LANDLOCK_ACCESS_FS_EXECUTE: Execute a fileLANDLOCK_ACCESS_FS_WRITE_FILE: Open a file with write accessLANDLOCK_ACCESS_FS_READ_FILE: Open a file with read access
LANDLOCK_ACCESS_FS_READ_DIR: Open a directory or list its contentLANDLOCK_ACCESS_FS_REMOVE_DIR: Remove an empty directory or rename itLANDLOCK_ACCESS_FS_REMOVE_FILE: Unlink or rename a file
LANDLOCK_ACCESS_FS_MAKE_CHAR: Create/link a character deviceLANDLOCK_ACCESS_FS_MAKE_DIR: Create/link a directoryLANDLOCK_ACCESS_FS_MAKE_REG: Create/link a regular fileLANDLOCK_ACCESS_FS_MAKE_SOCK: Create/link a UNIX domain socketLANDLOCK_ACCESS_FS_MAKE_FIFO: Create/link a named pipeLANDLOCK_ACCESS_FS_MAKE_BLOCK: Create/link a block deviceLANDLOCK_ACCESS_FS_MAKE_SYM: Create/link a symbolic link
LANDLOCK_ACCESS_FS_REFER: Link/rename between different directories
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);
}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;
}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.
- 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
- 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/homeaccess is automatically denied
Yes, by default everything is blocked unless explicitly allowed. This is a fundamental security property of Landlock.
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;
}// 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!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
};- Explicit Auditing: You can see exactly what's allowed by reading the code
- No Accidental Permissions: Forgotten paths don't accidentally get access
- Secure by Default: Applications must explicitly request each permission
- Predictable Security: Unknown paths are automatically protected
- 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.
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;
}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;
}The most secure and maintainable approach is to start with a restrictive base and then grant specific permissions where needed:
- Fail-closed security: Everything denied by default
- Explicit permissions: Only what's specifically allowed works
- Least privilege: Grant minimum necessary access
- Maintainable: Clear visibility of what gets which permissions
// 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;
}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/binint 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;
}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;
}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;
}#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;
}// 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;
}// 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;
}// 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;
}// 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
}- 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
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.
Landlock requires:
CONFIG_SECURITY_LANDLOCK=yin kernel configurationCONFIG_LSM=landlock,[...]to enable at boot time- OR
lsm=landlock,[...]kernel boot parameter to enable dynamically
Common error codes:
ENOSYS: Landlock not supported by kernelEOPNOTSUPP: Landlock disabled at boot timeEACCES: Access denied by Landlock rulesEXDEV: 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 has evolved significantly from Linux 6.1 to 6.12, with major new features added across different ABI versions:
- 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)
| 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 |
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)andftruncate(2)system callscreat(2)system call (which can truncate existing files)open(2)withO_TRUNCflag
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;
}Linux 6.12 introduced comprehensive network port control (ABI v4+), allowing restriction of TCP socket operations:
// 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)
};#define LANDLOCK_ACCESS_NET_BIND_TCP (1ULL << 0) // Bind TCP socket
#define LANDLOCK_ACCESS_NET_CONNECT_TCP (1ULL << 1) // Connect TCP socketint 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;
}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);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.)
- File descriptor operations (
// 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
};Linux 6.12 introduced IPC scoping to restrict inter-process communication:
#define LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET (1ULL << 0) // Restrict abstract UNIX sockets
#define LANDLOCK_SCOPE_SIGNAL (1ULL << 1) // Restrict signal sendingint 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;
}- 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
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);
}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;
}- Update ABI version check:
// Old way
if (abi == 2) { ... }
// New way
if (abi >= 3) {
// Can use LANDLOCK_ACCESS_FS_TRUNCATE
}- Add truncation control:
// For applications that need file overwriting
ruleset_attr.handled_access_fs |= LANDLOCK_ACCESS_FS_TRUNCATE;- Extended ruleset structure:
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = fs_rights,
.handled_access_net = net_rights, // NEW
.scoped = scope_flags // NEW
};- Add network support:
if (abi >= 4) {
// Use network features
add_network_port_rule(ruleset_fd, 80, LANDLOCK_ACCESS_NET_BIND_TCP);
}- 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.
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.
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
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;
}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.
system()forks a child process- Child inherits parent's Landlock domain
- Child calls
exec()to run shell (usually/bin/sh) - Shell inherits Landlock domain
- Shell executes command, which also inherits the domain
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;
}The inheritance mechanism prevents privilege escalation:
- Sandbox processes cannot spawn non-sandboxed children
execve()of SUID binaries preserves the sandbox whenPR_SET_NO_NEW_PRIVSis set- Children can add more restrictive layers but cannot remove existing ones
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);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- 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
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;
}- Reliable containment: Parents can trust children remain sandboxed
- Predictable security: Inheritance rules are consistent and well-defined
- Attack surface reduction: Compromised children cannot bypass parent policies
- Application simplicity: Developers only need to enforce security once
- 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()andsystem()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.