Skip to content

Commit

Permalink
Add a new plugin to enable Linux-specific namespace functionality
Browse files Browse the repository at this point in the history
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 <jsegitz@suse.de>

Fixes: #2632
Fixes: #2665
  • Loading branch information
pmatilai committed Sep 28, 2023
1 parent 5802192 commit 788d513
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 1 deletion.
3 changes: 3 additions & 0 deletions docs/man/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
40 changes: 40 additions & 0 deletions docs/man/rpm-plugin-unshare.8.md
Original file line number Diff line number Diff line change
@@ -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)
5 changes: 5 additions & 0 deletions macros.in
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,11 @@ Supplements: (%{name} = %{version}-%{release} and langpacks-%{1})\
%__transaction_prioreset %{__plugindir}/prioreset.so
%__transaction_audit %{__plugindir}/audit.so
%__transaction_dbus_announce %{__plugindir}/dbus_announce.so
%__transaction_unshare %{__plugindir}/unshare.so

# Unshare specific configuration
%__transaction_unshare_paths /tmp:/home
%__transaction_unshare_nonet 1

#------------------------------------------------------------------------------
# Macros for further automated spec %setup and patch application
Expand Down
4 changes: 4 additions & 0 deletions plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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(plugindir ${CMAKE_INSTALL_FULL_LIBDIR}/rpm-plugins)

get_property(plugins DIRECTORY PROPERTY BUILDSYSTEM_TARGETS)
Expand Down
74 changes: 74 additions & 0 deletions plugins/unshare.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#include "system.h"

#include <sched.h>
#include <sys/mount.h>
#include <errno.h>
#include <string.h>

#include <rpm/rpmts.h>
#include <rpm/rpmplugin.h>
#include <rpm/rpmlog.h>
#include <rpm/rpmmacro.h>

#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,
};
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ if (${WITH_INTERNAL_OPENPGP})
else()
set(CRYPTO sequoia)
endif()
set(HAVE_UNSHARE ${HAVE_UNSHARE})

set(TESTSUITE_AT
rpmtests.at
Expand Down
2 changes: 1 addition & 1 deletion tests/atlocal.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ RPMTREE=/
RPMLIBDIR="@CMAKE_INSTALL_FULL_LIBDIR@"
export RPMLIBDIR

HAVE_UNSHARE=@HAVE_UNSHARE@
PYTHON=@PYTHON@

CRYPTO=@CRYPTO@
export CRYPTO
if test x$CRYPTO = xlibgcrypt
Expand Down
29 changes: 29 additions & 0 deletions tests/rpmscript.at
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 788d513

Please sign in to comment.