1
0
mirror of https://github.com/systemd/systemd.git synced 2025-01-09 01:18:19 +03:00

measure: add new tool to precalculate PCR values for a kernel image

For now, this simply outputs the PCR hash values expected for a kernel
image, if it's measured like sd-stub would do it.

(Later on, we can extend the tool, to optionally sign these
pre-calculated measurements, in order to implement signed PCR policies
for disk encryption.)
This commit is contained in:
Lennart Poettering 2022-07-26 00:13:16 +02:00
parent 51470e1e56
commit ca1092dc15
4 changed files with 700 additions and 0 deletions

View File

@ -953,6 +953,7 @@ manpages = [
'systemd-makefs',
'systemd-mkswap@.service'],
''],
['systemd-measure', '1', [], 'HAVE_GNU_EFI'],
['systemd-modules-load.service', '8', ['systemd-modules-load'], 'HAVE_KMOD'],
['systemd-mount', '1', ['systemd-umount'], ''],
['systemd-network-generator.service', '8', ['systemd-network-generator'], ''],

154
man/systemd-measure.xml Normal file
View File

@ -0,0 +1,154 @@
<?xml version="1.0"?>
<!--*-nxml-*-->
<!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-or-later -->
<refentry id="systemd-measure" xmlns:xi="http://www.w3.org/2001/XInclude" conditional='HAVE_GNU_EFI'>
<refentryinfo>
<title>systemd-measure</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>systemd-measure</refentrytitle>
<manvolnum>1</manvolnum>
</refmeta>
<refnamediv>
<refname>systemd-measure</refname>
<refpurpose>Pre-calculate expected TPM2 PCR values for booted unified kernel images</refpurpose>
</refnamediv>
<refsynopsisdiv>
<cmdsynopsis>
<command>/usr/lib/systemd/systemd-measure <arg choice="opt" rep="repeat">OPTIONS</arg></command>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para>Note: this command is experimental for now. While it is likely to become a regular component of
systemd, it might still change in behaviour and interface.</para>
<para><command>systemd-measure</command> is a tool that may be used to pre-calculate the expected TPM2
PCR 11 values that should be seen when a unified Linux kernel image based on
<citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry> is
booted up. It accepts paths to the ELF kernel image file, initial ram disk image file, devicetree file,
kernel command line file,
<citerefentry><refentrytitle>os-release</refentrytitle><manvolnum>5</manvolnum></citerefentry> file, and
boot splash file that make up the unified kernel image, and determines the PCR values expected to be in
place after booting the image. Calculation starts with a zero-initialized PCR 11, and is executed in a
fashion compatible with what <filename>systemd-stub</filename> does at boot.</para>
</refsect1>
<refsect1>
<title>Commands</title>
<para>The following commands are understood:</para>
<variablelist>
<varlistentry>
<term><command>status</command></term>
<listitem><para>This is the default command if none is specified. This queries the local system's
TPM2 PCR 11+12+13 values and displays them. The data is written in a similar format as the
<command>calculate</command> command below, and may be used to quickly compare expectation with
reality.</para></listitem>
</varlistentry>
<varlistentry>
<term><command>calculate</command></term>
<listitem><para>Pre-calculate the expected value seen in PCR register 11 after boot-up of a unified
kernel image consisting of the components specified with <option>--linux=</option>,
<option>--osrel=</option>, <option>--cmdline=</option>, <option>--initrd=</option>,
<option>--splash=</option>, <option>--dtb=</option>, see below. Only <option>--linux=</option> is
mandatory.</para></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>Options</title>
<para>The following options are understood:</para>
<variablelist>
<varlistentry>
<term><option>--linux=PATH</option></term>
<term><option>--osrel=PATH</option></term>
<term><option>--cmdline=PATH</option></term>
<term><option>--initrd=PATH</option></term>
<term><option>--splash=PATH</option></term>
<term><option>--dtb=PATH</option></term>
<listitem><para>When used with the <command>calculate</command> verb, configures the files to read
the unified kernel image components from. Each option corresponds with the equally named section in
the unified kernel PE file. The <option>--linux=</option> switch expects the path to the ELF kernel
file that the unified PE kernel will wrap. All switches except <option>--linux=</option> are
optional. Each option may be used at most once.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--bank=DIGEST</option></term>
<listitem><para>Controls the PCR banks to pre-calculate the PCR values for in case
<command>calculate</command> is invoked , or the banks to show in the <command>status</command>
output. May be used more then once to specify multiple banks. If not specified, defaults to the four
banks <literal>sha1</literal>, <literal>sha256</literal>, <literal>sha384</literal>,
<literal>sha512</literal>.</para></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />
</variablelist>
</refsect1>
<refsect1>
<title>Examples</title>
<example>
<title>Generate a unified kernel image, and calculate the expected TPM PCR 11 value</title>
<programlisting># objcopy \
--add-section .linux=vmlinux --change-section-vma .linux=0x2000000 \
--add-section .osrel=os-release.txt --change-section-vma .osrel=0x20000 \
--add-section .cmdline=cmdline.txt --change-section-vma .cmdline=0x30000 \
--add-section .initrd=initrd.cpio --change-section-vma .initrd=0x3000000 \
--add-section .splash=splash.bmp --change-section-vma .splash=0x100000 \
--add-section .dtb=devicetree.dtb --change-section-vma .dtb=0x40000 \
/usr/lib/systemd/boot/efi/linuxx64.efi.stub \
foo.efi
# systemd-measure calculate \
--linux=vmlinux \
--osrel=os-release \
--cmdline=cmdline.txt \
--initrd=initrd.cpio \
--splash=splash.bmp \
--dtb=devicetree.dtb
11:sha1=d775a7b4482450ac77e03ee19bda90bd792d6ec7
11:sha256=bc6170f9ce28eb051ab465cd62be8cf63985276766cf9faf527ffefb66f45651
11:sha384=1cf67dff4757e61e5a73d2a21a6694d668629bbc3761747d493f7f49ad720be02fd07263e1f93061243aec599d1ee4b4
11:sha512=8e79acd3ddbbc8282e98091849c3530f996303c8ac8e87a3b2378b71c8b3a6e86d5c4f41ecea9e1517090c3e8ec0c714821032038f525f744960bcd082d937da
</programlisting>
</example>
</refsect1>
<refsect1>
<title>Exit status</title>
<para>On success, 0 is returned, a non-zero failure code otherwise.</para>
</refsect1>
<refsect1>
<title>See Also</title>
<para>
<citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry>,
<citerefentry project='man-pages'><refentrytitle>objcopy</refentrytitle><manvolnum>1</manvolnum></citerefentry>
</para>
</refsect1>
</refentry>

View File

@ -2541,6 +2541,18 @@ if conf.get('HAVE_BLKID') == 1 and conf.get('HAVE_GNU_EFI') == 1
install_rpath : rootpkglibdir,
install : true,
install_dir : systemgeneratordir)
if conf.get('HAVE_OPENSSL') == 1
executable(
'systemd-measure',
'src/boot/measure.c',
include_directories : includes,
link_with : [libshared],
dependencies : [libopenssl],
install_rpath : rootpkglibdir,
install : true,
install_dir : rootlibexecdir)
endif
endif
executable(

533
src/boot/measure.c Normal file
View File

@ -0,0 +1,533 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <getopt.h>
#include <unistd.h>
#include "alloc-util.h"
#include "efi-loader.h"
#include "fd-util.h"
#include "fileio.h"
#include "hexdecoct.h"
#include "main-func.h"
#include "openssl-util.h"
#include "parse-argument.h"
#include "parse-util.h"
#include "pretty-print.h"
#include "terminal-util.h"
#include "tpm-pcr.h"
#include "tpm2-util.h"
#include "verbs.h"
/* Tool for pre-calculating expected TPM PCR values based on measured resources. This is intended to be used
* to pre-calculate suitable values for PCR 11, the way sd-stub measures into it. */
static char *arg_sections[_UNIFIED_SECTION_MAX] = {};
static char **arg_banks = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep);
static inline void free_sections(char*(*sections)[_UNIFIED_SECTION_MAX]) {
for (UnifiedSection c = 0; c < _UNIFIED_SECTION_MAX; c++)
free((*sections)[c]);
}
STATIC_DESTRUCTOR_REGISTER(arg_sections, free_sections);
static int help(int argc, char *argv[], void *userdata) {
_cleanup_free_ char *link = NULL;
int r;
r = terminal_urlify_man("systemd-measure", "1", &link);
if (r < 0)
return log_oom();
printf("%1$s [OPTIONS...] COMMAND ...\n"
"\n%5$sPre-calculate PCR hash for kernel image.%6$s\n"
"\n%3$sCommands:%4$s\n"
" status Show current PCR values\n"
" calculate Calculate expected PCR values\n"
"\n%3$sOptions:%4$s\n"
" -h --help Show this help\n"
" --version Print version\n"
" --linux=PATH Path Linux kernel ELF image\n"
" --osrel=PATH Path to os-release file\n"
" --cmdline=PATH Path to file with kernel command line\n"
" --initrd=PATH Path to initrd image\n"
" --splash=PATH Path to splash bitmap\n"
" --dtb=PATH Path to Devicetree file\n"
" --bank=DIGEST Select TPM bank (SHA1, SHA256)\n"
"\nSee the %2$s for details.\n",
program_invocation_short_name,
link,
ansi_underline(),
ansi_normal(),
ansi_highlight(),
ansi_normal());
return 0;
}
static int parse_argv(int argc, char *argv[]) {
enum {
ARG_VERSION = 0x100,
_ARG_SECTION_FIRST,
ARG_LINUX = _ARG_SECTION_FIRST,
ARG_OSREL,
ARG_CMDLINE,
ARG_INITRD,
ARG_SPLASH,
_ARG_SECTION_LAST,
ARG_DTB = _ARG_SECTION_LAST,
ARG_BANK,
};
static const struct option options[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, ARG_VERSION },
{ "linux", required_argument, NULL, ARG_LINUX },
{ "osrel", required_argument, NULL, ARG_OSREL },
{ "cmdline", required_argument, NULL, ARG_CMDLINE },
{ "initrd", required_argument, NULL, ARG_INITRD },
{ "splash", required_argument, NULL, ARG_SPLASH },
{ "dtb", required_argument, NULL, ARG_DTB },
{ "bank", required_argument, NULL, ARG_BANK },
{}
};
int c, r;
assert(argc >= 0);
assert(argv);
/* Make sure the arguments list and the section list, stays in sync */
assert_cc(_ARG_SECTION_FIRST + _UNIFIED_SECTION_MAX == _ARG_SECTION_LAST + 1);
while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
switch (c) {
case 'h':
help(0, NULL, NULL);
return 0;
case ARG_VERSION:
return version();
case _ARG_SECTION_FIRST..._ARG_SECTION_LAST: {
UnifiedSection section = c - _ARG_SECTION_FIRST;
r = parse_path_argument(optarg, /* suppress_root= */ false, arg_sections + section);
if (r < 0)
return r;
break;
}
case ARG_BANK: {
const EVP_MD *implementation;
implementation = EVP_get_digestbyname(optarg);
if (!implementation)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown bank '%s', refusing.", optarg);
if (strv_extend(&arg_banks, EVP_MD_name(implementation)) < 0)
return log_oom();
break;
}
case '?':
return -EINVAL;
default:
assert_not_reached();
}
if (strv_isempty(arg_banks)) {
/* If no banks are specifically selected, pick all known banks */
arg_banks = strv_new("SHA1", "SHA256", "SHA384", "SHA512");
if (!arg_banks)
return log_oom();
}
strv_sort(arg_banks);
strv_uniq(arg_banks);
return 1;
}
typedef struct PcrState {
const EVP_MD *md;
void *value;
size_t value_size;
} PcrState;
static void pcr_state_free_all(PcrState **pcr_state) {
assert(pcr_state);
if (!*pcr_state)
return;
for (size_t i = 0; (*pcr_state)[i].value; i++)
free((*pcr_state)[i].value);
*pcr_state = mfree(*pcr_state);
}
static void evp_md_ctx_free_all(EVP_MD_CTX **md[]) {
assert(md);
if (!*md)
return;
for (size_t i = 0; (*md)[i]; i++)
EVP_MD_CTX_free((*md)[i]);
*md = mfree(*md);
}
static int pcr_state_extend(PcrState *pcr_state, const void *data, size_t sz) {
_cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *mc = NULL;
unsigned value_size;
assert(pcr_state);
assert(data || sz == 0);
assert(pcr_state->md);
assert(pcr_state->value);
assert(pcr_state->value_size > 0);
/* Extends a (virtual) PCR by the given data */
mc = EVP_MD_CTX_new();
if (!mc)
return log_oom();
if (EVP_DigestInit_ex(mc, pcr_state->md, NULL) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize %s context.", EVP_MD_name(pcr_state->md));
/* First thing we do, is hash the old PCR value */
if (EVP_DigestUpdate(mc, pcr_state->value, pcr_state->value_size) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to run digest.");
/* Then, we hash the new data */
if (EVP_DigestUpdate(mc, data, sz) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to run digest.");
if (EVP_DigestFinal_ex(mc, pcr_state->value, &value_size) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finalize hash context.");
assert(value_size == pcr_state->value_size);
return 0;
}
#define BUFFER_SIZE (16U * 1024U)
static int measure_pcr(PcrState *pcr_states, size_t n) {
_cleanup_free_ void *buffer = NULL;
int r;
assert(n > 0);
assert(pcr_states);
buffer = malloc(BUFFER_SIZE);
if (!buffer)
return log_oom();
for (UnifiedSection c = 0; c < _UNIFIED_SECTION_MAX; c++) {
_cleanup_(evp_md_ctx_free_all) EVP_MD_CTX **mdctx = NULL;
_cleanup_close_ int fd = -1;
uint64_t m = 0;
if (!arg_sections[c])
continue;
fd = open(arg_sections[c], O_RDONLY|O_CLOEXEC);
if (fd < 0)
return log_error_errno(errno, "Failed to open '%s': %m", arg_sections[c]);
/* Allocate one message digest context per bank (NULL terminated) */
mdctx = new0(EVP_MD_CTX*, n + 1);
if (!mdctx)
return log_oom();
for (size_t i = 0; i < n; i++) {
mdctx[i] = EVP_MD_CTX_new();
if (!mdctx[i])
return log_oom();
if (EVP_DigestInit_ex(mdctx[i], pcr_states[i].md, NULL) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize data %s context.", EVP_MD_name(pcr_states[i].md));
}
for (;;) {
ssize_t sz;
sz = read(fd, buffer, BUFFER_SIZE);
if (sz < 0)
return log_error_errno(errno, "Failed to read '%s': %m", arg_sections[c]);
if (sz == 0) /* EOF */
break;
for (size_t i = 0; i < n; i++)
if (EVP_DigestUpdate(mdctx[i], buffer, sz) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to run digest.");
m += sz;
}
fd = safe_close(fd);
if (m == 0) /* We skip over empty files, the stub does so too */
continue;
for (size_t i = 0; i < n; i++) {
_cleanup_free_ void *data_hash = NULL;
unsigned data_hash_size;
data_hash = malloc(pcr_states[i].value_size);
if (!data_hash)
return log_oom();
/* Measure name of section */
if (EVP_Digest(unified_sections[c], strlen(unified_sections[c]) + 1, data_hash, &data_hash_size, pcr_states[i].md, NULL) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to hash section name with %s.", EVP_MD_name(pcr_states[i].md));
assert(data_hash_size == (unsigned) pcr_states[i].value_size);
r = pcr_state_extend(pcr_states + i, data_hash, data_hash_size);
if (r < 0)
return r;
/* Retrieve hash of data an measure it*/
if (EVP_DigestFinal_ex(mdctx[i], data_hash, &data_hash_size) != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finalize hash context.");
assert(data_hash_size == (unsigned) pcr_states[i].value_size);
r = pcr_state_extend(pcr_states + i, data_hash, data_hash_size);
if (r < 0)
return r;
}
}
return 0;
}
static int verb_calculate(int argc, char *argv[], void *userdata) {
_cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL;
size_t n = 0;
int r;
if (!arg_sections[UNIFIED_SECTION_LINUX])
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--linux= switch must be specified, refusing.");
pcr_states = new0(PcrState, strv_length(arg_banks) + 1);
if (!pcr_states)
return log_oom();
/* Allocate a PCR state structure, one for each bank */
STRV_FOREACH(d, arg_banks) {
const EVP_MD *implementation;
_cleanup_free_ void *v = NULL;
int sz;
assert_se(implementation = EVP_get_digestbyname(*d)); /* Must work, we already checked while parsing command line */
sz = EVP_MD_size(implementation);
if (sz <= 0 || sz >= INT_MAX)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unexpected digest size: %i", sz);
v = malloc0(sz); /* initial PCR state is all zeroes */
if (!v)
return log_oom();
pcr_states[n++] = (struct PcrState) {
.md = implementation,
.value = TAKE_PTR(v),
.value_size = sz,
};
}
r = measure_pcr(pcr_states, n);
if (r < 0)
return r;
for (size_t i = 0; i < n; i++) {
_cleanup_free_ char *hd = NULL, *b = NULL;
hd = hexmem(pcr_states[i].value, pcr_states[i].value_size);
if (!hd)
return log_oom();
b = strdup(EVP_MD_name(pcr_states[i].md));
if (!b)
return log_oom();
printf("%" PRIu32 ":%s=%s\n", TPM_PCR_INDEX_KERNEL_IMAGE, ascii_strlower(b), hd);
}
return 0;
}
static int compare_reported_pcr_nr(uint32_t pcr, const char *varname, const char *description) {
_cleanup_free_ char *s = NULL;
uint32_t v;
int r;
r = efi_get_variable_string(varname, &s);
if (r == -ENOENT)
return 0;
if (r < 0)
return log_error_errno(r, "Failed to read EFI variable '%s': %m", varname);
r = safe_atou32(s, &v);
if (r < 0)
return log_error_errno(r, "Failed to parse EFI variable '%s': %s", varname, s);
if (pcr != v)
log_warning("PCR number reported by stub for %s (%" PRIu32 ") different from our expectation (%" PRIu32 ").\n"
"The measurements are likely inconsistent.", description, v, pcr);
return 0;
}
static int validate_stub(void) {
uint64_t features;
bool found = false;
int r;
if (tpm2_support() != TPM2_SUPPORT_FULL)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Sorry, system lacks full TPM2 support.");
r = efi_stub_get_features(&features);
if (r < 0)
return log_error_errno(r, "Unable to get stub features: %m");
if (!FLAGS_SET(features, EFI_STUB_FEATURE_THREE_PCRS))
log_warning("Warning: current kernel image does not support measuring itself, the command line or initrd system extension images.\n"
"The PCR measurements seen are unlikely to be valid.");
r = compare_reported_pcr_nr(TPM_PCR_INDEX_KERNEL_IMAGE, EFI_LOADER_VARIABLE("StubPcrKernelImage"), "kernel image");
if (r < 0)
return r;
r = compare_reported_pcr_nr(TPM_PCR_INDEX_KERNEL_PARAMETERS, EFI_LOADER_VARIABLE("StubPcrKernelParameters"), "kernel parameters");
if (r < 0)
return r;
r = compare_reported_pcr_nr(TPM_PCR_INDEX_INITRD_SYSEXTS, EFI_LOADER_VARIABLE("StubPcrInitRDSysExts"), "initrd system extension images");
if (r < 0)
return r;
STRV_FOREACH(bank, arg_banks) {
_cleanup_free_ char *b = NULL, *p = NULL;
b = strdup(*bank);
if (!b)
return log_oom();
if (asprintf(&p, "/sys/class/tpm/tpm0/pcr-%s/", ascii_strlower(b)) < 0)
return log_oom();
if (access(p, F_OK) < 0) {
if (errno != ENOENT)
return log_error_errno(errno, "Failed to detect if '%s' exists: %m", b);
} else
found = true;
}
if (!found)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "None of the select PCR banks appear to exist.");
return 0;
}
static int verb_status(int argc, char *argv[], void *userdata) {
static const struct {
uint32_t nr;
const char *description;
} relevant_pcrs[] = {
{ TPM_PCR_INDEX_KERNEL_IMAGE, "Unified Kernel Image" },
{ TPM_PCR_INDEX_KERNEL_PARAMETERS, "Kernel Parameters" },
{ TPM_PCR_INDEX_INITRD_SYSEXTS, "initrd System Extensions" },
};
int r;
r = validate_stub();
if (r < 0)
return r;
for (size_t i = 0; i < ELEMENTSOF(relevant_pcrs); i++) {
STRV_FOREACH(bank, arg_banks) {
_cleanup_free_ char *b = NULL, *p = NULL, *s = NULL, *f = NULL;
_cleanup_free_ void *h = NULL;
size_t l;
b = strdup(*bank);
if (!b)
return log_oom();
if (asprintf(&p, "/sys/class/tpm/tpm0/pcr-%s/%" PRIu32, ascii_strlower(b), relevant_pcrs[i].nr) < 0)
return log_oom();
r = read_virtual_file(p, 4096, &s, NULL);
if (r == -ENOENT)
continue;
if (r < 0)
return log_error_errno(r, "Failed to read '%s': %m", p);
r = unhexmem(strstrip(s), SIZE_MAX, &h, &l);
if (r < 0)
return log_error_errno(r, "Failed to decode PCR value '%s': %m", s);
f = hexmem(h, l);
if (!h)
return log_oom();
if (bank == arg_banks) {
/* before the first line for each PCR, write a short descriptive text to
* stderr, and leave the primary content on stdout */
fflush(stdout);
fprintf(stderr, "%s# PCR[%" PRIu32 "] %s%s%s\n",
ansi_grey(),
relevant_pcrs[i].nr,
relevant_pcrs[i].description,
memeqzero(h, l) ? " (NOT SET!)" : "",
ansi_normal());
fflush(stderr);
}
printf("%" PRIu32 ":%s=%s\n", relevant_pcrs[i].nr, b, f);
}
}
return 0;
}
static int measure_main(int argc, char *argv[]) {
static const Verb verbs[] = {
{ "help", VERB_ANY, VERB_ANY, 0, help },
{ "status", VERB_ANY, 1, VERB_DEFAULT, verb_status },
{ "calculate", VERB_ANY, 1, 0, verb_calculate },
{}
};
return dispatch_verb(argc, argv, verbs, NULL);
}
static int run(int argc, char *argv[]) {
int r;
log_show_color(true);
log_parse_environment();
log_open();
r = parse_argv(argc, argv);
if (r <= 0)
return r;
return measure_main(argc, argv);
}
DEFINE_MAIN_FUNCTION(run);