From 2d79259d27c8c1da38e794bec9eb68a331e77601 Mon Sep 17 00:00:00 2001 From: Panu Matilainen Date: Fri, 15 Sep 2023 10:16:50 +0300 Subject: [PATCH] Add a new plugin to enable Linux-specific namespace functionality A plugin is a convenient place to hide Linux-specific functionality. Implemented in this initial version are: - Optional private mounts during scriptlet execution, useful for protecting the system from scriptlets (eg /home) and the scriptlets from themselves (eg insecure /tmp usage) - Optionally disable network access during scriptlet execution Note that at this time, scriplets executed with the embedded Lua interpreter are not covered by this because they run inside the main rpm process instead of forking (#2635). Add a testcase for private /tmp Suggested-by: Johannes Segitz Fixes: #2632 Fixes: #2665 --- docs/man/CMakeLists.txt | 3 ++ docs/man/rpm-plugin-unshare.8.md | 40 ++++++++++++++++ plugins/CMakeLists.txt | 4 ++ plugins/macros.transaction_unshare | 3 ++ plugins/unshare.c | 74 ++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/atlocal.in | 1 + tests/data/SPECS/scriptwrite.spec | 18 ++++++++ tests/rpmscript.at | 29 ++++++++++++ 9 files changed, 173 insertions(+) create mode 100644 docs/man/rpm-plugin-unshare.8.md create mode 100644 plugins/macros.transaction_unshare create mode 100644 plugins/unshare.c create mode 100644 tests/data/SPECS/scriptwrite.spec diff --git a/docs/man/CMakeLists.txt b/docs/man/CMakeLists.txt index b1b901b05e..80c91d5eec 100644 --- a/docs/man/CMakeLists.txt +++ b/docs/man/CMakeLists.txt @@ -28,6 +28,9 @@ if (ENABLE_PLUGINS) if (WITH_SELINUX) list(APPEND manuals rpm-plugin-selinux.8) endif() + if (HAVE_UNSHARE) + list(APPEND manuals rpm-plugin-unshare.8) + endif() endif() foreach(man ${manuals}) diff --git a/docs/man/rpm-plugin-unshare.8.md b/docs/man/rpm-plugin-unshare.8.md new file mode 100644 index 0000000000..60d701a210 --- /dev/null +++ b/docs/man/rpm-plugin-unshare.8.md @@ -0,0 +1,40 @@ +--- +date: 15 Sep 2023 +section: 8 +title: 'RPM-UNSHARE' +--- + +NAME +==== + +rpm-plugin-unshare - Unshare plugin for the RPM Package Manager + +Description +=========== + +This plugin allows using various Linux-specific namespace-related +technologies inside transactions, such as to harden and limit +scriptlet access to resources. + +Configuration +============= + +This plugin implements the following configurables: + +`%__transaction_unshare_paths` + +: A colon-separated list of paths to privately mount during scriptlet + execution. Typical examples would be `/tmp` to protect against + insecure temporary file usage inside scriptlets, and `/home` to + prevent scriptlets from accessing user home directories. + +`%__transaction_unshare_nonet` + +: Non-zero value disables network access during scriptlet execution. + +See **rpm-plugins**(8) on how to control plugins in general. + +SEE ALSO +======== + +*dbus-monitor*(1) *rpm-plugins*(8) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index aab827f876..78d5c2d7a5 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -38,6 +38,10 @@ if(WITH_FSVERITY) target_include_directories(fsverity PRIVATE ${CMAKE_SOURCE_DIR}/sign) endif() +if (HAVE_UNSHARE) + add_library(unshare MODULE unshare.c) +endif() + set(RPM_PLUGINDIR ${CMAKE_INSTALL_FULL_LIBDIR}/rpm-plugins CACHE PATH "rpm plugin directory") diff --git a/plugins/macros.transaction_unshare b/plugins/macros.transaction_unshare new file mode 100644 index 0000000000..5124b9c5a9 --- /dev/null +++ b/plugins/macros.transaction_unshare @@ -0,0 +1,3 @@ +%__transaction_unshare %{__plugindir}/unshare.so +%__transaction_unshare_paths /tmp:/home +%__transaction_unshare_nonet 1 diff --git a/plugins/unshare.c b/plugins/unshare.c new file mode 100644 index 0000000000..bb02201e4a --- /dev/null +++ b/plugins/unshare.c @@ -0,0 +1,74 @@ +#include "system.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "debug.h" + +static ARGV_t private_mounts = NULL; +static int unshare_flags = 0; + +static rpmRC unshare_init(rpmPlugin plugin, rpmts ts) +{ + char *paths = rpmExpand("%{?__transaction_unshare_paths}", NULL); + private_mounts = argvSplitString(paths, ":", ARGV_SKIPEMPTY); + if (private_mounts) + unshare_flags |= CLONE_NEWNS; + free(paths); + + if (rpmExpandNumeric("%{?__transaction_unshare_nonet}")) + unshare_flags |= CLONE_NEWNET; + + return RPMRC_OK; +} + +static void unshare_cleanup(rpmPlugin plugin) +{ + /* ensure clean state for possible next transaction */ + private_mounts = argvFree(private_mounts); + unshare_flags = 0; +} + +static rpmRC unshare_scriptlet_fork_post(rpmPlugin plugin, + const char *path, int type) +{ + rpmRC rc = RPMRC_FAIL; + + if (unshare_flags && (unshare(unshare_flags) == -1)) { + rpmlog(RPMLOG_ERR, _("unshare with flags x%x failed: %s\n"), + unshare_flags, strerror(errno)); + goto exit; + } + + if (private_mounts) { + if (mount("/", "/", NULL, MS_REC | MS_PRIVATE, NULL) == -1) { + rpmlog(RPMLOG_ERR, _("failed to mount private %s: %s\n"), + "/", strerror(errno)); + goto exit; + } + for (ARGV_t mnt = private_mounts; mnt && *mnt; mnt++) { + if (mount("none", *mnt, "tmpfs", 0, NULL) == -1) { + rpmlog(RPMLOG_ERR, _("failed to mount private %s: %s\n"), + *mnt, strerror(errno)); + goto exit; + } + } + } + rc = RPMRC_OK; + +exit: + return rc; +} + +struct rpmPluginHooks_s unshare_hooks = { + .init = unshare_init, + .cleanup = unshare_cleanup, + .scriptlet_fork_post = unshare_scriptlet_fork_post, +}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d0f6297fe4..f3a6ec0f4a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -22,6 +22,7 @@ if (WITH_INTERNAL_OPENPGP) else() set(PGP sequoia) endif() +set(HAVE_UNSHARE ${HAVE_UNSHARE}) set(TESTSUITE_AT rpmtests.at diff --git a/tests/atlocal.in b/tests/atlocal.in index e71030f095..f259ce54e1 100644 --- a/tests/atlocal.in +++ b/tests/atlocal.in @@ -3,6 +3,7 @@ RPMTREE=/ RPMLIBDIR="@CMAKE_INSTALL_FULL_LIBDIR@" export RPMLIBDIR +HAVE_UNSHARE=@HAVE_UNSHARE@ PYTHON=@PYTHON@ PGP=@PGP@ diff --git a/tests/data/SPECS/scriptwrite.spec b/tests/data/SPECS/scriptwrite.spec new file mode 100644 index 0000000000..760ed70745 --- /dev/null +++ b/tests/data/SPECS/scriptwrite.spec @@ -0,0 +1,18 @@ +Name: scriptwrite +Version: 1.0 +Release: 1 +Summary: Testing script running environment +Group: Testing +License: GPL +BuildArch: noarch + +%description +%{summary} + +%files + +%pre +echo "%{name}-%{version} pre" > /tmp/%{name}.log + +%post +echo "%{name}-%{version} post" >> /tmp/%{name}.log diff --git a/tests/rpmscript.at b/tests/rpmscript.at index 3be9587430..1c623f45d6 100644 --- a/tests/rpmscript.at +++ b/tests/rpmscript.at @@ -391,3 +391,32 @@ ERASE: []) RPMTEST_CLEANUP +AT_SETUP([script running environment]) +AT_KEYWORDS([script]) +AT_SKIP_IF([test ${HAVE_UNSHARE} = 0]) +RPMDB_INIT +runroot rpmbuild -bb --quiet \ + /data/SPECS/fakeshell.spec \ + /data/SPECS/scriptwrite.spec + +RPMTEST_CHECK([ +RPMDB_INIT +runroot_other rm -f /tmp/scriptwrite.log +runroot rpm -U /build/RPMS/noarch/fakeshell-1.0-1.noarch.rpm /build/RPMS/noarch/scriptwrite-1.0-1.noarch.rpm +runroot_other test -f /tmp/scriptwrite.log +], +[1], +[], +[]) + +RPMTEST_CHECK([ +RPMDB_INIT +runroot_other rm -f /tmp/scriptwrite.log +runroot rpm -U --noplugins /build/RPMS/noarch/fakeshell-1.0-1.noarch.rpm /build/RPMS/noarch/scriptwrite-1.0-1.noarch.rpm +runroot_other test -f /tmp/scriptwrite.log +], +[0], +[], +[]) +RPMTEST_CLEANUP +