[pacman-dev] Add optional sandboxing when downloading files

Message ID 20210830093745.21389-1-rgacogne@archlinux.org
State New
Headers show
Series [pacman-dev] Add optional sandboxing when downloading files | expand

Commit Message

Remi Gacogne Aug. 30, 2021, 9:37 a.m. UTC
[re-sending because my initial e-mail was rejected by spam filters, sorry!]

Hi all,

I would like to get some feedback on a optional sandboxing feature I would like
to implement in pacman. The gist of it is to use a separate process for some
sensitive operations, like downloading files, and to restrict the privileges
of that process to reduce the risk of an exploitable bug turning into a code
execution with full root privileges. I'm not too worried about the pacman code
itself, but downloading files over the internet involves parsing HTTP payloads,
as well as parsing X.509 certificates and doing cryptographic operations when
HTTPS is used, which is more error-prone.
For what it's worth, Debian's APT has similar options hidden behind the
APT::Sandbox::Seccomp and APT::Sandbox::User parameters.

The attached patch is clearly not in a finished state, but provides more details
about the kind of changes needed to support this new feature. In a nutshell,
what it currently does is that if the UseSandbox option is not set nothing
changes. Otherwise internal and external downloads trigger the spawning of a
new process, via fork(), whose result is communicated to the main process via a
pipe, as is already done today for external downloads. Then:
- If available, PR_SET_NO_NEW_PRIVS is called to ensure that the downloader will
  not be able to gain new privileges in the future, for example by executing a
  set-uid program.
- If pacman has been built with libseccomp support and the kernel supports it, a
  list of system calls that are clearly of no use to a regular process
  downloading files is denied via a seccomp filter. That list is currently
  hard-coded but it would make more sense to make it configurable.
- If the SandboxUser option is set to a valid user, the downloader process
  switches to that user via setgid, setgroups and setuid, dropping root
  privileges. This part requires making the download folder owned by that user.
- If pacman has been built with libcap support and the downloader process still
  has privileged capabilities at this point, these are dropped.
- If pacman has been built with landlock support (requires an updated
  linux-api-headers, >= 5.13) and the kernel supports it, the whole filesystem
  is set read-only with the exception of /tmp and DBPath. The list of writable
  paths is currently hard-coded but it would make sense to make it configurable
  as well.

Any feedback will be welcome. Please be aware that I tried to keep the new code
in line with the coding style and contributing guidelines but I'm not familiar
with pacman's source code so I'm sure I did not do everything the right way, and
that this code can be improved. What I would like to know first is whether there
is any interest in that feature, then to get feedback about the "big picture"
part of the design. If there is indeed interest and my approach is somewhat
sane, then I can work on making the code less ugly, write the much needed
documentation for the new feature and related options, and improve the logging
of errors.

Thanks!

Remi

Signed-off-by: Remi Gacogne <rgacogne@archlinux.org>
---
 lib/libalpm/alpm.h         |  10 ++
 lib/libalpm/alpm_sandbox.c | 341 +++++++++++++++++++++++++++++++++++++
 lib/libalpm/alpm_sandbox.h |  31 ++++
 lib/libalpm/dload.c        |  83 ++++++++-
 lib/libalpm/handle.c       |  20 +++
 lib/libalpm/handle.h       |   2 +
 lib/libalpm/meson.build    |   1 +
 meson.build                |  11 +-
 src/pacman/conf.c          |  23 ++-
 src/pacman/conf.h          |   2 +
 src/pacman/pacman-conf.c   |   6 +
 11 files changed, 526 insertions(+), 4 deletions(-)
 create mode 100644 lib/libalpm/alpm_sandbox.c
 create mode 100644 lib/libalpm/alpm_sandbox.h

Comments

Andrew Gregory Sept. 3, 2021, 1:58 a.m. UTC | #1
On 08/30/21 at 11:37am, Remi Gacogne wrote:
> [re-sending because my initial e-mail was rejected by spam filters, sorry!]
> 
> Hi all,
> 
> I would like to get some feedback on a optional sandboxing feature I would like
> to implement in pacman. The gist of it is to use a separate process for some
> sensitive operations, like downloading files, and to restrict the privileges
> of that process to reduce the risk of an exploitable bug turning into a code
> execution with full root privileges. I'm not too worried about the pacman code
> itself, but downloading files over the internet involves parsing HTTP payloads,
> as well as parsing X.509 certificates and doing cryptographic operations when
> HTTPS is used, which is more error-prone.
> For what it's worth, Debian's APT has similar options hidden behind the
> APT::Sandbox::Seccomp and APT::Sandbox::User parameters.
> 
> The attached patch is clearly not in a finished state, but provides more details
> about the kind of changes needed to support this new feature. In a nutshell,
> what it currently does is that if the UseSandbox option is not set nothing
> changes. Otherwise internal and external downloads trigger the spawning of a
> new process, via fork(), whose result is communicated to the main process via a
> pipe, as is already done today for external downloads. Then:
> - If available, PR_SET_NO_NEW_PRIVS is called to ensure that the downloader will
>   not be able to gain new privileges in the future, for example by executing a
>   set-uid program.
> - If pacman has been built with libseccomp support and the kernel supports it, a
>   list of system calls that are clearly of no use to a regular process
>   downloading files is denied via a seccomp filter. That list is currently
>   hard-coded but it would make more sense to make it configurable.
> - If the SandboxUser option is set to a valid user, the downloader process
>   switches to that user via setgid, setgroups and setuid, dropping root
>   privileges. This part requires making the download folder owned by that user.
> - If pacman has been built with libcap support and the downloader process still
>   has privileged capabilities at this point, these are dropped.
> - If pacman has been built with landlock support (requires an updated
>   linux-api-headers, >= 5.13) and the kernel supports it, the whole filesystem
>   is set read-only with the exception of /tmp and DBPath. The list of writable
>   paths is currently hard-coded but it would make sense to make it configurable
>   as well.
> 
> Any feedback will be welcome. Please be aware that I tried to keep the new code
> in line with the coding style and contributing guidelines but I'm not familiar
> with pacman's source code so I'm sure I did not do everything the right way, and
> that this code can be improved. What I would like to know first is whether there
> is any interest in that feature, then to get feedback about the "big picture"
> part of the design. If there is indeed interest and my approach is somewhat
> sane, then I can work on making the code less ugly, write the much needed
> documentation for the new feature and related options, and improve the logging
> of errors.
> 
> Thanks!
> 
> Remi
> 
> Signed-off-by: Remi Gacogne <rgacogne@archlinux.org>
> ---
>  lib/libalpm/alpm.h         |  10 ++
>  lib/libalpm/alpm_sandbox.c | 341 +++++++++++++++++++++++++++++++++++++
>  lib/libalpm/alpm_sandbox.h |  31 ++++
>  lib/libalpm/dload.c        |  83 ++++++++-
>  lib/libalpm/handle.c       |  20 +++
>  lib/libalpm/handle.h       |   2 +
>  lib/libalpm/meson.build    |   1 +
>  meson.build                |  11 +-
>  src/pacman/conf.c          |  23 ++-
>  src/pacman/conf.h          |   2 +
>  src/pacman/pacman-conf.c   |   6 +
>  11 files changed, 526 insertions(+), 4 deletions(-)
>  create mode 100644 lib/libalpm/alpm_sandbox.c
>  create mode 100644 lib/libalpm/alpm_sandbox.h

This is a lot.  Let's focus on the portable user switching first; if that gets
merged we can look at adding the extra Linux-specific stuff.

> +	if(pid != -1)  {
> +		int wret;
> +		while((wret = waitpid(pid, &ret, 0)) == -1 && errno == EINTR);
> +		if(wret > 0) {
> +			while(read(err_fd[0], &ret, sizeof(ret)) == -1 && errno == EINTR);
> +		}
> +	} else {
> +		/* fork failed, make sure errno is preserved after cleanup */
> +		err = errno;
> +	}

This is just returning an int, is there a need to pass it by pipe instead of
just using the _Exit value?

I'm also wondering if we could simplify the configuration.  We could infer that
a sandbox should be used if a sandbox user is set or infer the user from the
cache folder being downloaded to.

apg
Allan McRae Sept. 3, 2021, 2:47 a.m. UTC | #2
On 3/9/21 11:58 am, Andrew Gregory wrote:
> On 08/30/21 at 11:37am, Remi Gacogne wrote:
>> ---
>>  lib/libalpm/alpm.h         |  10 ++
>>  lib/libalpm/alpm_sandbox.c | 341 +++++++++++++++++++++++++++++++++++++
>>  lib/libalpm/alpm_sandbox.h |  31 ++++
>>  lib/libalpm/dload.c        |  83 ++++++++-
>>  lib/libalpm/handle.c       |  20 +++
>>  lib/libalpm/handle.h       |   2 +
>>  lib/libalpm/meson.build    |   1 +
>>  meson.build                |  11 +-
>>  src/pacman/conf.c          |  23 ++-
>>  src/pacman/conf.h          |   2 +
>>  src/pacman/pacman-conf.c   |   6 +
>>  11 files changed, 526 insertions(+), 4 deletions(-)
>>  create mode 100644 lib/libalpm/alpm_sandbox.c
>>  create mode 100644 lib/libalpm/alpm_sandbox.h
> 
> This is a lot.  Let's focus on the portable user switching first; if that gets
> merged we can look at adding the extra Linux-specific stuff.

I was just writing the same thing!

Other general comments:

Rename alpm_sandbox.c to sandbox.c.  We don't need the prefix for a file
inside the library.

Split out the libseccomp setup to sandbox-linux.c.  I realise we mostly
support Linux, but this will save this file becoming a mass of #ifdef if
other operating systems add something similar.

Allan

Patch

diff --git lib/libalpm/alpm.h lib/libalpm/alpm.h
index a5f4a6ae..f414d811 100644
--- lib/libalpm/alpm.h
+++ lib/libalpm/alpm.h
@@ -1837,6 +1837,11 @@  int alpm_option_set_gpgdir(alpm_handle_t *handle, const char *gpgdir);
 /* End of gpdir accessors */
 /** @} */
 
+/** Sets the user to switch to for sensitive operations like downloading a file.
+ * @param handle the context handle
+ * @param sandboxuser the user to set
+ */
+int alpm_option_set_sandboxuser(alpm_handle_t *handle, const char *sandboxuser);
 
 /** @name Accessors for use syslog
  *
@@ -1859,6 +1864,11 @@  int alpm_option_set_usesyslog(alpm_handle_t *handle, int usesyslog);
 /* End of usesyslog accessors */
 /** @} */
 
+/** Sets whether to use sandboxing for sensitive operations like downloading a file (0 is FALSE, TRUE otherwise).
+ * @param handle the context handle
+ * @param usesandbox whether to use the sandboxing (0 is FALSE, TRUE otherwise)
+ */
+int alpm_option_set_usesandbox(alpm_handle_t *handle, int usesandbox);
 
 /** @name Accessors to the list of no-upgrade files.
  * These functions modify the list of files which should
diff --git lib/libalpm/alpm_sandbox.c lib/libalpm/alpm_sandbox.c
new file mode 100644
index 00000000..29f46d58
--- /dev/null
+++ lib/libalpm/alpm_sandbox.c
@@ -0,0 +1,341 @@ 
+/*
+ *  sandbox.c
+ *
+ *  Copyright (c) 2021 Pacman Development Team <pacman-dev@archlinux.org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <pwd.h>
+#include <sys/capability.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#ifdef HAVE_LINUX_LANDLOCK_H
+# include <linux/landlock.h>
+# include <sys/prctl.h>
+# include <sys/syscall.h>
+#endif /* HAVE_LINUX_LANDLOCK_H */
+
+#ifdef HAVE_LIBSECCOMP
+# include <seccomp.h>
+#endif /* HAVE_LIBSECCOMP */
+
+#include "alpm_sandbox.h"
+
+#ifdef HAVE_LINUX_LANDLOCK_H
+#ifndef landlock_create_ruleset
+static inline int landlock_create_ruleset(const struct landlock_ruleset_attr *const attr,
+		const size_t size, const __u32 flags)
+{
+	return syscall(__NR_landlock_create_ruleset, attr, size, flags);
+}
+#endif /* landlock_create_ruleset */
+
+#ifndef landlock_add_rule
+static inline int landlock_add_rule(const int ruleset_fd,
+		const enum landlock_rule_type rule_type,
+		const void *const rule_attr, const __u32 flags)
+{
+	return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr, flags);
+}
+#endif /* landlock_add_rule */
+
+#ifndef landlock_restrict_self
+static inline int landlock_restrict_self(const int ruleset_fd, const __u32 flags)
+{
+	return syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
+}
+#endif /* landlock_restrict_self */
+
+#define _LANDLOCK_ACCESS_FS_WRITE ( \
+  LANDLOCK_ACCESS_FS_WRITE_FILE | \
+  LANDLOCK_ACCESS_FS_REMOVE_DIR | \
+  LANDLOCK_ACCESS_FS_REMOVE_FILE | \
+  LANDLOCK_ACCESS_FS_MAKE_CHAR | \
+  LANDLOCK_ACCESS_FS_MAKE_DIR | \
+  LANDLOCK_ACCESS_FS_MAKE_REG | \
+  LANDLOCK_ACCESS_FS_MAKE_SOCK | \
+  LANDLOCK_ACCESS_FS_MAKE_FIFO | \
+  LANDLOCK_ACCESS_FS_MAKE_BLOCK | \
+  LANDLOCK_ACCESS_FS_MAKE_SYM)
+
+#define _LANDLOCK_ACCESS_FS_READ ( \
+  LANDLOCK_ACCESS_FS_READ_FILE | \
+  LANDLOCK_ACCESS_FS_READ_DIR)
+
+static int sandbox_write_only_beneath_cwd(void)
+{
+	const struct landlock_ruleset_attr ruleset_attr = {
+		.handled_access_fs = \
+			_LANDLOCK_ACCESS_FS_READ | \
+			_LANDLOCK_ACCESS_FS_WRITE | \
+			LANDLOCK_ACCESS_FS_EXECUTE,
+	};
+	struct landlock_path_beneath_attr path_beneath = {
+		.allowed_access = _LANDLOCK_ACCESS_FS_WRITE,
+	};
+	int result = 0;
+	int ruleset_fd;
+
+	ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+	if(ruleset_fd < 0) {
+		return ruleset_fd;
+	}
+
+	/* allow / as read-only */
+	path_beneath.parent_fd = open("/", O_PATH | O_CLOEXEC | O_DIRECTORY);
+	path_beneath.allowed_access = _LANDLOCK_ACCESS_FS_READ | LANDLOCK_ACCESS_FS_EXECUTE;
+
+	if(landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0)) {
+		result = errno;
+	}
+
+	close(path_beneath.parent_fd);
+
+	if(result == 0) {
+		/* allow the current working directory as read-write */
+		path_beneath.parent_fd = open(".", O_PATH | O_CLOEXEC | O_DIRECTORY);
+		path_beneath.allowed_access = _LANDLOCK_ACCESS_FS_READ | _LANDLOCK_ACCESS_FS_WRITE | LANDLOCK_ACCESS_FS_EXECUTE;
+
+		if(!landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0)) {
+			if(landlock_restrict_self(ruleset_fd, 0)) {
+				result = errno;
+			}
+		} else {
+			result = errno;
+		}
+
+		close(path_beneath.parent_fd);
+
+		if(result == 0) {
+			/* allow /tmp as well */
+			path_beneath.parent_fd = open("/tmp", O_PATH | O_CLOEXEC | O_DIRECTORY);
+			path_beneath.allowed_access = _LANDLOCK_ACCESS_FS_READ | _LANDLOCK_ACCESS_FS_WRITE | LANDLOCK_ACCESS_FS_EXECUTE;
+
+			if(!landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0)) {
+				if(landlock_restrict_self(ruleset_fd, 0)) {
+					result = errno;
+				}
+			} else {
+				result = errno;
+			}
+
+			close(path_beneath.parent_fd);
+		}
+	}
+
+	close(ruleset_fd);
+	return result;
+}
+#endif /* HAVE_LINUX_LANDLOCK_H */
+
+#ifdef HAVE_LIBSECCOMP
+
+static int sandbox_filter_syscalls(void)
+{
+	int ret = 0;
+	/* see https://docs.docker.com/engine/security/seccomp/ for inspiration,
+		 as well as systemd's src/shared/seccomp-util.c */
+	const char* denied_syscalls[] = {
+		/* kernel modules */
+		"delete_module",
+		"finit_module",
+		"init_module",
+		/* mount */
+		"chroot",
+		"fsconfig",
+		"fsmount",
+		"fsopen",
+		"fspick",
+		"mount",
+		"move_mount",
+		"open_tree",
+		"pivot_root",
+		"umount",
+		"umount2",
+		/* keyring */
+		"add_key",
+		"keyctl",
+		"request_key",
+		/* CPU emulation */
+		"modify_ldt",
+		"subpage_prot",
+		"switch_endian",
+		"vm86",
+		"vm86old",
+		/* debug */
+		"kcmp",
+		"lookup_dcookie",
+		"perf_event_open",
+		"ptrace",
+		"rtas",
+		"sys_debug_setcontext",
+		/* set clock */
+		"adjtimex",
+		"clock_adjtime",
+		"clock_adjtime64",
+		"clock_settime",
+		"clock_settime64",
+		"settimeofday",
+		/* raw IO */
+		"ioperm",
+		"iopl",
+		"pciconfig_iobase",
+		"pciconfig_read",
+		"pciconfig_write",
+		/* kexec */
+		"kexec_file_load",
+		"kexec_load",
+		/* reboot */
+		"reboot",
+		/* privileged */
+		"acct",
+		"bpf",
+		"personality",
+		/* obsolete */
+		"_sysctl",
+		"afs_syscall",
+		"bdflush",
+		"break",
+		"create_module",
+		"ftime",
+		"get_kernel_syms",
+		"getpmsg",
+		"gtty",
+		"idle",
+		"lock",
+		"mpx",
+		"prof",
+		"profil",
+		"putpmsg",
+		"query_module",
+		"security",
+		"sgetmask",
+		"ssetmask",
+		"stime",
+		"stty",
+		"sysfs",
+		"tuxcall",
+		"ulimit",
+		"uselib",
+		"ustat",
+		"vserver",
+		/* swap */
+		"swapon",
+		"swapoff",
+	};
+	/* allow all syscalls that are not listed */
+	size_t idx;
+	scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
+	if(ctx == NULL) {
+		return errno;
+	}
+
+	for(idx = 0; idx < sizeof(denied_syscalls) / sizeof(*denied_syscalls); idx++) {
+		int syscall = seccomp_syscall_resolve_name(denied_syscalls[idx]);
+		if(syscall != __NR_SCMP_ERROR) {
+			if(seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), syscall, 0) != 0) {
+				/* Do not fail but log something like: _("Error blocking syscall %s\n"), denied_syscalls[idx]); */
+			}
+		}
+	}
+
+	if(seccomp_load(ctx) != 0) {
+		ret = errno;
+	}
+
+	seccomp_release(ctx);
+	return ret;
+}
+#endif /* HAVE_LIBSECCOMP */
+
+static int switch_to_user(const char *user)
+{
+	struct passwd const *pw = NULL;
+	if(getuid() != 0) {
+		return 1;
+	}
+	pw = getpwnam(user);
+	if(pw == NULL) {
+		return errno;
+	}
+	if(setgid(pw->pw_gid) != 0) {
+		return errno;
+	}
+	if(setgroups(0, NULL)) {
+		return errno;
+	}
+	if(setuid(pw->pw_uid) != 0) {
+		return errno;
+	}
+	return 0;
+}
+
+/* check exported library symbols with: nm -C -D <lib> */
+#define SYMEXPORT __attribute__((visibility("default")))
+
+int SYMEXPORT alpm_sandbox_child(const char* sandboxuser)
+{
+	int result = 0;
+#ifdef HAVE_LINUX_LANDLOCK_H
+	int ret = 0;
+#endif/* HAVE_LINUX_LANDLOCK_H */
+
+	/* make sure that we cannot gain more privileges later */
+	if(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) {
+		result = errno;
+	}
+
+#ifdef HAVE_LIBSECCOMP
+	result = sandbox_filter_syscalls();
+#endif /* HAVE_LIBSECCOMP */
+
+	if(sandboxuser != NULL) {
+		result = switch_to_user(sandboxuser);
+	}
+
+#ifdef HAVE_LIBCAP
+	/* we might have some capabilities remaining,
+	 * especially if sandboxuser is not set */
+	cap_t caps = cap_get_proc();
+	cap_clear(caps);
+	if(cap_set_mode(CAP_MODE_NOPRIV) != 0) {
+		cap_free(caps);
+		if(result == 0) {
+			result = errno;
+		}
+	}
+	if(cap_set_proc(caps) != 0) {
+		cap_free(caps);
+		if(result == 0) {
+			result = errno;
+		}
+	}
+	cap_free(caps);
+#endif /* HAVE_LIBCAP */
+
+#ifdef HAVE_LINUX_LANDLOCK_H
+	ret = sandbox_write_only_beneath_cwd();
+	if(result == 0) {
+		result = ret;
+	}
+#endif /* HAVE_LINUX_LANDLOCK_H */
+
+	return result;
+}
diff --git lib/libalpm/alpm_sandbox.h lib/libalpm/alpm_sandbox.h
new file mode 100644
index 00000000..47611575
--- /dev/null
+++ lib/libalpm/alpm_sandbox.h
@@ -0,0 +1,31 @@ 
+/*
+ *  sandbox.h
+ *
+ *  Copyright (c) 2021 Pacman Development Team <pacman-dev@archlinux.org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+#ifndef ALPM_SANDBOX_H
+#define ALPM_SANDBOX_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+int alpm_sandbox_child(const char *sandboxuser);
+
+#ifdef __cplusplus
+}
+#endif
+#endif /* ALPM_SANDBOX_H */
diff --git lib/libalpm/dload.c lib/libalpm/dload.c
index ca6be7b6..6bb06b4c 100644
--- lib/libalpm/dload.c
+++ lib/libalpm/dload.c
@@ -28,6 +28,7 @@ 
 #include <sys/time.h>
 #include <sys/types.h>
 #include <sys/stat.h>
+#include <sys/wait.h>
 #include <signal.h>
 
 #ifdef HAVE_NETINET_IN_H
@@ -44,6 +45,7 @@ 
 /* libalpm */
 #include "dload.h"
 #include "alpm_list.h"
+#include "alpm_sandbox.h"
 #include "alpm.h"
 #include "log.h"
 #include "util.h"
@@ -898,6 +900,81 @@  static int curl_download_internal(alpm_handle_t *handle,
 
 #endif
 
+static int curl_download_internal_sandboxed(alpm_handle_t *handle,
+		alpm_list_t *payloads /* struct dload_payload */,
+		const char *localpath)
+{
+	int pid, err = 0, ret = -1, err_fd[2];
+	sigset_t oldblock;
+	struct sigaction sa_ign = { .sa_handler = SIG_IGN }, oldint, oldquit;
+
+	if(pipe(err_fd) != 0) {
+		return -1;
+	}
+
+	sigaction(SIGINT, &sa_ign, &oldint);
+	sigaction(SIGQUIT, &sa_ign, &oldquit);
+	sigaddset(&sa_ign.sa_mask, SIGCHLD);
+	sigprocmask(SIG_BLOCK, &sa_ign.sa_mask, &oldblock);
+
+	pid = fork();
+
+	/* child */
+	if(pid == 0) {
+		close(err_fd[0]);
+		fcntl(err_fd[1], F_SETFD, FD_CLOEXEC);
+
+		/* restore signal handling for the child to inherit */
+		sigaction(SIGINT, &oldint, NULL);
+		sigaction(SIGQUIT, &oldquit, NULL);
+		sigprocmask(SIG_SETMASK, &oldblock, NULL);
+
+		/* cwd to the download directory */
+		ret = chdir(localpath);
+		if(ret != 0) {
+			_alpm_log(handle, ALPM_LOG_WARNING, _("could not chdir to download directory %s\n"), localpath);
+			ret = -1;
+		} else {
+			ret = alpm_sandbox_child(handle->sandboxuser);
+			if (ret != 0) {
+				_alpm_log(handle, ALPM_LOG_WARNING, _("sandboxing failed!\n"));
+			}
+
+			ret = curl_download_internal(handle, payloads, localpath);
+		}
+
+		/* pass the result back to the parent */
+		while(write(err_fd[1], &ret, sizeof(ret)) == -1 && errno == EINTR);
+		_Exit(ret < 0 ? 1 : 0);
+	}
+
+	/* parent */
+	close(err_fd[1]);
+
+	if(pid != -1)  {
+		int wret;
+		while((wret = waitpid(pid, &ret, 0)) == -1 && errno == EINTR);
+		if(wret > 0) {
+			while(read(err_fd[0], &ret, sizeof(ret)) == -1 && errno == EINTR);
+		}
+	} else {
+		/* fork failed, make sure errno is preserved after cleanup */
+		err = errno;
+	}
+
+	close(err_fd[0]);
+
+	sigaction(SIGINT, &oldint, NULL);
+	sigaction(SIGQUIT, &oldquit, NULL);
+	sigprocmask(SIG_SETMASK, &oldblock, NULL);
+
+	if(err) {
+		errno = err;
+		ret = -1;
+	}
+	return ret;
+}
+
 /* Returns -1 if an error happened for a required file
  * Returns 0 if a payload was actually downloaded
  * Returns 1 if no files were downloaded and all errors were non-fatal
@@ -908,7 +985,11 @@  int _alpm_download(alpm_handle_t *handle,
 {
 	if(handle->fetchcb == NULL) {
 #ifdef HAVE_LIBCURL
-		return curl_download_internal(handle, payloads, localpath);
+		if(handle->usesandbox) {
+			return curl_download_internal_sandboxed(handle, payloads, localpath);
+		} else {
+			return curl_download_internal(handle, payloads, localpath);
+		}
 #else
 		RET_ERR(handle, ALPM_ERR_EXTERNAL_DOWNLOAD, -1);
 #endif
diff --git lib/libalpm/handle.c lib/libalpm/handle.c
index e6b683cb..10ae44b2 100644
--- lib/libalpm/handle.c
+++ lib/libalpm/handle.c
@@ -573,6 +573,19 @@  int SYMEXPORT alpm_option_set_gpgdir(alpm_handle_t *handle, const char *gpgdir)
 	return 0;
 }
 
+int SYMEXPORT alpm_option_set_sandboxuser(alpm_handle_t *handle, const char *sandboxuser)
+{
+	CHECK_HANDLE(handle, return -1);
+	if(handle->sandboxuser) {
+		FREE(handle->sandboxuser);
+	}
+
+	STRDUP(handle->sandboxuser, sandboxuser, RET_ERR(handle, ALPM_ERR_MEMORY, -1));
+
+	_alpm_log(handle, ALPM_LOG_DEBUG, "option 'sandboxuser' = %s\n", handle->sandboxuser);
+	return 0;
+}
+
 int SYMEXPORT alpm_option_set_usesyslog(alpm_handle_t *handle, int usesyslog)
 {
 	CHECK_HANDLE(handle, return -1);
@@ -580,6 +593,13 @@  int SYMEXPORT alpm_option_set_usesyslog(alpm_handle_t *handle, int usesyslog)
 	return 0;
 }
 
+int SYMEXPORT alpm_option_set_usesandbox(alpm_handle_t *handle, int usesandbox)
+{
+	CHECK_HANDLE(handle, return -1);
+	handle->usesandbox = usesandbox;
+	return 0;
+}
+
 static int _alpm_option_strlist_add(alpm_handle_t *handle, alpm_list_t **list, const char *str)
 {
 	char *dup;
diff --git lib/libalpm/handle.h lib/libalpm/handle.h
index b1526c67..43550833 100644
--- lib/libalpm/handle.h
+++ lib/libalpm/handle.h
@@ -90,6 +90,7 @@  struct __alpm_handle_t {
 	char *logfile;           /* Name of the log file */
 	char *lockfile;          /* Name of the lock file */
 	char *gpgdir;            /* Directory where GnuPG files are stored */
+	char *sandboxuser;       /* User to switch to for sensitive operations like downloading files */
 	alpm_list_t *cachedirs;  /* Paths to pacman cache directories */
 	alpm_list_t *hookdirs;   /* Paths to hook directories */
 	alpm_list_t *overwrite_files; /* Paths that may be overwritten */
@@ -104,6 +105,7 @@  struct __alpm_handle_t {
 	/* options */
 	alpm_list_t *architectures; /* Architectures of packages we should allow */
 	int usesyslog;           /* Use syslog instead of logfile? */ /* TODO move to frontend */
+	int usesandbox;          /* Whether to enable sandboxing for sensitive operations like downloading files */
 	int checkspace;          /* Check disk space before installing */
 	char *dbext;             /* Sync DB extension */
 	int siglevel;            /* Default signature verification level */
diff --git lib/libalpm/meson.build lib/libalpm/meson.build
index 607e91a3..c36ae516 100644
--- lib/libalpm/meson.build
+++ lib/libalpm/meson.build
@@ -2,6 +2,7 @@  libalpm_sources = files('''
   add.h add.c
   alpm.h alpm.c
   alpm_list.h alpm_list.c
+  alpm_sandbox.h alpm_sandbox.c
   backup.h backup.c
   base64.h base64.c
   be_local.c
diff --git meson.build meson.build
index 26c92b8e..88f0e3a1 100644
--- meson.build
+++ meson.build
@@ -91,6 +91,10 @@  libarchive = dependency('libarchive',
                         version : '>=3.0.0',
                         static : get_option('buildstatic'))
 
+libcap = dependency('libcap',
+                    static : get_option('buildstatic'))
+conf.set('HAVE_LIBCAP', libcap.found())
+
 libcurl = dependency('libcurl',
                      version : '>=7.55.0',
                      required : get_option('curl'),
@@ -120,7 +124,12 @@  else
   error('unhandled crypto value @0@'.format(want_crypto))
 endif
 
+libseccomp = dependency('libseccomp',
+                        static : get_option('buildstatic'))
+conf.set('HAVE_LIBSECCOMP', libseccomp.found())
+
 foreach header : [
+    'linux/landlock.h',
     'mntent.h',
     'sys/mnttab.h',
     'sys/mount.h',
@@ -305,7 +314,7 @@  libcommon = static_library(
   gnu_symbol_visibility : 'hidden',
   install : false)
 
-alpm_deps = [crypto_provider, libarchive, libcurl, libintl, gpgme]
+alpm_deps = [crypto_provider, libarchive, libcap, libcurl, libintl, libseccomp, gpgme]
 
 libalpm_a = static_library(
   'alpm_objlib',
diff --git src/pacman/conf.c src/pacman/conf.c
index 12fee64c..9480a492 100644
--- src/pacman/conf.c
+++ src/pacman/conf.c
@@ -33,6 +33,8 @@ 
 #include <unistd.h>
 #include <signal.h>
 
+#include <alpm_sandbox.h>
+
 /* pacman */
 #include "conf.h"
 #include "ini.h"
@@ -215,7 +217,7 @@  static char *get_tempfile(const char *path, const char *filename)
  * - not thread-safe
  * - errno may be set by fork(), pipe(), or execvp()
  */
-static int systemvp(const char *file, char *const argv[])
+static int systemvp(const char *file, char *const argv[], bool sandbox, const char *sandboxuser)
 {
 	int pid, err = 0, ret = -1, err_fd[2];
 	sigset_t oldblock;
@@ -242,6 +244,13 @@  static int systemvp(const char *file, char *const argv[])
 		sigaction(SIGQUIT, &oldquit, NULL);
 		sigprocmask(SIG_SETMASK, &oldblock, NULL);
 
+		if (sandbox) {
+			ret = alpm_sandbox_child(sandboxuser);
+			if (ret != 0) {
+				pm_printf(ALPM_LOG_WARNING, _("sandboxing failed!\n"));
+			}
+		}
+
 		execvp(file, argv);
 
 		/* execvp failed, pass the error back to the parent */
@@ -352,7 +361,7 @@  static int download_with_xfercommand(void *ctx, const char *url,
 			free(cmd);
 		}
 	}
-	retval = systemvp(argv[0], (char**)argv);
+	retval = systemvp(argv[0], (char**)argv, config->usesandbox, config->sandboxuser);
 
 	if(retval == -1) {
 		pm_printf(ALPM_LOG_WARNING, _("running XferCommand: fork failed!\n"));
@@ -601,6 +610,9 @@  static int _parse_options(const char *key, char *value,
 		if(strcmp(key, "UseSyslog") == 0) {
 			config->usesyslog = 1;
 			pm_printf(ALPM_LOG_DEBUG, "config: usesyslog\n");
+		} else if(strcmp(key, "UseSandbox") == 0) {
+			config->usesandbox = 1;
+			pm_printf(ALPM_LOG_DEBUG, "config: usesandbox\n");
 		} else if(strcmp(key, "ILoveCandy") == 0) {
 			config->chomp = 1;
 			pm_printf(ALPM_LOG_DEBUG, "config: chomp\n");
@@ -668,6 +680,11 @@  static int _parse_options(const char *key, char *value,
 				config->logfile = strdup(value);
 				pm_printf(ALPM_LOG_DEBUG, "config: logfile: %s\n", value);
 			}
+		} else if(strcmp(key, "SandboxUser") == 0) {
+			if(!config->sandboxuser) {
+				config->sandboxuser = strdup(value);
+				pm_printf(ALPM_LOG_DEBUG, "config: sandboxuser: %s\n", value);
+			}
 		} else if(strcmp(key, "XferCommand") == 0) {
 			char **c;
 			if((config->xfercommand_argv = wordsplit(value)) == NULL) {
@@ -904,6 +921,8 @@  static int setup_libalpm(void)
 	alpm_option_set_architectures(handle, config->architectures);
 	alpm_option_set_checkspace(handle, config->checkspace);
 	alpm_option_set_usesyslog(handle, config->usesyslog);
+	alpm_option_set_usesandbox(handle, config->usesandbox);
+	alpm_option_set_sandboxuser(handle, config->sandboxuser);
 
 	alpm_option_set_ignorepkgs(handle, config->ignorepkg);
 	alpm_option_set_ignoregroups(handle, config->ignoregrp);
diff --git src/pacman/conf.h src/pacman/conf.h
index 04350d39..8fced284 100644
--- src/pacman/conf.h
+++ src/pacman/conf.h
@@ -55,6 +55,7 @@  typedef struct __config_t {
 	unsigned short print;
 	unsigned short checkspace;
 	unsigned short usesyslog;
+	unsigned short usesandbox;
 	unsigned short color;
 	unsigned short disable_dl_timeout;
 	char *print_format;
@@ -67,6 +68,7 @@  typedef struct __config_t {
 	char *logfile;
 	char *gpgdir;
 	char *sysroot;
+	char *sandboxuser;
 	alpm_list_t *hookdirs;
 	alpm_list_t *cachedirs;
 	alpm_list_t *architectures;
diff --git src/pacman/pacman-conf.c src/pacman/pacman-conf.c
index 600f1622..6da27937 100644
--- src/pacman/pacman-conf.c
+++ src/pacman/pacman-conf.c
@@ -251,6 +251,7 @@  static void dump_config(void)
 	show_list_str("HookDir", config->hookdirs);
 	show_str("GPGDir", config->gpgdir);
 	show_str("LogFile", config->logfile);
+	show_str("SandboxUser", config->sandboxuser);
 
 	show_list_str("HoldPkg", config->holdpkg);
 	show_list_str("IgnorePkg", config->ignorepkg);
@@ -268,6 +269,7 @@  static void dump_config(void)
 	show_bool("DisableDownloadTimeout", config->disable_dl_timeout);
 	show_bool("ILoveCandy", config->chomp);
 	show_bool("NoProgressBar", config->noprogressbar);
+	show_bool("UseSandbox", config->usesandbox);
 
 	show_int("ParallelDownloads", config->parallel_downloads);
 
@@ -349,6 +351,8 @@  static int list_directives(void)
 			show_str("GPGDir", config->gpgdir);
 		} else if(strcasecmp(i->data, "LogFile") == 0) {
 			show_str("LogFile", config->logfile);
+		} else if(strcasecmp(i->data, "SandboxUser") == 0) {
+			show_str("SandboxUser", config->sandboxuser);
 
 		} else if(strcasecmp(i->data, "HoldPkg") == 0) {
 			show_list_str("HoldPkg", config->holdpkg);
@@ -369,6 +373,8 @@  static int list_directives(void)
 
 		} else if(strcasecmp(i->data, "UseSyslog") == 0) {
 			show_bool("UseSyslog", config->usesyslog);
+		} else if(strcasecmp(i->data, "UseSandbox") == 0) {
+			show_bool("UseSandbox", config->usesandbox);
 		} else if(strcasecmp(i->data, "Color") == 0) {
 			show_bool("Color", config->color);
 		} else if(strcasecmp(i->data, "CheckSpace") == 0) {