From 2f75df5cd6dcd56775fec9e89fc79672e702d826 Mon Sep 17 00:00:00 2001
From: Eric DeVolder <eric.devolder@oracle.com>
Date: Thu, 16 May 2019 08:59:01 -0500
Subject: [PATCH] pstore: Tool to archive contents of pstore
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This patch introduces the systemd pstore service which will archive the
contents of the Linux persistent storage filesystem, pstore, to other storage,
thus preserving the existing information contained in the pstore, and clearing
pstore storage for future error events.

Linux provides a persistent storage file system, pstore[1], that can store
error records when the kernel dies (or reboots or powers-off). These records in
turn can be referenced to debug kernel problems (currently the kernel stuffs
the tail of the dmesg, which also contains a stack backtrace, into pstore).

The pstore file system supports a variety of backends that map onto persistent
storage, such as the ACPI ERST[2, Section 18.5 Error Serialization] and UEFI
variables[3 Appendix N Common Platform Error Record]. The pstore backends
typically offer a relatively small amount of persistent storage, e.g. 64KiB,
which can quickly fill up and thus prevent subsequent kernel crashes from
recording errors. Thus there is a need to monitor and extract the pstore
contents so that future kernel problems can also record information in the
pstore.

The pstore service is independent of the kdump service. In cloud environments
specifically, host and guest filesystems are on remote filesystems (eg. iSCSI
or NFS), thus kdump relies [implicitly and/or explicitly] upon proper operation
of networking software *and* hardware *and* infrastructure.  Thus it may not be
possible to capture a kernel coredump to a file since writes over the network
may not be possible.

The pstore backend, on the other hand, is completely local and provides a path
to store error records which will survive a reboot and aid in post-mortem
debugging.

Usage Notes:
This tool moves files from /sys/fs/pstore into /var/lib/systemd/pstore.

To enable kernel recording of error records into pstore, one must either pass
crash_kexec_post_notifiers[4] to the kernel command line or enable via 'echo Y
 > /sys/module/kernel/parameters/crash_kexec_post_notifiers'. This option
invokes the recording of errors into pstore *before* an attempt to kexec/kdump
on a kernel crash.

Optionally, to record reboots and shutdowns in the pstore, one can either pass
the printk.always_kmsg_dump[4] to the kernel command line or enable via 'echo Y >
/sys/module/printk/parameters/always_kmsg_dump'. This option enables code on the
shutdown path to record information via pstore.

This pstore service is a oneshot service. When run, the service invokes
systemd-pstore which is a tool that performs the following:
 - reads the pstore.conf configuration file
 - collects the lists of files in the pstore (eg. /sys/fs/pstore)
 - for certain file types (eg. dmesg) a handler is invoked
 - for all other files, the file is moved from pstore

 - In the case of dmesg handler, final processing occurs as such:
   - files processed in reverse lexigraphical order to faciliate
     reconstruction of original dmesg
   - the filename is examined to determine which dmesg it is a part
   - the file is appended to the reconstructed dmesg

For example, the following pstore contents:

 root@vm356:~# ls -al /sys/fs/pstore
 total 0
 drwxr-x--- 2 root root    0 May  9 09:50 .
 drwxr-xr-x 7 root root    0 May  9 09:50 ..
 -r--r--r-- 1 root root 1610 May  9 09:49 dmesg-efi-155741337601001
 -r--r--r-- 1 root root 1778 May  9 09:49 dmesg-efi-155741337602001
 -r--r--r-- 1 root root 1726 May  9 09:49 dmesg-efi-155741337603001
 -r--r--r-- 1 root root 1746 May  9 09:49 dmesg-efi-155741337604001
 -r--r--r-- 1 root root 1686 May  9 09:49 dmesg-efi-155741337605001
 -r--r--r-- 1 root root 1690 May  9 09:49 dmesg-efi-155741337606001
 -r--r--r-- 1 root root 1775 May  9 09:49 dmesg-efi-155741337607001
 -r--r--r-- 1 root root 1811 May  9 09:49 dmesg-efi-155741337608001
 -r--r--r-- 1 root root 1817 May  9 09:49 dmesg-efi-155741337609001
 -r--r--r-- 1 root root 1795 May  9 09:49 dmesg-efi-155741337710001
 -r--r--r-- 1 root root 1770 May  9 09:49 dmesg-efi-155741337711001
 -r--r--r-- 1 root root 1796 May  9 09:49 dmesg-efi-155741337712001
 -r--r--r-- 1 root root 1787 May  9 09:49 dmesg-efi-155741337713001
 -r--r--r-- 1 root root 1808 May  9 09:49 dmesg-efi-155741337714001
 -r--r--r-- 1 root root 1754 May  9 09:49 dmesg-efi-155741337715001

results in the following:

 root@vm356:~# ls -al /var/lib/systemd/pstore/155741337/
 total 92
 drwxr-xr-x 2 root root  4096 May  9 09:50 .
 drwxr-xr-x 4 root root    40 May  9 09:50 ..
 -rw-r--r-- 1 root root  1610 May  9 09:50 dmesg-efi-155741337601001
 -rw-r--r-- 1 root root  1778 May  9 09:50 dmesg-efi-155741337602001
 -rw-r--r-- 1 root root  1726 May  9 09:50 dmesg-efi-155741337603001
 -rw-r--r-- 1 root root  1746 May  9 09:50 dmesg-efi-155741337604001
 -rw-r--r-- 1 root root  1686 May  9 09:50 dmesg-efi-155741337605001
 -rw-r--r-- 1 root root  1690 May  9 09:50 dmesg-efi-155741337606001
 -rw-r--r-- 1 root root  1775 May  9 09:50 dmesg-efi-155741337607001
 -rw-r--r-- 1 root root  1811 May  9 09:50 dmesg-efi-155741337608001
 -rw-r--r-- 1 root root  1817 May  9 09:50 dmesg-efi-155741337609001
 -rw-r--r-- 1 root root  1795 May  9 09:50 dmesg-efi-155741337710001
 -rw-r--r-- 1 root root  1770 May  9 09:50 dmesg-efi-155741337711001
 -rw-r--r-- 1 root root  1796 May  9 09:50 dmesg-efi-155741337712001
 -rw-r--r-- 1 root root  1787 May  9 09:50 dmesg-efi-155741337713001
 -rw-r--r-- 1 root root  1808 May  9 09:50 dmesg-efi-155741337714001
 -rw-r--r-- 1 root root  1754 May  9 09:50 dmesg-efi-155741337715001
 -rw-r--r-- 1 root root 26754 May  9 09:50 dmesg.txt

where dmesg.txt is reconstructed from the group of related
dmesg-efi-155741337* files.

Configuration file:
The pstore.conf configuration file has four settings, described below.
 - Storage : one of "none", "external", or "journal". With "none", this
   tool leaves the contents of pstore untouched. With "external", the
   contents of the pstore are moved into the /var/lib/systemd/pstore,
   as well as logged into the journal.  With "journal", the contents of
   the pstore are recorded only in the systemd journal. The default is
   "external".
 - Unlink : is a boolean. When "true", the default, then files in the
   pstore are removed once processed. When "false", processing of the
   pstore occurs normally, but the pstore files remain.

References:
[1] "Persistent storage for a kernel's dying breath",
    March 23, 2011.
    https://lwn.net/Articles/434821/

[2] "Advanced Configuration and Power Interface Specification",
    version 6.2, May 2017.
    https://www.uefi.org/sites/default/files/resources/ACPI_6_2.pdf

[3] "Unified Extensible Firmware Interface Specification",
    version 2.8, March 2019.
    https://uefi.org/sites/default/files/resources/UEFI_Spec_2_8_final.pdf

[4] "The kernel’s command-line parameters",
    https://static.lwn.net/kerneldoc/admin-guide/kernel-parameters.html

(cherry picked from commit 9b4abc69b201e5d7295e1b0762883659f053e747)

Resolves: #2158832
---
 man/pstore.conf.xml             |  89 +++++++
 man/rules/meson.build           |   2 +
 man/systemd-pstore.xml          |  99 ++++++++
 meson.build                     |  20 ++
 meson_options.txt               |   2 +
 src/pstore/meson.build          |  10 +
 src/pstore/pstore.c             | 395 ++++++++++++++++++++++++++++++++
 src/pstore/pstore.conf          |  16 ++
 units/meson.build               |   1 +
 units/systemd-pstore.service.in |  24 ++
 10 files changed, 658 insertions(+)
 create mode 100644 man/pstore.conf.xml
 create mode 100644 man/systemd-pstore.xml
 create mode 100644 src/pstore/meson.build
 create mode 100644 src/pstore/pstore.c
 create mode 100644 src/pstore/pstore.conf
 create mode 100644 units/systemd-pstore.service.in

diff --git a/man/pstore.conf.xml b/man/pstore.conf.xml
new file mode 100644
index 0000000000..b5cda47d02
--- /dev/null
+++ b/man/pstore.conf.xml
@@ -0,0 +1,89 @@
+<?xml version='1.0'?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+  "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+<!-- SPDX-License-Identifier: LGPL-2.1+ -->
+
+<refentry id="pstore.conf" conditional="ENABLE_PSTORE"
+          xmlns:xi="http://www.w3.org/2001/XInclude">
+  <refentryinfo>
+    <title>pstore.conf</title>
+    <productname>systemd</productname>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>pstore.conf</refentrytitle>
+    <manvolnum>5</manvolnum>
+  </refmeta>
+
+  <refnamediv>
+    <refname>pstore.conf</refname>
+    <refname>pstore.conf.d</refname>
+    <refpurpose>PStore configuration file</refpurpose>
+  </refnamediv>
+
+  <refsynopsisdiv>
+    <para>
+    <filename>/etc/systemd/pstore.conf</filename>
+    <filename>/etc/systemd/pstore.conf.d/*</filename>
+    </para>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>Description</title>
+
+    <para>This file configures the behavior of
+    <citerefentry><refentrytitle>systemd-pstore</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+    a tool for archiving the contents of the persistent storage filesystem,
+    <ulink url="https://www.kernel.org/doc/Documentation/ABI/testing/pstore">pstore</ulink>.
+    </para>
+  </refsect1>
+
+  <xi:include href="standard-conf.xml" xpointer="main-conf" />
+
+  <refsect1>
+    <title>Options</title>
+
+    <para>All options are configured in the
+    <literal>[PStore]</literal> section:</para>
+
+    <variablelist>
+
+      <varlistentry>
+        <term><varname>Storage=</varname></term>
+
+        <listitem><para>Controls where to archive (i.e. copy) files from the pstore filesystem. One of <literal>none</literal>,
+        <literal>external</literal>, and <literal>journal</literal>. When
+        <literal>none</literal>, the tool exits without processing files in the pstore filesystem.
+        When <literal>external</literal> (the default), files are archived into <filename>/var/lib/systemd/pstore/</filename>,
+        and logged into the journal.
+        When <literal>journal</literal>, pstore file contents are logged only in the journal.</para>
+        </listitem>
+
+      </varlistentry>
+
+      <varlistentry>
+        <term><varname>Unlink=</varname></term>
+
+        <listitem><para>Controls whether or not files are removed from pstore after processing.
+        Takes a boolean value. When true, a pstore file is removed from the pstore once it has been
+        archived (either to disk or into the journal). When false, processing of pstore files occurs
+        normally, but the files remain in the pstore.
+        The default is true in order to maintain the pstore in a nearly empty state, so that the pstore
+        has storage available for the next kernel error event.
+        </para></listitem>
+      </varlistentry>
+    </variablelist>
+
+    <para>The defaults for all values are listed as comments in the
+    template <filename>/etc/systemd/pstore.conf</filename> file that
+    is installed by default.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>See Also</title>
+    <para>
+      <citerefentry><refentrytitle>systemd-journald.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+    </para>
+  </refsect1>
+
+</refentry>
diff --git a/man/rules/meson.build b/man/rules/meson.build
index e6c0a99bbd..6295330c5e 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -44,6 +44,7 @@ manpages = [
  ['os-release', '5', [], ''],
  ['pam_systemd', '8', [], 'HAVE_PAM'],
  ['portablectl', '1', [], 'ENABLE_PORTABLED'],
+ ['pstore.conf', '5', ['pstore.conf.d'], 'ENABLE_PSTORE'],
  ['resolvectl', '1', ['resolvconf'], 'ENABLE_RESOLVE'],
  ['resolved.conf', '5', ['resolved.conf.d'], 'ENABLE_RESOLVE'],
  ['runlevel', '8', [], 'ENABLE_UTMP'],
@@ -633,6 +634,7 @@ manpages = [
  ['systemd-nspawn', '1', [], ''],
  ['systemd-path', '1', [], ''],
  ['systemd-portabled.service', '8', ['systemd-portabled'], 'ENABLE_PORTABLED'],
+ ['systemd-pstore', '8', ['systemd-pstore.service'], 'ENABLE_PSTORE'],
  ['systemd-quotacheck.service',
   '8',
   ['systemd-quotacheck'],
diff --git a/man/systemd-pstore.xml b/man/systemd-pstore.xml
new file mode 100644
index 0000000000..dd1aa5e83b
--- /dev/null
+++ b/man/systemd-pstore.xml
@@ -0,0 +1,99 @@
+<?xml version='1.0'?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+  "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+<!-- SPDX-License-Identifier: LGPL-2.1+ -->
+
+<refentry id="systemd-pstore" conditional='ENABLE_PSTORE'
+          xmlns:xi="http://www.w3.org/2001/XInclude">
+
+  <refentryinfo>
+    <title>systemd-pstore</title>
+    <productname>systemd</productname>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>systemd-pstore</refentrytitle>
+    <manvolnum>8</manvolnum>
+  </refmeta>
+
+  <refnamediv>
+    <refname>systemd-pstore</refname>
+    <refname>systemd-pstore.service</refname>
+    <refpurpose>Tool to archive contents of the persistent storage filesytem</refpurpose>
+  </refnamediv>
+
+  <refsynopsisdiv>
+    <para><filename>/usr/lib/systemd/systemd-pstore</filename></para>
+    <para><filename>systemd-pstore.service</filename></para>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>Description</title>
+    <para><filename>systemd-pstore.service</filename> is a system service that archives the
+    contents of the Linux persistent storage filesystem, pstore, to other storage,
+    thus preserving the existing information contained in the pstore, and clearing
+    pstore storage for future error events.</para>
+
+    <para>Linux provides a persistent storage file system, pstore, that can store
+    error records when the kernel dies (or reboots or powers-off). These records in
+    turn can be referenced to debug kernel problems (currently the kernel stuffs
+    the tail of the dmesg, which also contains a stack backtrace, into pstore).</para>
+
+    <para>The pstore file system supports a variety of backends that map onto persistent
+    storage, such as the ACPI ERST and UEFI variables. The pstore backends
+    typically offer a relatively small amount of persistent storage, e.g. 64KiB,
+    which can quickly fill up and thus prevent subsequent kernel crashes from
+    recording errors. Thus there is a need to monitor and extract the pstore
+    contents so that future kernel problems can also record information in the
+    pstore.</para>
+
+    <para>The pstore service is independent of the kdump service. In cloud environments
+    specifically, host and guest filesystems are on remote filesystems (eg. iSCSI
+    or NFS), thus kdump relies [implicitly and/or explicitly] upon proper operation
+    of networking software *and* hardware *and* infrastructure.  Thus it may not be
+    possible to capture a kernel coredump to a file since writes over the network
+    may not be possible.</para>
+
+    <para>The pstore backend, on the other hand, is completely local and provides a path
+    to store error records which will survive a reboot and aid in post-mortem
+    debugging.</para>
+
+    <para>The <command>systemd-pstore</command> executable does the actual work. Upon starting,
+    the <filename>pstore.conf</filename> is read to obtain options, then the /sys/fs/pstore
+    directory contents are processed according to the options. Pstore files are written to the
+    journal, and optionally saved into /var/lib/systemd/pstore.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>Configuration</title>
+
+    <para>The behavior of <command>systemd-pstore</command> is configured through the configuration file
+    <filename>/etc/systemd/pstore.conf</filename> and corresponding snippets
+    <filename>/etc/systemd/pstore.conf.d/*.conf</filename>, see
+    <citerefentry><refentrytitle>pstore.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
+    </para>
+
+    <refsect2>
+      <title>Disabling pstore processing</title>
+
+      <para>To disable pstore processing by <command>systemd-pstore</command>,
+      set <programlisting>Storage=none</programlisting> in
+      <citerefentry><refentrytitle>pstore.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
+      </para>
+    </refsect2>
+  </refsect1>
+
+  <refsect1>
+    <title>Usage</title>
+    <para>Data stored in the journal can be viewed with
+    <citerefentry><refentrytitle>journalctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+    as usual.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>See Also</title>
+    <para>
+      <citerefentry><refentrytitle>pstore.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+    </para>
+  </refsect1>
+</refentry>
diff --git a/meson.build b/meson.build
index af4cf331da..972a8fb6f7 100644
--- a/meson.build
+++ b/meson.build
@@ -1224,6 +1224,7 @@ foreach term : ['utmp',
                 'environment-d',
                 'binfmt',
                 'coredump',
+                'pstore',
                 'resolve',
                 'logind',
                 'hostnamed',
@@ -1439,6 +1440,7 @@ subdir('src/network')
 subdir('src/analyze')
 subdir('src/journal-remote')
 subdir('src/coredump')
+subdir('src/pstore')
 subdir('src/hostname')
 subdir('src/import')
 subdir('src/kernel-install')
@@ -2151,6 +2153,23 @@ if conf.get('ENABLE_COREDUMP') == 1
         public_programs += [exe]
 endif
 
+if conf.get('ENABLE_PSTORE') == 1
+        executable('systemd-pstore',
+                   systemd_pstore_sources,
+                   include_directories : includes,
+                   link_with : [libshared],
+                   dependencies : [threads,
+                                   libacl,
+                                   libdw,
+                                   libxz,
+                                   liblz4],
+                   install_rpath : rootlibexecdir,
+                   install : true,
+                   install_dir : rootlibexecdir)
+
+        public_programs += exe
+endif
+
 if conf.get('ENABLE_BINFMT') == 1
         exe = executable('systemd-binfmt',
                          'src/binfmt/binfmt.c',
@@ -3014,6 +3033,7 @@ foreach tuple : [
         ['resolve'],
         ['DNS-over-TLS'],
         ['coredump'],
+        ['pstore'],
         ['polkit'],
         ['legacy pkla',      install_polkit_pkla],
         ['efi'],
diff --git a/meson_options.txt b/meson_options.txt
index 213079ac15..5624304bf4 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -76,6 +76,8 @@ option('binfmt', type : 'boolean',
        description : 'support for custom binary formats')
 option('coredump', type : 'boolean',
        description : 'install the coredump handler')
+option('pstore', type : 'boolean',
+       description : 'install the pstore archival tool')
 option('logind', type : 'boolean',
        description : 'install the systemd-logind stack')
 option('hostnamed', type : 'boolean',
diff --git a/src/pstore/meson.build b/src/pstore/meson.build
new file mode 100644
index 0000000000..adbac24b54
--- /dev/null
+++ b/src/pstore/meson.build
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: LGPL-2.1+
+
+systemd_pstore_sources = files('''
+        pstore.c
+'''.split())
+
+if conf.get('ENABLE_PSTORE') == 1
+        install_data('pstore.conf',
+                     install_dir : pkgsysconfdir)
+endif
diff --git a/src/pstore/pstore.c b/src/pstore/pstore.c
new file mode 100644
index 0000000000..f95e016eb6
--- /dev/null
+++ b/src/pstore/pstore.c
@@ -0,0 +1,395 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+/* Copyright © 2019 Oracle and/or its affiliates. */
+
+/* Generally speaking, the pstore contains a small number of files
+ * that in turn contain a small amount of data.  */
+#include <errno.h>
+#include <stdio.h>
+#include <stdio_ext.h>
+#include <sys/prctl.h>
+#include <sys/xattr.h>
+#include <unistd.h>
+
+#include "sd-daemon.h"
+#include "sd-journal.h"
+#include "sd-login.h"
+#include "sd-messages.h"
+
+#include "acl-util.h"
+#include "alloc-util.h"
+#include "capability-util.h"
+#include "cgroup-util.h"
+#include "compress.h"
+#include "conf-parser.h"
+#include "copy.h"
+#include "dirent-util.h"
+#include "escape.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "io-util.h"
+#include "journal-importer.h"
+#include "log.h"
+#include "macro.h"
+#include "missing.h"
+#include "mkdir.h"
+#include "parse-util.h"
+#include "process-util.h"
+#include "signal-util.h"
+#include "socket-util.h"
+#include "special.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+#include "user-util.h"
+#include "util.h"
+
+/* Command line argument handling */
+typedef enum PStoreStorage {
+        PSTORE_STORAGE_NONE,
+        PSTORE_STORAGE_EXTERNAL,
+        PSTORE_STORAGE_JOURNAL,
+        _PSTORE_STORAGE_MAX,
+        _PSTORE_STORAGE_INVALID = -1
+} PStoreStorage;
+
+static const char* const pstore_storage_table[_PSTORE_STORAGE_MAX] = {
+        [PSTORE_STORAGE_NONE] = "none",
+        [PSTORE_STORAGE_EXTERNAL] = "external",
+        [PSTORE_STORAGE_JOURNAL] = "journal",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP(pstore_storage, PStoreStorage);
+static DEFINE_CONFIG_PARSE_ENUM(config_parse_pstore_storage, pstore_storage, PStoreStorage, "Failed to parse storage setting");
+
+static PStoreStorage arg_storage = PSTORE_STORAGE_EXTERNAL;
+
+static bool arg_unlink = true;
+static const char *arg_sourcedir = "/sys/fs/pstore";
+static const char *arg_archivedir = "/var/lib/systemd/pstore";
+
+static int parse_config(void) {
+        static const ConfigTableItem items[] = {
+                { "PStore", "Unlink",  config_parse_bool,           0, &arg_unlink },
+                { "PStore", "Storage", config_parse_pstore_storage, 0, &arg_storage },
+                {}
+        };
+
+        return config_parse_many_nulstr(PKGSYSCONFDIR "/pstore.conf",
+                                        CONF_PATHS_NULSTR("systemd/pstore.conf.d"),
+                                        "PStore\0",
+                                        config_item_table_lookup, items,
+                                        CONFIG_PARSE_WARN, NULL);
+}
+
+/* File list handling - PStoreEntry is the struct and
+ * and PStoreEntry is the type that contains all info
+ * about a pstore entry.  */
+typedef struct PStoreEntry {
+        struct dirent dirent;
+        bool is_binary;
+        bool handled;
+        char *content;
+        size_t content_size;
+} PStoreEntry;
+
+typedef struct PStoreList {
+        PStoreEntry *entries;
+        size_t n_entries;
+        size_t n_entries_allocated;
+} PStoreList;
+
+static void pstore_entries_reset(PStoreList *list) {
+        for (size_t i = 0; i < list->n_entries; i++)
+                free(list->entries[i].content);
+        free(list->entries);
+        list->n_entries = 0;
+}
+
+static int compare_pstore_entries(const void *_a, const void *_b) {
+        PStoreEntry *a = (PStoreEntry *)_a, *b = (PStoreEntry *)_b;
+        return strcmp(a->dirent.d_name, b->dirent.d_name);
+}
+
+static int move_file(PStoreEntry *pe, const char *subdir) {
+        _cleanup_free_ char *ifd_path = NULL;
+        _cleanup_free_ char *ofd_path = NULL;
+        int r = 0;
+        struct iovec iovec[2] = {};
+        int n_iovec = 0;
+        _cleanup_free_ void *field = NULL;
+        const char *suffix = NULL;
+        size_t field_size;
+
+        if (pe->handled)
+                return 0;
+
+        ifd_path = path_join(NULL, arg_sourcedir, pe->dirent.d_name);
+        if (!ifd_path)
+                return log_oom();
+
+        ofd_path = path_join(arg_archivedir, subdir, pe->dirent.d_name);
+        if (!ofd_path)
+                return log_oom();
+
+        /* Always log to the journal */
+        suffix = arg_storage == PSTORE_STORAGE_EXTERNAL ? strjoina(" moved to ", ofd_path) : (char *)".";
+        field = strjoina("MESSAGE=PStore ", pe->dirent.d_name, suffix);
+        iovec[n_iovec++] = IOVEC_MAKE_STRING(field);
+
+        field_size = strlen("FILE=") + pe->content_size;
+        field = malloc(field_size);
+        if (!field)
+                return log_oom();
+        memcpy(stpcpy(field, "FILE="), pe->content, pe->content_size);
+        iovec[n_iovec++] = IOVEC_MAKE(field, field_size);
+
+        r = sd_journal_sendv(iovec, n_iovec);
+        if (r < 0)
+                return log_error_errno(r, "Failed to log pstore entry: %m");
+
+        if (arg_storage == PSTORE_STORAGE_EXTERNAL) {
+                /* Move file from pstore to external storage */
+                r = mkdir_parents(ofd_path, 0755);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to create directoy %s: %m", ofd_path);
+                r = copy_file_atomic(ifd_path, ofd_path, 0600, 0, COPY_REPLACE);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to copy_file_atomic: %s to %s", ifd_path, ofd_path);
+        }
+
+        /* If file copied properly, remove it from pstore */
+        if (arg_unlink)
+                (void) unlink(ifd_path);
+
+        pe->handled = true;
+
+        return 0;
+}
+
+static int write_dmesg(const char *dmesg, size_t size, const char *id) {
+        _cleanup_(unlink_and_freep) char *ofd_path = NULL;
+        _cleanup_free_ char *tmp_path = NULL;
+        _cleanup_close_ int ofd = -1;
+        ssize_t wr;
+        int r;
+
+        if (isempty(dmesg) || size == 0)
+                return 0;
+
+        /* log_info("Record ID %s", id); */
+
+        ofd_path = path_join(arg_archivedir, id, "dmesg.txt");
+        if (!ofd_path)
+                return log_oom();
+
+        ofd = open_tmpfile_linkable(ofd_path, O_CLOEXEC|O_CREAT|O_TRUNC|O_WRONLY, &tmp_path);
+        if (ofd < 0)
+                return log_error_errno(ofd, "Failed to open temporary file %s: %m", ofd_path);
+        wr = write(ofd, dmesg, size);
+        if (wr < 0)
+                return log_error_errno(errno, "Failed to store dmesg to %s: %m", ofd_path);
+        if (wr != (ssize_t)size)
+                return log_error_errno(-EIO, "Failed to store dmesg to %s. %zu bytes are lost.", ofd_path, size - wr);
+        r = link_tmpfile(ofd, tmp_path, ofd_path);
+        if (r < 0)
+                return log_error_errno(r, "Failed to write temporary file %s: %m", ofd_path);
+        ofd_path = mfree(ofd_path);
+
+        return 0;
+}
+
+static void process_dmesg_files(PStoreList *list) {
+        /* Move files, reconstruct dmesg.txt */
+        PStoreEntry *pe;
+        _cleanup_free_ char *dmesg = NULL;
+        size_t dmesg_size = 0;
+        _cleanup_free_ char *dmesg_id = NULL;
+
+        /* Handle each dmesg file: files processed in reverse
+         * order so as to properly reconstruct original dmesg */
+        for (size_t n = list->n_entries; n > 0; n--) {
+                bool move_file_and_continue = false;
+                _cleanup_free_ char *pe_id = NULL;
+                char *p;
+                size_t plen;
+
+                pe = &list->entries[n-1];
+
+                if (pe->handled)
+                        continue;
+                if (!startswith(pe->dirent.d_name, "dmesg-"))
+                        continue;
+
+                if (endswith(pe->dirent.d_name, ".enc.z")) /* indicates a problem */
+                        move_file_and_continue = true;
+                p = strrchr(pe->dirent.d_name, '-');
+                if (!p)
+                        move_file_and_continue = true;
+
+                if (move_file_and_continue) {
+                        /* A dmesg file on which we do NO additional processing */
+                        (void) move_file(pe, NULL);
+                        continue;
+                }
+
+                /* See if this file is one of a related group of files
+                 * in order to reconstruct dmesg */
+
+                /* When dmesg is written into pstore, it is done so in
+                 * small chunks, whatever the exchange buffer size is
+                 * with the underlying pstore backend (ie. EFI may be
+                 * ~2KiB), which means an example pstore with approximately
+                 * 64KB of storage may have up to roughly 32 dmesg files
+                 * that could be related, depending upon the size of the
+                 * original dmesg.
+                 *
+                 * Here we look at the dmesg filename and try to discern
+                 * if files are part of a related group, meaning the same
+                 * original dmesg.
+                 *
+                 * The two known pstore backends are EFI and ERST. These
+                 * backends store data in the Common Platform Error
+                 * Record, CPER, format. The dmesg- filename contains the
+                 * CPER record id, a 64bit number (in decimal notation).
+                 * In Linux, the record id is encoded with two digits for
+                 * the dmesg part (chunk) number and 3 digits for the
+                 * count number. So allowing an additional digit to
+                 * compensate for advancing time, this code ignores the
+                 * last six digits of the filename in determining the
+                 * record id.
+                 *
+                 * For the EFI backend, the record id encodes an id in the
+                 * upper 32 bits, and a timestamp in the lower 32-bits.
+                 * So ignoring the least significant 6 digits has proven
+                 * to generally identify related dmesg entries.  */
+#define PSTORE_FILENAME_IGNORE 6
+
+                /* determine common portion of record id */
+                ++p; /* move beyond dmesg- */
+                plen = strlen(p);
+                if (plen > PSTORE_FILENAME_IGNORE) {
+                        pe_id = memdup_suffix0(p, plen - PSTORE_FILENAME_IGNORE);
+                        if (!pe_id) {
+                                log_oom();
+                                return;
+                        }
+                } else
+                        pe_id = mfree(pe_id);
+
+                /* Now move file from pstore to archive storage */
+                move_file(pe, pe_id);
+
+                /* If the current record id is NOT the same as the
+                 * previous record id, then start a new dmesg.txt file */
+                if (!pe_id || !dmesg_id || !streq(pe_id, dmesg_id)) {
+                        /* Encountered a new dmesg group, close out old one, open new one */
+                        if (dmesg) {
+                                (void) write_dmesg(dmesg, dmesg_size, dmesg_id);
+                                dmesg = mfree(dmesg);
+                                dmesg_size = 0;
+                        }
+
+                        /* now point dmesg_id to storage of pe_id */
+                        free_and_replace(dmesg_id, pe_id);
+                }
+
+                /* Reconstruction of dmesg is done as a useful courtesy, do not log errors */
+                dmesg = realloc(dmesg, dmesg_size + strlen(pe->dirent.d_name) + strlen(":\n") + pe->content_size + 1);
+                if (dmesg) {
+                        dmesg_size += sprintf(&dmesg[dmesg_size], "%s:\n", pe->dirent.d_name);
+                        if (pe->content) {
+                                memcpy(&dmesg[dmesg_size], pe->content, pe->content_size);
+                                dmesg_size += pe->content_size;
+                        }
+                }
+
+                pe_id = mfree(pe_id);
+        }
+        if (dmesg)
+                (void) write_dmesg(dmesg, dmesg_size, dmesg_id);
+}
+
+static int list_files(PStoreList *list, const char *sourcepath) {
+        _cleanup_(closedirp) DIR *dirp = NULL;
+        struct dirent *de;
+        int r = 0;
+
+        dirp = opendir(sourcepath);
+        if (!dirp)
+                return log_error_errno(errno, "Failed to opendir %s: %m", sourcepath);
+
+        FOREACH_DIRENT(de, dirp, return log_error_errno(errno, "Failed to iterate through %s: %m", sourcepath)) {
+                _cleanup_free_ char *ifd_path = NULL;
+
+                ifd_path = path_join(NULL, sourcepath, de->d_name);
+                if (!ifd_path)
+                        return log_oom();
+
+                _cleanup_free_ char *buf = NULL;
+                size_t buf_size;
+
+                /* Now read contents of pstore file */
+                r = read_full_file(ifd_path, &buf, &buf_size);
+                if (r < 0) {
+                        log_warning_errno(r, "Failed to read file %s: %m", ifd_path);
+                        continue;
+                }
+
+                if (!GREEDY_REALLOC(list->entries, list->n_entries_allocated, list->n_entries + 1))
+                        return log_oom();
+
+                list->entries[list->n_entries++] = (PStoreEntry) {
+                        .dirent = *de,
+                        .content = TAKE_PTR(buf),
+                        .content_size = buf_size,
+                        .is_binary = true,
+                        .handled = false,
+                };
+        }
+
+        return r;
+}
+
+static int run(int argc, char *argv[]) {
+        _cleanup_(pstore_entries_reset) PStoreList list = {};
+        int r;
+
+        log_open();
+
+        /* Ignore all parse errors */
+        (void) parse_config();
+
+        log_debug("Selected storage '%s'.", pstore_storage_to_string(arg_storage));
+        log_debug("Selected Unlink '%d'.", arg_unlink);
+
+        if (arg_storage == PSTORE_STORAGE_NONE)
+                /* Do nothing, intentionally, leaving pstore untouched */
+                return 0;
+
+        /* Obtain list of files in pstore */
+        r = list_files(&list, arg_sourcedir);
+        if (r < 0)
+                return r;
+
+        /* Handle each pstore file */
+        /* Sort files lexigraphically ascending, generally needed by all */
+        qsort_safe(list.entries, list.n_entries, sizeof(PStoreEntry), compare_pstore_entries);
+
+        /* Process known file types */
+        process_dmesg_files(&list);
+
+        /* Move left over files out of pstore */
+        for (size_t n = 0; n < list.n_entries; n++)
+                move_file(&list.entries[n], NULL);
+
+        return 0;
+}
+
+int main(int argc, char *argv[]) {
+        int r;
+
+        r = run(argc, argv);
+        return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/src/pstore/pstore.conf b/src/pstore/pstore.conf
new file mode 100644
index 0000000000..93a8b6707c
--- /dev/null
+++ b/src/pstore/pstore.conf
@@ -0,0 +1,16 @@
+#  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.
+#
+# Entries in this file show the compile time defaults.
+# You can change settings by editing this file.
+# Defaults can be restored by simply deleting this file.
+#
+# See pstore.conf(5) for details.
+
+[PStore]
+#Storage=external
+#Unlink=yes
diff --git a/units/meson.build b/units/meson.build
index a74fa95195..e8e64eb30a 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -136,6 +136,7 @@ in_units = [
         ['systemd-binfmt.service',               'ENABLE_BINFMT',
          'sysinit.target.wants/'],
         ['systemd-coredump@.service',            'ENABLE_COREDUMP'],
+        ['systemd-pstore.service',               'ENABLE_PSTORE'],
         ['systemd-firstboot.service',            'ENABLE_FIRSTBOOT',
          'sysinit.target.wants/'],
         ['systemd-fsck-root.service',            ''],
diff --git a/units/systemd-pstore.service.in b/units/systemd-pstore.service.in
new file mode 100644
index 0000000000..fec2b1aebf
--- /dev/null
+++ b/units/systemd-pstore.service.in
@@ -0,0 +1,24 @@
+#  SPDX-License-Identifier: LGPL-2.1+
+#
+#  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.
+
+[Unit]
+Description=Platform Persistent Storage Archival
+Documentation=man:systemd-pstore(8)
+DefaultDependencies=no
+Wants=systemd-remount-fs.service
+After=systemd-remount-fs.service
+
+[Service]
+Type=oneshot
+ExecStart=@rootlibexecdir@/systemd-pstore
+RemainAfterExit=yes
+StateDirectory=systemd/pstore
+
+[Install]
+WantedBy=systemd-remount-fs.service
