Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 104 additions & 13 deletions src/sandbox/sandbox_landlock.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
* Probe the kernel ABI and pass only bits it understands. create_ruleset uses
* the ABI-1 attr size so older kernels are not E2BIG. Missing optional RO
* paths are skipped; the workspace rule and restrict_self fail closed.
* ABI below 3 cannot handle TRUNCATE (JetPack 6.2 is kernel 5.15, ABI 1),
* so restrict_self is followed by a seccomp denial of the path truncate(2).
* ftruncate(2) stays allowed: it needs a writable fd, and WRITE_FILE already
* rejects opening a host file. Shell '>' uses open(O_TRUNC), not truncate(2).
*/
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
Expand All @@ -22,19 +26,32 @@
#endif

#ifndef __linux__
int sandbox_landlock_restrict_to_workspace(const char *workspace)
int sandbox_landlock_restrict_for_abi(const char *workspace, int abi)
{
(void)workspace;
(void)abi;
return 0;
}

int sandbox_landlock_restrict_to_workspace(const char *workspace)
{
return sandbox_landlock_restrict_for_abi(workspace, 0);
}

int sandbox_landlock_prepare(const char *workspace)
{
(void)workspace;
return 0;
}
#else

#include <errno.h>
#include <stddef.h>
#include <linux/audit.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <sys/prctl.h>

#ifndef LANDLOCK_ACCESS_FS_IOCTL_DEV
#define LANDLOCK_ACCESS_FS_IOCTL_DEV (1ULL << 15)
#endif
Expand All @@ -56,17 +73,24 @@ static __u64 landlock_abi1_fs_rights(void)
LANDLOCK_ACCESS_FS_MAKE_SYM;
}

static int landlock_handled_fs(__u64 *handled_out)
static int landlock_probe_abi(void)
{
int abi;
__u64 handled;

if (!handled_out)
return -1;
abi = (int)syscall(__NR_landlock_create_ruleset, NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION);
if (abi < 1)
return -1;
return abi;
}

/* @p abi selects bits the kernel understands. ABI below 3 has no TRUNCATE. */
static int landlock_handled_fs(int abi, __u64 *handled_out)
{
__u64 handled;

if (!handled_out || abi < 1)
return -1;
handled = landlock_abi1_fs_rights();
#ifdef LANDLOCK_ACCESS_FS_REFER
if (abi >= 2)
Expand Down Expand Up @@ -149,7 +173,7 @@ static __u64 mask_workspace_access(__u64 handled)

#define LANDLOCK_FD_SKIP (-2)

static int landlock_make_ruleset(const char *workspace)
static int landlock_make_ruleset(const char *workspace, int abi)
{
static const char *const RO_PATHS[] = {
"/bin", "/usr", "/usr/local", "/lib", "/lib64", "/lib32",
Expand All @@ -174,7 +198,7 @@ static int landlock_make_ruleset(const char *workspace)

if (!workspace || !workspace[0])
return LANDLOCK_FD_SKIP;
if (landlock_handled_fs(&handled) != 0)
if (landlock_handled_fs(abi, &handled) != 0)
return -1;
workspace_access = mask_workspace_access(handled);
ro_dir = (LANDLOCK_ACCESS_FS_EXECUTE |
Expand Down Expand Up @@ -220,30 +244,97 @@ static int landlock_make_ruleset(const char *workspace)
return ruleset_fd;
}

static unsigned int native_audit_arch(void)
{
#if defined(__x86_64__)
return AUDIT_ARCH_X86_64;
#elif defined(__aarch64__)
return AUDIT_ARCH_AARCH64;
#elif defined(__arm__)
return AUDIT_ARCH_ARM;
#else
return 0;
#endif
}

/* Path truncate(2) is outside Landlock until ABI 3. Deny that syscall only. */
static int deny_path_truncate(void)
{
struct sock_filter filter[] = {
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_truncate, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
#ifdef __NR_truncate64
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_truncate64, 0, 1),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA)),
#endif
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog;
unsigned int arch;

arch = native_audit_arch();
if (arch == 0)
return -1;
/* Instruction 1 is the architecture compare. A wrong index denies every syscall. */
filter[1].k = arch;
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0)
return -1;
memset(&prog, 0, sizeof(prog));
prog.len = (unsigned short)(sizeof(filter) / sizeof(filter[0]));
prog.filter = filter;
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog, 0L, 0L) != 0)
return -1;
return 0;
}

int sandbox_landlock_prepare(const char *workspace)
{
int abi;
int fd;
fd = landlock_make_ruleset(workspace);
if (fd == LANDLOCK_FD_SKIP)

if (!workspace || !workspace[0])
return 0;
abi = landlock_probe_abi();
if (abi < 0)
return -1;
fd = landlock_make_ruleset(workspace, abi);
if (fd < 0)
return -1;
close(fd);
return 0;
}

int sandbox_landlock_restrict_to_workspace(const char *workspace)
int sandbox_landlock_restrict_for_abi(const char *workspace, int abi)
{
int fd;
long rc;
fd = landlock_make_ruleset(workspace);
if (fd == LANDLOCK_FD_SKIP)

if (!workspace || !workspace[0])
return 0;
if (abi <= 0) {
abi = landlock_probe_abi();
if (abi < 0)
return -1;
}
fd = landlock_make_ruleset(workspace, abi);
if (fd < 0)
return -1;
rc = syscall(__NR_landlock_restrict_self, fd, 0);
close(fd);
return (rc == 0) ? 0 : -1;
if (rc != 0)
return -1;
if (abi < 3 && deny_path_truncate() != 0)
return -1;
return 0;
}

int sandbox_landlock_restrict_to_workspace(const char *workspace)
{
return sandbox_landlock_restrict_for_abi(workspace, 0);
}

#endif /* __linux__ */
18 changes: 18 additions & 0 deletions src/sandbox/sandbox_landlock.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,31 @@ extern "C" {
* Restrict the calling thread to @p workspace (RW) plus a small RO/exec set
* needed by /bin/sh. Empty @p workspace skips Landlock (returns 0).
*
* On Landlock ABI 1 and 2 (JetPack 6.2 / kernel 5.15 is ABI 1) the kernel
* cannot gate truncate(2). This call then also denies that path syscall.
* ftruncate(2) stays allowed because it needs a writable descriptor, and
* WRITE_FILE already rejects opening a host file.
*
* @param workspace Absolute workspace directory, or NULL/empty to skip.
* @return 0 on success or skip; -1 if the ruleset cannot apply.
*
* Example: sandbox_landlock_restrict_to_workspace("/home/user/.shellclaw");
*/
int sandbox_landlock_restrict_to_workspace(const char *workspace);

/**
* Same bound as sandbox_landlock_restrict_to_workspace(), built as if the
* kernel Landlock ABI were @p abi. Pass 0 to probe the running kernel.
* An ABI below 3 installs the path-truncate denial even on a newer kernel.
*
* @param workspace Absolute workspace directory, or NULL/empty to skip.
* @param abi Landlock ABI to emulate, or 0 to probe.
* @return 0 on success or skip; -1 if the ruleset cannot apply.
*
* Example: sandbox_landlock_restrict_for_abi("/tmp/ws", 1);
*/
int sandbox_landlock_restrict_for_abi(const char *workspace, int abi);

/**
* Build and discard the workspace ruleset without restrict_self.
* Same skip/error contract as restrict. Used so tests can cover the
Expand Down
107 changes: 107 additions & 0 deletions tests/test_sandbox.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "sandbox/sandbox_env.h"
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
Expand Down Expand Up @@ -613,6 +614,111 @@ static int test_landlock_denies_etc_ssl_outside_certs(void)
}
return 0;
}

/* ABI 1 (JetPack 6.2 / kernel 5.15) does not handle LANDLOCK_ACCESS_FS_TRUNCATE.
* open(O_TRUNC) is still denied by WRITE_FILE. truncate(2) is not, so a
* user-owned file outside the workspace can be wiped. */
static int test_abi1_path_truncate_cannot_wipe_host_file(void)
{
char workspace[] = "/tmp/sc_ll_abi1_XXXXXX";
char victim[] = "/tmp/sc_ll_victim_XXXXXX";
char *ws;
pid_t pid;
int st;
int status;
int vfd;
struct stat before;
struct stat after;

ws = mkdtemp(workspace);
if (!ws) {
fprintf(stderr, "test_abi1_path_truncate_cannot_wipe_host_file: mkdtemp failed\n");
return 1;
}
vfd = mkstemp(victim);
if (vfd < 0) {
rmdir(ws);
fprintf(stderr, "test_abi1_path_truncate_cannot_wipe_host_file: mkstemp failed\n");
return 1;
}
if (write(vfd, "hello-victim-contents\n", 22) != 22 || fstat(vfd, &before) != 0) {
close(vfd);
unlink(victim);
rmdir(ws);
return 1;
}
close(vfd);
pid = fork();
if (pid < 0) {
unlink(victim);
rmdir(ws);
return 1;
}
if (pid == 0) {
char grown[PATH_MAX];
int ofd;
int wfd;
int n;
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0)
_exit(3);
if (sandbox_landlock_restrict_for_abi(ws, 1) != 0)
_exit(2);
ofd = open(victim, O_WRONLY | O_TRUNC | O_CLOEXEC);
if (ofd >= 0)
_exit(4);
if (truncate(victim, 0) == 0)
_exit(5);
if (errno != EPERM)
_exit(9);
n = snprintf(grown, sizeof(grown), "%s/grown", ws);
if (n < 0 || (size_t)n >= sizeof(grown))
_exit(6);
wfd = open(grown, O_CREAT | O_RDWR | O_TRUNC | O_CLOEXEC, 0600);
if (wfd < 0)
_exit(6);
if (write(wfd, "abcd", 4) != 4 || ftruncate(wfd, 2) != 0)
_exit(7);
close(wfd);
_exit(0);
}
if (waitpid(pid, &st, 0) < 0) {
unlink(victim);
rmdir(ws);
return 1;
}
if (stat(victim, &after) != 0) {
unlink(victim);
rmdir(ws);
return 1;
}
unlink(victim);
{
char grown[PATH_MAX];
snprintf(grown, sizeof(grown), "%s/grown", ws);
unlink(grown);
}
rmdir(ws);
if (!WIFEXITED(st)) {
fprintf(stderr,
"FAIL: tests/test_sandbox.c: abi1 truncate child did not exit (st=%d)\n", st);
return 1;
}
status = WEXITSTATUS(st);
if (status == 2 || status == 3) {
fprintf(stderr,
"test_sandbox: skip test_abi1_path_truncate_cannot_wipe_host_file (WEXITSTATUS=%d)\n",
status);
return 0;
}
if (status != 0 || after.st_size != before.st_size) {
fprintf(stderr,
"FAIL: tests/test_sandbox.c: abi1 truncate WEXITSTATUS=%d size %lld -> %lld "
"(0=blocked 4=open_write 5=truncate_wiped 6=ws_open 7=ftruncate 9=errno)\n",
status, (long long)before.st_size, (long long)after.st_size);
return 1;
}
return 0;
}
#endif

#ifdef __linux__
Expand Down Expand Up @@ -1079,6 +1185,7 @@ int main(void)
#ifdef __linux__
RUN(test_landlock_restrict_denies_etc_passwd());
RUN(test_landlock_denies_etc_ssl_outside_certs());
RUN(test_abi1_path_truncate_cannot_wipe_host_file());
#endif
printf("test_sandbox: all tests passed\n");
return 0;
Expand Down
Loading