On Fri, 25 Sep 2009, Jamie Strandboge wrote: > [PATCH 2] > patch_2_apparmor_driver.patch (updated and merged into one patch based > on prior feedback): > - Updates src/security.c for AppArmor > - Adds security_apparmor.c, security_apparmor.h, virt-aa-helper.c and > updates po/POTFILES.in. virt-aa-helper.c is a new binary which is used > exclusively by the AppArmor security driver to manipulate AppArmor. > - Adds tests for virt-aa-helper and the security driver. secaatest.c is > identical to seclabeltest.c except it initializes the 'apparmor' driver > instead of 'selinux'. These tests are integrated into 'make check' and > pass. > - Updates configure.in, Makefile.am, src/Makefile.am, tests/Makefile.am, > and tools/Makefile.am for AppArmor. It is based on and should operate > the same as the SELinux configuration. -- Jamie Strandboge | http://www.canonical.com
diff -Naurp libvirt.orig/configure.in libvirt/configure.in --- libvirt.orig/configure.in 2009-09-22 12:51:57.000000000 -0500 +++ libvirt/configure.in 2009-09-25 17:00:08.000000000 -0500 @@ -799,6 +799,84 @@ 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]) + AC_DEFINE_UNQUOTED([VIRT_AA_HELPER_PATH], "$prefix/bin/virt-aa-helper", [path to virt-aa-helper]) +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], @@ -1743,6 +1821,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([]) @@ -1790,6 +1869,11 @@ AC_MSG_NOTICE([ selinux: $SELINUX_CFLAGS 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 -Naurp libvirt.orig/po/POTFILES.in libvirt/po/POTFILES.in --- libvirt.orig/po/POTFILES.in 2009-09-22 12:51:57.000000000 -0500 +++ libvirt/po/POTFILES.in 2009-09-25 17:00:08.000000000 -0500 @@ -28,6 +28,7 @@ src/qemu/qemu_driver.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/storage/storage_backend.c src/storage/storage_backend_disk.c @@ -60,3 +61,4 @@ src/xen/xm_internal.c src/xen/xs_internal.c tools/console.c tools/virsh.c +tools/virt-aa-helper.c diff -Naurp libvirt.orig/src/Makefile.am libvirt/src/Makefile.am --- libvirt.orig/src/Makefile.am 2009-09-22 12:51:57.000000000 -0500 +++ libvirt/src/Makefile.am 2009-09-25 17:00:08.000000000 -0500 @@ -235,6 +235,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 @@ -638,9 +641,15 @@ noinst_LTLIBRARIES += libvirt_driver_sec 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 += \ @@ -668,6 +677,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) diff -Naurp libvirt.orig/src/security/security_apparmor.c libvirt/src/security/security_apparmor.c --- libvirt.orig/src/security/security_apparmor.c 1969-12-31 18:00:00.000000000 -0600 +++ libvirt/src/security/security_apparmor.c 2009-09-25 17:00:13.000000000 -0500 @@ -0,0 +1,597 @@ + +/* + * 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 <jamie@xxxxxxxxxxxxx> + * Based on security_selinux.c by James Morris <jmorris@xxxxxxxxx> + * + * AppArmor security driver. + */ + +#include <config.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <sys/apparmor.h> +#include <errno.h> +#include <unistd.h> +#include <wait.h> +#include <stdbool.h> + +#include "internal.h" + +#include "security_driver.h" +#include "security_apparmor.h" +#include "util.h" +#include "memory.h" +#include "virterror_internal.h" +#include "datatypes.h" + +#define VIR_FROM_THIS VIR_FROM_SECURITY +#define SECURITY_APPARMOR_VOID_DOI "0" +#define SECURITY_APPARMOR_NAME "apparmor" + +/* + * 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 '<str> \0' for accurate matching */ + if (virAsprintf(&tmp, "%s ", str) == -1) + return rc; + + if (check_enforcing != 0) { + /* create string that is '<str> (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); + VIR_FREE(tmp); + return rc; + } + + 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(tmp); + VIR_FREE(content); + + 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 ' <str> flags=(complain)\0' */ + if (virAsprintf(&tmp, " %s flags=(complain)", str) == -1) { + virSecurityReportError(NULL, VIR_ERR_ERROR, + "%s", _("could not allocate memory")); + VIR_FREE(content); + return rc; + } + + if (strstr(content, tmp) != NULL) + rc = 0; + else + rc = 1; + + VIR_FREE(tmp); + 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) +{ + const char *argv[7]; + int rc = -1, status; + 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) { + virSecurityReportError(conn, VIR_ERR_ERROR, + "%s", _("could not format XML")); + goto failed; + } + + if (profile_status_file(profile) >= 0) + create = false; + + argv[0] = VIRT_AA_HELPER_PATH; + argv[2] = (char *) "-u"; + argv[3] = profile; + argv[4] = NULL; + if (create) + argv[1] = (char *) "-c"; + else { + argv[1] = (char *) "-r"; + if (disk && disk->src) { + argv[4] = "-f"; + argv[5] = disk->src; + argv[6] = NULL; + } + } + + if (virExec(conn, argv, NULL, NULL, &child, + pipefd[0], NULL, NULL, VIR_EXEC_CLEAR_CAPS) < 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) +{ + const char *argv[5]; + int rc = -1; + + argv[0] = VIRT_AA_HELPER_PATH; + argv[1] = (char *) "-R"; + argv[2] = (char *) "-u"; + argv[3] = profile; + argv[4] = NULL; + + if (virRun(NULL, argv, NULL) == 0) + rc = 0; + + return rc; +} + +/* + * profile_name is buffer to hold name and len is how many bytes in the + * buffer + */ +static int +get_profile_name(virConnectPtr conn, virDomainObjPtr vm, + char *profile_name, const size_t len) +{ + virDomainPtr dom = NULL; + int rc = -1; + + if (len < PROFILE_NAME_SIZE) { + virSecurityReportError(conn, VIR_ERR_ERROR, + "%s", _("profile_name has wrong size")); + return rc; + } + profile_name[0] = '\0'; + strcat(profile_name, AA_PREFIX); + + /* generate the profile name */ + dom = virGetDomain(conn, vm->def->name, vm->def->uuid); + dom->id = vm->def->id; + + if (virDomainGetUUIDString(dom, + &profile_name[strlen(profile_name)]) != 0) { + virSecurityReportError(conn, VIR_ERR_ERROR, + "%s", _("could not find uuid for VM")); + return rc; + } + + return 0; +} + +/* returns -1 on error or profile for libvirtd is unconfined, 0 if complain + * mode and 1 if enforcing + */ +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[PROFILE_NAME_SIZE]; + + profile_name[0] = '\0'; + + 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 (get_profile_name(conn, vm, profile_name, sizeof(profile_name)) < 0) + 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); + return rc; + } + } + + vm->def->seclabel.label = strndup(profile_name, strlen(profile_name)); + if (!vm->def->seclabel.label) { + virSecurityReportError(conn, VIR_ERR_ERROR, + "%s", _("cannot generate AppArmor profile " + "name (label)")); + goto err; + } + + /* 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) { + virSecurityReportError(conn, VIR_ERR_ERROR, + "%s", + _("cannot generate AppArmor profile name " + "(imagelabel)")); + goto err; + } + + vm->def->seclabel.model = strdup(SECURITY_APPARMOR_NAME); + if (!vm->def->seclabel.model) { + virReportOOMError(conn); + goto err; + } + + rc = 0; + goto done; + + err: + remove_profile(profile_name); + VIR_FREE(vm->def->seclabel.label); + VIR_FREE(vm->def->seclabel.imagelabel); + VIR_FREE(vm->def->seclabel.model); + done: + return rc; +} + +/* Seen with 'virsh dominfo <vm>'. This function only called if the VM is + * running. + */ +static int +AppArmorGetSecurityLabel(virConnectPtr conn, + virDomainObjPtr vm, virSecurityLabelPtr sec) +{ + int rc = -1; + char profile_name[PROFILE_NAME_SIZE]; + + profile_name[0] = '\0'; + + if (get_profile_name(conn, vm, profile_name, sizeof(profile_name)) < 0) + return rc; + + if (virStrcpy(sec->label, profile_name, + VIR_SECURITY_LABEL_BUFLEN) == NULL) { + virSecurityReportError(conn, VIR_ERR_ERROR, + "%s", _("error copying profile name")); + return rc; + } + + if ((sec->enforcing = profile_status(profile_name, 1)) < 0) { + virSecurityReportError(conn, VIR_ERR_ERROR, + "%s", _("error calling profile_status()")); + return rc; + } + + return 0; +} + +/* 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/<vm name>.log + */ +static int +AppArmorSetSecurityLabel(virConnectPtr conn, + virSecurityDriverPtr drv, virDomainObjPtr vm) +{ + const virSecurityLabelDefPtr secdef = &vm->def->seclabel; + int rc = -1; + char profile_name[PROFILE_NAME_SIZE]; + + profile_name[0] = '\0'; + + if (get_profile_name(conn, vm, profile_name, sizeof(profile_name)) < 0) + 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) + return rc; + } + + if (aa_change_profile(profile_name) < 0) { + virSecurityReportError(conn, VIR_ERR_ERROR, + _("error calling aa_change_profile()")); + return rc; + } + + return 0; +} + + +/* 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[PROFILE_NAME_SIZE]; + + profile_name[0] = '\0'; + + if (secdef->imagelabel) { + if (get_profile_name(conn, vm, profile_name, + sizeof(profile_name)) < 0) + 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); + return rc; + } + } + } + + return 0; +} + +/* Called when hotplugging */ +static int +AppArmorSetSecurityImageLabel(virConnectPtr conn, + virDomainObjPtr vm, virDomainDiskDefPtr disk) +{ + const virSecurityLabelDefPtr secdef = &vm->def->seclabel; + int rc = -1; + char profile_name[PROFILE_NAME_SIZE]; + + profile_name[0] = '\0'; + + 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 (get_profile_name(conn, vm, profile_name, + sizeof(profile_name)) < 0) + 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); + return rc; + } + } + } + + return 0; +} + +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; +} + +virSecurityDriver virAppArmorSecurityDriver = { + .name = SECURITY_APPARMOR_NAME, + .probe = AppArmorSecurityDriverProbe, + .open = AppArmorSecurityDriverOpen, + .domainSecurityVerify = AppArmorSecurityVerify, + .domainSetSecurityImageLabel = AppArmorSetSecurityImageLabel, + .domainRestoreSecurityImageLabel = AppArmorRestoreSecurityImageLabel, + .domainGenSecurityLabel = AppArmorGenSecurityLabel, + .domainGetSecurityLabel = AppArmorGetSecurityLabel, + .domainRestoreSecurityLabel = AppArmorRestoreSecurityLabel, + .domainSetSecurityLabel = AppArmorSetSecurityLabel, +}; diff -Naurp libvirt.orig/src/security/security_apparmor.h libvirt/src/security/security_apparmor.h --- libvirt.orig/src/security/security_apparmor.h 1969-12-31 18:00:00.000000000 -0600 +++ libvirt/src/security/security_apparmor.h 2009-09-25 17:00:08.000000000 -0500 @@ -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 <jamie@xxxxxxxxxxxxx> + * + */ +#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 -Naurp libvirt.orig/src/security/security_driver.c libvirt/src/security/security_driver.c --- libvirt.orig/src/security/security_driver.c 2009-09-22 12:51:57.000000000 -0500 +++ libvirt/src/security/security_driver.c 2009-09-25 17:00:08.000000000 -0500 @@ -20,10 +20,17 @@ #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 -Naurp libvirt.orig/tests/Makefile.am libvirt/tests/Makefile.am --- libvirt.orig/tests/Makefile.am 2009-09-25 10:50:21.000000000 -0500 +++ libvirt/tests/Makefile.am 2009-09-25 17:00:08.000000000 -0500 @@ -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 -Naurp libvirt.orig/tests/secaatest.c libvirt/tests/secaatest.c --- libvirt.orig/tests/secaatest.c 1969-12-31 18:00:00.000000000 -0600 +++ libvirt/tests/secaatest.c 2009-09-25 17:00:08.000000000 -0500 @@ -0,0 +1,45 @@ +#include <config.h> + +#include <unistd.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <errno.h> +#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 -Naurp libvirt.orig/tests/virt-aa-helper-test libvirt/tests/virt-aa-helper-test --- libvirt.orig/tests/virt-aa-helper-test 1969-12-31 18:00:00.000000000 -0600 +++ libvirt/tests/virt-aa-helper-test 2009-09-25 17:00:08.000000000 -0500 @@ -0,0 +1,250 @@ +#!/bin/sh +set -e + +output="/dev/null" +use_valgrind="" +ld_library_path="../src/.libs/" +if [ ! -z "$1" ] && [ "$1" = "-d" ]; then + output="/dev/stdout" + shift +fi + +exe="../tools/.libs/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" <<EOM +<domain type='kvm'> + <name>virt-aa-helper-test</name> + <uuid>###UUID###</uuid> + <memory>524288</memory> + <currentMemory>524288</currentMemory> + <vcpu>1</vcpu> + <os> + <type arch='x86_64' machine='pc'>hvm</type> + <boot dev='hd'/> + </os> + <features> + <acpi/> + </features> + <clock offset='utc'/> + <on_poweroff>destroy</on_poweroff> + <on_reboot>restart</on_reboot> + <on_crash>destroy</on_crash> + <devices> + <emulator>/usr/bin/kvm</emulator> + <disk type='file' device='disk'> + <source file='###DISK###'/> + <target dev='hda' bus='ide'/> + </disk> + <interface type='network'> + <mac address='52:54:00:50:4b:26'/> + <source network='default'/> + <model type='virtio'/> + </interface> + <input type='tablet' bus='usb'/> + <input type='mouse' bus='ps2'/> + <graphics type='vnc' port='-1' autoport='yes' listen='127.0.0.1'/> + <video> + <model type='cirrus' vram='9216' heads='1'/> + </video> + </devices> +</domain> +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,</devices>,<disk type='file' device='disk'><source file='$disk2'<target dev='hda' bus='ide'/></disk></devices>,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,</devices>,<devices>,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,</disk>,</disk><disk type='file' device='disk'><source file='$disk2'/><target dev='hdb' bus='ide'/></disk>,g" > "$test_xml" +testme "0" "create multiple disks" "-c -u $valid_uuid" "$test_xml" + +# uncomment when hostusb patch goes through +#cat "$template_xml" | sed "s,###UUID###,$uuid,g" | sed "s,###DISK###,$disk1,g" | sed "s,</disk>,</disk><hostdev mode='subsystem' type='usb'><source><address bus='002' device='004'/></source></hostdev>,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###,$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,</devices>,<disk type='block' device='cdrom'><target dev='hdc' bus='ide'/><readonly/></disk></devices>,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,</devices>,<serial type='file'><source path='$tmpdir/serial.log'/><target port='0'/></serial></devices>,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,</devices>,<serial type='pty'><target port='0'/></serial></devices>,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,</devices>,<console type='file'><source path='$tmpdir/console.log'/><target port='0'/></console></devices>,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,</devices>,<console type='pty'><target port='0'/></console></devices>,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,</os>,<kernel>$tmpdir/kernel</kernel></os>,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,</os>,<initrd>$tmpdir/initrd</initrd></os>,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,</os>,<kernel>/boot/kernel</kernel></os>,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,</os>,<initrd>/boot/initrd</initrd></os>,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,</os>,<kernel>/vmlinuz</kernel></os>,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,</os>,<initrd>/initrd/ramdisk</initrd></os>,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,</os>,<initrd>/initrd.img</initrd></os>,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 diff -Naurp libvirt.orig/tools/Makefile.am libvirt/tools/Makefile.am --- libvirt.orig/tools/Makefile.am 2009-09-22 12:51:57.000000000 -0500 +++ libvirt/tools/Makefile.am 2009-09-25 17:00:13.000000000 -0500 @@ -51,6 +51,33 @@ virsh_CFLAGS = \ $(COVERAGE_CFLAGS) \ $(LIBXML_CFLAGS) \ $(READLINE_CFLAGS) + +if WITH_SECDRIVER_APPARMOR +bin_PROGRAMS += virt-aa-helper + +virt_aa_helper_SOURCES = \ + virt-aa-helper.c + +virt_aa_helper_LDFLAGS = $(WARN_CFLAGS) +virt_aa_helper_LDADD = \ + $(STATIC_BINARIES) \ + $(WARN_CFLAGS) \ + ../src/libvirt.la \ + ../gnulib/lib/libgnu.la \ + ../src/libvirt_util.la +virt_aa_helper_CFLAGS = \ + -I$(top_srcdir)/gnulib/lib -I../gnulib/lib \ + -I../include \ + -I$(top_srcdir)/src \ + -I$(top_srcdir)/src/conf \ + -I$(top_srcdir)/src/security \ + -I$(top_srcdir)/src/util \ + -DGETTEXT_PACKAGE=\"$(PACKAGE)\" \ + -DLOCALEBASEDIR=\""$(datadir)/locale"\" \ + -DLOCAL_STATE_DIR=\""$(localstatedir)"\" \ + $(LIBXML_CFLAGS) +endif + BUILT_SOURCES = virsh-net-edit.c virsh-pool-edit.c virsh-net-edit.c: virsh.c Makefile.am diff -Naurp libvirt.orig/tools/virt-aa-helper.c libvirt/tools/virt-aa-helper.c --- libvirt.orig/tools/virt-aa-helper.c 1969-12-31 18:00:00.000000000 -0600 +++ libvirt/tools/virt-aa-helper.c 2009-09-25 17:00:13.000000000 -0500 @@ -0,0 +1,966 @@ + +/* + * 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 <jamie@xxxxxxxxxxxxx> + * + */ + +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdarg.h> +#include <unistd.h> +#include <errno.h> +#include <sys/types.h> +#include "c-ctype.h" +#include <fcntl.h> +#include <getopt.h> +#include <stdbool.h> +#include <sys/utsname.h> + +#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" + +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> uuid (profile name)\n" + " -H | --hvm <hvm> hypervisor type\n" + " -b | --bits <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) +{ + const char *argv[4]; + 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; + } + + argv[0] = (char *) "/sbin/apparmor_parser"; + argv[1] = flag; + argv[2] = profile; + argv[3] = 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 <profile_name>\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<profile_files>\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) +{ + int i; + + if (strlen(uuid) != PROFILE_NAME_SIZE - 1) + return -1; + + if (STRNEQLEN(AA_PREFIX, uuid, strlen(AA_PREFIX))) + return -1; + + for (i = strlen(AA_PREFIX); i < PROFILE_NAME_SIZE - 1; i++) { + if (uuid[i] == '-') + continue; + if (!c_isxdigit(uuid[i])) + 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 */ + char *bad = (char *) " /[]*"; + int i; + + if (strlen(name) == 0 || strlen(name) > PATH_MAX - 1) + return -1; + + for (i = 0; i < strlen(bad); i++) + if (strchr(name, bad[i]) != NULL) + return -1; + + return 0; +} + +/* + * 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 i; + int npaths = 20; + char **restricted; + int rc = -1; + + if (path == NULL || strlen(path) > PATH_MAX - 1) { + vah_error(NULL, 0, "bad pathname"); + return rc; + } + + /* 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 (VIR_ALLOC_N(restricted, npaths) < 0) { + vah_error(NULL, 0, "could not allocate memory for paths"); + return rc; + } + + restricted[0] = (char *) "/bin/"; + restricted[1] = (char *) "/etc/"; + restricted[2] = (char *) "/lib"; + restricted[3] = (char *) "/lost+found/"; + restricted[4] = (char *) "/proc/"; + restricted[5] = (char *) "/sbin/"; + restricted[6] = (char *) "/selinux/"; + restricted[7] = (char *) "/sys/"; + restricted[8] = (char *) "/usr/bin/"; + restricted[9] = (char *) "/usr/lib"; + restricted[10] = (char *) "/usr/sbin/"; + restricted[11] = (char *) "/usr/share/"; + restricted[12] = (char *) "/usr/local/bin/"; + restricted[13] = (char *) "/usr/local/etc/"; + restricted[14] = (char *) "/usr/local/lib"; + restricted[15] = (char *) "/usr/local/sbin/"; + /* these paths are ok for readonly */ + restricted[16] = (char *) "/boot/"; + restricted[17] = (char *) "/vmlinuz"; + restricted[18] = (char *) "/initrd"; + restricted[19] = (char *) "/initrd.img"; + + /* don't check paths ok for readonly */ + if (readonly) + npaths = 16; + + if (!virFileExists(path)) + vah_warning("path does not exist, skipping file type checks"); + else { + if (stat(path, &sb) == -1) + goto end; + + rc = 1; + switch (sb.st_mode & S_IFMT) { + case S_IFDIR: + goto end; + break; + case S_IFIFO: + goto end; + break; + case S_IFSOCK: + goto end; + break; + default: + break; + } + } + + for (i = 0; i < npaths; i++) { + if (strlen(path) < strlen(restricted[i])) + continue; + + if (STREQLEN(path, restricted[i], strlen(restricted[i]))) { + rc = 1; + goto end; + } + } + rc = 0; + + end: + VIR_FREE(restricted); + return rc; +} + +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); + + if (virBufferError(buf)) { + vah_error(NULL, 0, "failed to allocate file buffer"); + rc = -1; + } + + clean: + free(tmp); + + return rc; +} + +static int +get_files(vahControl * ctl) +{ + virBuffer buf = VIR_BUFFER_INITIALIZER; + virBuffer uuid_buf = VIR_BUFFER_INITIALIZER; + int i; + int rc = -1; + 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); + virBufferVSprintf(&uuid_buf, "%s%s", AA_PREFIX, uuidstr); + if (virBufferError(&uuid_buf)) { + vah_error(ctl, 0, "failed to allocate file buffer"); + return rc; + } + uuid = virBufferContentAndReset(&uuid_buf); + 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) + if (vah_add_file(&buf, ctl->def->disks[i]->src, "rw") != 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; + + /* needs patch to hostusb.h to work + for (i = 0; i < ctl->def->nhostdevs; i++) + if (ctl->def->hostdevs[i]) { + virDomainHostdevDefPtr hostdev = ctl->def->hostdevs[i]; + if (hostdev->source.subsys.type == + VIR_DOMAIN_HOSTDEV_SUBSYS_TYPE_USB) { + usbDevice *usb = usbGetDevice(NULL, + hostdev->source.subsys.u.usb.bus, + hostdev->source.subsys.u.usb.device); + if (usb == NULL) + continue; + rc = vah_add_file(&buf, usb->path, "rw"); + usbFreeDevice(NULL, usb); + if (rc != 0) + goto clean; + } + } + */ + + if (ctl->newdisk) + if (vah_add_file(&buf, ctl->newdisk, "rw") != 0) + 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>] < 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 <libvirt/%s.files>\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); +}
Attachment:
signature.asc
Description: Digital signature
-- Libvir-list mailing list Libvir-list@xxxxxxxxxx https://www.redhat.com/mailman/listinfo/libvir-list