From 9050d01fe4a5c0ee9563a21e9dc3d881650f6e58 Mon Sep 17 00:00:00 2001 From: CismonX Date: Tue, 4 Mar 2025 12:05:17 +0800 Subject: [PATCH] test: add tests for sandbox --- tests/Makefile.am | 4 +- tests/check_lib.c | 2 + tests/check_lib.h | 6 ++ tests/check_sandbox.c | 193 ++++++++++++++++++++++++++++++++++++++++++ tests/lib_sandbox.at | 25 ++++++ tests/testsuite.at | 1 + 6 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 tests/check_sandbox.c create mode 100644 tests/lib_sandbox.at diff --git a/tests/Makefile.am b/tests/Makefile.am index 478e022..3f1bdf4 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -8,7 +8,7 @@ EXTRA_DIST = package.m4 testsuite.at $(TESTSUITE) $(TESTS_) -TESTS_ = lib_hash.at lib_prng.at lib_watcher.at +TESTS_ = lib_hash.at lib_prng.at lib_watcher.at lib_sandbox.at # Helper programs for testing @@ -20,7 +20,7 @@ if BOOKMARKFS_UTIL check_util_lib_CPPFLAGS = -I$(top_srcdir)/src check_util_lib_LDADD = $(top_builddir)/src/libbookmarkfs_util.la - check_util_lib_SOURCES = check_lib.c check_watcher.c + check_util_lib_SOURCES = check_lib.c check_watcher.c check_sandbox.c endif # BOOKMARKFS_UTIL # Autotest setup diff --git a/tests/check_lib.c b/tests/check_lib.c index 8598980..86155cc 100644 --- a/tests/check_lib.c +++ b/tests/check_lib.c @@ -59,6 +59,8 @@ dispatch_subcmds ( status = subcmd_prng(argc, argv); } else if (0 == strcmp("watcher", cmd)) { status = check_watcher(argc, argv); + } else if (0 == strcmp("sandbox", cmd)) { + status = check_sandbox(argc, argv); } else { log_printf("bad subcmd '%s'", cmd); } diff --git a/tests/check_lib.h b/tests/check_lib.h index 4d73dbe..eb6e0f7 100644 --- a/tests/check_lib.h +++ b/tests/check_lib.h @@ -33,6 +33,12 @@ action_if_false \ } while (0) +int +check_sandbox ( + int argc, + char *argv[] +); + int check_watcher ( int argc, diff --git a/tests/check_sandbox.c b/tests/check_sandbox.c new file mode 100644 index 0000000..023a27b --- /dev/null +++ b/tests/check_sandbox.c @@ -0,0 +1,193 @@ +/** +* bookmarkfs/tests/check_sandbox.c +* ---- +* +* Copyright (C) 2025 CismonX +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include + +#include +#include +#include +#include +#include + +#include "check_lib.h" +#include "frontend_util.h" +#include "sandbox.h" + +// Forward declaration start +static int do_check_sandbox (int, uint32_t); +// Forward declaration end + +static int +do_check_sandbox ( + int dirfd, + uint32_t flags +) { +#define FILE1_NAME "false.sh" +#define FILE2_NAME "foo.tmp" + +#if defined(__linux__) +# define ERR1 EACCES +# define ERR2 EPERM +#elif defined(__FreeBSD__) +# define ERR1 ENOTCAPABLE +# define ERR2 ECAPMODE +#else +# error "not implemented" +#endif + +#define ASSERT_BAD_SYS(expr, cleanup_action) \ + ASSERT_EXPR_INT(expr, r_, (err_ = errno, r_ < 0), { \ + cleanup_action \ + goto end; \ + }); \ + ASSERT_EXPR_INT(err_, r_, r_ == ERR1 || r_ == ERR2, goto end;) + +#define ASSERT_BAD_FD(expr) ASSERT_BAD_SYS(expr, close(r_);) +#define ASSERT_EQ(val, expr) ASSERT_EXPR_INT(expr, r_, (val) == r_, goto end;) +#define ASSERT_NE(val, expr) ASSERT_EXPR_INT(expr, r_, (val) != r_, goto end;) + + int err_; + int status = -1; + + int fd = socket(AF_INET, SOCK_STREAM, 0); +#if defined(__linux__) + ASSERT_EQ(-1, fd); +#elif defined(__FreeBSD__) + // In capability mode, socket() is allowed, + // but bind(), connect(), etc., are not. + ASSERT_NE(-1, fd); + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_addr.s_addr = htonl(INADDR_LOOPBACK), + }; + ASSERT_BAD_SYS(bind(fd, (struct sockaddr *)&addr, sizeof(addr)), ); + close(fd); +#else +# error "not implemented" +#endif + fd = -1; + + // Not allowed to perform filesystem lookups with AT_FDCWD. + ASSERT_BAD_FD(openat(AT_FDCWD, ".", O_RDONLY)); + struct stat buf; + ASSERT_BAD_SYS(fstatat(AT_FDCWD, ".", &buf, 0), ); + + bool check_above = true; + bool check_lookup_above = true; +#ifdef __linux__ +# ifndef BOOKMARKFS_SANDBOX_LANDLOCK + // If only we could filter renameat2() with seccomp... + check_above = false; +# endif + // LANDLOCK_RULE_PATH_BENEATH does not prohibit lookup + // above the given directory. + // Unfiltered syscalls (e.g., fstatat()) are still able to + // operate on the entire filesystem tree. + // This is not desired, but generally harmless. + check_lookup_above = false; +#endif + if (check_above) { + // Not allowed to operate above the directory. + ASSERT_BAD_FD(openat(dirfd, "..", O_RDONLY)); + if (check_lookup_above) { + ASSERT_BAD_SYS(fstatat(dirfd, "..", &buf, 0), ); + } + } + + fd = openat(dirfd, FILE1_NAME, O_RDONLY); + ASSERT_NE(-1, fd); + + // Not allowed to execute files. + char *argv[] = { FILE2_NAME, NULL }; + char *envp[] = { NULL }; + ASSERT_BAD_SYS(fexecve(fd, argv, envp), ); + + if (flags & SANDBOX_READONLY) { + // Not allowed to create, modify or delete files in read-only mode. + ASSERT_BAD_FD(openat(dirfd, FILE2_NAME, O_RDONLY | O_CREAT, 0600)); + ASSERT_BAD_SYS(renameat(dirfd, FILE1_NAME, dirfd, FILE2_NAME), ); + ASSERT_BAD_SYS(unlinkat(dirfd, FILE1_NAME, 0), ); + } else { + close(fd); + fd = openat(dirfd, FILE2_NAME, O_RDONLY | O_CREAT, 0600); + ASSERT_NE(-1, fd); + + ASSERT_EQ(0, renameat(dirfd, FILE1_NAME, dirfd, FILE2_NAME)); + ASSERT_EQ(0, unlinkat(dirfd, FILE2_NAME, 0)); + } + + status = 0; + + end: + if (fd >= 0) { + close(fd); + } + return status; +} + +int +check_sandbox ( + int argc, + char *argv[] +) { + uint32_t flags = 0; +#ifndef BOOKMARKFS_SANDBOX_LANDLOCK + flags |= SANDBOX_NO_LANDLOCK; +#endif + char const *path = NULL; + + getopt_foreach(argc, argv, ":d:r") { + case 'd': + path = optarg; + break; + + case 'r': + flags |= SANDBOX_READONLY; + break; + + default: + log_printf("bad option '-%c'", optopt); + return -1; + } + if (path == NULL) { + log_puts("path not specified"); + return -1; + } + + int dirfd = open(path, O_RDONLY | O_DIRECTORY); + if (dirfd < 0) { + log_printf("failed to open '%s'", path); + return -1; + } + int status = sandbox_enter(dirfd, flags); + if (status < 0) { + goto end; + } + status = do_check_sandbox(dirfd, flags); + + end: + close(dirfd); + return status; +} diff --git a/tests/lib_sandbox.at b/tests/lib_sandbox.at new file mode 100644 index 0000000..48db842 --- /dev/null +++ b/tests/lib_sandbox.at @@ -0,0 +1,25 @@ +dnl +dnl Copyright (C) 2025 CismonX +dnl +dnl Copying and distribution of this file, with or without modification, +dnl are permitted in any medium without royalty, +dnl provided the copyright notice and this notice are preserved. +dnl This file is offered as-is, without any warranty. +dnl + +AT_SETUP([util lib: sandbox]) +AT_KEYWORDS([lib sandbox]) + +ATX_CHECK_LIB([ + tmpdir=./$(ath_fn_rand_u64_hex).tmp.d + + mkdir $tmpdir + printf '%s\n\n%s\n' '#!/bin/sh' 'exit 2' > $tmpdir/false.sh + chmod +x $tmpdir/false.sh + + # Check both read-only and read/write mode. + check-util-lib sandbox -r -d $tmpdir + check-util-lib sandbox -d $tmpdir +]) + +AT_CLEANUP diff --git a/tests/testsuite.at b/tests/testsuite.at index 79c7c15..c78380c 100644 --- a/tests/testsuite.at +++ b/tests/testsuite.at @@ -75,3 +75,4 @@ AT_BANNER([The Utility Library]) m4_include([lib_hash.at]) m4_include([lib_prng.at]) m4_include([lib_watcher.at]) +m4_include([lib_sandbox.at])