diff --git a/configure.in b/configure.in index 27737b43b5..be0fbd064f 100644 --- a/configure.in +++ b/configure.in @@ -799,6 +799,83 @@ fi AM_CONDITIONAL([WITH_SECDRIVER_SELINUX], [test "$with_secdriver_selinux" != "no"]) +dnl AppArmor +AC_ARG_WITH([apparmor], + [ --with-apparmor use AppArmor to manage security], + [], + [with_apparmor=check]) + +APPARMOR_CFLAGS= +APPARMOR_LIBS= +if test "$with_apparmor" != "no"; then + old_cflags="$CFLAGS" + old_libs="$LIBS" + if test "$with_apparmor" = "check"; then + AC_CHECK_HEADER([sys/apparmor.h],[],[with_apparmor=no]) + AC_CHECK_LIB([apparmor], [aa_change_profile],[],[with_apparmor=no]) + AC_CHECK_LIB([apparmor], [aa_change_hat],[],[with_apparmor=no]) + if test "$with_apparmor" != "no"; then + with_apparmor="yes" + fi + else + fail=0 + AC_CHECK_HEADER([sys/apparmor.h],[],[fail=1]) + AC_CHECK_LIB([apparmor], [aa_change_profile],[],[fail=1]) + AC_CHECK_LIB([apparmor], [aa_change_hat],[],[fail=1]) + test $fail = 1 && + AC_MSG_ERROR([You must install the AppArmor development package in order to compile libvirt]) + fi + CFLAGS="$old_cflags" + LIBS="$old_libs" +fi +if test "$with_apparmor" = "yes"; then + APPARMOR_LIBS="-lapparmor" + AC_DEFINE_UNQUOTED([HAVE_APPARMOR], 1, [whether AppArmor is available for security]) + AC_DEFINE_UNQUOTED([APPARMOR_DIR], "/etc/apparmor.d", [path to apparmor directory]) + AC_DEFINE_UNQUOTED([APPARMOR_PROFILES_PATH], "/sys/kernel/security/apparmor/profiles", [path to kernel profiles]) +fi +AM_CONDITIONAL([HAVE_APPARMOR], [test "$with_apparmor" != "no"]) +AC_SUBST([APPARMOR_CFLAGS]) +AC_SUBST([APPARMOR_LIBS]) + + +AC_ARG_WITH([secdriver-apparmor], + [ --with-secdriver-apparmor use AppArmor security driver], + [], + [with_secdriver_apparmor=check]) + +if test "$with_apparmor" != "yes" ; then + if test "$with_secdriver_apparmor" = "check" ; then + with_secdriver_apparmor=no + else + AC_MSG_ERROR([You must install the AppArmor development package in order to compile libvirt]) + fi +else + old_cflags="$CFLAGS" + old_libs="$LIBS" + CFLAGS="$CFLAGS $APPARMOR_CFLAGS" + LIBS="$CFLAGS $APPARMOR_LIBS" + + fail=0 + AC_CHECK_FUNC([change_hat], [], [fail=1]) + AC_CHECK_FUNC([aa_change_profile], [], [fail=1]) + CFLAGS="$old_cflags" + LIBS="$old_libs" + + if test "$fail" = "1" ; then + if test "$with_secdriver_apparmor" = "check" ; then + with_secdriver_apparmor=no + else + AC_MSG_ERROR([You must install the AppArmor development package in order to compile libvirt]) + fi + else + with_secdriver_apparmor=yes + AC_DEFINE_UNQUOTED([WITH_SECDRIVER_APPARMOR], 1, [whether AppArmor security driver is available]) + fi +fi +AM_CONDITIONAL([WITH_SECDRIVER_APPARMOR], [test "$with_secdriver_apparmor" != "no"]) + + dnl NUMA lib AC_ARG_WITH([numactl], @@ -1745,6 +1822,7 @@ AC_MSG_NOTICE([]) AC_MSG_NOTICE([Security Drivers]) AC_MSG_NOTICE([]) AC_MSG_NOTICE([ SELinux: $with_secdriver_selinux]) +AC_MSG_NOTICE([ AppArmor: $with_secdriver_apparmor]) AC_MSG_NOTICE([]) AC_MSG_NOTICE([Driver Loadable Modules]) AC_MSG_NOTICE([]) @@ -1792,6 +1870,11 @@ AC_MSG_NOTICE([ selinux: $SELINUX_CFLAGS $SELINUX_LIBS]) else AC_MSG_NOTICE([ selinux: no]) fi +if test "$with_apparmor" = "yes" ; then +AC_MSG_NOTICE([ apparmor: $APPARMOR_CFLAGS $APPARMOR_LIBS]) +else +AC_MSG_NOTICE([ apparmor: no]) +fi if test "$with_numactl" = "yes" ; then AC_MSG_NOTICE([ numactl: $NUMACTL_CFLAGS $NUMACTL_LIBS]) else diff --git a/po/POTFILES.in b/po/POTFILES.in index df935a2837..1a12a39e6f 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -30,7 +30,9 @@ src/qemu/qemu_monitor_text.c src/remote/remote_driver.c src/secret/secret_driver.c src/security/security_driver.c +src/security/security_apparmor.c src/security/security_selinux.c +src/security/virt-aa-helper.c src/storage/storage_backend.c src/storage/storage_backend_disk.c src/storage/storage_backend_fs.c diff --git a/src/Makefile.am b/src/Makefile.am index f3d4559c34..26ffafffa1 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -153,6 +153,9 @@ LXC_CONTROLLER_SOURCES = \ lxc/lxc_controller.c \ lxc/veth.c lxc/veth.h +SECURITY_DRIVER_APPARMOR_HELPER_SOURCES = \ + security/virt-aa-helper.c + PHYP_DRIVER_SOURCES = \ phyp/phyp_driver.c phyp/phyp_driver.h @@ -238,6 +241,9 @@ SECURITY_DRIVER_SOURCES = \ SECURITY_DRIVER_SELINUX_SOURCES = \ security/security_selinux.h security/security_selinux.c +SECURITY_DRIVER_APPARMOR_SOURCES = \ + security/security_apparmor.h security/security_apparmor.c + NODE_DEVICE_DRIVER_SOURCES = \ node_device/node_device_driver.c node_device/node_device_driver.h @@ -641,9 +647,15 @@ noinst_LTLIBRARIES += libvirt_driver_security.la libvirt_la_LIBADD += libvirt_driver_security.la libvirt_driver_security_la_CFLAGS = \ -I@top_srcdir@/src/conf +libvirt_driver_security_la_LDFLAGS = if WITH_SECDRIVER_SELINUX libvirt_driver_security_la_SOURCES += $(SECURITY_DRIVER_SELINUX_SOURCES) endif +if WITH_SECDRIVER_APPARMOR +libvirt_driver_security_la_SOURCES += $(SECURITY_DRIVER_APPARMOR_SOURCES) +libvirt_driver_security_la_CFLAGS += $(APPARMOR_CFLAGS) +libvirt_driver_security_la_LDFLAGS += $(APPARMOR_LIBS) +endif # Add all conditional sources just in case... EXTRA_DIST += \ @@ -671,6 +683,7 @@ EXTRA_DIST += \ $(NODE_DEVICE_DRIVER_HAL_SOURCES) \ $(NODE_DEVICE_DRIVER_DEVKIT_SOURCES) \ $(SECURITY_DRIVER_SELINUX_SOURCES) \ + $(SECURITY_DRIVER_APPARMOR_SOURCES) \ $(SECRET_DRIVER_SOURCES) \ $(VBOX_DRIVER_EXTRA_DIST) @@ -795,6 +808,26 @@ endif endif EXTRA_DIST += $(LXC_CONTROLLER_SOURCES) +if WITH_SECDRIVER_APPARMOR +if WITH_LIBVIRTD +libexec_PROGRAMS += virt-aa-helper + +virt_aa_helper_SOURCES = $(SECURITY_DRIVER_APPARMOR_HELPER_SOURCES) + +virt_aa_helper_LDFLAGS = $(WARN_CFLAGS) +virt_aa_helper_LDADD = \ + $(WARN_CFLAGS) \ + $(LIBXML_LIBS) \ + @top_srcdir@/gnulib/lib/libgnu.la \ + @top_srcdir@/src/libvirt_conf.la \ + @top_srcdir@/src/libvirt_util.la +virt_aa_helper_CFLAGS = \ + -I@top_srcdir@/src/conf \ + -I@top_srcdir@/src/security +endif +endif +EXTRA_DIST += $(SECURITY_DRIVER_APPARMOR_HELPER_SOURCES) + install-data-local: $(MKDIR_P) "$(DESTDIR)$(localstatedir)/cache/libvirt" $(MKDIR_P) "$(DESTDIR)$(localstatedir)/lib/libvirt/images" diff --git a/src/security/security_apparmor.c b/src/security/security_apparmor.c new file mode 100644 index 0000000000..16de0f26f4 --- /dev/null +++ b/src/security/security_apparmor.c @@ -0,0 +1,607 @@ + +/* + * AppArmor security driver for libvirt + * Copyright (C) 2009 Canonical Ltd. + * + * 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. + * + * Author: + * Jamie Strandboge + * Based on security_selinux.c by James Morris + * + * AppArmor security driver. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "internal.h" + +#include "security_driver.h" +#include "security_apparmor.h" +#include "util.h" +#include "memory.h" +#include "virterror_internal.h" +#include "datatypes.h" +#include "uuid.h" + +#define VIR_FROM_THIS VIR_FROM_SECURITY +#define SECURITY_APPARMOR_VOID_DOI "0" +#define SECURITY_APPARMOR_NAME "apparmor" +#define VIRT_AA_HELPER BINDIR "/virt-aa-helper" + +/* + * profile_status returns '-1' on error, '0' if loaded + * + * If check_enforcing is set to '1', then returns '-1' on error, '0' if + * loaded in complain mode, and '1' if loaded in enforcing mode. + */ +static int +profile_status(const char *str, const int check_enforcing) +{ + char *content = NULL; + char *tmp = NULL; + char *etmp = NULL; + int rc = -1; + + /* create string that is ' \0' for accurate matching */ + if (virAsprintf(&tmp, "%s ", str) == -1) + return rc; + + if (check_enforcing != 0) { + /* create string that is ' (enforce)\0' for accurate matching */ + if (virAsprintf(&etmp, "%s (enforce)", str) == -1) { + VIR_FREE(tmp); + return rc; + } + } + + if (virFileReadAll(APPARMOR_PROFILES_PATH, MAX_FILE_LEN, &content) < 0) { + virReportSystemError(NULL, errno, + _("Failed to read AppArmor profiles list " + "\'%s\'"), APPARMOR_PROFILES_PATH); + if (check_enforcing != 0) + VIR_FREE(etmp); + goto clean; + } + + if (strstr(content, tmp) != NULL) + rc = 0; + if (check_enforcing != 0) { + if (rc == 0 && strstr(content, etmp) != NULL) + rc = 1; /* return '1' if loaded and enforcing */ + VIR_FREE(etmp); + } + + VIR_FREE(content); + clean: + VIR_FREE(tmp); + + return rc; +} + +static int +profile_loaded(const char *str) +{ + return profile_status(str, 0); +} + +/* + * profile_status_file returns '-1' on error, '0' if file on disk is in + * complain mode and '1' if file on disk is in enforcing mode + */ +static int +profile_status_file(const char *str) +{ + char profile[PATH_MAX]; + char *content = NULL; + char *tmp = NULL; + int rc = -1; + int len; + + if (snprintf(profile, PATH_MAX, "%s/%s", APPARMOR_DIR "/libvirt", str) + > PATH_MAX - 1) { + virSecurityReportError(NULL, VIR_ERR_ERROR, + "%s", _("profile name exceeds maximum length")); + } + + if (!virFileExists(profile)) { + return rc; + } + + if ((len = virFileReadAll(profile, MAX_FILE_LEN, &content)) < 0) { + virReportSystemError(NULL, errno, + _("Failed to read \'%s\'"), profile); + return rc; + } + + /* create string that is ' flags=(complain)\0' */ + if (virAsprintf(&tmp, " %s flags=(complain)", str) == -1) { + virReportOOMError(NULL); + goto clean; + } + + if (strstr(content, tmp) != NULL) + rc = 0; + else + rc = 1; + + VIR_FREE(tmp); + clean: + VIR_FREE(content); + + return rc; +} + +/* + * load (add) a profile. Will create one if necessary + */ +static int +load_profile(virConnectPtr conn, const char *profile, virDomainObjPtr vm, + virDomainDiskDefPtr disk) +{ + int rc = -1, status, ret; + bool create = true; + char *xml = NULL; + int pipefd[2]; + pid_t child; + + if (pipe(pipefd) < -1) { + virReportSystemError(conn, errno, "%s", _("unable to create pipe")); + return rc; + } + + xml = virDomainDefFormat(conn, vm->def, VIR_DOMAIN_XML_SECURE); + if (!xml) + goto failed; + + if (profile_status_file(profile) >= 0) + create = false; + + if (create) { + const char *const argv[] = { + VIRT_AA_HELPER, "-c", "-u", profile, NULL + }; + ret = virExec(conn, argv, NULL, NULL, &child, + pipefd[0], NULL, NULL, VIR_EXEC_CLEAR_CAPS); + } else if (disk && disk->src) { + const char *const argv[] = { + VIRT_AA_HELPER, "-r", "-u", profile, "-f", disk->src, NULL + }; + ret = virExec(conn, argv, NULL, NULL, &child, + pipefd[0], NULL, NULL, VIR_EXEC_CLEAR_CAPS); + } else { + const char *const argv[] = { + VIRT_AA_HELPER, "-r", "-u", profile, NULL + }; + ret = virExec(conn, argv, NULL, NULL, &child, + pipefd[0], NULL, NULL, VIR_EXEC_CLEAR_CAPS); + } + if (ret < 0) + goto clean; + + /* parent continues here */ + if (safewrite(pipefd[1], xml, strlen(xml)) < 0) { + virReportSystemError(conn, errno, "%s", _("unable to write to pipe")); + goto clean; + } + close(pipefd[1]); + rc = 0; + + rewait: + if (waitpid(child, &status, 0) != child) { + if (errno == EINTR) + goto rewait; + + virSecurityReportError(conn, VIR_ERR_ERROR, + _("Unexpected exit status from virt-aa-helper " + "%d pid %lu"), + WEXITSTATUS(status), (unsigned long)child); + rc = -1; + } + + clean: + VIR_FREE(xml); + + failed: + if (pipefd[0] > 0) + close(pipefd[0]); + if (pipefd[1] > 0) + close(pipefd[1]); + + return rc; +} + +static int +remove_profile(const char *profile) +{ + int rc = -1; + const char * const argv[] = { + VIRT_AA_HELPER, "-R", "-u", profile, NULL + }; + + if (virRun(NULL, argv, NULL) == 0) + rc = 0; + + return rc; +} + +static char * +get_profile_name(virConnectPtr conn, virDomainObjPtr vm) +{ + char uuidstr[VIR_UUID_STRING_BUFLEN]; + char *name = NULL; + + virUUIDFormat(vm->def->uuid, uuidstr); + if (virAsprintf(&name, "%s%s", AA_PREFIX, uuidstr) < 0) { + virReportOOMError(conn); + return NULL; + } + + return name; +} + +/* returns -1 on error or profile for libvirtd is unconfined, 0 if complain + * mode and 1 if enforcing. This is required because at present you cannot + * aa_change_profile() from a process that is unconfined. + */ +static int +use_apparmor(void) +{ + char libvirt_daemon[PATH_MAX]; + int rc = -1; + ssize_t len = 0; + + if ((len = readlink("/proc/self/exe", libvirt_daemon, + PATH_MAX - 1)) < 0) { + virSecurityReportError(NULL, VIR_ERR_ERROR, + "%s", _("could not find libvirtd")); + return rc; + } + libvirt_daemon[len] = '\0'; + + if (access(APPARMOR_PROFILES_PATH, R_OK) != 0) + return rc; + + return profile_status(libvirt_daemon, 1); +} + +/* Called on libvirtd startup to see if AppArmor is available */ +static int +AppArmorSecurityDriverProbe(void) +{ + char template[PATH_MAX]; + + if (use_apparmor() < 0) + return SECURITY_DRIVER_DISABLE; + + /* see if template file exists */ + if (snprintf(template, PATH_MAX, "%s/TEMPLATE", + APPARMOR_DIR "/libvirt") > PATH_MAX - 1) { + virSecurityReportError(NULL, VIR_ERR_ERROR, + "%s", _("template too large")); + return SECURITY_DRIVER_DISABLE; + } + + if (!virFileExists(template)) { + virSecurityReportError(NULL, VIR_ERR_ERROR, + _("template \'%s\' does not exist"), template); + return SECURITY_DRIVER_DISABLE; + } + + return SECURITY_DRIVER_ENABLE; +} + +/* Security driver initialization. DOI is for 'Domain of Interpretation' and is + * currently not used. + */ +static int +AppArmorSecurityDriverOpen(virConnectPtr conn, virSecurityDriverPtr drv) +{ + virSecurityDriverSetDOI(conn, drv, SECURITY_APPARMOR_VOID_DOI); + return 0; +} + +/* Currently called in qemudStartVMDaemon to setup a 'label'. We look for and + * use a profile based on the UUID, otherwise create one based on a template. + * Keep in mind that this is called on 'start' with RestoreSecurityLabel being + * called on shutdown. +*/ +static int +AppArmorGenSecurityLabel(virConnectPtr conn, virDomainObjPtr vm) +{ + int rc = -1; + char *profile_name = NULL; + + if ((vm->def->seclabel.label) || + (vm->def->seclabel.model) || (vm->def->seclabel.imagelabel)) { + virSecurityReportError(conn, VIR_ERR_ERROR, + "%s", + _("security label already defined for VM")); + return rc; + } + + if ((profile_name = get_profile_name(conn, vm)) == NULL) + return rc; + + /* if the profile is not already loaded, then load one */ + if (profile_loaded(profile_name) < 0) { + if (load_profile(conn, profile_name, vm, NULL) < 0) { + virSecurityReportError(conn, VIR_ERR_ERROR, + _("cannot generate AppArmor profile " + "\'%s\'"), profile_name); + goto clean; + } + } + + vm->def->seclabel.label = strndup(profile_name, strlen(profile_name)); + if (!vm->def->seclabel.label) { + virReportOOMError(NULL); + goto clean; + } + + /* set imagelabel the same as label (but we won't use it) */ + vm->def->seclabel.imagelabel = strndup(profile_name, + strlen(profile_name)); + if (!vm->def->seclabel.imagelabel) { + virReportOOMError(NULL); + goto err; + } + + vm->def->seclabel.model = strdup(SECURITY_APPARMOR_NAME); + if (!vm->def->seclabel.model) { + virReportOOMError(conn); + goto err; + } + + rc = 0; + goto clean; + + err: + remove_profile(profile_name); + VIR_FREE(vm->def->seclabel.label); + VIR_FREE(vm->def->seclabel.imagelabel); + VIR_FREE(vm->def->seclabel.model); + + clean: + VIR_FREE(profile_name); + + return rc; +} + +/* Seen with 'virsh dominfo '. This function only called if the VM is + * running. + */ +static int +AppArmorGetSecurityLabel(virConnectPtr conn, + virDomainObjPtr vm, virSecurityLabelPtr sec) +{ + int rc = -1; + char *profile_name = NULL; + + if ((profile_name = get_profile_name(conn, vm)) == NULL) + return rc; + + if (virStrcpy(sec->label, profile_name, + VIR_SECURITY_LABEL_BUFLEN) == NULL) { + virSecurityReportError(conn, VIR_ERR_ERROR, + "%s", _("error copying profile name")); + goto clean; + } + + if ((sec->enforcing = profile_status(profile_name, 1)) < 0) { + virSecurityReportError(conn, VIR_ERR_ERROR, + "%s", _("error calling profile_status()")); + goto clean; + } + rc = 0; + + clean: + VIR_FREE(profile_name); + + return rc; +} + +/* Called on VM shutdown and destroy. See AppArmorGenSecurityLabel (above) for + * more details. Currently called via qemudShutdownVMDaemon. + */ +static int +AppArmorRestoreSecurityLabel(virConnectPtr conn, virDomainObjPtr vm) +{ + const virSecurityLabelDefPtr secdef = &vm->def->seclabel; + int rc = 0; + + if (secdef->imagelabel) { + if ((rc = remove_profile(secdef->label)) != 0) { + virSecurityReportError(conn, VIR_ERR_ERROR, + _("could not remove profile for \'%s\'"), + secdef->label); + } + VIR_FREE(secdef->model); + VIR_FREE(secdef->label); + VIR_FREE(secdef->imagelabel); + } + return rc; +} + +/* Called via virExecWithHook. Output goes to + * LOCAL_STATE_DIR/log/libvirt/qemu/.log + */ +static int +AppArmorSetSecurityLabel(virConnectPtr conn, + virSecurityDriverPtr drv, virDomainObjPtr vm) +{ + const virSecurityLabelDefPtr secdef = &vm->def->seclabel; + int rc = -1; + char *profile_name = NULL; + + if ((profile_name = get_profile_name(conn, vm)) == NULL) + return rc; + + if (STRNEQ(drv->name, secdef->model)) { + virSecurityReportError(conn, VIR_ERR_ERROR, + _("security label driver mismatch: " + "\'%s\' model configured for domain, but " + "hypervisor driver is \'%s\'."), + secdef->model, drv->name); + if (use_apparmor() > 0) + goto clean; + } + + if (aa_change_profile(profile_name) < 0) { + virSecurityReportError(conn, VIR_ERR_ERROR, + _("error calling aa_change_profile()")); + goto clean; + } + rc = 0; + + clean: + VIR_FREE(profile_name); + + return rc; +} + + +/* Called when hotplugging */ +static int +AppArmorRestoreSecurityImageLabel(virConnectPtr conn, + virDomainObjPtr vm, + virDomainDiskDefPtr disk ATTRIBUTE_UNUSED) +{ + const virSecurityLabelDefPtr secdef = &vm->def->seclabel; + int rc = -1; + char *profile_name = NULL; + + if (secdef->imagelabel) { + if ((profile_name = get_profile_name(conn, vm)) == NULL) + return rc; + + /* Update the profile only if it is loaded */ + if (profile_loaded(secdef->imagelabel) >= 0) { + if (load_profile(conn, secdef->imagelabel, vm, NULL) < 0) { + virSecurityReportError(conn, VIR_ERR_ERROR, + _("cannot update AppArmor profile " + "\'%s\'"), + secdef->imagelabel); + goto clean; + } + } + } + rc = 0; + clean: + VIR_FREE(profile_name); + + return rc; +} + +/* Called when hotplugging */ +static int +AppArmorSetSecurityImageLabel(virConnectPtr conn, + virDomainObjPtr vm, virDomainDiskDefPtr disk) +{ + const virSecurityLabelDefPtr secdef = &vm->def->seclabel; + int rc = -1; + char *profile_name; + + if (!disk->src) + return 0; + + if (secdef->imagelabel) { + /* if the device doesn't exist, error out */ + if (!virFileExists(disk->src)) { + virSecurityReportError(conn, VIR_ERR_ERROR, + _("\'%s\' does not exist"), disk->src); + return rc; + } + + if ((profile_name = get_profile_name(conn, vm)) == NULL) + return rc; + + /* update the profile only if it is loaded */ + if (profile_loaded(secdef->imagelabel) >= 0) { + if (load_profile(conn, secdef->imagelabel, vm, disk) < 0) { + virSecurityReportError(conn, VIR_ERR_ERROR, + _("cannot update AppArmor profile " + "\'%s\'"), + secdef->imagelabel); + goto clean; + } + } + } + rc = 0; + + clean: + VIR_FREE(profile_name); + + return rc; +} + +static int +AppArmorSecurityVerify(virConnectPtr conn, virDomainDefPtr def) +{ + const virSecurityLabelDefPtr secdef = &def->seclabel; + + if (secdef->type == VIR_DOMAIN_SECLABEL_STATIC) { + if (use_apparmor() < 0 || profile_status(secdef->label, 0) < 0) { + virSecurityReportError(conn, VIR_ERR_XML_ERROR, + _("Invalid security label \'%s\'"), + secdef->label); + return -1; + } + } + return 0; +} + +static int +AppArmorReserveSecurityLabel(virConnectPtr conn ATTRIBUTE_UNUSED, + virDomainObjPtr vm ATTRIBUTE_UNUSED) +{ + /* NOOP. Nothing to reserve with AppArmor */ + return 0; +} + +static int +AppArmorSetSecurityHostdevLabel(virConnectPtr conn ATTRIBUTE_UNUSED, + virDomainObjPtr vm ATTRIBUTE_UNUSED, + virDomainHostdevDefPtr dev ATTRIBUTE_UNUSED) + +{ + /* TODO: call load_profile with an update vm->def */ + return 0; +} + +static int +AppArmorRestoreSecurityHostdevLabel(virConnectPtr conn ATTRIBUTE_UNUSED, + virDomainHostdevDefPtr dev ATTRIBUTE_UNUSED) + +{ + /* TODO: call load_profile (needs virDomainObjPtr vm) */ + return 0; +} + +virSecurityDriver virAppArmorSecurityDriver = { + .name = SECURITY_APPARMOR_NAME, + .probe = AppArmorSecurityDriverProbe, + .open = AppArmorSecurityDriverOpen, + .domainSecurityVerify = AppArmorSecurityVerify, + .domainSetSecurityImageLabel = AppArmorSetSecurityImageLabel, + .domainRestoreSecurityImageLabel = AppArmorRestoreSecurityImageLabel, + .domainGenSecurityLabel = AppArmorGenSecurityLabel, + .domainReserveSecurityLabel = AppArmorReserveSecurityLabel, + .domainGetSecurityLabel = AppArmorGetSecurityLabel, + .domainRestoreSecurityLabel = AppArmorRestoreSecurityLabel, + .domainSetSecurityLabel = AppArmorSetSecurityLabel, + .domainSetSecurityHostdevLabel = AppArmorSetSecurityHostdevLabel, + .domainRestoreSecurityHostdevLabel = AppArmorRestoreSecurityHostdevLabel, +}; diff --git a/src/security/security_apparmor.h b/src/security/security_apparmor.h new file mode 100644 index 0000000000..6d431da82b --- /dev/null +++ b/src/security/security_apparmor.h @@ -0,0 +1,23 @@ + +/* + * Copyright (C) 2009 Canonical Ltd. + * + * 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. + * + * Author: + * Jamie Strandboge + * + */ +#ifndef __VIR_SECURITY_APPARMOR_H__ +#define __VIR_SECURITY_APPARMOR_H__ + +extern virSecurityDriver virAppArmorSecurityDriver; + +#define AA_PREFIX "libvirt-" +#define PROFILE_NAME_SIZE 8 + VIR_UUID_STRING_BUFLEN /* AA_PREFIX + uuid */ +#define MAX_FILE_LEN (1024*1024*10) /* 10MB limit for sanity check */ + +#endif /* __VIR_SECURITY_APPARMOR_H__ */ diff --git a/src/security/security_driver.c b/src/security/security_driver.c index f94a2e2473..43d52b18a0 100644 --- a/src/security/security_driver.c +++ b/src/security/security_driver.c @@ -20,9 +20,16 @@ #include "security_selinux.h" #endif +#ifdef WITH_SECDRIVER_APPARMOR +#include "security_apparmor.h" +#endif + static virSecurityDriverPtr security_drivers[] = { #ifdef WITH_SECDRIVER_SELINUX &virSELinuxSecurityDriver, +#endif +#ifdef WITH_SECDRIVER_APPARMOR + &virAppArmorSecurityDriver, #endif NULL }; diff --git a/src/security/virt-aa-helper.c b/src/security/virt-aa-helper.c new file mode 100644 index 0000000000..4d05b094f5 --- /dev/null +++ b/src/security/virt-aa-helper.c @@ -0,0 +1,1005 @@ + +/* + * virt-aa-helper: wrapper program used by AppArmor security driver. + * Copyright (C) 2009 Canonical Ltd. + * + * See COPYING.LIB for the License of this software + * + * Author: + * Jamie Strandboge + * + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "internal.h" +#include "buf.h" +#include "util.h" +#include "memory.h" + +#include "security_driver.h" +#include "security_apparmor.h" +#include "domain_conf.h" +#include "xml.h" +#include "uuid.h" +#include "hostusb.h" +#include "pci.h" + +static char *progname; + +typedef struct { + char uuid[PROFILE_NAME_SIZE]; /* UUID of vm */ + bool dryrun; /* dry run */ + char cmd; /* 'c' create + * 'a' add (load) + * 'r' replace + * 'R' remove */ + char *files; /* list of files */ + virDomainDefPtr def; /* VM definition */ + virCapsPtr caps; /* VM capabilities */ + char *hvm; /* type of hypervisor (eg hvm, xen) */ + int bits; /* bits in the guest */ + char *newdisk; /* newly added disk */ +} vahControl; + +static int +vahDeinit(vahControl * ctl) +{ + if (ctl == NULL) + return -1; + + VIR_FREE(ctl->def); + if (ctl->caps) + virCapabilitiesFree(ctl->caps); + free(ctl->files); + free(ctl->hvm); + free(ctl->newdisk); + + return 0; +} + +/* + * Print usage + */ +static void +vah_usage(void) +{ + fprintf(stdout, "\n%s [options] [< def.xml]\n\n" + " Options:\n" + " -a | --add load profile\n" + " -c | --create create profile from template\n" + " -D | --delete unload and delete profile\n" + " -r | --replace reload profile\n" + " -R | --remove unload profile\n" + " -h | --help this help\n" + " -u | --uuid uuid (profile name)\n" + " -H | --hvm hypervisor type\n" + " -b | --bits architecture bits\n" + "\n", progname); + + fprintf(stdout, "This command is intended to be used by libvirtd " + "and not used directly.\n"); + return; +} + +static void +vah_error(vahControl * ctl, int doexit, const char *str) +{ + fprintf(stderr, _("%s: error: %s\n"), progname, str); + + if (doexit) { + if (ctl != NULL) + vahDeinit(ctl); + exit(EXIT_FAILURE); + } +} + +static void +vah_warning(const char *str) +{ + fprintf(stderr, _("%s: warning: %s\n"), progname, str); +} + +static void +vah_info(const char *str) +{ + fprintf(stderr, _("%s:\n%s\n"), progname, str); +} + +/* + * Replace @oldstr in @orig with @repstr + * @len is number of bytes allocated for @orig. Assumes @orig, @oldstr and + * @repstr are null terminated + */ +static int +replace_string(char *orig, const size_t len, const char *oldstr, + const char *repstr) +{ + int idx; + char *pos = NULL; + char *tmp = NULL; + + if ((pos = strstr(orig, oldstr)) == NULL) { + vah_error(NULL, 0, "could not find replacement string"); + return -1; + } + + if (VIR_ALLOC_N(tmp, len) < 0) { + vah_error(NULL, 0, "could not allocate memory for string"); + return -1; + } + tmp[0] = '\0'; + + idx = abs(pos - orig); + + /* copy everything up to oldstr */ + strncat(tmp, orig, idx); + + /* add the replacement string */ + if (strlen(tmp) + strlen(repstr) > len - 1) { + vah_error(NULL, 0, "not enough space in target buffer"); + VIR_FREE(tmp); + return -1; + } + strcat(tmp, repstr); + + /* add everything after oldstr */ + if (strlen(tmp) + strlen(orig) - (idx + strlen(oldstr)) > len - 1) { + vah_error(NULL, 0, "not enough space in target buffer"); + VIR_FREE(tmp); + return -1; + } + strncat(tmp, orig + idx + strlen(oldstr), + strlen(orig) - (idx + strlen(oldstr))); + + if (virStrcpy(orig, tmp, len) == NULL) { + vah_error(NULL, 0, "error replacing string"); + VIR_FREE(tmp); + return -1; + } + VIR_FREE(tmp); + + return 0; +} + +/* + * run an apparmor_parser command + */ +static int +parserCommand(const char *profile_name, const char cmd) +{ + char flag[3]; + char profile[PATH_MAX]; + + if (strchr("arR", cmd) == NULL) { + vah_error(NULL, 0, "invalid flag"); + return -1; + } + + snprintf(flag, 3, "-%c", cmd); + + if (snprintf(profile, PATH_MAX, "%s/%s", + APPARMOR_DIR "/libvirt", profile_name) > PATH_MAX - 1) { + vah_error(NULL, 0, "profile name exceeds maximum length"); + return -1; + } + + if (!virFileExists(profile)) { + vah_error(NULL, 0, "profile does not exist"); + return -1; + } else { + const char * const argv[] = { + "/sbin/apparmor_parser", flag, profile, NULL + }; + if (virRun(NULL, argv, NULL) != 0) { + vah_error(NULL, 0, "failed to run apparmor_parser"); + return -1; + } + } + + return 0; +} + +/* + * Update the dynamic files + */ +static int +update_include_file(const char *include_file, const char *included_files) +{ + int rc = -1; + int plen; + int fd; + char *pcontent = NULL; + const char *warning = + "# DO NOT EDIT THIS FILE DIRECTLY. IT IS MANAGED BY LIBVIRT.\n"; + + if (virAsprintf(&pcontent, "%s%s", warning, included_files) == -1) { + vah_error(NULL, 0, "could not allocate memory for profile"); + return rc; + } + + plen = strlen(pcontent); + if (plen > MAX_FILE_LEN) { + vah_error(NULL, 0, "invalid length for new profile"); + goto clean; + } + + /* only update the disk profile if it is different */ + if (virFileExists(include_file)) { + char *existing = NULL; + int flen = virFileReadAll(include_file, MAX_FILE_LEN, &existing); + if (flen < 0) + goto clean; + + if (flen == plen) { + if (STREQLEN(existing, pcontent, plen)) { + rc = 0; + VIR_FREE(existing); + goto clean; + } + } + VIR_FREE(existing); + } + + /* write the file */ + if ((fd = open(include_file, O_CREAT | O_TRUNC | O_WRONLY, 0644)) == -1) { + vah_error(NULL, 0, "failed to create include file"); + goto clean; + } + + if (safewrite(fd, pcontent, plen) < 0) { /* don't write the '\0' */ + close(fd); + vah_error(NULL, 0, "failed to write to profile"); + goto clean; + } + + if (close(fd) != 0) { + vah_error(NULL, 0, "failed to close or write to profile"); + goto clean; + } + rc = 0; + + clean: + VIR_FREE(pcontent); + + return rc; +} + +/* + * Create a profile based on a template + */ +static int +create_profile(const char *profile, const char *profile_name, + const char *profile_files) +{ + char template[PATH_MAX]; + char *tcontent = NULL; + char *pcontent = NULL; + char *replace_name = NULL; + char *replace_files = NULL; + const char *template_name = "\nprofile LIBVIRT_TEMPLATE"; + const char *template_end = "\n}"; + int tlen, plen; + int fd; + int rc = -1; + + if (virFileExists(profile)) { + vah_error(NULL, 0, "profile exists"); + goto end; + } + + if (snprintf(template, PATH_MAX, "%s/TEMPLATE", + APPARMOR_DIR "/libvirt") > PATH_MAX - 1) { + vah_error(NULL, 0, "template name exceeds maximum length"); + goto end; + } + + if (!virFileExists(template)) { + vah_error(NULL, 0, "template does not exist"); + goto end; + } + + if ((tlen = virFileReadAll(template, MAX_FILE_LEN, &tcontent)) < 0) { + vah_error(NULL, 0, "failed to read AppArmor template"); + goto end; + } + + if (strstr(tcontent, template_name) == NULL) { + vah_error(NULL, 0, "no replacement string in template"); + goto clean_tcontent; + } + + if (strstr(tcontent, template_end) == NULL) { + vah_error(NULL, 0, "no replacement string in template"); + goto clean_tcontent; + } + + /* '\nprofile \0' */ + if (virAsprintf(&replace_name, "\nprofile %s", profile_name) == -1) { + vah_error(NULL, 0, "could not allocate memory for profile name"); + goto clean_tcontent; + } + + /* '\n\n}\0' */ + if (virAsprintf(&replace_files, "\n%s\n}", profile_files) == -1) { + vah_error(NULL, 0, "could not allocate memory for profile files"); + VIR_FREE(replace_name); + goto clean_tcontent; + } + + plen = tlen + strlen(replace_name) - strlen(template_name) + + strlen(replace_files) - strlen(template_end) + 1; + if (plen > MAX_FILE_LEN || plen < tlen) { + vah_error(NULL, 0, "invalid length for new profile"); + goto clean_replace; + } + + if (VIR_ALLOC_N(pcontent, plen) < 0) { + vah_error(NULL, 0, "could not allocate memory for profile"); + goto clean_replace; + } + pcontent[0] = '\0'; + strcpy(pcontent, tcontent); + + if (replace_string(pcontent, plen, template_name, replace_name) < 0) + goto clean_all; + + if (replace_string(pcontent, plen, template_end, replace_files) < 0) + goto clean_all; + + /* write the file */ + if ((fd = open(profile, O_CREAT | O_EXCL | O_WRONLY, 0644)) == -1) { + vah_error(NULL, 0, "failed to create profile"); + goto clean_all; + } + + if (safewrite(fd, pcontent, plen - 1) < 0) { /* don't write the '\0' */ + close(fd); + vah_error(NULL, 0, "failed to write to profile"); + goto clean_all; + } + + if (close(fd) != 0) { + vah_error(NULL, 0, "failed to close or write to profile"); + goto clean_all; + } + rc = 0; + + clean_all: + VIR_FREE(pcontent); + clean_replace: + VIR_FREE(replace_name); + VIR_FREE(replace_files); + clean_tcontent: + VIR_FREE(tcontent); + end: + return rc; +} + +/* + * Load an existing profile + */ +static int +parserLoad(const char *profile_name) +{ + return parserCommand(profile_name, 'a'); +} + +/* + * Remove an existing profile + */ +static int +parserRemove(const char *profile_name) +{ + return parserCommand(profile_name, 'R'); +} + +/* + * Replace an existing profile + */ +static int +parserReplace(const char *profile_name) +{ + return parserCommand(profile_name, 'r'); +} + +static int +valid_uuid(const char *uuid) +{ + unsigned char rawuuid[VIR_UUID_BUFLEN]; + + if (strlen(uuid) != PROFILE_NAME_SIZE - 1) + return -1; + + if (!STRPREFIX(uuid, AA_PREFIX)) + return -1; + + if (virUUIDParse(uuid + strlen(AA_PREFIX), rawuuid) < 0) + return -1; + + return 0; +} + +static int +valid_name(const char *name) +{ + /* just try to filter out any dangerous characters in the name that can be + * used to subvert the profile */ + const char *bad = " /[]*"; + + if (strlen(name) == 0 || strlen(name) > PATH_MAX - 1) + return -1; + + if (strcspn(name, bad) != strlen(name)) + return -1; + + return 0; +} + +/* see if one of the strings in arr starts with str */ +static int +array_starts_with(const char *str, const char * const *arr, const long size) +{ + int i; + for (i = 0; i < size; i++) { + if (strlen(str) < strlen(arr[i])) + continue; + + if (STRPREFIX(str, arr[i])) + return 0; + } + return 1; +} + +/* + * Don't allow access to special files or restricted paths such as /bin, /sbin, + * /usr/bin, /usr/sbin and /etc. This is in an effort to prevent read/write + * access to system files which could be used to elevate privileges. This is a + * safety measure in case libvirtd is under a restrictive profile and is + * subverted and trying to escape confinement. + * + * Note that we cannot exclude block devices because they are valid devices. + * The TEMPLATE file can be adjusted to explicitly disallow these if needed. + * + * RETURN: -1 on error, 0 if ok, 1 if blocked + */ +static int +valid_path(const char *path, const bool readonly) +{ + struct stat sb; + int npaths; + const char * const restricted[] = { + "/bin/", + "/etc/", + "/lib", + "/lost+found/", + "/proc/", + "/sbin/", + "/selinux/", + "/sys/", + "/usr/bin/", + "/usr/lib", + "/usr/sbin/", + "/usr/share/", + "/usr/local/bin/", + "/usr/local/etc/", + "/usr/local/lib", + "/usr/local/sbin/" + }; + /* these paths are ok for readonly, but not read/write */ + const char * const restricted_rw[] = { + "/boot/", + "/vmlinuz", + "/initrd", + "/initrd.img" + }; + + if (path == NULL || strlen(path) > PATH_MAX - 1) { + vah_error(NULL, 0, "bad pathname"); + return -1; + } + + /* Don't allow double quotes, since we use them to quote the filename + * and this will confuse the apparmor parser. + */ + if (strchr(path, '"') != NULL) + return 1; + + if (!virFileExists(path)) + vah_warning("path does not exist, skipping file type checks"); + else { + if (stat(path, &sb) == -1) + return -1; + + switch (sb.st_mode & S_IFMT) { + case S_IFDIR: + return 1; + break; + case S_IFIFO: + return 1; + break; + case S_IFSOCK: + return 1; + break; + default: + break; + } + } + + npaths = sizeof(restricted)/sizeof *(restricted); + if (array_starts_with(path, restricted, npaths) == 0) + return 1; + + npaths = sizeof(restricted_rw)/sizeof *(restricted_rw); + if (!readonly) { + if (array_starts_with(path, restricted_rw, npaths) == 0) + return 1; + } + + return 0; +} + +static int +get_definition(vahControl * ctl, const char *xmlStr) +{ + int rc = -1; + struct utsname utsname; + virCapsGuestPtr guest; /* this is freed when caps is freed */ + + /* + * mock up some capabilities. We don't currently use these explicitly, + * but need them for virDomainDefParseString(). + */ + + /* Really, this never fails - look at the man-page. */ + uname (&utsname); + + /* set some defaults if not specified */ + if (!ctl->bits) + ctl->bits = 32; + if (!ctl->hvm) + ctl->hvm = strdup("hvm"); + + if ((ctl->caps = virCapabilitiesNew(utsname.machine, 1, 1)) == NULL) { + vah_error(ctl, 0, "could not allocate memory"); + goto exit; + } + + if ((guest = virCapabilitiesAddGuest(ctl->caps, + ctl->hvm, + utsname.machine, + ctl->bits, + NULL, + NULL, + 0, + NULL)) == NULL) { + vah_error(ctl, 0, "could not allocate memory"); + goto exit; + } + + ctl->def = virDomainDefParseString(NULL, ctl->caps, xmlStr, 0); + if (ctl->def == NULL) { + vah_error(ctl, 0, "could not parse XML"); + goto exit; + } + + if (!ctl->def->name) { + vah_error(ctl, 0, "could not find name in XML"); + goto exit; + } + + if (valid_name(ctl->def->name) != 0) { + vah_error(ctl, 0, "bad name"); + goto exit; + } + + rc = 0; + + exit: + return rc; +} + +static int +vah_add_file(virBufferPtr buf, const char *path, const char *perms) +{ + char *tmp = NULL; + int rc = -1; + bool readonly = true; + + if (path == NULL) + return rc; + + if (virFileExists(path)) { + if ((tmp = realpath(path, NULL)) == NULL) { + vah_error(NULL, 0, path); + vah_error(NULL, 0, " could not find realpath for disk"); + return rc; + } + } else + if ((tmp = strdup(path)) == NULL) + return rc; + + if (strchr(perms, 'w') != NULL) + readonly = false; + + rc = valid_path(tmp, readonly); + if (rc != 0) { + if (rc > 0) { + vah_error(NULL, 0, path); + vah_error(NULL, 0, " skipped restricted file"); + } + goto clean; + } + + virBufferVSprintf(buf, " \"%s\" %s,\n", tmp, perms); + + clean: + free(tmp); + + return rc; +} + +static int +file_iterate_cb(virConnectPtr conn ATTRIBUTE_UNUSED, + usbDevice *dev ATTRIBUTE_UNUSED, + const char *file, void *opaque) +{ + virBufferPtr buf = opaque; + return vah_add_file(buf, file, "rw"); +} + +static int +get_files(vahControl * ctl) +{ + virBuffer buf = VIR_BUFFER_INITIALIZER; + int rc = -1; + int i; + char *uuid; + char uuidstr[VIR_UUID_STRING_BUFLEN]; + + /* verify uuid is same as what we were given on the command line */ + virUUIDFormat(ctl->def->uuid, uuidstr); + if (virAsprintf(&uuid, "%s%s", AA_PREFIX, uuidstr) == -1) { + vah_error(ctl, 0, "could not allocate memory"); + return rc; + } + + if (STRNEQ(uuid, ctl->uuid)) { + vah_error(ctl, 0, "given uuid does not match XML uuid"); + goto clean; + } + + for (i = 0; i < ctl->def->ndisks; i++) + if (ctl->def->disks[i] && ctl->def->disks[i]->src) { + int ret; + + if (ctl->def->disks[i]->readonly) + ret = vah_add_file(&buf, ctl->def->disks[i]->src, "r"); + else + ret = vah_add_file(&buf, ctl->def->disks[i]->src, "rw"); + + if (ret != 0) + goto clean; + } + + for (i = 0; i < ctl->def->nserials; i++) + if (ctl->def->serials[i] && ctl->def->serials[i]->data.file.path) + if (vah_add_file(&buf, + ctl->def->serials[i]->data.file.path, "w") != 0) + goto clean; + + if (ctl->def->console && ctl->def->console->data.file.path) + if (vah_add_file(&buf, ctl->def->console->data.file.path, "w") != 0) + goto clean; + + if (ctl->def->os.kernel && ctl->def->os.kernel) + if (vah_add_file(&buf, ctl->def->os.kernel, "r") != 0) + goto clean; + + if (ctl->def->os.initrd && ctl->def->os.initrd) + if (vah_add_file(&buf, ctl->def->os.initrd, "r") != 0) + goto clean; + + if (ctl->def->os.loader && ctl->def->os.loader) + if (vah_add_file(&buf, ctl->def->os.loader, "r") != 0) + goto clean; + + for (i = 0; i < ctl->def->nhostdevs; i++) + if (ctl->def->hostdevs[i]) { + virDomainHostdevDefPtr dev = ctl->def->hostdevs[i]; + switch (dev->source.subsys.type) { + case VIR_DOMAIN_HOSTDEV_SUBSYS_TYPE_USB: { + if (dev->source.subsys.u.usb.bus && + dev->source.subsys.u.usb.device) { + usbDevice *usb = usbGetDevice(NULL, + dev->source.subsys.u.usb.bus, + dev->source.subsys.u.usb.device); + if (usb == NULL) + continue; + rc = usbDeviceFileIterate(NULL, usb, + file_iterate_cb, &buf); + usbFreeDevice(NULL, usb); + if (rc != 0) + goto clean; + else { + /* TODO: deal with product/vendor better */ + rc = 0; + } + } + break; + } +/* TODO: update so files in /sys are readonly + case VIR_DOMAIN_HOSTDEV_SUBSYS_TYPE_PCI: { + pciDevice *pci = pciGetDevice(NULL, + dev->source.subsys.u.pci.domain, + dev->source.subsys.u.pci.bus, + dev->source.subsys.u.pci.slot, + dev->source.subsys.u.pci.function); + + if (pci == NULL) + continue; + + rc = pciDeviceFileIterate(NULL, pci, file_iterate_cb, &buf); + pciFreeDevice(NULL, pci); + + break; + } +*/ + default: + rc = 0; + break; + } /* switch */ + } + + if (ctl->newdisk) + if (vah_add_file(&buf, ctl->newdisk, "rw") != 0) + goto clean; + + if (virBufferError(&buf)) { + vah_error(NULL, 0, "failed to allocate file buffer"); + goto clean; + } + + rc = 0; + ctl->files = virBufferContentAndReset(&buf); + + clean: + VIR_FREE(uuid); + return rc; +} + +static int +vahParseArgv(vahControl * ctl, int argc, char **argv) +{ + int arg, idx = 0; + struct option opt[] = { + {"add", 0, 0, 'a'}, + {"create", 0, 0, 'c'}, + {"dryrun", 0, 0, 'd'}, + {"delete", 0, 0, 'D'}, + {"add-file", 0, 0, 'f'}, + {"help", 0, 0, 'h'}, + {"replace", 0, 0, 'r'}, + {"remove", 0, 0, 'R'}, + {"uuid", 1, 0, 'u'}, + {"hvm", 1, 0, 'H'}, + {"bits", 1, 0, 'b'}, + {0, 0, 0, 0} + }; + int bits; + + while ((arg = getopt_long(argc, argv, "acdDhrRH:b:u:f:", opt, + &idx)) != -1) { + switch (arg) { + case 'a': + ctl->cmd = 'a'; + break; + case 'b': + bits = atoi(optarg); + if (bits == 32 || bits == 64) + ctl->bits = bits; + else + vah_error(ctl, 1, "invalid bits (should be 32 or 64)"); + break; + case 'c': + ctl->cmd = 'c'; + break; + case 'd': + ctl->dryrun = true; + break; + case 'D': + ctl->cmd = 'D'; + break; + case 'f': + if ((ctl->newdisk = strdup(optarg)) == NULL) + vah_error(ctl, 1, "could not allocate memory for disk"); + break; + case 'h': + vah_usage(); + exit(EXIT_SUCCESS); + break; + case 'H': + if ((ctl->hvm = strdup(optarg)) == NULL) + vah_error(ctl, 1, "could not allocate memory for hvm"); + break; + case 'r': + ctl->cmd = 'r'; + break; + case 'R': + ctl->cmd = 'R'; + break; + case 'u': + if (strlen(optarg) > PROFILE_NAME_SIZE - 1) + vah_error(ctl, 1, "invalid UUID"); + if (virStrcpy((char *) ctl->uuid, optarg, + PROFILE_NAME_SIZE) == NULL) + vah_error(ctl, 1, "error copying UUID"); + break; + default: + vah_error(ctl, 1, "unsupported option"); + break; + } + } + if (strchr("acDrR", ctl->cmd) == NULL) + vah_error(ctl, 1, "bad command"); + + if (valid_uuid(ctl->uuid) != 0) + vah_error(ctl, 1, "invalid UUID"); + + if (!ctl->cmd) { + vah_usage(); + exit(EXIT_FAILURE); + } + + if (ctl->cmd == 'c' || ctl->cmd == 'r') { + char *xmlStr = NULL; + if (virFileReadLimFD(STDIN_FILENO, MAX_FILE_LEN, &xmlStr) < 0) + vah_error(ctl, 1, "could not read xml file"); + + if (get_definition(ctl, xmlStr) != 0 || ctl->def == NULL) { + VIR_FREE(xmlStr); + vah_error(ctl, 1, "could not get VM definition"); + } + VIR_FREE(xmlStr); + + if (get_files(ctl) != 0) + vah_error(ctl, 1, "invalid VM definition"); + } + return 0; +} + + +/* + * virt-aa-helper -c -u UUID < file.xml + * virt-aa-helper -r -u UUID [-f ] < file.xml + * virt-aa-helper -a -u UUID + * virt-aa-helper -R -u UUID + * virt-aa-helper -D -u UUID + */ +int +main(int argc, char **argv) +{ + vahControl _ctl, *ctl = &_ctl; + virBuffer buf = VIR_BUFFER_INITIALIZER; + int rc = -1; + char profile[PATH_MAX]; + char include_file[PATH_MAX]; + + /* clear the environment */ + environ = NULL; + if (setenv("PATH", "/sbin:/usr/sbin", 1) != 0) { + vah_error(ctl, 1, "could not set PATH"); + } + if (setenv("IFS", " \t\n", 1) != 0) { + vah_error(ctl, 1, "could not set IFS"); + } + + if (!(progname = strrchr(argv[0], '/'))) + progname = argv[0]; + else + progname++; + + memset(ctl, 0, sizeof(vahControl)); + + if (vahParseArgv(ctl, argc, argv) != 0) + vah_error(ctl, 1, "could not parse arguments"); + + if (snprintf(profile, PATH_MAX, "%s/%s", + APPARMOR_DIR "/libvirt", ctl->uuid) > PATH_MAX - 1) + vah_error(ctl, 1, "profile name exceeds maximum length"); + + if (snprintf(include_file, PATH_MAX, "%s/%s.files", + APPARMOR_DIR "/libvirt", ctl->uuid) > PATH_MAX - 1) + vah_error(ctl, 1, "disk profile name exceeds maximum length"); + + if (ctl->cmd == 'a') + rc = parserLoad(ctl->uuid); + else if (ctl->cmd == 'R' || ctl->cmd == 'D') { + rc = parserRemove(ctl->uuid); + if (ctl->cmd == 'D') { + unlink(include_file); + unlink(profile); + } + } else if (ctl->cmd == 'c' || ctl->cmd == 'r') { + char *included_files = NULL; + + if (ctl->cmd == 'c' && virFileExists(profile)) + vah_error(ctl, 1, "profile exists"); + + virBufferVSprintf(&buf, " \"%s/log/libvirt/**/%s.log\" w,\n", + LOCAL_STATE_DIR, ctl->def->name); + virBufferVSprintf(&buf, " \"%s/lib/libvirt/**/%s.monitor\" rw,\n", + LOCAL_STATE_DIR, ctl->def->name); + virBufferVSprintf(&buf, " \"%s/run/libvirt/**/%s.pid\" rwk,\n", + LOCAL_STATE_DIR, ctl->def->name); + if (ctl->files) + virBufferVSprintf(&buf, "%s", ctl->files); + + if (virBufferError(&buf)) + vah_error(ctl, 1, "failed to allocate buffer"); + + included_files = virBufferContentAndReset(&buf); + + /* (re)create the include file using included_files */ + if (ctl->dryrun) { + vah_info(include_file); + vah_info(included_files); + rc = 0; + } else if ((rc = update_include_file(include_file, + included_files)) != 0) + goto clean; + + + /* create the profile from TEMPLATE */ + if (ctl->cmd == 'c') { + char *tmp = NULL; + if (virAsprintf(&tmp, " #include \n", + ctl->uuid) == -1) { + vah_error(ctl, 0, "could not allocate memory"); + goto clean; + } + + if (ctl->dryrun) { + vah_info(profile); + vah_info(ctl->uuid); + vah_info(tmp); + rc = 0; + } else if ((rc = create_profile(profile, ctl->uuid, tmp)) != 0) { + vah_error(ctl, 0, "could not create profile"); + unlink(include_file); + } + VIR_FREE(tmp); + } + + if (rc == 0 && !ctl->dryrun) { + if (ctl->cmd == 'c') + rc = parserLoad(ctl->uuid); + else + rc = parserReplace(ctl->uuid); + + /* cleanup */ + if (rc != 0) { + unlink(include_file); + if (ctl->cmd == 'c') + unlink(profile); + } + } + clean: + VIR_FREE(included_files); + } + + vahDeinit(ctl); + exit(rc == 0 ? EXIT_SUCCESS : EXIT_FAILURE); +} diff --git a/tests/Makefile.am b/tests/Makefile.am index a837150ffa..7882e52211 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -16,6 +16,7 @@ INCLUDES = \ $(GNUTLS_CFLAGS) \ $(SASL_CFLAGS) \ $(SELINUX_CFLAGS) \ + $(APPARMOR_CFLAGS) \ -DGETTEXT_PACKAGE=\"$(PACKAGE)\" \ $(COVERAGE_CFLAGS) \ $(WARN_CFLAGS) @@ -31,6 +32,7 @@ LDADDS = \ $(GNUTLS_LIBS) \ $(SASL_LIBS) \ $(SELINUX_LIBS) \ + $(APPARMOR_LIBS) \ $(WARN_CFLAGS) \ ../src/libvirt_test.la \ ../gnulib/lib/libgnu.la \ @@ -84,6 +86,10 @@ if WITH_SECDRIVER_SELINUX noinst_PROGRAMS += seclabeltest endif +if WITH_SECDRIVER_APPARMOR +noinst_PROGRAMS += secaatest +endif + if WITH_CIL noinst_PROGRAMS += object-locking endif @@ -119,6 +125,9 @@ test_scripts += \ virsh-synopsis endif +if WITH_SECDRIVER_APPARMOR +test_scripts += virt-aa-helper-test +endif EXTRA_DIST += $(test_scripts) TESTS = virshtest \ @@ -149,6 +158,10 @@ if WITH_SECDRIVER_SELINUX TESTS += seclabeltest endif +if WITH_SECDRIVER_APPARMOR +TESTS += secaatest +endif + if WITH_LIBVIRTD noinst_PROGRAMS += eventtest TESTS += eventtest @@ -285,6 +298,14 @@ else EXTRA_DIST += seclabeltest.c endif +if WITH_SECDRIVER_APPARMOR +secaatest_SOURCES = \ + secaatest.c +secaatest_LDADD = ../src/libvirt_driver_security.la $(LDADDS) +else +EXTRA_DIST += secaatest.c +endif + qparamtest_SOURCES = \ qparamtest.c testutils.h testutils.c qparamtest_LDADD = $(LDADDS) diff --git a/tests/secaatest.c b/tests/secaatest.c new file mode 100644 index 0000000000..54fdb44b7f --- /dev/null +++ b/tests/secaatest.c @@ -0,0 +1,45 @@ +#include + +#include +#include +#include +#include +#include +#include "security/security_driver.h" + +int +main (int argc ATTRIBUTE_UNUSED, char **argv ATTRIBUTE_UNUSED) +{ + int ret; + + const char *doi, *model; + virSecurityDriverPtr security_drv; + + ret = virSecurityDriverStartup (&security_drv, "apparmor"); + if (ret == -1) + { + fprintf (stderr, "Failed to start security driver"); + exit (-1); + } + /* No security driver wanted to be enabled: just return */ + if (ret == -2) + return 0; + + model = virSecurityDriverGetModel (security_drv); + if (!model) + { + fprintf (stderr, "Failed to copy secModel model: %s", + strerror (errno)); + exit (-1); + } + + doi = virSecurityDriverGetDOI (security_drv); + if (!doi) + { + fprintf (stderr, "Failed to copy secModel DOI: %s", + strerror (errno)); + exit (-1); + } + + return 0; +} diff --git a/tests/virt-aa-helper-test b/tests/virt-aa-helper-test new file mode 100644 index 0000000000..332e993437 --- /dev/null +++ b/tests/virt-aa-helper-test @@ -0,0 +1,263 @@ +#!/bin/sh +set -e + +test_hostdev="no" +if [ "$1" = "test_hostdev" ]; then + test_hostdev="yes" + shift +fi + +output="/dev/null" +use_valgrind="" +ld_library_path="../src/.libs/" +if [ ! -z "$1" ] && [ "$1" = "-d" ]; then + output="/dev/stdout" + shift +fi + +exe="../src/virt-aa-helper" +if [ ! -z "$1" ]; then + if [ "$1" = "-v" ]; then + use_valgrind="yes" + shift + fi + if [ -n "$1" ]; then + exe="$1" + shift + fi +fi + +if [ ! -x "$exe" ]; then + echo "Could not find '$exe'" + exit 1 +fi + +echo "testing `basename $exe`" >$output +if [ "$use_valgrind" = "yes" ]; then + exe="valgrind --error-exitcode=2 --track-origins=yes $exe" +fi + +extra_args="--dryrun" +errors=0 + +tmpdir=`mktemp -d` +trap "rm -rf $tmpdir" EXIT HUP INT QUIT TERM + +template_xml="$tmpdir/template.xml" +test_xml="$tmpdir/test.xml" + +uuid="00000000-0000-0000-0000-0123456789ab" +disk1="$tmpdir/1.img" +disk2="$tmpdir/2.img" +relative_disk1="$tmpdir/./../`basename $tmpdir`//./1.img" +nonexistent="$tmpdir/nonexistant.img" +bad_disk="/etc/passwd" +valid_uuid="libvirt-$uuid" +nonexistent_uuid="libvirt-00000000-0000-0000-0000-000000000001" + +cat > "$template_xml" < + virt-aa-helper-test + ###UUID### + 524288 + 524288 + 1 + + hvm + + + + + + + destroy + restart + destroy + + /usr/bin/kvm + + + + + + + + + + + + + + + +EOM + +touch "$disk1" "$disk2" + +testme() { + expected="$1" + outstr="$2" + args="$3" + input="" + + if [ -n "$4" ]; then + input="$4" + if [ ! -e "$input" ]; then + echo "FAIL: could not find $input" >$output + echo "FAIL: could not find $input" + echo " '$extra_args $args': " + errors=$(($errors + 1)) + fi + fi + + echo -n " $outstr: " >$output + echo -n " '$extra_args $args" >$output + if [ -n "$input" ]; then + echo -n " < $input" >$output + fi + echo "': " >$output + set +e + if [ -n "$input" ]; then + LD_LIBRARY_PATH="$ld_library_path" $exe $extra_args $args < $input >$output 2>&1 + else + LD_LIBRARY_PATH="$ld_library_path" $exe $extra_args $args >$output 2>&1 + fi + rc="$?" + set -e + if [ "$rc" = "$expected" ]; then + echo "pass" >$output + else + echo "FAIL: exited with '$rc'" >$output + echo "FAIL: exited with '$rc'" + echo -n " $outstr: " + echo " '$extra_args $args': " + errors=$(($errors + 1)) + #exit $rc + fi +} + +# Expected failures +echo "Expected failures:" >$output +testme "1" "invalid arg" "-z" +testme "1" "invalid case" "-A" +testme "1" "not enough args" "-c" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" > "$test_xml" +testme "1" "no -u with -c" "-c" "$test_xml" +testme "1" "bad uuid (bad digit)" "-c -u libvirt-00000000-0000-0000-0000-00000000000g" "$test_xml" +testme "1" "bad uuid (too long)" "-c -u ${valid_uuid}abcdef" "$test_xml" +testme "1" "bad uuid (too short)" "-c -u libvirt-00000000-0000-0000-0000-0123456789a" "$test_xml" +testme "1" "non-matching uuid" "-c -u libvirt-00000000-0000-0000-0000-00000000000a" "$test_xml" +testme "1" "missing uuid" "-c -u" "$test_xml" +testme "1" "no -u with -R" "-R" +testme "1" "non-existent uuid" "-R -u $nonexistent_uuid" +testme "1" "no -u with -r" "-r" +testme "1" "old '-n' option" "-c -n foo -u $valid_uuid" "$test_xml" +testme "1" "invalid bits" "-c -b 15 -u $valid_uuid" "$test_xml" +testme "1" "invalid bits2" "-c -b a -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$bad_disk,g" > "$test_xml" +testme "1" "bad disk" "-c -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$bad_disk,g" | sed "s,,,g" > "$test_xml" +testme "1" "bad disk2" "-c -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,,,g" > "$test_xml" +testme "1" "malformed xml" "-c -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,/boot/initrd,g" > "$test_xml" +testme "1" "disk in /boot" "-r -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,/boot/initrd,g" > "$test_xml" +testme "1" "-r with invalid -f" "-r -u $valid_uuid -f $bad_disk" "$test_xml" + + +echo "Expected pass:" >$output +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" > "$test_xml" +testme "0" "create" "-c -u $valid_uuid" "$test_xml" +testme "0" "create with bits (32)" "-c -b 32 -u $valid_uuid" "$test_xml" +testme "0" "create with bits (64)" "-c -b 64 -u $valid_uuid" "$test_xml" +testme "0" "create with hvm" "-c -H hvm -u $valid_uuid" "$test_xml" +testme "0" "create with hvm and bits" "-c -H hvm --bits 32 -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,,,g" > "$test_xml" +testme "0" "create multiple disks" "-c -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###',${disk1}'/> "$test_xml" +testme "0" "create (readonly)" "-c -u $valid_uuid" "$test_xml" + +if [ "$test_hostdev" = "yes" ]; then + cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,,
,g" > "$test_xml" + testme "0" "create hostdev (USB)" "-c -u $valid_uuid" "$test_xml" + + cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,,
,g" > "$test_xml" + testme "0" "create hostdev (PCI)" "-c -u $valid_uuid" "$test_xml" +fi + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$nonexistent,g" > "$test_xml" +testme "0" "create (non-existent disk)" "-c -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$relative_disk1,g" > "$test_xml" +testme "0" "create (relative path)" "-c -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk2,g" > "$test_xml" +testme "0" "replace" "-r -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$nonexistent,g" > "$test_xml" +testme "0" "replace (non-existent disk)" "-r -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" > "$test_xml" +testme "0" "replace (adding disk)" "-r -u $valid_uuid -f $disk2" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" > "$test_xml" +testme "0" "replace (adding non-existent disk)" "-r -u $valid_uuid -f $nonexistent" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,,,g" > "$test_xml" +testme "0" "disk (empty cdrom)" "-r -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,,,g" > "$test_xml" +testme "0" "serial" "-r -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,,,g" > "$test_xml" +testme "0" "serial (pty)" "-r -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,,,g" > "$test_xml" +touch "$tmpdir/console.log" +testme "0" "console" "-r -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,,,g" > "$test_xml" +testme "0" "console (pty)" "-r -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,,$tmpdir/kernel,g" > "$test_xml" +touch "$tmpdir/kernel" +testme "0" "kernel" "-r -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,,$tmpdir/initrd,g" > "$test_xml" +touch "$tmpdir/initrd" +testme "0" "initrd" "-r -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,,/boot/kernel,g" > "$test_xml" +testme "0" "kernel in /boot" "-r -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,,/boot/initrd,g" > "$test_xml" +testme "0" "initrd in /boot" "-r -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,,/vmlinuz,g" > "$test_xml" +testme "0" "kernel is /vmlinuz" "-r -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,,/initrd/ramdisk,g" > "$test_xml" +testme "0" "initrd is /initrd/ramdisk" "-r -u $valid_uuid" "$test_xml" + +cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,,/initrd.img,g" > "$test_xml" +testme "0" "initrd is /initrd.img" "-r -u $valid_uuid" "$test_xml" + +testme "0" "help" "-h" + +echo "" >$output +if [ "$errors" != "0" ]; then + echo "FAIL: $errors error(s)" >$output + exit 1 +fi +echo PASS >$output