Skip to content

Commit fd8eaa5

Browse files
committed
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 <[email protected]> Fixes: #2632 Fixes: #2665
1 parent 2fb0dda commit fd8eaa5

File tree

9 files changed

+173
-0
lines changed

9 files changed

+173
-0
lines changed

docs/man/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ if (ENABLE_PLUGINS)
2828
if (WITH_SELINUX)
2929
list(APPEND manuals rpm-plugin-selinux.8)
3030
endif()
31+
if (HAVE_UNSHARE)
32+
list(APPEND manuals rpm-plugin-unshare.8)
33+
endif()
3134
endif()
3235

3336
foreach(man ${manuals})

docs/man/rpm-plugin-unshare.8.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
date: 15 Sep 2023
3+
section: 8
4+
title: 'RPM-UNSHARE'
5+
---
6+
7+
NAME
8+
====
9+
10+
rpm-plugin-unshare - Unshare plugin for the RPM Package Manager
11+
12+
Description
13+
===========
14+
15+
This plugin allows using various Linux-specific namespace-related
16+
technologies inside transactions, such as to harden and limit
17+
scriptlet access to resources.
18+
19+
Configuration
20+
=============
21+
22+
This plugin implements the following configurables:
23+
24+
`%__transaction_unshare_paths`
25+
26+
: A colon-separated list of paths to privately mount during scriptlet
27+
execution. Typical examples would be `/tmp` to protect against
28+
insecure temporary file usage inside scriptlets, and `/home` to
29+
prevent scriptlets from accessing user home directories.
30+
31+
`%__transaction_unshare_nonet`
32+
33+
: Non-zero value disables network access during scriptlet execution.
34+
35+
See **rpm-plugins**(8) on how to control plugins in general.
36+
37+
SEE ALSO
38+
========
39+
40+
*dbus-monitor*(1) *rpm-plugins*(8)

plugins/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ if(WITH_FSVERITY)
3838
target_include_directories(fsverity PRIVATE ${CMAKE_SOURCE_DIR}/sign)
3939
endif()
4040

41+
if (HAVE_UNSHARE)
42+
add_library(unshare MODULE unshare.c)
43+
endif()
44+
4145
set(RPM_PLUGINDIR ${CMAKE_INSTALL_FULL_LIBDIR}/rpm-plugins
4246
CACHE PATH "rpm plugin directory")
4347

plugins/macros.transaction_unshare

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
%__transaction_unshare %{__plugindir}/unshare.so
2+
%__transaction_unshare_paths /tmp:/home
3+
%__transaction_unshare_nonet 1

plugins/unshare.c

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#include "system.h"
2+
3+
#include <sched.h>
4+
#include <sys/mount.h>
5+
#include <errno.h>
6+
#include <string.h>
7+
8+
#include <rpm/rpmts.h>
9+
#include <rpm/rpmplugin.h>
10+
#include <rpm/rpmlog.h>
11+
#include <rpm/rpmmacro.h>
12+
13+
#include "debug.h"
14+
15+
static ARGV_t private_mounts = NULL;
16+
static int unshare_flags = 0;
17+
18+
static rpmRC unshare_init(rpmPlugin plugin, rpmts ts)
19+
{
20+
char *paths = rpmExpand("%{?__transaction_unshare_paths}", NULL);
21+
private_mounts = argvSplitString(paths, ":", ARGV_SKIPEMPTY);
22+
if (private_mounts)
23+
unshare_flags |= CLONE_NEWNS;
24+
free(paths);
25+
26+
if (rpmExpandNumeric("%{?__transaction_unshare_nonet}"))
27+
unshare_flags |= CLONE_NEWNET;
28+
29+
return RPMRC_OK;
30+
}
31+
32+
static void unshare_cleanup(rpmPlugin plugin)
33+
{
34+
/* ensure clean state for possible next transaction */
35+
private_mounts = argvFree(private_mounts);
36+
unshare_flags = 0;
37+
}
38+
39+
static rpmRC unshare_scriptlet_fork_post(rpmPlugin plugin,
40+
const char *path, int type)
41+
{
42+
rpmRC rc = RPMRC_FAIL;
43+
44+
if (unshare_flags && (unshare(unshare_flags) == -1)) {
45+
rpmlog(RPMLOG_ERR, _("unshare with flags x%x failed: %s\n"),
46+
unshare_flags, strerror(errno));
47+
goto exit;
48+
}
49+
50+
if (private_mounts) {
51+
if (mount("/", "/", NULL, MS_REC | MS_PRIVATE, NULL) == -1) {
52+
rpmlog(RPMLOG_ERR, _("failed to mount private %s: %s\n"),
53+
"/", strerror(errno));
54+
goto exit;
55+
}
56+
for (ARGV_t mnt = private_mounts; mnt && *mnt; mnt++) {
57+
if (mount("none", *mnt, "tmpfs", 0, NULL) == -1) {
58+
rpmlog(RPMLOG_ERR, _("failed to mount private %s: %s\n"),
59+
*mnt, strerror(errno));
60+
goto exit;
61+
}
62+
}
63+
}
64+
rc = RPMRC_OK;
65+
66+
exit:
67+
return rc;
68+
}
69+
70+
struct rpmPluginHooks_s unshare_hooks = {
71+
.init = unshare_init,
72+
.cleanup = unshare_cleanup,
73+
.scriptlet_fork_post = unshare_scriptlet_fork_post,
74+
};

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ if (WITH_INTERNAL_OPENPGP)
2222
else()
2323
set(PGP sequoia)
2424
endif()
25+
set(HAVE_UNSHARE ${HAVE_UNSHARE})
2526

2627
set(TESTSUITE_AT
2728
rpmtests.at

tests/atlocal.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ RPMTREE=/
33
RPMLIBDIR="@CMAKE_INSTALL_FULL_LIBDIR@"
44
export RPMLIBDIR
55

6+
HAVE_UNSHARE=@HAVE_UNSHARE@
67
PYTHON=@PYTHON@
78
PGP=@PGP@
89

tests/data/SPECS/scriptwrite.spec

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Name: scriptwrite
2+
Version: 1.0
3+
Release: 1
4+
Summary: Testing script running environment
5+
Group: Testing
6+
License: GPL
7+
BuildArch: noarch
8+
9+
%description
10+
%{summary}
11+
12+
%files
13+
14+
%pre
15+
echo "%{name}-%{version} pre" > /tmp/%{name}.log
16+
17+
%post
18+
echo "%{name}-%{version} post" >> /tmp/%{name}.log

tests/rpmscript.at

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,3 +391,32 @@ ERASE:
391391
[])
392392
RPMTEST_CLEANUP
393393

394+
AT_SETUP([script running environment])
395+
AT_KEYWORDS([script])
396+
AT_SKIP_IF([test ${HAVE_UNSHARE} = 0])
397+
RPMDB_INIT
398+
runroot rpmbuild -bb --quiet \
399+
/data/SPECS/fakeshell.spec \
400+
/data/SPECS/scriptwrite.spec
401+
402+
RPMTEST_CHECK([
403+
RPMDB_INIT
404+
runroot_other rm -f /tmp/scriptwrite.log
405+
runroot rpm -U /build/RPMS/noarch/fakeshell-1.0-1.noarch.rpm /build/RPMS/noarch/scriptwrite-1.0-1.noarch.rpm
406+
runroot_other test -f /tmp/scriptwrite.log
407+
],
408+
[1],
409+
[],
410+
[])
411+
412+
RPMTEST_CHECK([
413+
RPMDB_INIT
414+
runroot_other rm -f /tmp/scriptwrite.log
415+
runroot rpm -U --noplugins /build/RPMS/noarch/fakeshell-1.0-1.noarch.rpm /build/RPMS/noarch/scriptwrite-1.0-1.noarch.rpm
416+
runroot_other test -f /tmp/scriptwrite.log
417+
],
418+
[0],
419+
[],
420+
[])
421+
RPMTEST_CLEANUP
422+

0 commit comments

Comments
 (0)