[systemd-devel] [PATCH] [RFCv7] Optionally save core dumps as plain files

Oleksii Shevchuk alxchk at gmail.com
Tue May 14 09:09:54 PDT 2013


Introduce configuration file: /etc/systemd/coredump.conf with
configurable core dumps backend storages (journal/file/both/none),
per storage size limits and preprocessing.

Default filestorage choosed as /run/log/coredump or /var/log/coredump
with next reason:
1. These files produced with systemd component
2. These files registered with journal

Main differences between v7 and v6:
1. As Colin suggested, create-then-drop policy used when storing to
   files.

2. {/var/,/run}/log/coredump/MACHINE-ID folder will be created with
   root:systemd-journal owner and drwxr-s-- permissions.

3. Trivial preprocessing support added. For example:

   PreprocessFile=gzip -9 <%i>%o
   PreprocessJournal=gdb -nw --batch --quiet --silent --ex 'thread \
   apply all bt' --core %i %e > %o

   This will save to file storage compressed cores and backtraces to
   journal COREDUMP= section.

4. Copying "optimizations" removed for now. Now core always stored
   to temporary folder, then optionaly preprocessed to temporary
   files, then temporary files copied to storage.

Weak places:
   coredump.c:495

   To prevent copying of large memory segments, I contruct virtual
   space manually. Probably it will not work as expected in some cases,
   so recomendations and comments needed..
---
 Makefile-man.am                  |   1 +
 Makefile.am                      |  14 +-
 man/coredump.conf.xml            | 217 +++++++++++
 src/core/manager.c               |   1 +
 src/journal/coredump-gperf.gperf |  24 ++
 src/journal/coredump.c           | 764 ++++++++++++++++++++++++++++++++-------
 src/journal/coredump.conf        |  18 +
 src/journal/coredump.h           |  89 +++++
 src/journal/journald-server.h    |   2 +-
 9 files changed, 992 insertions(+), 138 deletions(-)
 create mode 100644 man/coredump.conf.xml
 create mode 100644 src/journal/coredump-gperf.gperf
 create mode 100644 src/journal/coredump.conf
 create mode 100644 src/journal/coredump.h

diff --git a/Makefile-man.am b/Makefile-man.am
index 481423a..e3ff8dd 100644
--- a/Makefile-man.am
+++ b/Makefile-man.am
@@ -8,6 +8,7 @@ MANPAGES += \
 	man/hostname.5 \
 	man/journalctl.1 \
 	man/journald.conf.5 \
+	man/coredump.conf.5 \
 	man/kernel-command-line.7 \
 	man/kernel-install.8 \
 	man/locale.conf.5 \
diff --git a/Makefile.am b/Makefile.am
index eb85c8d..96e5bc3 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -2908,7 +2908,8 @@ nodist_systemunit_DATA += \
 	units/systemd-journal-flush.service
 
 dist_pkgsysconf_DATA += \
-	src/journal/journald.conf
+	src/journal/journald.conf \
+	src/journal/coredump.conf
 
 pkgconfiglib_DATA += \
 	src/journal/libsystemd-journal.pc
@@ -2927,10 +2928,12 @@ EXTRA_DIST += \
 	src/journal/libsystemd-journal.sym \
 	units/systemd-journald.service.in \
 	units/systemd-journal-flush.service.in \
-	src/journal/journald-gperf.gperf
+	src/journal/journald-gperf.gperf \
+	src/journal/coredump-gperf.gperf
 
 CLEANFILES += \
-	src/journal/journald-gperf.c
+	src/journal/journald-gperf.c \
+	src/journal/coredump-gperf.c
 
 # ------------------------------------------------------------------------------
 if HAVE_MICROHTTPD
@@ -2975,10 +2978,13 @@ EXTRA_DIST += \
 # ------------------------------------------------------------------------------
 if ENABLE_COREDUMP
 systemd_coredump_SOURCES = \
-	src/journal/coredump.c
+	src/shared/specifier.c \
+	src/journal/coredump.c \
+	src/journal/coredump-gperf.c
 
 systemd_coredump_LDADD = \
 	libsystemd-journal-internal.la \
+	libsystemd-id128-internal.la \
 	libsystemd-label.la \
 	libsystemd-shared.la
 
diff --git a/man/coredump.conf.xml b/man/coredump.conf.xml
new file mode 100644
index 0000000..491b9d0
--- /dev/null
+++ b/man/coredump.conf.xml
@@ -0,0 +1,217 @@
+<?xml version='1.0'?> <!--*-nxml-*-->
+<?xml-stylesheet type="text/xsl" href="http://docbook.sourceforge.net/release/xsl/current/xhtml/docbook.xsl"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+        "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+
+<!--
+  This file is part of systemd.
+
+  Copyright 2013 Lennart Poettering
+                 Oleksii Shevchuk
+
+  systemd 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.
+
+  systemd is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+  Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with systemd; If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<refentry id="coredump.conf">
+        <refentryinfo>
+                <title>coredump.conf</title>
+                <productname>systemd</productname>
+
+                <authorgroup>
+                        <author>
+                                <contrib>Developer</contrib>
+                                <firstname>Lennart</firstname>
+                                <surname>Poettering</surname>
+                                <email>lennart at poettering.net</email>
+                        </author>
+                        <author>
+                                <contrib>Developer</contrib>
+                                <firstname>Oleksii</firstname>
+                                <surname>Shevchuk</surname>
+                                <email>alxchk at gmail.com</email>
+                        </author>
+                </authorgroup>
+        </refentryinfo>
+
+        <refmeta>
+                <refentrytitle>coredump.conf</refentrytitle>
+                <manvolnum>5</manvolnum>
+        </refmeta>
+
+        <refnamediv>
+                <refname>coredump.conf</refname>
+                <refpurpose>Core dump utility configuration file</refpurpose>
+        </refnamediv>
+
+        <refsynopsisdiv>
+                <para><filename>/etc/systemd/coredump.conf</filename></para>
+        </refsynopsisdiv>
+
+        <refsect1>
+                <title>Description</title>
+
+                <para>This files configures several parameters of the
+                systemd-coredump utility.</para>
+
+        </refsect1>
+
+        <refsect1>
+                <title>Options</title>
+
+                <para>All options are configured in the
+                <literal>[Coredump]</literal> section:</para>
+
+                <variablelist>
+
+                        <varlistentry>
+                                <term><varname>FileStorage=</varname></term>
+
+                                <listitem><para>One of
+                                <literal>volatile</literal> or,
+                                <literal>persistent</literal>
+                                value. If <literal>volatile</literal>, core dump will be
+                                stored as file to /run/log/coredump/MACHINE-ID/COMM-TMPSUFFIX location;
+                                if <literal>persistent</literal> -- to
+                                /var/log/coredump/MACHINE-ID/COMM-TMPSUFFIX location.
+                                Storage directory will be created if not exists.
+                                COMM will be escaped.
+                                Field COREDUMP_FILE= will be added to log
+                                message with COMM-TMPSUFFIX value.</para></listitem>
+                        </varlistentry>
+
+                        <varlistentry>
+                                <term><varname>MaxJournalCoreSize=</varname></term>
+                                <term><varname>MaxFileCoreSize=</varname></term>
+
+                                <listitem><para>Enforce size limits on the core
+                                dumps stored. The option prefixed with <literal>Journal</literal>
+                                apply to the COREDUMP= journal message field. The option prefixed
+                                with <literal>File</literal> apply to the files when stored on a
+                                persistent file system, more specifically
+                                <filename>/var/log/coredump/MACHINE-ID</filename>. If value equals
+                                to zero, then backend will be ommited.
+                                </para></listitem>
+                        </varlistentry>
+
+                        <varlistentry>
+                          <term><varname>PreprocessUser=</varname></term>
+
+                          <listitem><para>If unspecified <literal>systemd-coredump</literal> will drop privileges to nobody user
+                          to store core to temporary folder and process it. When value is unset, <literal>systemd-coredump</literal>
+                          will drop privileges to process owner uid/gid. When specified, privileges will be changed to that user.
+                        </para></listitem>
+                        </varlistentry>
+
+                        <varlistentry>
+                          <term><varname>PreprocessJournal=</varname></term>
+                          <term><varname>PreprocessFile=</varname></term>
+
+                          <listitem><para>Core dumps can be preprocessed before storing to storages via command specified
+                          by this option. Processing done by calling specified tools via system() call. Next specifiers can be
+                          used:</para>
+
+                          <table>
+                            <title>Specifiers available in processing commands</title>
+                            <tgroup cols='3' align='left' colsep='1' rowsep='1'>
+                              <colspec colname="spec" />
+                              <colspec colname="mean" />
+                              <colspec colname="detail" />
+                              <thead>
+                                <row>
+                                  <entry>Specifier</entry>
+                                  <entry>Meaning</entry>
+                                  <entry>Details</entry>
+                                </row>
+                              </thead>
+                              <tbody>
+                                <row>
+                                  <entry><literal>%p</literal></entry>
+                                  <entry>Crashed process PID</entry>
+                                  <entry></entry>
+                                </row>
+                                <row>
+                                  <entry><literal>%u</literal></entry>
+                                  <entry>Crashed process UID</entry>
+                                  <entry></entry>
+                                </row>
+                                <row>
+                                  <entry><literal>%g</literal></entry>
+                                  <entry>Crashed process GID</entry>
+                                  <entry></entry>
+                                </row>
+                                <row>
+                                  <entry><literal>%s</literal></entry>
+                                  <entry>Signal, which caused dumping the core</entry>
+                                  <entry></entry>
+                                </row>
+                                <row>
+                                  <entry><literal>%t</literal></entry>
+                                  <entry>Timestamp of crash</entry>
+                                  <entry></entry>
+                                </row>
+                                <row>
+                                  <entry><literal>%e</literal></entry>
+                                  <entry>Crashed process EXE path</entry>
+                                  <entry></entry>
+                                </row>
+                                <row>
+                                  <entry><literal>%U</literal></entry>
+                                  <entry>Crashed process unit name</entry>
+                                  <entry></entry>
+                                </row>
+                                <row>
+                                  <entry><literal>%S</literal></entry>
+                                  <entry>Crashed process session</entry>
+                                  <entry>Available only when builded with logind</entry>
+                                </row>
+                                <row>
+                                  <entry><literal>%C</literal></entry>
+                                  <entry>Crashed process command line</entry>
+                                  <entry></entry>
+                                </row>
+                                <row>
+                                  <entry><literal>%i</literal></entry>
+                                  <entry>Temporary file with stored core</entry>
+                                  <entry>This file shouldn't be removed or modified</entry>
+                                </row>
+                                <row>
+                                  <entry><literal>%o</literal></entry>
+                                  <entry>Temporary file that should contain result of processing</entry>
+                                  <entry>If this file doesn't exists, then input file will be used</entry>
+                                </row>
+                              </tbody>
+                            </tgroup>
+                          </table>
+                        </listitem>
+                        </varlistentry>
+
+                        <varlistentry><term><varname>PreprocessMaxSize=</varname></term>
+
+                        <listitem><para>This will limit stored and processed core size.</para></listitem>
+                        </varlistentry>
+                      </variablelist>
+                    </refsect1>
+
+        <refsect1>
+                  <title>See Also</title>
+                  <para>
+                          <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+                          <citerefentry><refentrytitle>systemd-coredumpctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+                          <citerefentry><refentrytitle>journalctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+                          <citerefentry><refentrytitle>systemd.journal-fields</refentrytitle><manvolnum>7</manvolnum></citerefentry>,
+                          <citerefentry><refentrytitle>systemd-journald.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+                  </para>
+        </refsect1>
+
+</refentry>
diff --git a/src/core/manager.c b/src/core/manager.c
index 0508628..efce9ac 100644
--- a/src/core/manager.c
+++ b/src/core/manager.c
@@ -51,6 +51,7 @@
 #include "hashmap.h"
 #include "macro.h"
 #include "strv.h"
+#include "env-util.h"
 #include "log.h"
 #include "util.h"
 #include "mkdir.h"
diff --git a/src/journal/coredump-gperf.gperf b/src/journal/coredump-gperf.gperf
new file mode 100644
index 0000000..1bc599c
--- /dev/null
+++ b/src/journal/coredump-gperf.gperf
@@ -0,0 +1,24 @@
+%{
+#include <stddef.h>
+#include "coredump.h"
+#include "conf-parser.h"
+%}
+struct ConfigPerfItem;
+%null_strings
+%language=ANSI-C
+%define slot-name section_and_lvalue
+%define hash-function-name coredump_gperf_hash
+%define lookup-function-name coredump_gperf_lookup
+%readonly-tables
+%omit-struct-type
+%struct-type
+%includes
+%%
+Coredump.MaxJournalCoreSize,      config_parse_bytes_off,  0, offsetof(Coredump, journal_max_size)
+Coredump.MaxFileCoreSize,         config_parse_bytes_off,  0, offsetof(Coredump, file_max_size)
+Coredump.FileStorage,             config_parse_storage,    0, offsetof(Coredump, storage)
+Coredump.PreprocessUser,          config_parse_string,     0, offsetof(Coredump, preprocess_user)
+Coredump.PreprocessJournal,       config_parse_string,     0, offsetof(Coredump, preprocess_journal)
+Coredump.PreprocessFile,          config_parse_string,     0, offsetof(Coredump, preprocess_file)
+Coredump.PreprocessMaxSize,       config_parse_bytes_off,  0, offsetof(Coredump, preprocess_max_size)
+Coredump.PreprocessDirectoryPath, config_parse_string,     0, offsetof(Coredump, preprocess_directory_path)
diff --git a/src/journal/coredump.c b/src/journal/coredump.c
index fd03e38..473499d 100644
--- a/src/journal/coredump.c
+++ b/src/journal/coredump.c
@@ -3,7 +3,8 @@
 /***
   This file is part of systemd.
 
-  Copyright 2012 Lennart Poettering
+  Copyright 2012-2013 Lennart Poettering
+                      Oleksii Shevchuk
 
   systemd is free software; you can redistribute it and/or modify it
   under the terms of the GNU Lesser General Public License as published by
@@ -22,7 +23,12 @@
 #include <errno.h>
 #include <unistd.h>
 #include <stdio.h>
+#include <stdint.h>
+#include <fcntl.h>
+#include <sys/mman.h>
 #include <sys/prctl.h>
+#include <sys/wait.h>
+#include <sys/ptrace.h>
 
 #include <systemd/sd-journal.h>
 
@@ -35,13 +41,13 @@
 #include "macro.h"
 #include "mkdir.h"
 #include "special.h"
+#include "sd-id128.h"
+#include "coredump.h"
 #include "cgroup-util.h"
+#include "specifier.h"
 
-/* Few programs have less than 3MiB resident */
-#define COREDUMP_MIN_START (3*1024*1024)
-/* Make sure to not make this larger than the maximum journal entry
- * size. See ENTRY_SIZE_MAX in journald-native.c. */
-#define COREDUMP_MAX (768*1024*1024)
+#define COREDUMP_MAX_DEFAULT ( 24 * 1024 * 1024 )
+#define COREDUMP_CONFIG "/etc/systemd/coredump.conf"
 
 enum {
         ARG_PID = 1,
@@ -53,26 +59,84 @@ enum {
         _ARG_MAX
 };
 
-static int divert_coredump(void) {
-        _cleanup_fclose_ FILE *f = NULL;
+static const char* const storage_table[] = {
+        [STORAGE_VOLATILE] = "volatile",
+        [STORAGE_PERSISTENT] = "persistent",
+};
 
-        log_info("Detected coredump of the journal daemon itself, diverting coredump to /var/lib/systemd/coredump/.");
+DEFINE_STRING_TABLE_LOOKUP(storage, Storage);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_storage, storage, Storage, "Failed to parse storage setting");
 
-        mkdir_p_label("/var/lib/systemd/coredump", 0755);
+static int coredump_drop_creds(uid_t uid, uid_t gid) {
 
-        f = fopen("/var/lib/systemd/coredump/core.systemd-journald", "we");
-        if (!f) {
-                log_error("Failed to create coredump file: %m");
+
+        if (setresgid(gid, gid, gid) < 0 ||
+            setresuid(uid, uid, uid) < 0) {
+                log_error("Failed to drop privileges: %m");
                 return -errno;
         }
 
+        umask(0337);
+
+        return 0;
+}
+
+static char * coredump_tmp_directory(Coredump *s) {
+        assert(s);
+
+        return strdup(s->preprocess_directory_path);
+}
+
+static char * coredump_directory(Coredump *s) {
+        sd_id128_t machineid;
+        char buffer[33];
+
+        int r = sd_id128_get_machine(&machineid);
+        if (r)
+                return NULL;
+
+        return strjoin(s->storage == STORAGE_VOLATILE ?
+                       "/run/log/coredump/":
+                       "/var/log/coredump/",
+                       sd_id128_to_string(machineid, buffer),
+                       NULL);
+}
+
+static int coredump_ensure_directory_exists(Coredump *s) {
+        static const mode_t mode = S_ISGID
+                | S_IRUSR | S_IWUSR | S_IXUSR
+                | S_IRGRP | S_IXGRP
+                ;
+        int r;
+        _cleanup_free_ char *d = NULL;
+
+        assert(s);
+
+        d = coredump_directory(s);
+        if (!d)
+                return log_oom();
+
+        r = mkdir_parents_label(d, 0755);
+        if (r)
+                return -errno;
+
+        r = mkdir_safe_label(d, mode, 0, s->journal);
+        if (r && (errno != EEXIST))
+                return -errno;
+
+        return 0;
+}
+
+static int coredump_stdio_to_file(FILE *corefile, size_t max_size) {
+        size_t offset = 0;
         for (;;) {
                 uint8_t buffer[4096];
                 size_t l, q;
 
-                l = fread(buffer, 1, sizeof(buffer), stdin);
+                l = fread(buffer, 1, MIN(sizeof(buffer), max_size - offset),
+                          stdin);
                 if (l <= 0) {
-                        if (ferror(f)) {
+                        if (ferror(corefile)) {
                                 log_error("Failed to read coredump: %m");
                                 return -errno;
                         }
@@ -80,16 +144,19 @@ static int divert_coredump(void) {
                         break;
                 }
 
-                q = fwrite(buffer, 1, l, f);
+                q = fwrite(buffer, 1, l, corefile);
                 if (q != l) {
                         log_error("Failed to write coredump: %m");
                         return -errno;
                 }
+
+                if ((offset += q) >= max_size)
+                        break;
         }
 
-        fflush(f);
+        fflush(corefile);
 
-        if (ferror(f)) {
+        if (ferror(corefile)) {
                 log_error("Failed to write coredump: %m");
                 return -errno;
         }
@@ -97,181 +164,612 @@ static int divert_coredump(void) {
         return 0;
 }
 
-int main(int argc, char* argv[]) {
-        int r, j = 0;
-        char *t;
-        ssize_t n;
-        pid_t pid;
-        uid_t uid;
-        gid_t gid;
-        struct iovec iovec[14];
-        size_t coredump_bufsize, coredump_size;
-        _cleanup_free_ char *core_pid = NULL, *core_uid = NULL, *core_gid = NULL, *core_signal = NULL,
-                *core_timestamp = NULL, *core_comm = NULL, *core_exe = NULL, *core_unit = NULL,
-                *core_session = NULL, *core_message = NULL, *core_cmdline = NULL, *coredump_data = NULL;
+static int coredump_create_file(Coredump *s, char ** name, bool shared) {
+        _cleanup_free_ char *template = NULL, *escaped_comm = NULL, *directory = NULL;
+        int fd;
 
-        prctl(PR_SET_DUMPABLE, 0);
+        assert(s);
+        assert(name);
+        assert(s->core_comm);
 
-        if (argc != _ARG_MAX) {
-                log_set_target(LOG_TARGET_JOURNAL_OR_KMSG);
-                log_open();
+        directory = shared
+                    ? coredump_tmp_directory(s)
+                    : coredump_directory(s)
+                    ;
 
-                log_error("Invalid number of arguments passed from kernel.");
-                r = -EINVAL;
-                goto finish;
+        if (! directory)
+                return log_oom();
+
+        escaped_comm = xescape(s->core_comm, "/ ");
+        if (! escaped_comm)
+                return log_oom();
+
+        template = strjoin(directory, "/", escaped_comm, "-XXXXXX", NULL);
+        if (! template)
+                return log_oom();
+
+        fd = mkostemp(template, O_CREAT | O_EXCL | O_APPEND | O_RDWR | shared ? 0 : O_CLOEXEC);
+
+        if (fd < 0)
+                return -errno;
+
+        if (fchmod(fd, S_IRUSR | S_IRGRP))
+                return -errno;
+
+        *name = strdup(template);
+        if (! *name) {
+                close(fd);
+                return log_oom();
         }
 
-        r = parse_pid(argv[ARG_PID], &pid);
+        return fd;
+}
+
+static void coredump_cleanup_str(char *mem, size_t size) {
+        (void)size;
+        free(mem);
+}
+
+static void coredump_cleanup_mem(char *mem, size_t size) {
+        munmap(mem, size);
+}
+
+static char* specifier_quoted_string(char specifier, void *data, void *userdata) {
+        return strjoin("'", strempty(data), "'", NULL);
+}
+
+static char* coredump_printf(Coredump *s,
+                             char *input,
+                             char *output,
+                             char *format) {
+
+        const Specifier table[] = {
+                { 'p', specifier_string, s->core_pid },
+                { 'u', specifier_string, s->core_uid },
+                { 'g', specifier_string, s->core_gid },
+                { 's', specifier_string, s->core_signal },
+                { 't', specifier_string, s->core_timestamp },
+                { 'e', specifier_quoted_string, s->core_exe },
+                { 'U', specifier_quoted_string, s->core_unit },
+#ifdef HAVE_LOGIND
+                { 'S', specifier_quoted_string, s->core_session },
+#endif
+                { 'C', specifier_quoted_string, s->core_cmdline },
+                { 'i', specifier_quoted_string, input },
+                { 'o', specifier_quoted_string, output },
+                { 0, NULL, NULL }
+        };
+
+        return specifier_printf(format, table, s);
+}
+
+static int coredump_mmap_file(const char *path, char *addr, char **mem, size_t *size)
+{
+        struct stat stat;
+        int fd;
+
+        assert(path);
+        assert(mem);
+        assert(size);
+
+        fd = open(path, O_NOFOLLOW | O_RDONLY);
+
+        if (fd != -1 && !fstat(fd, &stat) &&
+            (*mem = mmap(addr, stat.st_size,
+                         PROT_READ, MAP_SHARED,
+                         fd,
+                         0)) != MAP_FAILED) {
+                *size = stat.st_size;
+                close(fd);
+                return 0;
+        } else {
+                *mem = NULL;
+                *size = 0;
+                log_error("Couldn't mmap file %s: %m", path);
+                return -errno;
+        }
+}
+
+
+static int coredump_load_state(char ** argv, Coredump *s) {
+        int r;
+        const char * journal_group = "systemd-journal";
+        const char * preprocess_user = "nobody";
+        _cleanup_fclose_ FILE *f = NULL;
+
+        assert(s);
+
+        zero(*s);
+
+        s->journal_max_size = COREDUMP_MAX_DEFAULT;
+        s->file_max_size = 0;
+        s->preprocess_max_size = SIZE_MAX;
+        s->storage = STORAGE_VOLATILE;
+        s->file_fd = -1;
+        s->journal = 0;
+        s->core_user_unit = false;
+        s->preprocess_directory_path = strdup("/tmp");
+        s->preprocess_user = strdup("nobody");
+
+        s->core_pid = argv[ARG_PID];
+        s->core_uid = argv[ARG_UID];
+        s->core_gid = argv[ARG_GID];
+        s->core_signal = argv[ARG_SIGNAL];
+        s->core_comm = argv[ARG_COMM];
+        s->core_timestamp = argv[ARG_TIMESTAMP];
+
+        r = parse_pid(s->core_pid, &s->process_pid);
         if (r < 0) {
                 log_set_target(LOG_TARGET_JOURNAL_OR_KMSG);
                 log_open();
-
                 log_error("Failed to parse PID.");
-                goto finish;
+                return r;
         }
 
-        if (cg_pid_get_unit(pid, &t) >= 0) {
+        if (cg_pid_get_unit(s->process_pid, &s->core_unit) >= 0) {
+                char *t = NULL;
+                s->core_user_unit = false;
 
-                if (streq(t, SPECIAL_JOURNALD_SERVICE)) {
-                        /* Make sure we don't make use of the journal,
-                         * if it's the journal which is crashing */
+                if(streq(t, SPECIAL_JOURNALD_SERVICE)) {
+                        s->journal_core_dump = true;
                         log_set_target(LOG_TARGET_KMSG);
-                        log_open();
-
-                        r = divert_coredump();
-                        goto finish;
-                }
-
-                core_unit = strappend("COREDUMP_UNIT=", t);
-        } else if (cg_pid_get_user_unit(pid, &t) >= 0)
-                core_unit = strappend("COREDUMP_USER_UNIT=", t);
+                } else
+                        log_set_target(LOG_TARGET_JOURNAL_OR_KMSG);
+                free(t);
+        } else {
+                log_set_target(LOG_TARGET_JOURNAL_OR_KMSG);
+                if (cg_pid_get_user_unit(s->process_pid, &s->core_unit) >= 0)
+                        s->core_user_unit = true;
+        }
 
-        if (core_unit)
-                IOVEC_SET_STRING(iovec[j++], core_unit);
+        if (s->journal_core_dump) {
+                s->file_max_size = SIZE_MAX;
+                s->journal_max_size = 0;
+                if (! s->storage)
+                        s->storage = STORAGE_VOLATILE;
+                log_info(SPECIAL_JOURNALD_SERVICE " failed. Dumping to directory");
+        }
 
-        /* OK, now we know it's not the journal, hence make use of
-         * it */
-        log_set_target(LOG_TARGET_JOURNAL_OR_KMSG);
         log_open();
 
-        r = parse_uid(argv[ARG_UID], &uid);
+        r = parse_uid(argv[ARG_UID], &s->process_uid);
         if (r < 0) {
                 log_error("Failed to parse UID.");
-                goto finish;
+                return r;
         }
 
-        r = parse_gid(argv[ARG_GID], &gid);
+        r = parse_gid(argv[ARG_GID], &s->process_gid);
         if (r < 0) {
                 log_error("Failed to parse GID.");
-                goto finish;
+                return r;
         }
 
-        core_pid = strappend("COREDUMP_PID=", argv[ARG_PID]);
-        if (core_pid)
-                IOVEC_SET_STRING(iovec[j++], core_pid);
+        if (get_group_creds(&journal_group, &s->journal))
+                log_warning("Failed to get group %s creds", journal_group);
 
-        core_uid = strappend("COREDUMP_UID=", argv[ARG_UID]);
-        if (core_uid)
-                IOVEC_SET_STRING(iovec[j++], core_uid);
+        s->preprocess_uid = s->process_uid;
+        s->preprocess_gid = s->process_gid;
 
-        core_gid = strappend("COREDUMP_GID=", argv[ARG_GID]);
-        if (core_gid)
-                IOVEC_SET_STRING(iovec[j++], core_gid);
+        if (s->preprocess_user) {
+                preprocess_user = s->preprocess_user;
+                if (get_user_creds(&preprocess_user, &s->preprocess_uid, &s->preprocess_gid, NULL, NULL))
+                        log_warning("Failed to get user %s creds", preprocess_user);
+        }
 
-        core_signal = strappend("COREDUMP_SIGNAL=", argv[ARG_SIGNAL]);
-        if (core_signal)
-                IOVEC_SET_STRING(iovec[j++], core_signal);
+        f = fopen(COREDUMP_CONFIG, "re");
+        if (!f) {
+                if (errno == ENOENT)
+                        return 0;
 
-        core_comm = strappend("COREDUMP_COMM=", argv[ARG_COMM]);
-        if (core_comm)
-                IOVEC_SET_STRING(iovec[j++], core_comm);
+                log_warning("Failed to open configuration file " COREDUMP_CONFIG ": %m");
+                return -errno;
+        }
+
+        r = config_parse(NULL, COREDUMP_CONFIG, f, "Coredump\0", config_item_perf_lookup,
+                         (void*) coredump_gperf_lookup, false, false, s);
+        if (r < 0)
+                log_warning("Failed to parse configuration file: %s", strerror(-r));
+
+        if (s->file_max_size) {
+                r = coredump_ensure_directory_exists(s);
+                if (r) {
+                        log_error("Couldn't create coredump directory: %m");
+                        return r;
+                }
+
+                s->file_fd = coredump_create_file(s, &s->file_path, false);
+                if (s->file_fd < 0) {
+                        log_error("Couldn't create core dump file: %m");
+                        return s->file_fd;
+                }
+        }
 
 #ifdef HAVE_LOGIND
-        if (sd_pid_get_session(pid, &t) >= 0) {
-                core_session = strappend("COREDUMP_SESSION=", t);
-                free(t);
+        sd_pid_get_session(s->process_pid, &s->core_session);
+#endif
+        get_process_exe(s->process_pid, &s->core_exe);
+        get_process_cmdline(s->process_pid, 0, false, &s->core_cmdline);
 
-                if (core_session)
-                        IOVEC_SET_STRING(iovec[j++], core_session);
+        return 0;
+}
+
+static void coredump_release_state(Coredump * s) {
+        assert(s);
+
+        if (s->file_max_size) {
+                close(s->file_fd);
+                free(s->file_path);
         }
 
+        if (s->processed_journal_data)
+                munmap(s->processed_journal_data,
+                       s->processed_journal_data_size);
+        if (s->processed_file_data)
+                munmap(s->processed_file_data,
+                       s->processed_file_data_size);
+#ifdef HAVE_LOGIND
+        free(s->core_session);
 #endif
+        free(s->core_cmdline);
+        free(s->core_unit);
+        free(s->core_exe);
+        free(s->preprocess_user);
+        free(s->preprocess_journal);
+        free(s->preprocess_file);
+        free(s->preprocess_directory_path);
+}
 
-        if (get_process_exe(pid, &t) >= 0) {
-                core_exe = strappend("COREDUMP_EXE=", t);
-                free(t);
+static int coredump_preprocess(Coredump *s) {
+        int r;
+
+        _cleanup_fclose_ FILE *tmp_file = NULL;
+        _cleanup_close_ int tmp_fd = -1, journal_tmp_fd = -1, file_tmp_fd = -1;
+        _cleanup_free_ char *tmp_filename = NULL, *journal_tmp_filename = NULL,
+                       *file_tmp_filename = NULL, *preprocess_journal = NULL,
+                         *preprocess_file = NULL;
+
+        struct stat stat;
+
+        int child;
+        int status;
+
+        assert(s);
+
+        tmp_fd = coredump_create_file(s, &tmp_filename, true);
+        if (tmp_fd < 0) {
+                log_error("Couldn't create temporary storage file: %m");
+                return -errno;
+        }
+
+        if (fchown(tmp_fd, s->preprocess_uid, s->preprocess_gid))
+                return -errno;
+
+        if (s->preprocess_file) {
+                file_tmp_fd = coredump_create_file(s, &file_tmp_filename, true);
+                if (file_tmp_fd < 0)
+                        return file_tmp_fd;
+                if (fchmod(file_tmp_fd, 0600))
+                        return -errno;
+                if (fchown(file_tmp_fd, s->preprocess_uid, s->preprocess_gid))
+                        return -errno;
+                close(file_tmp_fd);
+                file_tmp_fd = -1;
+        }
+
+        if (s->preprocess_journal) {
+                journal_tmp_fd = coredump_create_file(s, &journal_tmp_filename, true);
+                if (journal_tmp_fd < 0)
+                        return file_tmp_fd;
+                if (fchmod(journal_tmp_fd, 0600))
+                        return -errno;
+                if (fchown(journal_tmp_fd, s->preprocess_uid, s->preprocess_gid))
+                        return -errno;
+                close(journal_tmp_fd);
+                journal_tmp_fd = -1;
+        }
+
+        child = fork();
+        if (child == -1)
+                return -errno;
+
+        if (child) {
+                static const char COREDUMP[] = "COREDUMP=";
+                int rj, rf;
+                int header_size = (strlen(COREDUMP)/(sysconf(_SC_PAGE_SIZE)) + 1) * sysconf(_SC_PAGE_SIZE);
+
+                if (waitpid(child, &status, 0) == -1)
+                        return -errno;
+                if (! (WIFEXITED(status) && WEXITSTATUS(status) == 0))
+                        return -1;
+
+                rj = coredump_mmap_file(s->preprocess_journal ? journal_tmp_filename : tmp_filename,
+                                        NULL,
+                                        &s->processed_journal_data,
+                                        &s->processed_journal_data_size);
+
+                if (!rj) {
+                        /* Ok. sd_journal_sendv doesn't support scatter/gatter.
+                         * Let's workaround it with dark mmap magic. If you know how to fix that without
+                         * moving unknown size of data - let me know */
+
+                        s->journal_message_header = mmap(s->processed_journal_data-header_size,
+                                                         header_size,
+                                                         PROT_WRITE | PROT_READ,
+                                                         MAP_ANONYMOUS | MAP_PRIVATE,
+                                                         -1,
+                                                         0);
+
+                        if (s->journal_message_header == MAP_FAILED) {
+                                r = -errno;
+                                log_error("Couldn't create padding for journal output: %m");
+                                return r;
+                        }
+
+                        assert(s->processed_journal_data - s->journal_message_header == header_size);
+                        memcpy(s->processed_journal_data-strlen(COREDUMP), COREDUMP, strlen(COREDUMP));
+                }
+
+                rf = coredump_mmap_file(s->preprocess_file ? file_tmp_filename : tmp_filename,
+                                        NULL,
+                                        &s->processed_file_data,
+                                        &s->processed_file_data_size);
+
+                unlink(tmp_filename);
+
+                if (s->preprocess_journal)
+                        unlink(journal_tmp_filename);
+
+                if (s->preprocess_file)
+                        unlink(file_tmp_filename);
+
+                return - (rj && rf);
+        }
+
+        coredump_drop_creds(s->preprocess_uid, s->preprocess_gid);
+
+        tmp_file = fdopen(tmp_fd, "rew+");
+        if (! tmp_file)
+                return -errno;
+
+        r = coredump_stdio_to_file(tmp_file, s->preprocess_max_size);
+        if (r)
+                return r;
+
+        r = fstat(fileno(tmp_file), &stat);
+        if (r)
+                return -errno;
+
+        fflush(tmp_file);
+
+        if (s->preprocess_journal) {
+                preprocess_journal = coredump_printf(s, tmp_filename, journal_tmp_filename, s->preprocess_journal);
+                unlink(journal_tmp_filename);
+                if (system(preprocess_journal))
+                        unlink(journal_tmp_filename);
+        }
+
+        if (s->preprocess_file) {
+                preprocess_file = coredump_printf(s, tmp_filename, file_tmp_filename, s->preprocess_file);
+                unlink(file_tmp_filename);
+                if (system(preprocess_file))
+                        unlink(file_tmp_filename);
+        }
+
+        exit(0);
+}
+
+static int coredump_store_memory_to_fd(const char * memory,
+                                       size_t memory_size,
+                                       int fd,
+                                       const char * fd_path,
+                                       size_t max_size,
+                                       char ** message,
+                                       size_t *message_size) {
+        static const char COREDUMP[] = "COREDUMP_FILE=";
+        _cleanup_fclose_ FILE *core = NULL;
+        char * m = NULL;
+
+        int r;
+
+        assert_se(fd >= 0);
+        assert_se(message);
+        assert_se(message_size);
+
+        *message = NULL;
+        memory_size = MIN(memory_size, max_size);
+
+        r = ftruncate(fd, memory_size);
+        if (r) {
+                log_error("Couldn't truncate backed file: %m");
+                close(fd);
+                return -errno;
+        }
+
+        m = mmap(NULL, memory_size, PROT_WRITE, MAP_SHARED, fd, 0);
+        if (m == MAP_FAILED) {
+                log_error("Couldn't mmap backed file: %m");
+                close(fd);
+                return -errno;
+        }
+
+        memcpy(m, memory, memory_size);
+        munmap(m, memory_size);
+        close(fd);
+
+        assert(strrchr(fd_path, '/'));
+
+        *message = strjoin(COREDUMP, strrchr(fd_path, '/') + 1, NULL);
+
+        if (! *message) {
+                *message = NULL;
+                return log_oom();
+        }
+
+        *message_size = strlen(*message);
+
+        return 0;
+}
+
+static int coredump_submit_message(const Coredump * s) {
+        int r, j = 0;
+        _cleanup_free_ char *p = NULL;
+        struct iovec iovec[16];
+        _cleanup_free_ char *core_pid = NULL, *core_uid = NULL, *core_gid = NULL, *core_signal = NULL,
+                      *core_timestamp = NULL, *core_comm = NULL, *core_exe = NULL, *core_unit = NULL,
+                        *core_session = NULL, *core_message = NULL, *core_cmdline = NULL;
 
+        char *journal_message = NULL, *file_message = NULL;
+        size_t journal_message_size = 0, file_message_size = 0;
+
+        if (s->core_unit) {
+                if (s->core_user_unit)
+                        core_unit = strappend("COREDUMP_USER_UNIT=", s->core_unit);
+                else
+                        core_unit = strappend("COREDUMP_UNIT=", s->core_unit);
+
+                if (core_unit)
+                        IOVEC_SET_STRING(iovec[j++], core_unit);
+        }
+
+        if (s->core_pid) {
+                core_pid = strappend("COREDUMP_PID=", s->core_pid);
+                if (core_pid)
+                        IOVEC_SET_STRING(iovec[j++], core_pid);
+        }
+
+        if (s->core_uid) {
+                core_uid = strappend("COREDUMP_UID=", s->core_uid);
+                if (core_uid)
+                        IOVEC_SET_STRING(iovec[j++], core_uid);
+        }
+
+        if (s->core_gid) {
+                core_gid = strappend("COREDUMP_GID=", s->core_gid);
+                if (core_gid)
+                        IOVEC_SET_STRING(iovec[j++], core_gid);
+        }
+
+        if (s->core_signal) {
+                core_signal = strappend("COREDUMP_SIGNAL=", s->core_signal);
+                if (core_signal)
+                        IOVEC_SET_STRING(iovec[j++], core_signal);
+        }
+
+        if (s->core_comm) {
+                core_comm = strappend("COREDUMP_COMM=", s->core_comm);
+                if (core_comm)
+                        IOVEC_SET_STRING(iovec[j++], core_comm);
+        }
+
+#ifdef HAVE_LOGIND
+        if (s->core_session) {
+                core_session = strappend("COREDUMP_SESSION=", s->core_session);
+                if (core_session)
+                        IOVEC_SET_STRING(iovec[j++], core_session);
+        }
+
+#endif
+        if (s->core_exe) {
+                core_exe = strappend("COREDUMP_EXE=", s->core_exe);
                 if (core_exe)
                         IOVEC_SET_STRING(iovec[j++], core_exe);
         }
 
-        if (get_process_cmdline(pid, 0, false, &t) >= 0) {
-                core_cmdline = strappend("COREDUMP_CMDLINE=", t);
-                free(t);
-
+        if (s->core_cmdline) {
+                core_cmdline = strappend("COREDUMP_CMDLINE=", s->core_cmdline);
                 if (core_cmdline)
                         IOVEC_SET_STRING(iovec[j++], core_cmdline);
         }
 
-        core_timestamp = strjoin("COREDUMP_TIMESTAMP=", argv[ARG_TIMESTAMP], "000000", NULL);
-        if (core_timestamp)
-                IOVEC_SET_STRING(iovec[j++], core_timestamp);
+        if (s->core_timestamp) {
+                core_timestamp = strjoin("COREDUMP_TIMESTAMP=", s->core_timestamp, "000000", NULL);
+                if (core_timestamp)
+                        IOVEC_SET_STRING(iovec[j++], core_timestamp);
+        }
 
-        IOVEC_SET_STRING(iovec[j++], "MESSAGE_ID=fc2e22bc6ee647b6b90729ab34a250b1");
         IOVEC_SET_STRING(iovec[j++], "PRIORITY=2");
 
-        core_message = strjoin("MESSAGE=Process ", argv[ARG_PID], " (", argv[ARG_COMM], ") dumped core.", NULL);
+        core_message = strjoin("MESSAGE=Process ", s->core_pid, " (", s->core_exe, ") dumped core.", NULL);
         if (core_message)
                 IOVEC_SET_STRING(iovec[j++], core_message);
 
-        /* Now, let's drop privileges to become the user who owns the
-         * segfaulted process and allocate the coredump memory under
-         * his uid. This also ensures that the credentials journald
-         * will see are the ones of the coredumping user, thus making
-         * sure the user himself gets access to the core dump. */
+        if (s->journal_max_size && s->processed_journal_data) {
+                iovec[j].iov_len = MIN(s->journal_max_size,
+                                       s->processed_journal_data_size + 9);
+                iovec[j].iov_base = s->processed_journal_data - 9;
 
-        if (setresgid(gid, gid, gid) < 0 ||
-            setresuid(uid, uid, uid) < 0) {
-                log_error("Failed to drop privileges: %m");
-                r = -errno;
-                goto finish;
+                j ++;
         }
 
-        coredump_bufsize = COREDUMP_MIN_START;
-        coredump_data = malloc(coredump_bufsize);
-        if (!coredump_data) {
-                r = log_oom();
-                goto finish;
-        }
-
-        memcpy(coredump_data, "COREDUMP=", 9);
-        coredump_size = 9;
-
-        for (;;) {
-                n = loop_read(STDIN_FILENO, coredump_data + coredump_size,
-                              coredump_bufsize - coredump_size, false);
-                if (n < 0) {
-                        log_error("Failed to read core dump data: %s", strerror(-n));
-                        r = (int) n;
-                        goto finish;
-                } else if (n == 0)
-                        break;
-
-                coredump_size += n;
-                if (!GREEDY_REALLOC(coredump_data, coredump_bufsize, coredump_size + 1)) {
-                        r = log_oom();
-                        goto finish;
+        if (s->file_max_size && s->processed_file_data) {
+                r = coredump_store_memory_to_fd(s->processed_file_data,
+                                                s->processed_file_data_size,
+                                                s->file_fd, s->file_path,
+                                                s->file_max_size,
+                                                &file_message, &file_message_size);
+
+                if (! r) {
+                        iovec[j].iov_len = file_message_size;
+                        iovec[j].iov_base = file_message;
+
+                        j ++;
+                } else {
+                        if (r == -EACCES)
+                                log_warning("User uid=%s gid=%s is not allowed to store coredump to file",
+                                            s->core_uid, s->core_gid);
+                        else {
+                                log_error("Failed to store coredump to file: %s", strerror(-r));
+                                goto finish;
+                        }
                 }
         }
 
-        iovec[j].iov_base = coredump_data;
-        iovec[j].iov_len = coredump_size;
-        j++;
-
         r = sd_journal_sendv(iovec, j);
         if (r < 0)
                 log_error("Failed to send coredump: %s", strerror(-r));
 
-finish:
+ finish:
+        if (journal_message)
+                coredump_cleanup_mem(journal_message, journal_message_size);
+
+        if (file_message)
+                coredump_cleanup_str(file_message, file_message_size);
+
         return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
 }
+
+int main(int argc, char ** argv) {
+        Coredump s;
+        int r = 0;
+
+        prctl(PR_SET_DUMPABLE, 0);
+
+        if (argc != _ARG_MAX) {
+                log_error("Invalid number of arguments passed from kernel.");
+                return -EINVAL;
+        }
+
+        r = coredump_load_state(argv, &s);
+        if (r) {
+                log_error("State loading failed");
+                return r;
+        }
+
+        r = coredump_preprocess(&s);
+        if (r) {
+                log_error("Coredump preprocessing failed");
+                return r;
+        }
+
+        r = coredump_drop_creds(s.process_uid, s.process_gid);
+        if (r) {
+                coredump_release_state(&s);
+                return r;
+        }
+
+        r = coredump_submit_message(&s);
+
+        coredump_release_state(&s);
+        return r;
+
+}
diff --git a/src/journal/coredump.conf b/src/journal/coredump.conf
new file mode 100644
index 0000000..001a2b5
--- /dev/null
+++ b/src/journal/coredump.conf
@@ -0,0 +1,18 @@
+#  This file is part of systemd.
+#
+#  systemd 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.
+#
+# See coredump.conf(5) for details
+
+[Coredump]
+#MaxJournalCoreSize=25M
+#MaxFileCoreSize=0
+#FileStorage=volatile
+#JournalStoragePreprocess=gdb --quiet --nx --batch -ex "thread apply all bt" --core %c %e
+#PreprocessMaxSize=INT_MAX
+#PreprocessUser=nobody
+#PreprocessFile=
+#PreprocessJournal=
diff --git a/src/journal/coredump.h b/src/journal/coredump.h
new file mode 100644
index 0000000..ec7c388
--- /dev/null
+++ b/src/journal/coredump.h
@@ -0,0 +1,89 @@
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+#pragma once
+
+/***
+  This file is part of systemd.
+
+  Copyright 2012-2013 Lennart Poettering
+                      Oleksii Shevchuk
+
+  systemd 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.
+
+  systemd is distributed in the hope that it will be useful, but
+  WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+  Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with systemd; If not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include "conf-parser.h"
+
+#define COREDUMP_MESSAGE_ID SD_ID128_MAKE(fc,2e,22,bc,6e,e6,47,b6,b9,07,29,ab,34,a2,50,b1
+
+typedef enum Storage {
+        STORAGE_VOLATILE,
+        STORAGE_PERSISTENT,
+        _STORAGE_MAX,
+        _STORAGE_INVALID = -1
+} Storage;
+
+typedef struct Coredump {
+        size_t journal_max_size;
+        size_t file_max_size;
+        size_t preprocess_max_size;
+
+        bool journal_core_dump;
+
+        Storage storage;
+
+        int file_fd;
+        char *file_path;
+
+        gid_t journal;
+
+        pid_t process_pid;
+        uid_t process_uid;
+        gid_t process_gid;
+
+        bool core_user_unit;
+        char *core_pid;
+        char *core_uid;
+        char *core_gid;
+        char *core_signal;
+        char *core_timestamp;
+        char *core_comm;
+        char *core_exe;
+        char *core_unit;
+#ifdef HAVE_LOGIND
+        char *core_session;
+#endif
+        char *core_cmdline;
+
+        uid_t preprocess_uid;
+        gid_t preprocess_gid;
+
+        char *preprocess_user;
+
+        char *preprocess_journal;
+        char *preprocess_file;
+        char *preprocess_directory_path;
+
+        char *journal_message_header;
+        char *processed_journal_data;
+        size_t processed_journal_data_size;
+        char *processed_file_data;
+        size_t processed_file_data_size;
+} Coredump;
+
+const struct ConfigPerfItem* coredump_gperf_lookup(const char *key, unsigned length);
+
+int config_parse_storage(const char *unit, const char *filename, unsigned line, const char *section, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata);
+
+const char *storage_to_string(Storage s);
+Storage storage_from_string(const char *s);
diff --git a/src/journal/journald-server.h b/src/journal/journald-server.h
index 129f7e8..31ff6b4 100644
--- a/src/journal/journald-server.h
+++ b/src/journal/journald-server.h
@@ -125,7 +125,7 @@ typedef struct Server {
         bool sync_scheduled;
 } Server;
 
-#define N_IOVEC_META_FIELDS 17
+#define N_IOVEC_META_FIELDS 19
 #define N_IOVEC_KERNEL_FIELDS 64
 #define N_IOVEC_UDEV_FIELDS 32
 
-- 
1.8.1.2



More information about the systemd-devel mailing list