From 374868d81d473ab56556a1cfd6b1b36a1fab348b Mon Sep 17 00:00:00 2001 From: Lasse Collin Date: Sat, 17 Feb 2024 23:07:35 +0200 Subject: [PATCH] xz: Move sandboxing code to sandbox.c and improve Landlock sandbox. Landlock is now always used just like pledge(2) is: first in more permissive mode and later (under certain common conditions) in a strict mode that doesn't allow opening more files. I put pledge(2) first in sandbox.c because it's the simplest API to use and still somewhat fine-grained for basic applications. So it's the simplest thing to understand for anyone reading sandbox.c. --- CMakeLists.txt | 2 + src/xz/Makefile.am | 2 + src/xz/file_io.c | 170 +------------------------- src/xz/file_io.h | 6 - src/xz/main.c | 50 +++----- src/xz/private.h | 6 +- src/xz/sandbox.c | 295 +++++++++++++++++++++++++++++++++++++++++++++ src/xz/sandbox.h | 39 ++++++ 8 files changed, 357 insertions(+), 213 deletions(-) create mode 100644 src/xz/sandbox.c create mode 100644 src/xz/sandbox.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f30a82b6..96ff980b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1393,6 +1393,8 @@ if(NOT MSVC OR MSVC_VERSION GREATER_EQUAL 1900) src/xz/options.c src/xz/options.h src/xz/private.h + src/xz/sandbox.c + src/xz/sandbox.h src/xz/signals.c src/xz/signals.h src/xz/suffix.c diff --git a/src/xz/Makefile.am b/src/xz/Makefile.am index 847405d7..4ff061f3 100644 --- a/src/xz/Makefile.am +++ b/src/xz/Makefile.am @@ -21,6 +21,8 @@ xz_SOURCES = \ options.c \ options.h \ private.h \ + sandbox.c \ + sandbox.h \ signals.c \ signals.h \ suffix.c \ diff --git a/src/xz/file_io.c b/src/xz/file_io.c index 876ee4de..678a9a5c 100644 --- a/src/xz/file_io.c +++ b/src/xz/file_io.c @@ -28,15 +28,6 @@ static bool warn_fchown; # include #endif -#ifdef HAVE_CAP_RIGHTS_LIMIT -# include -#endif - -#ifdef HAVE_LINUX_LANDLOCK_H -# include -# include -#endif - #include "tuklib_open_stdxxx.h" #ifdef _MSC_VER @@ -92,11 +83,6 @@ typedef enum { /// If true, try to create sparse files when decompressing. static bool try_sparse = true; -#ifdef ENABLE_SANDBOX -/// True if the conditions for sandboxing (described in main()) have been met. -static bool sandbox_allowed = false; -#endif - #ifndef TUKLIB_DOSLIKE /// File status flags of standard input. This is used by io_open_src() /// and io_close_src(). @@ -181,159 +167,6 @@ io_no_sparse(void) } -#ifdef ENABLE_SANDBOX -extern void -io_allow_sandbox(void) -{ - sandbox_allowed = true; - return; -} - - -/// Enables operating-system-specific sandbox if it is possible. -/// src_fd is the file descriptor of the input file. -static void -io_sandbox_enter(int src_fd) -{ - if (!sandbox_allowed) { - // This message is more often annoying than useful so - // it's commented out. It can be useful when developing - // the sandboxing code. - //message(V_DEBUG, _("Sandbox is disabled due " - // "to incompatible command line arguments")); - return; - } - - const char dummy_str[] = "x"; - - // Try to ensure that both libc and xz locale files have been - // loaded when NLS is enabled. - snprintf(NULL, 0, "%s%s", _(dummy_str), strerror(EINVAL)); - - // Try to ensure that iconv data files needed for handling multibyte - // characters have been loaded. This is needed at least with glibc. - tuklib_mbstr_width(dummy_str, NULL); - -#ifdef HAVE_CAP_RIGHTS_LIMIT - // Capsicum needs FreeBSD 10.2 or later. - cap_rights_t rights; - - if (cap_enter()) - goto error; - - if (cap_rights_limit(src_fd, cap_rights_init(&rights, - CAP_EVENT, CAP_FCNTL, CAP_LOOKUP, CAP_READ, CAP_SEEK))) - goto error; - - // If not reading from stdin, remove all capabilities from it. - if (src_fd != STDIN_FILENO && cap_rights_limit( - STDIN_FILENO, cap_rights_clear(&rights))) - goto error; - - if (cap_rights_limit(STDOUT_FILENO, cap_rights_init(&rights, - CAP_EVENT, CAP_FCNTL, CAP_FSTAT, CAP_LOOKUP, - CAP_WRITE, CAP_SEEK))) - goto error; - - if (cap_rights_limit(STDERR_FILENO, cap_rights_init(&rights, - CAP_WRITE))) - goto error; - - if (cap_rights_limit(user_abort_pipe[0], cap_rights_init(&rights, - CAP_EVENT))) - goto error; - - if (cap_rights_limit(user_abort_pipe[1], cap_rights_init(&rights, - CAP_WRITE))) - goto error; - -#elif defined(HAVE_PLEDGE) - // pledge() was introduced in OpenBSD 5.9. - // - // main() unconditionally calls pledge() with fairly relaxed - // promises which work in all situations. Here we make the - // sandbox more strict. - if (pledge("stdio", "")) - goto error; - - (void)src_fd; - -#elif defined(HAVE_LINUX_LANDLOCK_H) - int landlock_abi = syscall(SYS_landlock_create_ruleset, - (void *)NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); - - if (landlock_abi > 0) { - // We support ABI versions 1-3. - if (landlock_abi > 3) - landlock_abi = 3; - - // We want to set all supported flags in handled_access_fs. - // This way the ruleset will initially forbid access to all - // actions that the available Landlock ABI version supports. - // Exceptions can be added using landlock_add_rule(2) to - // allow certain actions on certain files or directories. - // - // The same flag values are used on all archs. ABI v2 and v3 - // both add one new flag. - // - // First in ABI v1: LANDLOCK_ACCESS_FS_EXECUTE = 1ULL << 0 - // Last in ABI v1: LANDLOCK_ACCESS_FS_MAKE_SYM = 1ULL << 12 - // Last in ABI v2: LANDLOCK_ACCESS_FS_REFER = 1ULL << 13 - // Last in ABI v3: LANDLOCK_ACCESS_FS_TRUNCATE = 1ULL << 14 - // - // This makes it simple to set the mask based on the ABI - // version and we don't need to care which flags are #defined - // in the installed . - const struct landlock_ruleset_attr attr = { - .handled_access_fs = (1ULL << (12 + landlock_abi)) - 1 - }; - - const int ruleset_fd = syscall(SYS_landlock_create_ruleset, - &attr, sizeof(attr), 0U); - if (ruleset_fd < 0) - goto error; - - // All files we need should have already been opened. Thus, - // we don't need to add any rules using landlock_add_rule(2) - // before activating the sandbox. - // - // NOTE: It's possible that the hack at the beginning of this - // function isn't be good enough. It tries to get translations - // and libc-specific files loaded but if it's not good enough - // then perhaps a Landlock rule to allow reading from /usr - // and/or the xz installation prefix would be needed. - // - // prctl(PR_SET_NO_NEW_PRIVS, ...) was already called in - // main() so we don't do it here again. - if (syscall(SYS_landlock_restrict_self, ruleset_fd, 0U) != 0) - goto error; - } - - (void)src_fd; - -#else -# error ENABLE_SANDBOX is defined but no sandboxing method was found. -#endif - - // This message is annoying in xz -lvv. - //message(V_DEBUG, _("Sandbox was successfully enabled")); - return; - -error: -#ifdef HAVE_CAP_RIGHTS_LIMIT - // If a kernel is configured without capability mode support or - // used in an emulator that does not implement the capability - // system calls, then the Capsicum system calls will fail and set - // errno to ENOSYS. In that case xz will silently run without - // the sandbox. - if (errno == ENOSYS) - return; -#endif - message_fatal(_("Failed to enable the sandbox")); -} -#endif // ENABLE_SANDBOX - - #ifndef TUKLIB_DOSLIKE /// \brief Waits for input or output to become available or for a signal /// @@ -889,7 +722,8 @@ io_open_src(const char *src_name) #ifdef ENABLE_SANDBOX if (!error) - io_sandbox_enter(pair.src_fd); + sandbox_enable_strict_if_allowed(pair.src_fd, + user_abort_pipe[0], user_abort_pipe[1]); #endif return error ? NULL : &pair; diff --git a/src/xz/file_io.h b/src/xz/file_io.h index b16a8faa..ae7e2f38 100644 --- a/src/xz/file_io.h +++ b/src/xz/file_io.h @@ -99,12 +99,6 @@ extern void io_write_to_user_abort_pipe(void); extern void io_no_sparse(void); -#ifdef ENABLE_SANDBOX -/// \brief main() calls this if conditions for sandboxing have been met. -extern void io_allow_sandbox(void); -#endif - - /// \brief Open the source file extern file_pair *io_open_src(const char *src_name); diff --git a/src/xz/main.c b/src/xz/main.c index 3d3d11d7..c3e81467 100644 --- a/src/xz/main.c +++ b/src/xz/main.c @@ -12,12 +12,6 @@ #include "private.h" #include -// prctl(PR_SET_NO_NEW_PRIVS, ...) is required with Landlock but it can be -// activated even when conditions for strict sandboxing aren't met. -#ifdef HAVE_LINUX_LANDLOCK_H -# include -#endif - /// Exit status to use. This can be changed with set_exit_status(). static enum exit_status_type exit_status = E_SUCCESS; @@ -148,32 +142,6 @@ read_name(const args_info *args) int main(int argc, char **argv) { -#ifdef HAVE_PLEDGE - // OpenBSD's pledge(2) sandbox - // - // Unconditionally enable sandboxing with fairly relaxed promises. - // This is still way better than having no sandbox at all. :-) - // More strict promises will be made later in file_io.c if possible. - if (pledge("stdio rpath wpath cpath fattr", "")) { - // Don't translate the string or use message_fatal() as - // those haven't been initialized yet. - fprintf(stderr, "%s: Failed to enable the sandbox\n", argv[0]); - return E_ERROR; - } -#endif - -#ifdef HAVE_LINUX_LANDLOCK_H - // Prevent the process from gaining new privileges. This must be done - // before landlock_restrict_self(2) in file_io.c but since we will - // never need new privileges, this call can be done here already. - // - // This is supported since Linux 3.5. Ignore the return value to - // keep compatibility with old kernels. landlock_restrict_self(2) - // will fail if the no_new_privs attribute isn't set, thus if prctl() - // fails here the error will still be detected when it matters. - (void)prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); -#endif - #if defined(_WIN32) && !defined(__CYGWIN__) InitializeCriticalSection(&exit_status_cs); #endif @@ -187,6 +155,20 @@ main(int argc, char **argv) // even indirectly like locale and gettext initializations. io_init(); +#ifdef ENABLE_SANDBOX + // Enable such sandboxing that can always be enabled. + // This requires that progname has been set up. + // It's also good that io_init() has been called because it + // might need to do things that the initial sandbox won't allow. + // Otherwise this should be called as early as possible. + // + // NOTE: Calling this before tuklib_gettext_init() means that + // translated error message won't be available if sandbox + // initialization fails. However, sandbox_init() shouldn't + // fail and this order simply feels better. + sandbox_init(); +#endif + // Set up the locale and message translations. tuklib_gettext_init(PACKAGE, LOCALEDIR); @@ -241,7 +223,7 @@ main(int argc, char **argv) signals_init(); #ifdef ENABLE_SANDBOX - // Set a flag that sandboxing is allowed if all these are true: + // Set a flag that strict sandboxing is allowed if all these are true: // - --files or --files0 wasn't used. // - There is exactly one input file or we are reading from stdin. // - We won't create any files: output goes to stdout or --test @@ -255,7 +237,7 @@ main(int argc, char **argv) if (args.files_name == NULL && args.arg_count == 1 && (opt_stdout || strcmp("-", args.arg_names[0]) == 0 || opt_mode == MODE_LIST)) - io_allow_sandbox(); + sandbox_allow_strict(); #endif // coder_run() handles compression, decompression, and testing. diff --git a/src/xz/private.h b/src/xz/private.h index 0ab2ab4e..b370472e 100644 --- a/src/xz/private.h +++ b/src/xz/private.h @@ -51,11 +51,6 @@ # define STDERR_FILENO (fileno(stderr)) #endif -#if defined(HAVE_CAP_RIGHTS_LIMIT) || defined(HAVE_PLEDGE) \ - || defined(HAVE_LINUX_LANDLOCK_H) -# define ENABLE_SANDBOX 1 -#endif - // Handling SIGTSTP keeps time-keeping for progress indicator correct // if xz is stopped. It requires use of clock_gettime() as that is // async-signal safe in POSIX. Require also SIGALRM support since @@ -75,6 +70,7 @@ #include "hardware.h" #include "file_io.h" #include "options.h" +#include "sandbox.h" #include "signals.h" #include "suffix.h" #include "util.h" diff --git a/src/xz/sandbox.c b/src/xz/sandbox.c new file mode 100644 index 00000000..2c40db71 --- /dev/null +++ b/src/xz/sandbox.c @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: 0BSD + +/////////////////////////////////////////////////////////////////////////////// +// +/// \file sandbox.c +/// \brief Sandbox support +// +// Author: Lasse Collin +// +/////////////////////////////////////////////////////////////////////////////// + +#include "private.h" + + +#ifndef ENABLE_SANDBOX + +// Prevent an empty translation unit when no sandboxing is supported. +typedef int dummy; + +#else + +/// If the conditions for strict sandboxing (described in main()) +/// have been met, sandbox_allow_strict() can be called to set this +/// variable to true. +static bool strict_sandbox_allowed = false; + + +extern void +sandbox_allow_strict(void) +{ + strict_sandbox_allowed = true; + return; +} + + +// Strict sandboxing prevents opening any files. This *tries* to ensure +// that any auxiliary files that might be required are already open. +// +// Returns true if strict sandboxing is allowed, false otherwise. +static bool +prepare_for_strict_sandbox(void) +{ + if (!strict_sandbox_allowed) + return false; + + const char dummy_str[] = "x"; + + // Try to ensure that both libc and xz locale files have been + // loaded when NLS is enabled. + snprintf(NULL, 0, "%s%s", _(dummy_str), strerror(EINVAL)); + + // Try to ensure that iconv data files needed for handling multibyte + // characters have been loaded. This is needed at least with glibc. + tuklib_mbstr_width(dummy_str, NULL); + + return true; +} + +#endif + + +#if defined(HAVE_PLEDGE) + +/////////////// +// pledge(2) // +/////////////// + +#include + + +extern void +sandbox_init(void) +{ + if (pledge("stdio rpath wpath cpath fattr", "")) { + // gettext hasn't been initialized yet so + // there's no point to call it here. + message_fatal("Failed to enable the sandbox"); + } + + return; +} + + +extern void +sandbox_enable_strict_if_allowed(int src_fd lzma_attribute((__unused__)), + int pipe_event_fd lzma_attribute((__unused__)), + int pipe_write_fd lzma_attribute((__unused__))) +{ + if (!prepare_for_strict_sandbox()) + return; + + if (pledge("stdio", "")) + message_fatal(_("Failed to enable the sandbox")); + + return; +} + + +#elif defined(HAVE_LINUX_LANDLOCK_H) + +////////////// +// Landlock // +////////////// + +#include +#include +#include + + +// Highest Landlock ABI version supported by this file +#define LANDLOCK_ABI_MAX 3 + +/// Landlock ABI version supported by the kernel +static int landlock_abi; + + +// The required_rights should have those bits set that must not be restricted. +// This function will then bitwise-and ~required_rights with a mask matching +// the Landlock ABI version, leaving only those bits set that are supported +// by the ABI and allowed to be restricted by the function argument. +static void +enable_landlock(uint64_t required_rights) +{ + assert(landlock_abi <= LANDLOCK_ABI_MAX); + + if (landlock_abi <= 0) + return; + + // We want to set all supported flags in handled_access_fs. + // This way the ruleset will initially forbid access to all + // actions that the available Landlock ABI version supports. + // Exceptions can be added using landlock_add_rule(2) to + // allow certain actions on certain files or directories. + // + // The same flag values are used on all archs. ABI v2 and v3 + // both add one new flag. + // + // First in ABI v1: LANDLOCK_ACCESS_FS_EXECUTE = 1ULL << 0 + // Last in ABI v1: LANDLOCK_ACCESS_FS_MAKE_SYM = 1ULL << 12 + // Last in ABI v2: LANDLOCK_ACCESS_FS_REFER = 1ULL << 13 + // Last in ABI v3: LANDLOCK_ACCESS_FS_TRUNCATE = 1ULL << 14 + // + // This makes it simple to set the mask based on the ABI + // version and we don't need to care which flags are #defined + // in the installed . + const struct landlock_ruleset_attr attr = { + .handled_access_fs = ((1ULL << (12 + landlock_abi)) - 1) + & ~required_rights, + }; + + const int ruleset_fd = syscall(SYS_landlock_create_ruleset, + &attr, sizeof(attr), 0U); + if (ruleset_fd < 0) + message_fatal(_("Failed to enable the sandbox")); + + // All files we need should have already been opened. Thus, + // we don't need to add any rules using landlock_add_rule(2) + // before activating the sandbox. + // + // NOTE: It's possible that the hack prepare_for_strict_sandbox() + // isn't be good enough. It tries to get translations and + // libc-specific files loaded but if it's not good enough + // then perhaps a Landlock rule to allow reading from /usr + // and/or the xz installation prefix would be needed. + // + // prctl(PR_SET_NO_NEW_PRIVS, ...) was already called in + // sandbox_init() so we don't do it here again. + if (syscall(SYS_landlock_restrict_self, ruleset_fd, 0U) != 0) + message_fatal(_("Failed to enable the sandbox")); + + return; +} + + +extern void +sandbox_init(void) +{ + // Prevent the process from gaining new privileges. This must be done + // before landlock_restrict_self(2) but since we will never need new + // privileges, this call can be done here already. + // + // This is supported since Linux 3.5. Ignore the return value to + // keep compatibility with old kernels. landlock_restrict_self(2) + // will fail if the no_new_privs attribute isn't set, thus if prctl() + // fails here the error will still be detected when it matters. + (void)prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); + + // Get the highest Landlock ABI version supported by the kernel. + landlock_abi = syscall(SYS_landlock_create_ruleset, + (void *)NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); + + // The kernel might support a newer ABI than this file. + if (landlock_abi > LANDLOCK_ABI_MAX) + landlock_abi = LANDLOCK_ABI_MAX; + + // These are all in ABI version 1 already. We don't need truncate + // rights because files are created with open() using O_EXCL and + // without O_TRUNC. + const uint64_t required_rights + = LANDLOCK_ACCESS_FS_WRITE_FILE + | LANDLOCK_ACCESS_FS_READ_FILE + | LANDLOCK_ACCESS_FS_REMOVE_FILE + | LANDLOCK_ACCESS_FS_MAKE_REG; + + enable_landlock(required_rights); + return; +} + + +extern void +sandbox_enable_strict_if_allowed(int src_fd lzma_attribute((__unused__)), + int pipe_event_fd lzma_attribute((__unused__)), + int pipe_write_fd lzma_attribute((__unused__))) +{ + if (!prepare_for_strict_sandbox()) + return; + + // Allow all restrictions that the kernel supports with the + // highest Landlock ABI version that the kernel or xz supports. + enable_landlock(0); + return; +} + + +#elif defined(HAVE_CAP_RIGHTS_LIMIT) + +////////////// +// Capsicum // +////////////// + +#include + + +extern void +sandbox_init(void) +{ + // Nothing to do. + return; +} + + +extern void +sandbox_enable_strict_if_allowed( + int src_fd, int pipe_event_fd, int pipe_write_fd) +{ + if (!prepare_for_strict_sandbox()) + return; + + // Capsicum needs FreeBSD 10.2 or later. + cap_rights_t rights; + + if (cap_enter()) + goto error; + + if (cap_rights_limit(src_fd, cap_rights_init(&rights, + CAP_EVENT, CAP_FCNTL, CAP_LOOKUP, CAP_READ, CAP_SEEK))) + goto error; + + // If not reading from stdin, remove all capabilities from it. + if (src_fd != STDIN_FILENO && cap_rights_limit( + STDIN_FILENO, cap_rights_clear(&rights))) + goto error; + + if (cap_rights_limit(STDOUT_FILENO, cap_rights_init(&rights, + CAP_EVENT, CAP_FCNTL, CAP_FSTAT, CAP_LOOKUP, + CAP_WRITE, CAP_SEEK))) + goto error; + + if (cap_rights_limit(STDERR_FILENO, cap_rights_init(&rights, + CAP_WRITE))) + goto error; + + if (cap_rights_limit(user_abort_pipe[0], cap_rights_init(&rights, + CAP_EVENT))) + goto error; + + if (cap_rights_limit(user_abort_pipe[1], cap_rights_init(&rights, + CAP_WRITE))) + goto error; + + return; + +error: + // If a kernel is configured without capability mode support or + // used in an emulator that does not implement the capability + // system calls, then the Capsicum system calls will fail and set + // errno to ENOSYS. In that case xz will silently run without + // the sandbox. + if (errno == ENOSYS) + return; + + message_fatal(_("Failed to enable the sandbox")); +} + +#endif diff --git a/src/xz/sandbox.h b/src/xz/sandbox.h new file mode 100644 index 00000000..795c550f --- /dev/null +++ b/src/xz/sandbox.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: 0BSD + +/////////////////////////////////////////////////////////////////////////////// +// +/// \file sandbox.h +/// \brief Sandbox support +// +// Author: Lasse Collin +// +/////////////////////////////////////////////////////////////////////////////// + +#if defined(HAVE_PLEDGE) || defined(HAVE_LINUX_LANDLOCK_H) \ + || defined(HAVE_CAP_RIGHTS_LIMIT) +# define ENABLE_SANDBOX 1 +#endif + + +/// \brief Enables early sandboxing that can always be enabled +/// +/// This requires that tuklib_progname() and io_init() have been called. +extern void sandbox_init(void); + + +/// \brief Tell sandboxing code that strict sandboxing can be used +/// +/// This function only sets a flag which will be read by +/// sandbox_enable_strict_if_allowed(). +extern void sandbox_allow_strict(void); + + +/// \brief Enable sandboxing that allows reading from one file +/// +/// This does nothing if sandbox_allow_strict() hasn't been called. +/// +/// \param src_fd File descriptor open for reading +/// \param pipe_event_fd user_abort_pipe[0] from file_io.c +/// \param pipe_write_fd user_abort_pipe[1] from file_io.c +extern void sandbox_enable_strict_if_allowed( + int src_fd, int pipe_event_fd, int pipe_write_fd);