diff --git a/src/sandbox/sandbox_landlock.c b/src/sandbox/sandbox_landlock.c index 4dac6a7..fc67c86 100644 --- a/src/sandbox/sandbox_landlock.c +++ b/src/sandbox/sandbox_landlock.c @@ -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 @@ -22,12 +26,18 @@ #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; @@ -35,6 +45,13 @@ int sandbox_landlock_prepare(const char *workspace) } #else +#include +#include +#include +#include +#include +#include + #ifndef LANDLOCK_ACCESS_FS_IOCTL_DEV #define LANDLOCK_ACCESS_FS_IOCTL_DEV (1ULL << 15) #endif @@ -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) @@ -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", @@ -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 | @@ -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__ */ diff --git a/src/sandbox/sandbox_landlock.h b/src/sandbox/sandbox_landlock.h index ff48819..94421d5 100644 --- a/src/sandbox/sandbox_landlock.h +++ b/src/sandbox/sandbox_landlock.h @@ -16,6 +16,11 @@ 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. * @@ -23,6 +28,19 @@ extern "C" { */ 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 diff --git a/tests/test_sandbox.c b/tests/test_sandbox.c index e082059..c9e0254 100644 --- a/tests/test_sandbox.c +++ b/tests/test_sandbox.c @@ -20,6 +20,7 @@ #include "sandbox/sandbox_env.h" #include #include +#include #include #include #include @@ -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__ @@ -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;