Merge pull request #4185 from endocode/djalal-sandbox-first-protection-v1
core:sandbox: Add new ProtectKernelTunables=, ProtectControlGroups=, ProtectSystem=strict and fixes
This commit is contained in:
@@ -597,3 +597,190 @@ int inotify_add_watch_fd(int fd, int what, uint32_t mask) {
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
int chase_symlinks(const char *path, const char *_root, char **ret) {
|
||||
_cleanup_free_ char *buffer = NULL, *done = NULL, *root = NULL;
|
||||
_cleanup_close_ int fd = -1;
|
||||
unsigned max_follow = 32; /* how many symlinks to follow before giving up and returning ELOOP */
|
||||
char *todo;
|
||||
int r;
|
||||
|
||||
assert(path);
|
||||
|
||||
/* This is a lot like canonicalize_file_name(), but takes an additional "root" parameter, that allows following
|
||||
* symlinks relative to a root directory, instead of the root of the host.
|
||||
*
|
||||
* Note that "root" matters only if we encounter an absolute symlink, it's unused otherwise. Most importantly
|
||||
* this means the path parameter passed in is not prefixed by it.
|
||||
*
|
||||
* Algorithmically this operates on two path buffers: "done" are the components of the path we already
|
||||
* processed and resolved symlinks, "." and ".." of. "todo" are the components of the path we still need to
|
||||
* process. On each iteration, we move one component from "todo" to "done", processing it's special meaning
|
||||
* each time. The "todo" path always starts with at least one slash, the "done" path always ends in no
|
||||
* slash. We always keep an O_PATH fd to the component we are currently processing, thus keeping lookup races
|
||||
* at a minimum. */
|
||||
|
||||
r = path_make_absolute_cwd(path, &buffer);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (_root) {
|
||||
r = path_make_absolute_cwd(_root, &root);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
fd = open("/", O_CLOEXEC|O_NOFOLLOW|O_PATH);
|
||||
if (fd < 0)
|
||||
return -errno;
|
||||
|
||||
todo = buffer;
|
||||
for (;;) {
|
||||
_cleanup_free_ char *first = NULL;
|
||||
_cleanup_close_ int child = -1;
|
||||
struct stat st;
|
||||
size_t n, m;
|
||||
|
||||
/* Determine length of first component in the path */
|
||||
n = strspn(todo, "/"); /* The slashes */
|
||||
m = n + strcspn(todo + n, "/"); /* The entire length of the component */
|
||||
|
||||
/* Extract the first component. */
|
||||
first = strndup(todo, m);
|
||||
if (!first)
|
||||
return -ENOMEM;
|
||||
|
||||
todo += m;
|
||||
|
||||
/* Just a single slash? Then we reached the end. */
|
||||
if (isempty(first) || path_equal(first, "/"))
|
||||
break;
|
||||
|
||||
/* Just a dot? Then let's eat this up. */
|
||||
if (path_equal(first, "/."))
|
||||
continue;
|
||||
|
||||
/* Two dots? Then chop off the last bit of what we already found out. */
|
||||
if (path_equal(first, "/..")) {
|
||||
_cleanup_free_ char *parent = NULL;
|
||||
int fd_parent = -1;
|
||||
|
||||
if (isempty(done) || path_equal(done, "/"))
|
||||
return -EINVAL;
|
||||
|
||||
parent = dirname_malloc(done);
|
||||
if (!parent)
|
||||
return -ENOMEM;
|
||||
|
||||
/* Don't allow this to leave the root dir */
|
||||
if (root &&
|
||||
path_startswith(done, root) &&
|
||||
!path_startswith(parent, root))
|
||||
return -EINVAL;
|
||||
|
||||
free(done);
|
||||
done = parent;
|
||||
parent = NULL;
|
||||
|
||||
fd_parent = openat(fd, "..", O_CLOEXEC|O_NOFOLLOW|O_PATH);
|
||||
if (fd_parent < 0)
|
||||
return -errno;
|
||||
|
||||
safe_close(fd);
|
||||
fd = fd_parent;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Otherwise let's see what this is. */
|
||||
child = openat(fd, first + n, O_CLOEXEC|O_NOFOLLOW|O_PATH);
|
||||
if (child < 0)
|
||||
return -errno;
|
||||
|
||||
if (fstat(child, &st) < 0)
|
||||
return -errno;
|
||||
|
||||
if (S_ISLNK(st.st_mode)) {
|
||||
_cleanup_free_ char *destination = NULL;
|
||||
|
||||
/* This is a symlink, in this case read the destination. But let's make sure we don't follow
|
||||
* symlinks without bounds. */
|
||||
if (--max_follow <= 0)
|
||||
return -ELOOP;
|
||||
|
||||
r = readlinkat_malloc(fd, first + n, &destination);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (isempty(destination))
|
||||
return -EINVAL;
|
||||
|
||||
if (path_is_absolute(destination)) {
|
||||
|
||||
/* An absolute destination. Start the loop from the beginning, but use the root
|
||||
* directory as base. */
|
||||
|
||||
safe_close(fd);
|
||||
fd = open(root ?: "/", O_CLOEXEC|O_NOFOLLOW|O_PATH);
|
||||
if (fd < 0)
|
||||
return -errno;
|
||||
|
||||
free(buffer);
|
||||
buffer = destination;
|
||||
destination = NULL;
|
||||
|
||||
todo = buffer;
|
||||
free(done);
|
||||
|
||||
/* Note that we do not revalidate the root, we take it as is. */
|
||||
if (isempty(root))
|
||||
done = NULL;
|
||||
else {
|
||||
done = strdup(root);
|
||||
if (!done)
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
} else {
|
||||
char *joined;
|
||||
|
||||
/* A relative destination. If so, this is what we'll prefix what's left to do with what
|
||||
* we just read, and start the loop again, but remain in the current directory. */
|
||||
|
||||
joined = strjoin("/", destination, todo, NULL);
|
||||
if (!joined)
|
||||
return -ENOMEM;
|
||||
|
||||
free(buffer);
|
||||
todo = buffer = joined;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
/* If this is not a symlink, then let's just add the name we read to what we already verified. */
|
||||
if (!done) {
|
||||
done = first;
|
||||
first = NULL;
|
||||
} else {
|
||||
if (!strextend(&done, first, NULL))
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
/* And iterate again, but go one directory further down. */
|
||||
safe_close(fd);
|
||||
fd = child;
|
||||
child = -1;
|
||||
}
|
||||
|
||||
if (!done) {
|
||||
/* Special case, turn the empty string into "/", to indicate the root directory. */
|
||||
done = strdup("/");
|
||||
if (!done)
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
*ret = done;
|
||||
done = NULL;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -77,3 +77,5 @@ union inotify_event_buffer {
|
||||
};
|
||||
|
||||
int inotify_add_watch_fd(int fd, int what, uint32_t mask);
|
||||
|
||||
int chase_symlinks(const char *path, const char *_root, char **ret);
|
||||
|
||||
+48
-23
@@ -36,6 +36,7 @@
|
||||
#include "set.h"
|
||||
#include "stdio-util.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
|
||||
static int fd_fdinfo_mnt_id(int fd, const char *filename, int flags, int *mnt_id) {
|
||||
char path[strlen("/proc/self/fdinfo/") + DECIMAL_STR_MAX(int)];
|
||||
@@ -287,10 +288,12 @@ int umount_recursive(const char *prefix, int flags) {
|
||||
continue;
|
||||
|
||||
if (umount2(p, flags) < 0) {
|
||||
r = -errno;
|
||||
r = log_debug_errno(errno, "Failed to umount %s: %m", p);
|
||||
continue;
|
||||
}
|
||||
|
||||
log_debug("Successfully unmounted %s", p);
|
||||
|
||||
again = true;
|
||||
n++;
|
||||
|
||||
@@ -311,24 +314,21 @@ static int get_mount_flags(const char *path, unsigned long *flags) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int bind_remount_recursive(const char *prefix, bool ro) {
|
||||
int bind_remount_recursive(const char *prefix, bool ro, char **blacklist) {
|
||||
_cleanup_set_free_free_ Set *done = NULL;
|
||||
_cleanup_free_ char *cleaned = NULL;
|
||||
int r;
|
||||
|
||||
/* Recursively remount a directory (and all its submounts)
|
||||
* read-only or read-write. If the directory is already
|
||||
* mounted, we reuse the mount and simply mark it
|
||||
* MS_BIND|MS_RDONLY (or remove the MS_RDONLY for read-write
|
||||
* operation). If it isn't we first make it one. Afterwards we
|
||||
* apply MS_BIND|MS_RDONLY (or remove MS_RDONLY) to all
|
||||
* submounts we can access, too. When mounts are stacked on
|
||||
* the same mount point we only care for each individual
|
||||
* "top-level" mount on each point, as we cannot
|
||||
* influence/access the underlying mounts anyway. We do not
|
||||
* have any effect on future submounts that might get
|
||||
* propagated, they migt be writable. This includes future
|
||||
* submounts that have been triggered via autofs. */
|
||||
/* Recursively remount a directory (and all its submounts) read-only or read-write. If the directory is already
|
||||
* mounted, we reuse the mount and simply mark it MS_BIND|MS_RDONLY (or remove the MS_RDONLY for read-write
|
||||
* operation). If it isn't we first make it one. Afterwards we apply MS_BIND|MS_RDONLY (or remove MS_RDONLY) to
|
||||
* all submounts we can access, too. When mounts are stacked on the same mount point we only care for each
|
||||
* individual "top-level" mount on each point, as we cannot influence/access the underlying mounts anyway. We
|
||||
* do not have any effect on future submounts that might get propagated, they migt be writable. This includes
|
||||
* future submounts that have been triggered via autofs.
|
||||
*
|
||||
* If the "blacklist" parameter is specified it may contain a list of subtrees to exclude from the
|
||||
* remount operation. Note that we'll ignore the blacklist for the top-level path. */
|
||||
|
||||
cleaned = strdup(prefix);
|
||||
if (!cleaned)
|
||||
@@ -385,6 +385,33 @@ int bind_remount_recursive(const char *prefix, bool ro) {
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (!path_startswith(p, cleaned))
|
||||
continue;
|
||||
|
||||
/* Ignore this mount if it is blacklisted, but only if it isn't the top-level mount we shall
|
||||
* operate on. */
|
||||
if (!path_equal(cleaned, p)) {
|
||||
bool blacklisted = false;
|
||||
char **i;
|
||||
|
||||
STRV_FOREACH(i, blacklist) {
|
||||
|
||||
if (path_equal(*i, cleaned))
|
||||
continue;
|
||||
|
||||
if (!path_startswith(*i, cleaned))
|
||||
continue;
|
||||
|
||||
if (path_startswith(p, *i)) {
|
||||
blacklisted = true;
|
||||
log_debug("Not remounting %s, because blacklisted by %s, called for %s", p, *i, cleaned);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (blacklisted)
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Let's ignore autofs mounts. If they aren't
|
||||
* triggered yet, we want to avoid triggering
|
||||
* them, as we don't make any guarantees for
|
||||
@@ -396,12 +423,9 @@ int bind_remount_recursive(const char *prefix, bool ro) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (path_startswith(p, cleaned) &&
|
||||
!set_contains(done, p)) {
|
||||
|
||||
if (!set_contains(done, p)) {
|
||||
r = set_consume(todo, p);
|
||||
p = NULL;
|
||||
|
||||
if (r == -EEXIST)
|
||||
continue;
|
||||
if (r < 0)
|
||||
@@ -418,8 +442,7 @@ int bind_remount_recursive(const char *prefix, bool ro) {
|
||||
|
||||
if (!set_contains(done, cleaned) &&
|
||||
!set_contains(todo, cleaned)) {
|
||||
/* The prefix directory itself is not yet a
|
||||
* mount, make it one. */
|
||||
/* The prefix directory itself is not yet a mount, make it one. */
|
||||
if (mount(cleaned, cleaned, NULL, MS_BIND|MS_REC, NULL) < 0)
|
||||
return -errno;
|
||||
|
||||
@@ -430,6 +453,8 @@ int bind_remount_recursive(const char *prefix, bool ro) {
|
||||
if (mount(NULL, prefix, NULL, orig_flags|MS_BIND|MS_REMOUNT|(ro ? MS_RDONLY : 0), NULL) < 0)
|
||||
return -errno;
|
||||
|
||||
log_debug("Made top-level directory %s a mount point.", prefix);
|
||||
|
||||
x = strdup(cleaned);
|
||||
if (!x)
|
||||
return -ENOMEM;
|
||||
@@ -447,8 +472,7 @@ int bind_remount_recursive(const char *prefix, bool ro) {
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
/* Deal with mount points that are obstructed by a
|
||||
* later mount */
|
||||
/* Deal with mount points that are obstructed by a later mount */
|
||||
r = path_is_mount_point(x, 0);
|
||||
if (r == -ENOENT || r == 0)
|
||||
continue;
|
||||
@@ -463,6 +487,7 @@ int bind_remount_recursive(const char *prefix, bool ro) {
|
||||
if (mount(NULL, x, NULL, orig_flags|MS_BIND|MS_REMOUNT|(ro ? MS_RDONLY : 0), NULL) < 0)
|
||||
return -errno;
|
||||
|
||||
log_debug("Remounted %s read-only.", x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ int path_is_mount_point(const char *path, int flags);
|
||||
int repeat_unmount(const char *path, int flags);
|
||||
|
||||
int umount_recursive(const char *target, int flags);
|
||||
int bind_remount_recursive(const char *prefix, bool ro);
|
||||
int bind_remount_recursive(const char *prefix, bool ro, char **blacklist);
|
||||
|
||||
int mount_move_root(const char *path);
|
||||
|
||||
|
||||
+31
-1
@@ -31,14 +31,15 @@
|
||||
#include <unistd.h>
|
||||
#include <utmp.h>
|
||||
|
||||
#include "missing.h"
|
||||
#include "alloc-util.h"
|
||||
#include "fd-util.h"
|
||||
#include "formats-util.h"
|
||||
#include "macro.h"
|
||||
#include "missing.h"
|
||||
#include "parse-util.h"
|
||||
#include "path-util.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
#include "user-util.h"
|
||||
#include "utf8.h"
|
||||
|
||||
@@ -175,6 +176,35 @@ int get_user_creds(
|
||||
return 0;
|
||||
}
|
||||
|
||||
int get_user_creds_clean(
|
||||
const char **username,
|
||||
uid_t *uid, gid_t *gid,
|
||||
const char **home,
|
||||
const char **shell) {
|
||||
|
||||
int r;
|
||||
|
||||
/* Like get_user_creds(), but resets home/shell to NULL if they don't contain anything relevant. */
|
||||
|
||||
r = get_user_creds(username, uid, gid, home, shell);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (shell &&
|
||||
(isempty(*shell) || PATH_IN_SET(*shell,
|
||||
"/bin/nologin",
|
||||
"/sbin/nologin",
|
||||
"/usr/bin/nologin",
|
||||
"/usr/sbin/nologin")))
|
||||
*shell = NULL;
|
||||
|
||||
if (home &&
|
||||
(isempty(*home) || path_equal(*home, "/")))
|
||||
*home = NULL;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int get_group_creds(const char **groupname, gid_t *gid) {
|
||||
struct group *g;
|
||||
gid_t id;
|
||||
|
||||
@@ -40,6 +40,7 @@ char* getlogname_malloc(void);
|
||||
char* getusername_malloc(void);
|
||||
|
||||
int get_user_creds(const char **username, uid_t *uid, gid_t *gid, const char **home, const char **shell);
|
||||
int get_user_creds_clean(const char **username, uid_t *uid, gid_t *gid, const char **home, const char **shell);
|
||||
int get_group_creds(const char **groupname, gid_t *gid);
|
||||
|
||||
char* uid_to_name(uid_t uid);
|
||||
|
||||
Reference in New Issue
Block a user