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 +