Currently the QEMU stdout/stderr streams are written directly to
a regular file (eg /var/log/libvirt/qemu/$GUEST.log). While those
can be rotated by logrotate (using copytruncate option) this is
not very efficient. It also leaves open a window of opportunity
for a compromised/broken QEMU to DOS the host filesystem by
writing lots of text to stdout/stderr.
This makes it possible to connect the stdout/stderr file handles
to a pipe that is provided by virtlogd. The virtlogd daemon will
read from this pipe and write data to the log file, performing
file rotation whenever a pre-determined size limit is reached.
Signed-off-by: Daniel P. Berrange <berrange(a)redhat.com>
---
cfg.mk | 2 +-
src/qemu/libvirtd_qemu.aug | 1 +
src/qemu/qemu.conf | 15 ++++
src/qemu/qemu_conf.c | 18 +++++
src/qemu/qemu_conf.h | 1 +
src/qemu/qemu_domain.c | 153 ++++++++++++++++++++++++++-----------
src/qemu/test_libvirtd_qemu.aug.in | 1 +
7 files changed, 145 insertions(+), 46 deletions(-)
diff --git a/cfg.mk b/cfg.mk
index 2a23b33..e35c79b 100644
--- a/cfg.mk
+++ b/cfg.mk
@@ -775,7 +775,7 @@ sc_prohibit_gettext_markup:
# lower-level code must not include higher-level headers.
cross_dirs=$(patsubst $(srcdir)/src/%.,%,$(wildcard $(srcdir)/src/*/.))
cross_dirs_re=($(subst / ,/|,$(cross_dirs)))
-mid_dirs=access|conf|cpu|locking|network|node_device|rpc|security|storage
+mid_dirs=access|conf|cpu|locking|logging|network|node_device|rpc|security|storage
sc_prohibit_cross_inclusion:
@for dir in $(cross_dirs); do \
case $$dir in \
diff --git a/src/qemu/libvirtd_qemu.aug b/src/qemu/libvirtd_qemu.aug
index 62951da..b6f6dc4 100644
--- a/src/qemu/libvirtd_qemu.aug
+++ b/src/qemu/libvirtd_qemu.aug
@@ -71,6 +71,7 @@ module Libvirtd_qemu =
| bool_entry "set_process_name"
| int_entry "max_processes"
| int_entry "max_files"
+ | str_entry "stdio_handler"
let device_entry = bool_entry "mac_filter"
| bool_entry "relaxed_acs_check"
diff --git a/src/qemu/qemu.conf b/src/qemu/qemu.conf
index 1c589a2..4fa5e8a 100644
--- a/src/qemu/qemu.conf
+++ b/src/qemu/qemu.conf
@@ -515,3 +515,18 @@
# "/usr/share/OVMF/OVMF_CODE.fd:/usr/share/OVMF/OVMF_VARS.fd",
# "/usr/share/AAVMF/AAVMF_CODE.fd:/usr/share/AAVMF/AAVMF_VARS.fd"
#]
+
+# The backend to use for handling stdout/stderr output from
+# QEMU processes.
+#
+# 'file': QEMU writes directly to a plain file. This is the
+# historical default, but allows QEMU to inflict a
+# denial of service attack on the host by exhausting
+# filesystem space
+#
+# 'logd': QEMU writes to a pipe provided by virtlogd daemon.
+# This is the current default, providing protection
+# against denial of service by performing log file
+# rollover when a size limit is hit.
+#
+#stdio_handler = "logd"
diff --git a/src/qemu/qemu_conf.c b/src/qemu/qemu_conf.c
index 658a50e..14683f5 100644
--- a/src/qemu/qemu_conf.c
+++ b/src/qemu/qemu_conf.c
@@ -454,6 +454,7 @@ int virQEMUDriverConfigLoadFile(virQEMUDriverConfigPtr cfg,
virConfValuePtr p;
int ret = -1;
size_t i;
+ char *stdioHandler = NULL;
/* Just check the file is readable before opening it, otherwise
* libvirt emits an error.
@@ -781,6 +782,23 @@ int virQEMUDriverConfigLoadFile(virQEMUDriverConfigPtr cfg,
GET_VALUE_ULONG("max_files", cfg->maxFiles);
GET_VALUE_STR("lock_manager", cfg->lockManagerName);
+ GET_VALUE_STR("stdio_handler", stdioHandler);
+ if (stdioHandler) {
+ if (STREQ(stdioHandler, "logd")) {
+ cfg->stdioLogD = true;
+ } else if (STREQ(stdioHandler, "file")) {
+ cfg->stdioLogD = false;
+ } else {
+ virReportError(VIR_ERR_CONFIG_UNSUPPORTED,
+ _("Unknown stdio handler %s"),
+ stdioHandler);
+ VIR_FREE(stdioHandler);
+ goto cleanup;
+ }
+ VIR_FREE(stdioHandler);
+ } else {
+ cfg->stdioLogD = true;
+ }
GET_VALUE_ULONG("max_queued", cfg->maxQueuedJobs);
diff --git a/src/qemu/qemu_conf.h b/src/qemu/qemu_conf.h
index ed9cd46..4b33770 100644
--- a/src/qemu/qemu_conf.h
+++ b/src/qemu/qemu_conf.h
@@ -173,6 +173,7 @@ struct _virQEMUDriverConfig {
int migrationPortMax;
bool logTimestamp;
+ bool stdioLogD;
/* Pairs of loader:nvram paths. The list is @nloader items long */
char **loader;
diff --git a/src/qemu/qemu_domain.c b/src/qemu/qemu_domain.c
index 2417cb7..466b5b7 100644
--- a/src/qemu/qemu_domain.c
+++ b/src/qemu/qemu_domain.c
@@ -41,6 +41,7 @@
#include "virstring.h"
#include "virthreadjob.h"
#include "viratomic.h"
+#include "logging/log_manager.h"
#include "storage/storage_driver.h"
@@ -80,8 +81,12 @@ VIR_ENUM_IMPL(qemuDomainAsyncJob, QEMU_ASYNC_JOB_LAST,
struct _qemuDomainLogContext {
int refs;
int writefd;
- int readfd;
+ int readfd; /* Only used if manager == NULL */
off_t pos;
+ ino_t inode; /* Only used if manager != NULL */
+ unsigned char uuid[VIR_UUID_BUFLEN]; /* Only used if manager != NULL */
+ char *name; /* Only used if manager != NULL */
+ virLogManagerPtr manager;
};
const char *
@@ -2260,46 +2265,68 @@ qemuDomainLogContextPtr qemuDomainLogContextNew(virQEMUDriverPtr
driver,
if (VIR_ALLOC(ctxt) < 0)
goto error;
+ VIR_DEBUG("Context new %p stdioLogD=%d", ctxt, cfg->stdioLogD);
ctxt->writefd = -1;
ctxt->readfd = -1;
virAtomicIntSet(&ctxt->refs, 1);
- if (virAsprintf(&logfile, "%s/%s.log", cfg->logDir,
vm->def->name) < 0)
- goto error;
+ if (cfg->stdioLogD) {
+ ctxt->manager = virLogManagerNew(virQEMUDriverIsPrivileged(driver));
+ if (!ctxt->manager)
+ goto error;
- if ((ctxt->writefd = open(logfile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR |
S_IWUSR)) < 0) {
- virReportSystemError(errno, _("failed to create logfile %s"),
- logfile);
- goto error;
- }
- if (virSetCloseExec(ctxt->writefd) < 0) {
- virReportSystemError(errno, _("failed to set close-on-exec flag on
%s"),
- logfile);
- goto error;
- }
+ if (VIR_STRDUP(ctxt->name, vm->def->name) < 0)
+ goto error;
- /* For unprivileged startup we must truncate the file since
- * we can't rely on logrotate. We don't use O_TRUNC since
- * it is better for SELinux policy if we truncate afterwards */
- if (mode == QEMU_DOMAIN_LOG_CONTEXT_MODE_START &&
- !virQEMUDriverIsPrivileged(driver) &&
- ftruncate(ctxt->writefd, 0) < 0) {
- virReportSystemError(errno, _("failed to truncate %s"),
- logfile);
- goto error;
- }
+ memcpy(ctxt->uuid, vm->def->uuid, VIR_UUID_BUFLEN);
- if (mode == QEMU_DOMAIN_LOG_CONTEXT_MODE_START) {
- if ((ctxt->readfd = open(logfile, O_RDONLY, S_IRUSR | S_IWUSR)) < 0) {
- virReportSystemError(errno, _("failed to open logfile %s"),
+ ctxt->writefd = virLogManagerDomainOpenLogFile(ctxt->manager,
+ "qemu",
+ vm->def->uuid,
+ vm->def->name,
+ 0,
+ &ctxt->inode,
+ &ctxt->pos);
+ if (ctxt->writefd < 0)
+ goto error;
+ } else {
+ if (virAsprintf(&logfile, "%s/%s.log", cfg->logDir,
vm->def->name) < 0)
+ goto error;
+
+ if ((ctxt->writefd = open(logfile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR |
S_IWUSR)) < 0) {
+ virReportSystemError(errno, _("failed to create logfile %s"),
logfile);
goto error;
}
- if (virSetCloseExec(ctxt->readfd) < 0) {
+ if (virSetCloseExec(ctxt->writefd) < 0) {
virReportSystemError(errno, _("failed to set close-on-exec flag on
%s"),
logfile);
goto error;
}
+
+ /* For unprivileged startup we must truncate the file since
+ * we can't rely on logrotate. We don't use O_TRUNC since
+ * it is better for SELinux policy if we truncate afterwards */
+ if (mode == QEMU_DOMAIN_LOG_CONTEXT_MODE_START &&
+ !virQEMUDriverIsPrivileged(driver) &&
+ ftruncate(ctxt->writefd, 0) < 0) {
+ virReportSystemError(errno, _("failed to truncate %s"),
+ logfile);
+ goto error;
+ }
+
+ if (mode == QEMU_DOMAIN_LOG_CONTEXT_MODE_START) {
+ if ((ctxt->readfd = open(logfile, O_RDONLY, S_IRUSR | S_IWUSR)) < 0) {
+ virReportSystemError(errno, _("failed to open logfile %s"),
+ logfile);
+ goto error;
+ }
+ if (virSetCloseExec(ctxt->readfd) < 0) {
+ virReportSystemError(errno, _("failed to set close-on-exec flag on
%s"),
+ logfile);
+ goto error;
+ }
+ }
}
virObjectUnref(cfg);
@@ -2323,9 +2350,10 @@ int qemuDomainLogContextWrite(qemuDomainLogContextPtr ctxt,
if (virVasprintf(&message, fmt, argptr) < 0)
goto cleanup;
- if (lseek(ctxt->writefd, 0, SEEK_END) < 0) {
+ if (!ctxt->manager &&
+ lseek(ctxt->writefd, 0, SEEK_END) < 0) {
virReportSystemError(errno, "%s",
- _("Unable to see to end of domain logfile"));
+ _("Unable to seek to end of domain logfile"));
goto cleanup;
}
if (safewrite(ctxt->writefd, message, strlen(message)) < 0) {
@@ -2346,29 +2374,51 @@ int qemuDomainLogContextWrite(qemuDomainLogContextPtr ctxt,
ssize_t qemuDomainLogContextRead(qemuDomainLogContextPtr ctxt,
char **msg)
{
+ VIR_DEBUG("Context read %p manager=%p inode=%llu pos=%llu",
+ ctxt, ctxt->manager,
+ (unsigned long long)ctxt->inode,
+ (unsigned long long)ctxt->pos);
char *buf;
- size_t buflen = 1024 * 128;
- ssize_t got;
+ size_t buflen;
+ if (ctxt->manager) {
+ buf = virLogManagerDomainReadLogFile(ctxt->manager,
+ "qemu",
+ ctxt->uuid,
+ ctxt->name,
+ ctxt->inode,
+ ctxt->pos,
+ 1024 * 128,
+ 0);
+ if (!buf)
+ return -1;
+ buflen = strlen(buf);
+ } else {
+ ssize_t got;
- /* Best effort jump to start of messages */
- ignore_value(lseek(ctxt->readfd, ctxt->pos, SEEK_SET));
+ buflen = 1024 * 128;
- if (VIR_ALLOC_N(buf, buflen) < 0)
- return -1;
+ /* Best effort jump to start of messages */
+ ignore_value(lseek(ctxt->readfd, ctxt->pos, SEEK_SET));
- got = saferead(ctxt->readfd, buf, buflen - 1);
- if (got < 0) {
- virReportSystemError(errno, "%s",
- _("Unable to read from log file"));
- return -1;
- }
+ if (VIR_ALLOC_N(buf, buflen) < 0)
+ return -1;
- buf[got] = '\0';
+ got = saferead(ctxt->readfd, buf, buflen - 1);
+ if (got < 0) {
+ virReportSystemError(errno, "%s",
+ _("Unable to read from log file"));
+ return -1;
+ }
+
+ buf[got] = '\0';
+
+ ignore_value(VIR_REALLOC_N_QUIET(buf, got + 1));
+ buflen = got;
+ }
- ignore_value(VIR_REALLOC_N_QUIET(buf, got + 1));
*msg = buf;
- return got;
+ return buflen;
}
@@ -2380,12 +2430,22 @@ int qemuDomainLogContextGetWriteFD(qemuDomainLogContextPtr ctxt)
void qemuDomainLogContextMarkPosition(qemuDomainLogContextPtr ctxt)
{
- ctxt->pos = lseek(ctxt->writefd, 0, SEEK_END);
+ if (ctxt->manager)
+ virLogManagerDomainGetLogFilePosition(ctxt->manager,
+ "qemu",
+ ctxt->uuid,
+ ctxt->name,
+ 0,
+ &ctxt->inode,
+ &ctxt->pos);
+ else
+ ctxt->pos = lseek(ctxt->writefd, 0, SEEK_END);
}
void qemuDomainLogContextRef(qemuDomainLogContextPtr ctxt)
{
+ VIR_DEBUG("Context ref %p", ctxt);
virAtomicIntInc(&ctxt->refs);
}
@@ -2398,9 +2458,12 @@ void qemuDomainLogContextFree(qemuDomainLogContextPtr ctxt)
return;
lastRef = virAtomicIntDecAndTest(&ctxt->refs);
+ VIR_DEBUG("Context free %p lastref=%d", ctxt, lastRef);
if (!lastRef)
return;
+ virLogManagerFree(ctxt->manager);
+ VIR_FREE(ctxt->name);
VIR_FORCE_CLOSE(ctxt->writefd);
VIR_FORCE_CLOSE(ctxt->readfd);
VIR_FREE(ctxt);
diff --git a/src/qemu/test_libvirtd_qemu.aug.in b/src/qemu/test_libvirtd_qemu.aug.in
index fc4935b..8bec743 100644
--- a/src/qemu/test_libvirtd_qemu.aug.in
+++ b/src/qemu/test_libvirtd_qemu.aug.in
@@ -78,3 +78,4 @@ module Test_libvirtd_qemu =
{ "1" =
"/usr/share/OVMF/OVMF_CODE.fd:/usr/share/OVMF/OVMF_VARS.fd" }
{ "2" =
"/usr/share/AAVMF/AAVMF_CODE.fd:/usr/share/AAVMF/AAVMF_VARS.fd" }
}
+{ "stdio_handler" = "logd" }
--
2.5.0