[BUG] Landlock denies LANDLOCK_ACCESS_FS_EXECUTE despite a correctly-anchored PathBeneath rule (contradicts selftest layout1.execute)
From: Ken Grimes <hidden>
Date: 2026-07-14 21:08:16
Hey all, this is my first bug report for linux. The issue was discovered
alongside llm-assisted coding on a downstream project. The
investigation/testing of the bug was a mostly manual process so I could
be sure this was something real. Please let me know if I can provide any
further details or assistance. Hope this is helpful, thank you for all of your
hard work!
Summary
-------
On Linux 7.1.3 (Fedora 44, Landlock ABI version 9), a minimal, correctly
constructed Landlock ruleset denies execve()/execveat() of that file with
EACCES. This directly contradicts the expected behavior of the official
selftest assertion: tools/testing/selftests/landlock/fs_test.c's
TEST_F_FORK(layout1, execute), specifically the file1_s1d2 case,
expects exactly this shape of rule to permit execution
(test_execute(_metadata, 0, file1_s1d2)).
This was confirmed two ways: a minimal standalone reproduction (below),
and by building and running your own unmodified
tools/testing/selftests/landlock/fs_test.c from the v7.1.3 tag (exact
match to the running kernel and its kernel-headers package) against this
kernel. layout1.execute fails on this kernel:
# RUN layout1.execute ...
# fs_test.c:1949:execute:Expected err ? -1 : 0 (0) == execve(path, argv, NULL) (-1)
# fs_test.c:1951:execute:Failed to execute "tmp/s1d1/s1d2/f1": Permission denied
# execute: Test terminated by assertion
# FAIL layout1.execute
not ok 29 layout1.execute
tmp/s1d1/s1d2/f1 is file1_s1d2 is the file living directly inside
dir_s1d2, which is the directory the test's own PathBeneath rule
anchors on (rules[0] = { .path = dir_s1d2, .access =
LANDLOCK_ACCESS_FS_EXECUTE }). The failing assertion is
test_execute(_metadata, 0, file1_s1d2) (fs_test.c:1949 in the v7.1.3
tree, quoted below). The test's author expects this call to succeed
(err == 0), and on this kernel it doesn't. Full raw test output is
attached (official-selftest-output.log).
Relevant excerpt of the actual test body (identical across the v7.1.3
tag and current master):
TEST_F_FORK(layout1, execute)
{
const struct rule rules[] = {
{
.path = dir_s1d2,
.access = LANDLOCK_ACCESS_FS_EXECUTE,
},
{},
};
copy_file(_metadata, bin_true, file1_s1d1);
copy_file(_metadata, bin_true, file1_s1d2);
copy_file(_metadata, bin_true, file1_s1d3);
/* Checks before file1_s1d1 being denied. */
test_execute(_metadata, 0, file1_s1d1);
test_check_exec(_metadata, 0, file1_s1d1);
enforce_fs(_metadata, rules[0].access, rules);
ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY));
ASSERT_EQ(0, test_open(file1_s1d1, O_RDONLY));
test_execute(_metadata, EACCES, file1_s1d1);
test_check_exec(_metadata, EACCES, file1_s1d1);
ASSERT_EQ(0, test_open(dir_s1d2, O_RDONLY));
ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY));
test_execute(_metadata, 0, file1_s1d2); /* <-- fails here */
test_check_exec(_metadata, 0, file1_s1d2);
...
(Reproducing the official fixture required running from a
world-writable directory: prepare_layout_opt() calls disable_caps()
which drops CAP_DAC_OVERRIDE from the otherwise-root test process,
before creating its working directory, so it must be run from
somewhere an unprivileged-DAC root can still write, e.g. under /tmp
directly. This is unrelated to the actual bug, just a note for anyone
else reproducing it.)
Environment
-----------
- Kernel: 7.1.3-200.fc44.x86_64 (Fedora 44 Workstation)
- Landlock ABI version: 9 (via landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION))
- kernel-headers package: kernel-headers-7.1.3-200.fc44.x86_64
(exact version match to the running kernel; the reproduction below was
built against these headers)
- No root/CAP_SYS_ADMIN involved anywhere in the reproduction -- Landlock
is unprivileged by design, and this reproduces identically as an
ordinary user.
Reproduction
------------
Official selftest (strongest evidence):
$ git clone --branch v7.1.3 --depth 1 https://github.com/gregkh/linux
$ cd linux/tools/testing/selftests/landlock
$ gcc -O0 -g -Wall -o fs_test fs_test.c -lcap -lpthread
$ gcc -O0 -o true true.c
$ mkdir -p /tmp/lltest && chmod 1777 /tmp/lltest
$ cp fs_test true /tmp/lltest/ && cd /tmp/lltest
$ sudo ./fs_test -f layout1 -t execute
(Run from a world-writable directory. See the note above about
disable_caps()) Full output attached as official-selftest-output.log.
Minimal standalone reproduction (self-contained, no external
dependencies beyond libc and the system's own <linux/landlock.h>,
unprivileged, attached as repro.c):
/*
* Minimal standalone reproduction of a Landlock EXECUTE-right denial that
* contradicts the upstream kernel's own selftest expectation.
*
* This mirrors tools/testing/selftests/landlock/fs_test.c's
* TEST_F_FORK(layout1, execute) -- specifically the file1_s1d2 case: a
* PathBeneath rule granting only LANDLOCK_ACCESS_FS_EXECUTE, anchored on a
* directory, is expected to permit execve() of a file directly inside that
* directory (upstream asserts test_execute(_metadata, 0, file1_s1d2), i.e.
* "no error"). On this kernel it is denied with EACCES instead.
*
* No root/CAP_SYS_ADMIN required -- Landlock is designed for unprivileged
* use, and this reproduces the same way as an ordinary user.
*
* Build: gcc -O0 -g -Wall -o repro repro.c
* Run: ./repro
*
* Observed on: Linux 7.1.3-200.fc44.x86_64 (Fedora 44 Workstation),
* Landlock ABI version 9 (per landlock_create_ruleset(NULL, 0,
* LANDLOCK_CREATE_RULESET_VERSION)).
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <unistd.h>
static int landlock_create_ruleset(const struct landlock_ruleset_attr *attr,
size_t size, __u32 flags)
{
return syscall(SYS_landlock_create_ruleset, attr, size, flags);
}
static int landlock_add_rule(int ruleset_fd, enum landlock_rule_type type,
const void *attr, __u32 flags)
{
return syscall(SYS_landlock_add_rule, ruleset_fd, type, attr, flags);
}
static int landlock_restrict_self(int ruleset_fd, __u32 flags)
{
return syscall(SYS_landlock_restrict_self, ruleset_fd, flags);
}
static void die(const char *what)
{
fprintf(stderr, "%s: %s\n", what, strerror(errno));
exit(1);
}
/* Runs `path` via a fresh execve() in a child, prints and returns its result. */
static int try_execve(const char *path)
{
pid_t pid = fork();
if (pid < 0)
die("fork");
if (pid == 0) {
char *const argv[] = { (char *)path, NULL };
execve(path, argv, NULL);
/* Only reached on failure. */
fprintf(stderr, "execve(%s) failed: %s (errno %d)\n", path,
strerror(errno), errno);
_exit(1);
}
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
return 0;
return 1;
}
int main(void)
{
int abi = landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION);
printf("Landlock ABI version: %d\n", abi);
if (abi < 0)
die("landlock_create_ruleset(VERSION)");
/* Build tmp/dir/exe, matching layout1's dir_s1d2/file1_s1d2 shape:
* a rule anchored on the parent directory, target file directly
* inside it (zero levels of extra nesting). */
if (mkdir("repro-dir", 0700) < 0 && errno != EEXIST)
die("mkdir repro-dir");
/* Copy /bin/true in as our target executable. */
{
char cmd[256];
snprintf(cmd, sizeof(cmd),
"cp /bin/true repro-dir/exe && chmod 755 repro-dir/exe");
if (system(cmd) != 0)
die("cp /bin/true");
}
/* Baseline: confirm it executes fine before Landlock is involved. */
if (try_execve("repro-dir/exe") != 0) {
fprintf(stderr, "baseline execve failed even without Landlock -- "
"environment problem, not a Landlock issue\n");
return 1;
}
printf("Baseline (no Landlock): execve succeeded, as expected.\n");
/* Minimal ruleset: EXECUTE only, matching the official selftest's
* rules[0].access = LANDLOCK_ACCESS_FS_EXECUTE for layout1.execute. */
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE,
};
int ruleset_fd = landlock_create_ruleset(&ruleset_attr,
sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
die("landlock_create_ruleset");
int parent_fd = open("repro-dir", O_PATH);
if (parent_fd < 0)
die("open repro-dir O_PATH");
struct landlock_path_beneath_attr path_beneath = {
.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE,
.parent_fd = parent_fd,
};
if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
&path_beneath, 0) != 0)
die("landlock_add_rule");
close(parent_fd);
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0)
die("prctl(PR_SET_NO_NEW_PRIVS)");
if (landlock_restrict_self(ruleset_fd, 0) != 0)
die("landlock_restrict_self");
close(ruleset_fd);
printf("Landlock ruleset installed: EXECUTE-only, one PathBeneath "
"rule anchored on repro-dir (the target file's own parent).\n");
int result = try_execve("repro-dir/exe");
if (result == 0) {
printf("PASS: execve succeeded, as the upstream selftest "
"(layout1.execute, file1_s1d2 case) expects.\n");
} else {
printf("FAIL: execve was denied (EACCES) despite an "
"explicit, correctly-anchored PathBeneath rule "
"granting LANDLOCK_ACCESS_FS_EXECUTE on the file's "
"own parent directory -- this contradicts "
"tools/testing/selftests/landlock/fs_test.c's "
"TEST_F_FORK(layout1, execute) expectation for the "
"file1_s1d2 case.\n");
}
system("rm -rf repro-dir");
return result;
}
Build and run:
$ gcc -O0 -g -Wall -o repro repro.c
$ ./repro
Landlock ABI version: 9
Baseline (no Landlock): execve succeeded, as expected.
Landlock ruleset installed: EXECUTE-only, one PathBeneath rule anchored on repro-dir (the target file's own parent).
execve(repro-dir/exe) failed: Permission denied (errno 13)
FAIL: execve was denied (EACCES) despite an explicit, correctly-anchored PathBeneath rule granting LANDLOCK_ACCESS_FS_EXECUTE on the file's own parent directory -- this contradicts tools/testing/selftests/landlock/fs_test.c's TEST_F_FORK(layout1, execute) expectation for the file1_s1d2 case.
What was ruled out before concluding this is a kernel-level issue
--------------------------------------------------------------------
This was investigated extensively in the course of debugging a real
downstream project (a Rust codebase using the landlock crate 0.4.5) that
hit this exact denial. Before concluding it's a kernel bug rather than a
userspace mistake, the following were each tested and ruled out as the
cause:
1. SELinux: this host runs SELinux Enforcing, but the reproduction
above runs entirely under the interactive shell's unconfined_t
domain, and the identical target file executes fine via a plain
(non-Landlock) execve() beforehand in the same process. Only adding
the Landlock ruleset flips the result to EACCES.
2. IPE (Integrity Policy Enforcement): enforce=1 on this host, but
/sys/kernel/security/ipe/policies/ is empty (no policy loaded), and
no IPE-related denial appears in the audit log around the failure. A
plain execve() of the same file with no Landlock domain active
succeeds, which would not be the case if IPE were unconditionally
denying this file.
3. Kernel lockdown mode: active at "integrity" level on this host
(Fedora enables this automatically under Secure Boot), but lockdown's
documented restrictions (module signing, /dev/mem, hibernation,
kexec, debugfs, etc.) govern kernel-image-integrity operations, not
ordinary unprivileged LSM policy decisions like Landlock's own
filesystem checks, and no documented interaction between the two was
found.
4. tmpfs vs. a real on-disk filesystem: the denial reproduces
identically whether the target directory is on tmpfs (/tmp) or a
real on-disk btrfs filesystem (the root filesystem).
5. fd-based (execveat+AT_EMPTY_PATH) vs. path-based execve(): both
fail identically; this is not specific to executing via an
already-open file descriptor.
6. Direct child vs. nested subdirectory: a file nested one level deeper
than the anchored rule fails identically to a file directly in the
anchored directory.
7. O_PATH vs. plain open() for the rule's anchor fd: no difference
(the landlock Rust crate's own documentation warns plain File opens
"may lead to unexpected errors," but explicitly testing O_PATH
produced the identical denial).
8. Ruleset rights breadth: tested with handled_access_fs as EXECUTE-only
as the full V3-era 15-right set, and with an additional explicit
file-anchored rule (correctly scoped to file-valid rights) layered
alongside the directory rule. All identical.
9. landlock_ruleset_attr struct size: tested with the struct passed at
24, 32, 48, and 64 bytes (padded with zeroed trailing fields, in case
of an extensible-struct zero-extension gap for fields introduced at
ABI 8/9, e.g. the quiet_access_fs/quiet_access_net/quiet_scoped
fields added around ABI 7-9). No difference; the reproduction
attached uses sizeof(ruleset_attr) against the real system header,
matching upstream selftest convention exactly.
10. Published errata: checked all three currently-documented entries
in security/landlock/errata/ (disconnected-directory rename
widening, TCP socket protocol misclassification, thread signal
scoping). None match this symptom.
11. Direct read of security/landlock/fs.c on torvalds/linux master:
the is_access_to_paths_allowed()/current_check_access_path()/
hook_file_open() hierarchy-walk logic appears correct for this
simple case (walk starts at the target file's own dentry, checks
find_rule() at each level while walking up via dget_parent(), and
our anchor is exactly one level up). No obvious logic defect was
found by inspection, which is part of why this is being reported
rather than fixed downstream.
Given (11), if the master-branch logic is correct, this may already be
fixed in a tree newer than 7.1.3, or the regression window may be
narrow but as of this exact kernel, the discrepancy against the
selftest's own documented expectation is real and reproducible.
Impact
------
This affects any unprivileged (or privileged, self-restricting) process
that installs a minimal Landlock domain and then tries to execute a file
covered by a directory-anchored rule which is a common Landlock usage
pattern (matching the kernel's own selftest).
Downstream, it silently breaks a security-hardening code path
(Landlock-based filesystem confinement for a privileged process tree)
with no diagnostic beyond a generic EACCES, since the process attempting
the exec has already dropped privileges and cannot meaningfully report
the failure back to a parent through advanced means.
Request
-------
Could you confirm whether this is a known/already-fixed issue, and if
not, whether the attached reproduction is sufficient to track down the
regression? Let me know if you'd like me test a patch or provide more
instrumentation from this exact kernel build if useful.
Attachments
-----------
- repro.c - minimal standalone reproduction (also inlined above)
- official-selftest-output.log - full raw TAP output from running the
unmodified v7.1.3 fs_test -f layout1 -t execute against this kernel