mirror of
https://gitlab.com/libvirt/libvirt.git
synced 2024-08-26 18:41:17 +00:00
676 lines
15 KiB
C
676 lines
15 KiB
C
/*
|
|
* Copyright (C) 2018 Red Hat, Inc.
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library 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
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library. If not, see
|
|
* <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <config.h>
|
|
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
|
|
#ifdef WITH_SELINUX
|
|
# include <selinux/selinux.h>
|
|
# include <selinux/label.h>
|
|
#endif
|
|
|
|
#include "virmock.h"
|
|
#include "virfile.h"
|
|
#include "virthread.h"
|
|
#include "virhash.h"
|
|
#include "virstring.h"
|
|
#include "viralloc.h"
|
|
#include "qemusecuritytest.h"
|
|
#include "security/security_manager.h"
|
|
#include "virhostuptime.h"
|
|
|
|
#define VIR_FROM_THIS VIR_FROM_NONE
|
|
|
|
/* Okay, here's the deal. The qemusecuritytest calls several
|
|
* virSecurityManager public APIs in order to check if XATTRs
|
|
* work as expected. Therefore there is a lot we have to mock
|
|
* (chown, stat, XATTR APIs, etc.). Since the test won't run as
|
|
* root chown() would fail, therefore we have to keep everything
|
|
* in memory. By default, all files are owned by 1:2 and have a
|
|
* SELinux label.
|
|
* By the way, since there are some cases where real stat needs
|
|
* to be called, the mocked functions are effective only if
|
|
* $ENVVAR is set.
|
|
*/
|
|
|
|
#define DEFAULT_UID 1
|
|
#define DEFAULT_GID 2
|
|
#define DEFAULT_SELINUX_LABEL "system_u:object_r:default_t:s0"
|
|
|
|
|
|
static int (*real_chown)(const char *path, uid_t uid, gid_t gid);
|
|
static int (*real_open)(const char *path, int flags, ...);
|
|
static int (*real_close)(int fd);
|
|
#ifdef WITH_SELINUX
|
|
static int (*real_setfilecon_raw)(const char *path, const char *context);
|
|
static int (*real_getfilecon_raw)(const char *path, char **context);
|
|
#endif
|
|
|
|
|
|
/* Global mutex to avoid races */
|
|
virMutex m = VIR_MUTEX_INITIALIZER;
|
|
|
|
/* Hash table to store XATTRs for paths. For simplicity, key is
|
|
* "$path:$name" and value is just XATTR "$value". We don't need
|
|
* to list XATTRs a path has, therefore we don't need something
|
|
* more clever. */
|
|
GHashTable *xattr_paths = NULL;
|
|
|
|
|
|
/* The UID:GID is stored in a hash table. Again, for simplicity,
|
|
* the path is the key and the value is an uint32_t , where
|
|
* the lower half is UID and the higher is GID. */
|
|
GHashTable *chown_paths = NULL;
|
|
|
|
/* The SELinux label is stored in a hash table. For simplicity,
|
|
* the path is the key and the value is the label. */
|
|
GHashTable *selinux_paths = NULL;
|
|
|
|
|
|
static void
|
|
init_hash(void)
|
|
{
|
|
/* The reason the init is split is that virHash calls
|
|
* virRandomBits() which in turn calls a gnutls function.
|
|
* However, when gnutls is initializing itself it calls
|
|
* stat() so we would call a gnutls function before it is
|
|
* initialized which will lead to a crash.
|
|
*/
|
|
|
|
if (xattr_paths)
|
|
return;
|
|
|
|
if (!(xattr_paths = virHashNew(g_free))) {
|
|
fprintf(stderr, "Unable to create hash table for XATTR paths\n");
|
|
abort();
|
|
}
|
|
|
|
if (!(chown_paths = virHashNew(g_free))) {
|
|
fprintf(stderr, "Unable to create hash table for chowned paths\n");
|
|
abort();
|
|
}
|
|
|
|
if (!(selinux_paths = virHashNew(g_free))) {
|
|
fprintf(stderr, "Unable to create hash table for selinux labels\n");
|
|
abort();
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
init_syms(void)
|
|
{
|
|
if (real_chown)
|
|
return;
|
|
|
|
VIR_MOCK_REAL_INIT(chown);
|
|
VIR_MOCK_REAL_INIT(open);
|
|
VIR_MOCK_REAL_INIT(close);
|
|
#ifdef WITH_SELINUX
|
|
VIR_MOCK_REAL_INIT(setfilecon_raw);
|
|
VIR_MOCK_REAL_INIT(getfilecon_raw);
|
|
#endif
|
|
|
|
/* Intentionally not calling init_hash() here */
|
|
}
|
|
|
|
|
|
static char *
|
|
get_key(const char *path,
|
|
const char *name)
|
|
{
|
|
char *ret;
|
|
|
|
ret = g_strdup_printf("%s:%s", path, name);
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
int
|
|
virFileGetXAttrQuiet(const char *path,
|
|
const char *name,
|
|
char **value)
|
|
{
|
|
int ret = -1;
|
|
g_autofree char *key = NULL;
|
|
char *val;
|
|
|
|
key = get_key(path, name);
|
|
|
|
virMutexLock(&m);
|
|
init_syms();
|
|
init_hash();
|
|
|
|
if (!(val = virHashLookup(xattr_paths, key))) {
|
|
errno = ENODATA;
|
|
goto cleanup;
|
|
}
|
|
|
|
*value = g_strdup(val);
|
|
|
|
ret = 0;
|
|
cleanup:
|
|
virMutexUnlock(&m);
|
|
return ret;
|
|
}
|
|
|
|
|
|
/*
|
|
* This may look redundant but is needed to work around an
|
|
* compiler quirk. The call from the real virFileGetXAttr
|
|
* to the real virFileGetXAttrQuiet has a quirk where the
|
|
* return value from virFileGetXAttrQuiet gets scrambled
|
|
* if we mock virFileGetXAttrQuiet, returning -1 instead
|
|
* of 0 despite succeeding. This happens on FreeBSD 11/12
|
|
* hosts with Clang, and is suspected to be some kind of
|
|
* compiler optimization. By mocking this function too we
|
|
* can workaround it.
|
|
*/
|
|
int
|
|
virFileGetXAttr(const char *path,
|
|
const char *name,
|
|
char **value)
|
|
{
|
|
int ret;
|
|
|
|
if ((ret = virFileGetXAttrQuiet(path, name, value)) < 0) {
|
|
virReportSystemError(errno,
|
|
"Unable to get XATTR %s on %s",
|
|
name, path);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
int virFileSetXAttr(const char *path,
|
|
const char *name,
|
|
const char *value)
|
|
{
|
|
int ret = -1;
|
|
g_autofree char *key = NULL;
|
|
g_autofree char *val = NULL;
|
|
|
|
key = get_key(path, name);
|
|
val = g_strdup(value);
|
|
|
|
virMutexLock(&m);
|
|
init_syms();
|
|
init_hash();
|
|
|
|
if (virHashUpdateEntry(xattr_paths, key, val) < 0)
|
|
goto cleanup;
|
|
val = NULL;
|
|
|
|
ret = 0;
|
|
cleanup:
|
|
virMutexUnlock(&m);
|
|
return ret;
|
|
}
|
|
|
|
|
|
int virFileRemoveXAttr(const char *path,
|
|
const char *name)
|
|
{
|
|
int ret = -1;
|
|
g_autofree char *key = NULL;
|
|
|
|
key = get_key(path, name);
|
|
|
|
virMutexLock(&m);
|
|
init_syms();
|
|
init_hash();
|
|
|
|
if ((ret = virHashRemoveEntry(xattr_paths, key)) < 0)
|
|
errno = ENODATA;
|
|
|
|
virMutexUnlock(&m);
|
|
return ret;
|
|
}
|
|
|
|
|
|
#define VIR_MOCK_STAT_HOOK \
|
|
do { \
|
|
if (getenv(ENVVAR)) { \
|
|
uint32_t *val; \
|
|
\
|
|
virMutexLock(&m); \
|
|
init_hash(); \
|
|
\
|
|
memset(sb, 0, sizeof(*sb)); \
|
|
\
|
|
sb->st_mode = S_IFREG | 0666; \
|
|
sb->st_size = 123456; \
|
|
sb->st_ino = 1; \
|
|
\
|
|
if (!(val = virHashLookup(chown_paths, path))) { \
|
|
/* New path. Set the defaults */ \
|
|
sb->st_uid = DEFAULT_UID; \
|
|
sb->st_gid = DEFAULT_GID; \
|
|
} else { \
|
|
/* Known path. Set values passed to chown() earlier */ \
|
|
sb->st_uid = *val & 0xffff; \
|
|
sb->st_gid = *val >> 16; \
|
|
} \
|
|
\
|
|
virMutexUnlock(&m); \
|
|
\
|
|
return 0; \
|
|
} \
|
|
} while (0)
|
|
|
|
static int
|
|
mock_chown(const char *path,
|
|
uid_t uid,
|
|
gid_t gid)
|
|
{
|
|
uint32_t *val = NULL;
|
|
int ret = -1;
|
|
|
|
if (gid >> 16 || uid >> 16) {
|
|
fprintf(stderr, "Attempt to set too high UID or GID: %llu %llu",
|
|
(unsigned long long) uid, (unsigned long long) gid);
|
|
abort();
|
|
}
|
|
|
|
val = g_new0(uint32_t, 1);
|
|
|
|
*val = (gid << 16) + uid;
|
|
|
|
virMutexLock(&m);
|
|
init_hash();
|
|
|
|
if (virHashUpdateEntry(chown_paths, path, val) < 0)
|
|
goto cleanup;
|
|
val = NULL;
|
|
|
|
ret = 0;
|
|
cleanup:
|
|
virMutexUnlock(&m);
|
|
VIR_FREE(val);
|
|
return ret;
|
|
}
|
|
|
|
|
|
#include "virmockstathelpers.c"
|
|
|
|
static int
|
|
virMockStatRedirect(const char *path G_GNUC_UNUSED, char **newpath G_GNUC_UNUSED)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
|
|
int
|
|
chown(const char *path, uid_t uid, gid_t gid)
|
|
{
|
|
int ret;
|
|
|
|
init_syms();
|
|
|
|
if (getenv(ENVVAR))
|
|
ret = mock_chown(path, uid, gid);
|
|
else
|
|
ret = real_chown(path, uid, gid);
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
int
|
|
open(const char *path, int flags, ...)
|
|
{
|
|
int ret;
|
|
|
|
init_syms();
|
|
|
|
if (getenv(ENVVAR)) {
|
|
ret = 42; /* Some dummy FD */
|
|
} else if (flags & O_CREAT) {
|
|
va_list ap;
|
|
mode_t mode;
|
|
va_start(ap, flags);
|
|
mode = (mode_t) va_arg(ap, int);
|
|
va_end(ap);
|
|
ret = real_open(path, flags, mode);
|
|
} else {
|
|
ret = real_open(path, flags);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
int
|
|
close(int fd)
|
|
{
|
|
int ret;
|
|
|
|
init_syms();
|
|
|
|
if (fd == 42 && getenv(ENVVAR))
|
|
ret = 0;
|
|
else
|
|
ret = real_close(fd);
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
int virFileLock(int fd G_GNUC_UNUSED,
|
|
bool shared G_GNUC_UNUSED,
|
|
off_t start G_GNUC_UNUSED,
|
|
off_t len G_GNUC_UNUSED,
|
|
bool waitForLock G_GNUC_UNUSED)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
|
|
int virFileUnlock(int fd G_GNUC_UNUSED,
|
|
off_t start G_GNUC_UNUSED,
|
|
off_t len G_GNUC_UNUSED)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
|
|
typedef struct _checkOwnerData checkOwnerData;
|
|
struct _checkOwnerData {
|
|
const char **paths;
|
|
bool chown_fail;
|
|
bool selinux_fail;
|
|
};
|
|
|
|
|
|
static int
|
|
checkSELinux(void *payload,
|
|
const char *name,
|
|
void *opaque)
|
|
{
|
|
checkOwnerData *data = opaque;
|
|
char *label = payload;
|
|
|
|
if (STRNEQ(label, DEFAULT_SELINUX_LABEL) &&
|
|
!virStringListHasString(data->paths, name)) {
|
|
fprintf(stderr,
|
|
"Path %s wasn't restored back to its original SELinux label\n",
|
|
name);
|
|
data->selinux_fail = true;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int
|
|
checkOwner(void *payload,
|
|
const char *name,
|
|
void *opaque)
|
|
{
|
|
checkOwnerData *data = opaque;
|
|
uint32_t owner = *((uint32_t*) payload);
|
|
|
|
if ((owner % 16 != DEFAULT_UID ||
|
|
owner >> 16 != DEFAULT_GID) &&
|
|
!virStringListHasString(data->paths, name)) {
|
|
fprintf(stderr,
|
|
"Path %s wasn't restored back to its original owner\n",
|
|
name);
|
|
data->chown_fail = true;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int
|
|
printXATTR(void *payload,
|
|
const char *name,
|
|
void *data)
|
|
{
|
|
bool *xattr_fail = data;
|
|
|
|
/* The fact that we are in this function means that there are
|
|
* some XATTRs left behind. This is enough to claim an error. */
|
|
*xattr_fail = true;
|
|
|
|
/* Hash table key consists of "$path:$xattr_name", xattr
|
|
* value is then the value stored in the hash table. */
|
|
printf("key=%s val=%s\n", name, (const char *) payload);
|
|
return 0;
|
|
}
|
|
|
|
|
|
/**
|
|
* checkPaths:
|
|
* @paths: a NULL terminated list of paths expected not to be restored
|
|
*
|
|
* Check if all paths were restored and if no XATTR was left
|
|
* behind. Since restore is not done on all domain's paths, some
|
|
* paths are expected to be not restored. A list of such paths
|
|
* can be passed in @paths argument. If a path is not restored
|
|
* but it's on the list no error is indicated.
|
|
*/
|
|
int checkPaths(const char **paths)
|
|
{
|
|
int ret = -1;
|
|
checkOwnerData data = { .paths = paths, .chown_fail = false, .selinux_fail = false };
|
|
bool xattr_fail = false;
|
|
size_t i;
|
|
|
|
virMutexLock(&m);
|
|
init_hash();
|
|
|
|
for (i = 0; paths && paths[i]; i++) {
|
|
if (!virHashLookup(chown_paths, paths[i])) {
|
|
fprintf(stderr, "Unexpected path restored: %s\n", paths[i]);
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
if (virHashForEach(selinux_paths, checkSELinux, &data) < 0)
|
|
goto cleanup;
|
|
|
|
if (virHashForEach(chown_paths, checkOwner, &data) < 0)
|
|
goto cleanup;
|
|
|
|
if (virHashForEach(xattr_paths, printXATTR, &xattr_fail) < 0)
|
|
goto cleanup;
|
|
|
|
if (data.chown_fail || data.selinux_fail || xattr_fail)
|
|
goto cleanup;
|
|
|
|
ret = 0;
|
|
cleanup:
|
|
virMutexUnlock(&m);
|
|
return ret;
|
|
}
|
|
|
|
|
|
void freePaths(void)
|
|
{
|
|
virMutexLock(&m);
|
|
init_hash();
|
|
|
|
virHashFree(selinux_paths);
|
|
virHashFree(chown_paths);
|
|
virHashFree(xattr_paths);
|
|
selinux_paths = chown_paths = xattr_paths = NULL;
|
|
virMutexUnlock(&m);
|
|
}
|
|
|
|
|
|
int
|
|
virProcessRunInFork(virProcessForkCallback cb,
|
|
void *opaque)
|
|
{
|
|
return cb(-1, opaque);
|
|
}
|
|
|
|
|
|
/* We don't really need to mock this function. The qemusecuritytest doesn't
|
|
* care about the actual value. However, travis runs build and tests in a
|
|
* container where utmp is missing and thus this function fails. */
|
|
int
|
|
virHostGetBootTime(unsigned long long *when)
|
|
{
|
|
*when = 1234567890;
|
|
return 0;
|
|
}
|
|
|
|
|
|
#ifdef WITH_SELINUX
|
|
int
|
|
is_selinux_enabled(void)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
|
|
struct selabel_handle *
|
|
selabel_open(unsigned int backend G_GNUC_UNUSED,
|
|
const struct selinux_opt *opts G_GNUC_UNUSED,
|
|
unsigned nopts G_GNUC_UNUSED)
|
|
{
|
|
return (void*)((intptr_t) 0x1);
|
|
}
|
|
|
|
|
|
void
|
|
selabel_close(struct selabel_handle *rec G_GNUC_UNUSED)
|
|
{
|
|
/* nada */
|
|
}
|
|
|
|
|
|
const char *
|
|
selinux_virtual_domain_context_path(void)
|
|
{
|
|
return abs_srcdir "/qemusecuritydata/virtual_domain_context";
|
|
}
|
|
|
|
|
|
const char *
|
|
selinux_virtual_image_context_path(void)
|
|
{
|
|
return abs_srcdir "/qemusecuritydata/virtual_image_context";
|
|
}
|
|
|
|
|
|
int getcon_raw(char **context)
|
|
{
|
|
*context = g_strdup("system_u:system_r:virtd_t:s0-s0:c0.c1023");
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int
|
|
mock_setfilecon_raw(const char *path,
|
|
const char *context)
|
|
{
|
|
g_autofree char *val = g_strdup(context);
|
|
int ret = -1;
|
|
|
|
virMutexLock(&m);
|
|
init_hash();
|
|
|
|
if (virHashUpdateEntry(selinux_paths, path, val) < 0)
|
|
goto cleanup;
|
|
val = NULL;
|
|
|
|
ret = 0;
|
|
cleanup:
|
|
virMutexUnlock(&m);
|
|
return ret;
|
|
}
|
|
|
|
|
|
static int
|
|
mock_getfilecon_raw(const char *path,
|
|
char **context)
|
|
{
|
|
const char *val;
|
|
|
|
virMutexLock(&m);
|
|
init_hash();
|
|
|
|
val = virHashLookup(selinux_paths, path);
|
|
if (!val)
|
|
val = DEFAULT_SELINUX_LABEL;
|
|
|
|
*context = g_strdup(val);
|
|
virMutexUnlock(&m);
|
|
return 0;
|
|
}
|
|
|
|
|
|
int
|
|
setfilecon_raw(const char *path,
|
|
const char *context)
|
|
{
|
|
int ret;
|
|
|
|
init_syms();
|
|
|
|
if (getenv(ENVVAR))
|
|
ret = mock_setfilecon_raw(path, context);
|
|
else
|
|
ret = real_setfilecon_raw(path, context);
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
int
|
|
getfilecon_raw(const char *path,
|
|
char **context)
|
|
{
|
|
int ret;
|
|
|
|
init_syms();
|
|
|
|
if (getenv(ENVVAR))
|
|
ret = mock_getfilecon_raw(path, context);
|
|
else
|
|
ret = real_getfilecon_raw(path, context);
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
int
|
|
selabel_lookup_raw(struct selabel_handle *hnd G_GNUC_UNUSED,
|
|
char **context,
|
|
const char *key G_GNUC_UNUSED,
|
|
int type G_GNUC_UNUSED)
|
|
{
|
|
/* This function will be called only if we haven't found original label in
|
|
* XATTRs. Return something else than DEFAULT_SELINUX_LABEL so that it is
|
|
* considered as error. */
|
|
*context = g_strdup("system_u:object_r:default_t:s1");
|
|
return 0;
|
|
}
|
|
#endif
|