Thomas Heijligen has uploaded this change for review.

View Change

layout/fmap: reimplement for endian independence

Change-Id: I6d38649327d2021a9917d1294f1541376ed6717d
Signed-off-by: Thomas Heijligen <thomas.heijligen@secunet.de>
---
M fmap.c
M layout.h
M libflashrom.c
3 files changed, 177 insertions(+), 375 deletions(-)

git pull ssh://review.coreboot.org:29418/flashrom refs/changes/65/57265/1
diff --git a/fmap.c b/fmap.c
index b18cbf7..903ffc2 100644
--- a/fmap.c
+++ b/fmap.c
@@ -1,331 +1,158 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
/*
- * Copyright 2010, Google LLC.
- * Copyright 2018-present, Facebook Inc.
- * All rights reserved.
+ * This file is part of flashrom project. For the full license text see LICENSE.txt.
*
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the following disclaimer
- * in the documentation and/or other materials provided with the
- * distribution.
- * * Neither the name of Google Inc. nor the names of its
- * contributors may be used to endorse or promote products derived from
- * this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * Alternatively, this software may be distributed under the terms of the
- * GNU General Public License ("GPL") version 2 as published by the Free
- * Software Foundation.
+ * Copyright (C) 2021, secunet Secure Networks AG,
+ * written by Thomas Heijligen <thomas.heijligen@secunet.com>
*/

-#include <ctype.h>
-#include <stdlib.h>
+/*
+ * This file contais functions to find and read layout information from a fmap structure.
+ */
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
#include <string.h>
-#include <sys/types.h>
-#include "flash.h"
-#include "fmap.h"
+#include "layout.h"
+#include "libflashrom.h"
+#include "hwaccess.h"

-static size_t fmap_size(const struct fmap *fmap)
+#define FMAP_SIGNATURE "__FMAP__"
+#define FMAP_NAME_SIZE 32
+#define FMAP_VERSION_MAJOR 1
+#define FMAP_VERSION_MINOR 1
+
+#define FMAP_HEADER_SIZE 56
+#define FMAP_HEADER_OFFSET_SIGNATURE 0
+#define FMAP_HEADER_OFFSET_VER_MAJOR 8
+#define FMAP_HEADER_OFFSET_VER_MINOR 9
+#define FMAP_HEADER_OFFSET_BASE 10
+#define FMAP_HEADER_OFFSET_SIZE 18
+#define FMAP_HEADER_OFFSET_NAME 22
+#define FMAP_HEADER_OFFSET_NAREAS 54
+
+#define FMAP_AREA_SIZE 42
+#define FMAP_AREA_OFFSET_OFFSET 0
+#define FMAP_AERA_OFFSET_SIZE 4
+#define FMAP_AREA_OFFSET_NAME 8
+#define FMAP_AREA_OFFSET_FLAGS 40
+
+/**
+ * @brief find the signature of a FMAP in a buffer, uses linear search
+ * @param[in] buffer
+ * @param[in] buffer_size
+ * @param[out] fmap_offset, on SUCCESS the offset where to find the fmap, if NULL or FAILURE don't change
+ * @return 0 SUCCESS
+ * 1 UNEXPECTED_NULL_POINTER
+ * 2 NOT_FOUND - FMAP signature not found
+ */
+int find_fmap_in_buffer(const void *const buffer, const uint64_t buffer_size, uint64_t *const fmap_offset)
{
- return sizeof(*fmap) + (fmap->nareas * sizeof(struct fmap_area));
-}
+ /* test for unexpected NULL pointer */
+ if (buffer == NULL) {
+ return 1; /* UNEXPECTED_NULL_POINTER */
+ }

-static int is_valid_fmap(const struct fmap *fmap)
-{
- if (memcmp(fmap, FMAP_SIGNATURE, strlen(FMAP_SIGNATURE)) != 0)
- return 0;
- /* strings containing the magic tend to fail here */
- if (fmap->ver_major > FMAP_VER_MAJOR)
- return 0;
- if (fmap->ver_minor > FMAP_VER_MINOR)
- return 0;
- /* a basic consistency check: flash address space size should be larger
- * than the size of the fmap data structure */
- if (fmap->size < fmap_size(fmap))
- return 0;
-
- /* fmap-alikes along binary data tend to fail on having a valid,
- * null-terminated string in the name field.*/
- int i;
- for (i = 0; i < FMAP_STRLEN; i++) {
- if (fmap->name[i] == 0)
- break;
- if (!isgraph(fmap->name[i]))
- return 0;
- if (i == FMAP_STRLEN - 1) {
- /* name is specified to be null terminated single-word string
- * without spaces. We did not break in the 0 test, we know it
- * is a printable spaceless string but we're seeing FMAP_STRLEN
- * symbols, which is one too many.
- */
- return 0;
+ for (uint64_t offset = 0; (offset + FMAP_HEADER_SIZE) < buffer_size; offset++) {
+ if (memcmp(buffer + offset + FMAP_HEADER_OFFSET_SIGNATURE, FMAP_SIGNATURE, strlen(FMAP_SIGNATURE)) == 0) {
+ if (fmap_offset != NULL) {
+ *fmap_offset = offset;
+ }
+ return 0; /* SUCCESS */
}
}
- return 1;
-
+ return 2; /* NOT_FOUND */
}

/**
- * @brief Do a brute-force linear search for fmap in provided buffer
+ * TODO: implement
*
- * @param[in] buffer The buffer to search
- * @param[in] len Length (in bytes) to search
- *
- * @return offset in buffer where fmap is found if successful
- * -1 to indicate that fmap was not found
- * -2 to indicate fmap is truncated or exceeds buffer + len
+ * @brief find the signature of a FMAP on the flash chip
+ * @param[in] flashctx
+ * @param[out] fmap_offset on SUCCESS offset of the FMAP, else not changed
+ * @param[out] fmap_size on SUCCESS size of the FMAP, else not changed
+ * @return 0 SUCESS
+ * 2 NOT_FOUND - FMAP signature not found
*/
-static off_t fmap_lsearch(const uint8_t *buf, size_t len)
+int find_fmap_in_rom(const struct flashrom_flashctx *const flashctx, uint64_t *const fmap_offset, uint64_t *const fmap_size)
{
- off_t offset;
- bool fmap_found = 0;
+ return 2;
+}

- for (offset = 0; offset <= (off_t)(len - sizeof(struct fmap)); offset++) {
- if (is_valid_fmap((struct fmap *)&buf[offset])) {
- fmap_found = 1;
- break;
+
+/**
+ * @brief read out the layout information from a FMAP in buffer
+ * @param[out] layout
+ * @param[in] buffer
+ * @param[in] buffer_size
+ * @param[in] fmap_offset
+ * @return 0 SUCCESS
+ * 1 UNEXPECTED_NULL_POINTER
+ * 3 BUFFER_TO_SMALL
+ * 4 INVALID_SIGNATURE
+ * 5 INVALID_VERSION
+ * 6 MEMORY_ALLOCATION_FAILED
+ */
+int read_layout_from_fmap(struct flashrom_layout **layout, const void *const buffer, const uint64_t buffer_size, const uint64_t fmap_offset)
+{
+ /* buffer must not be NULL */
+ if (buffer == NULL) {
+ return 1; /* FLASHROM_UNEXPECTED_NULL_POINTER */
+ }
+ /* buffer_size must be larger than fmap_offset + FMAP_HEADER_SIZE */
+ if (buffer_size < fmap_offset + FMAP_HEADER_SIZE) {
+ return 3; /* FLASHROM_BUFFER_TO_SMALL */
+ }
+ /* fmap signature check */
+ if (memcmp(buffer + fmap_offset + FMAP_HEADER_OFFSET_SIGNATURE, FMAP_SIGNATURE, strlen(FMAP_SIGNATURE)) != 0) {
+ return 4; /* FLASHROM_INVALID_SIGNATURE */
+ }
+ /* fmap version check */
+ if ((read_le8(buffer + fmap_offset + FMAP_HEADER_OFFSET_VER_MAJOR) != FMAP_VERSION_MAJOR) &&
+ (read_le8(buffer + fmap_offset + FMAP_HEADER_OFFSET_VER_MINOR) != FMAP_VERSION_MINOR)) {
+ return 5; /* FLASHROM_INVALID_VERSION */
+ }
+
+ const void* const fmap = buffer + fmap_offset;
+
+ struct flashrom_layout *l = NULL;
+ if (flashrom_layout_new(&l) != 0) {
+ return 6; /* FLASHROM_MEMORY_ALLOCATION_FAILED */
+ }
+ uint64_t firmware_base = read_le64(fmap + FMAP_HEADER_OFFSET_BASE);
+ /* uint32_t firmware_size = read_le32(fmap + FMAP_HEADER_OFFSET_SIZE); TODO: mode checks */
+ char firmware_name[FMAP_NAME_SIZE + 1] = {'\0'};
+ memcpy(firmware_name, fmap + FMAP_HEADER_OFFSET_NAME, FMAP_NAME_SIZE);
+ uint16_t firmware_nareas = read_le16(fmap + FMAP_HEADER_OFFSET_NAREAS);
+
+ /* check if buffer includes all fmap areas */
+ if (buffer_size < (fmap_offset + FMAP_HEADER_SIZE + (firmware_nareas * FMAP_AREA_SIZE))) {
+ return 3; /* FLASHROM_BUFFER_TO_SMALL */
+ }
+
+ for (unsigned int i = 0; i < firmware_nareas; i++) {
+ const void* const region = fmap + FMAP_HEADER_SIZE + (i * FMAP_AREA_SIZE);
+
+ uint32_t area_offset = read_le32(region + FMAP_AREA_OFFSET_OFFSET);
+ uint32_t area_size = read_le32(region + FMAP_AERA_OFFSET_SIZE);
+ char area_name[FMAP_NAME_SIZE + 1] = {'\0'};
+ char combined_name[(2 * FMAP_NAME_SIZE) + 2];
+ memcpy(area_name, region + FMAP_AREA_OFFSET_NAME, FMAP_NAME_SIZE);
+ snprintf(combined_name, sizeof(combined_name), "%s_%s", firmware_name, area_name);
+
+ int ret = flashrom_layout_add_region(l,
+ firmware_base + area_offset,
+ firmware_base + area_offset + area_size,
+ combined_name );
+
+ if (ret != 0) {
+ flashrom_layout_release(l);
+ return 6; /* FLASHROM_MEMORY_ALLOCATION_FAILED */
}
}

- if (!fmap_found)
- return -1;
-
- if (offset + fmap_size((struct fmap *)&buf[offset]) > len) {
- msg_gerr("fmap size exceeds buffer boundary.\n");
- return -2;
- }
-
- return offset;
-}
-
-/**
- * @brief Read fmap from provided buffer and copy it to fmap_out
- *
- * @param[out] fmap_out Double-pointer to location to store fmap contents.
- * Caller must free allocated fmap contents.
- * @param[in] buf Buffer to search
- * @param[in] len Length (in bytes) to search
- *
- * @return 0 if successful
- * 1 to indicate error
- * 2 to indicate fmap is not found
- */
-int fmap_read_from_buffer(struct fmap **fmap_out, const uint8_t *const buf, size_t len)
-{
- off_t offset = fmap_lsearch(buf, len);
- if (offset < 0) {
- msg_gdbg("Unable to find fmap in provided buffer.\n");
- return 2;
- }
- msg_gdbg("Found fmap at offset 0x%06zx\n", (size_t)offset);
-
- const struct fmap *fmap = (const struct fmap *)(buf + offset);
- *fmap_out = malloc(fmap_size(fmap));
- if (*fmap_out == NULL) {
- msg_gerr("Out of memory.\n");
- return 1;
- }
-
- memcpy(*fmap_out, fmap, fmap_size(fmap));
- return 0;
-}
-
-static int fmap_lsearch_rom(struct fmap **fmap_out,
- struct flashctx *const flashctx, size_t rom_offset, size_t len)
-{
- int ret = -1;
- uint8_t *buf;
-
- if (prepare_flash_access(flashctx, true, false, false, false))
- goto _finalize_ret;
-
- /* likely more memory than we need, but it simplifies handling and
- * printing offsets to keep them uniform with what's on the ROM */
- buf = malloc(rom_offset + len);
- if (!buf) {
- msg_gerr("Out of memory.\n");
- goto _finalize_ret;
- }
-
- ret = flashctx->chip->read(flashctx, buf + rom_offset, rom_offset, len);
- if (ret) {
- msg_pdbg("Cannot read ROM contents.\n");
- goto _free_ret;
- }
-
- ret = fmap_read_from_buffer(fmap_out, buf + rom_offset, len);
-_free_ret:
- free(buf);
-_finalize_ret:
- finalize_flash_access(flashctx);
- return ret;
-}
-
-static int fmap_bsearch_rom(struct fmap **fmap_out, struct flashctx *const flashctx,
- size_t rom_offset, size_t len, size_t min_stride)
-{
- size_t stride, fmap_len = 0;
- int ret = 1, fmap_found = 0, check_offset_0 = 1;
- struct fmap *fmap;
- const unsigned int chip_size = flashctx->chip->total_size * 1024;
- const int sig_len = strlen(FMAP_SIGNATURE);
-
- if (rom_offset + len > flashctx->chip->total_size * 1024)
- return 1;
-
- if (len < sizeof(*fmap))
- return 1;
-
- if (prepare_flash_access(flashctx, true, false, false, false))
- return 1;
-
- fmap = malloc(sizeof(*fmap));
- if (!fmap) {
- msg_gerr("Out of memory.\n");
- goto _free_ret;
- }
-
- /*
- * For efficient operation, we start with the largest stride possible
- * and then decrease the stride on each iteration. Also, check for a
- * remainder when modding the offset with the previous stride. This
- * makes it so that each offset is only checked once.
- *
- * Zero (rom_offset == 0) is a special case and is handled using a
- * variable to track whether or not we've checked it.
- */
- size_t offset;
- for (stride = chip_size / 2; stride >= min_stride; stride /= 2) {
- if (stride > len)
- continue;
-
- for (offset = rom_offset;
- offset <= rom_offset + len - sizeof(struct fmap);
- offset += stride) {
- if ((offset % (stride * 2) == 0) && (offset != 0))
- continue;
- if (offset == 0 && !check_offset_0)
- continue;
- check_offset_0 = 0;
-
- /* Read errors are considered non-fatal since we may
- * encounter locked regions and want to continue. */
- if (flashctx->chip->read(flashctx, (uint8_t *)fmap, offset, sig_len)) {
- /*
- * Print in verbose mode only to avoid excessive
- * messages for benign errors. Subsequent error
- * prints should be done as usual.
- */
- msg_cdbg("Cannot read %d bytes at offset %zu\n", sig_len, offset);
- continue;
- }
-
- if (memcmp(fmap, FMAP_SIGNATURE, sig_len) != 0)
- continue;
-
- if (flashctx->chip->read(flashctx, (uint8_t *)fmap + sig_len,
- offset + sig_len, sizeof(*fmap) - sig_len)) {
- msg_cerr("Cannot read %zu bytes at offset %06zx\n",
- sizeof(*fmap) - sig_len, offset + sig_len);
- continue;
- }
-
- if (is_valid_fmap(fmap)) {
- msg_gdbg("fmap found at offset 0x%06zx\n", offset);
- fmap_found = 1;
- break;
- }
- msg_gerr("fmap signature found at %zu but header is invalid.\n", offset);
- ret = 2;
- }
-
- if (fmap_found)
- break;
- }
-
- if (!fmap_found)
- goto _free_ret;
-
- fmap_len = fmap_size(fmap);
- struct fmap *tmp = fmap;
- fmap = realloc(fmap, fmap_len);
- if (!fmap) {
- msg_gerr("Failed to realloc.\n");
- free(tmp);
- goto _free_ret;
- }
-
- if (flashctx->chip->read(flashctx, (uint8_t *)fmap + sizeof(*fmap),
- offset + sizeof(*fmap), fmap_len - sizeof(*fmap))) {
- msg_cerr("Cannot read %zu bytes at offset %06zx\n",
- fmap_len - sizeof(*fmap), offset + sizeof(*fmap));
- /* Treat read failure to be fatal since this
- * should be a valid, usable fmap. */
- ret = 2;
- goto _free_ret;
- }
-
- *fmap_out = fmap;
- ret = 0;
-_free_ret:
- if (ret)
- free(fmap);
- finalize_flash_access(flashctx);
- return ret;
-}
-
-/**
- * @brief Read fmap from ROM
- *
- * @param[out] fmap_out Double-pointer to location to store fmap contents.
- * Caller must free allocated fmap contents.
- * @param[in] flashctx Flash context
- * @param[in] rom_offset Offset in ROM to begin search
- * @param[in] len Length to search relative to rom_offset
- *
- * @return 0 on success,
- * 2 if the fmap couldn't be read,
- * 1 on any other error.
- */
-int fmap_read_from_rom(struct fmap **fmap_out,
- struct flashctx *const flashctx, size_t rom_offset, size_t len)
-{
- int ret;
-
- if (!flashctx || !flashctx->chip)
- return 1;
-
- /*
- * Binary search is used at first to see if we can find an fmap quickly
- * in a usual location (often at a power-of-2 offset). However, once we
- * reach a small enough stride the transaction overhead will reverse the
- * speed benefit of using bsearch at which point we need to use brute-
- * force instead.
- *
- * TODO: Since flashrom is often used with high-latency external
- * programmers we should not be overly aggressive with bsearch.
- */
- ret = fmap_bsearch_rom(fmap_out, flashctx, rom_offset, len, 256);
- if (ret) {
- msg_gdbg("Binary search failed, trying linear search...\n");
- ret = fmap_lsearch_rom(fmap_out, flashctx, rom_offset, len);
- }
-
- return ret;
-}
+ *layout = l;
+ return 0; /* FLASHROM_SUCCESS */
+}
\ No newline at end of file
diff --git a/layout.h b/layout.h
index 713241f..6e084ae 100644
--- a/layout.h
+++ b/layout.h
@@ -68,4 +68,8 @@
void prepare_layout_for_extraction(struct flashrom_flashctx *);
int layout_sanity_checks(const struct flashrom_flashctx *);

+int find_fmap_in_buffer(const void *const buffer, const uint64_t buffer_size, uint64_t *const fmap_offset);
+int find_fmap_in_rom(const struct flashrom_flashctx *const flashctx, uint64_t *const fmap_offset, uint64_t *const fmap_size);
+int read_layout_from_fmap(struct flashrom_layout **layout, const void *const buffer, const uint64_t buffer_size, const uint64_t fmap_offset);
+
#endif /* !__LAYOUT_H__ */
diff --git a/libflashrom.c b/libflashrom.c
index 1ecf650..ae8cd21 100644
--- a/libflashrom.c
+++ b/libflashrom.c
@@ -495,31 +495,6 @@
#endif
}

-#ifdef __FLASHROM_LITTLE_ENDIAN__
-static int flashrom_layout_parse_fmap(struct flashrom_layout **layout,
- struct flashctx *const flashctx, const struct fmap *const fmap)
-{
- int i;
- char name[FMAP_STRLEN + 1];
- const struct fmap_area *area;
- struct flashrom_layout *l;
-
- if (!fmap || flashrom_layout_new(&l))
- return 1;
-
- for (i = 0, area = fmap->areas; i < fmap->nareas; i++, area++) {
- snprintf(name, sizeof(name), "%s", area->name);
- if (flashrom_layout_add_region(l, area->offset, area->offset + area->size - 1, name)) {
- flashrom_layout_release(l);
- return 1;
- }
- }
-
- *layout = l;
- return 0;
-}
-#endif /* __FLASHROM_LITTLE_ENDIAN__ */
-
/**
* @brief Read a layout by searching the flash chip for fmap.
*
@@ -537,27 +512,35 @@
int flashrom_layout_read_fmap_from_rom(struct flashrom_layout **const layout,
struct flashctx *const flashctx, off_t offset, size_t len)
{
-#ifndef __FLASHROM_LITTLE_ENDIAN__
- return 3;
-#else
- struct fmap *fmap = NULL;
- int ret = 0;
+ (void)(offset); // TODO: remove unused parameter

- msg_gdbg("Attempting to read fmap from ROM content.\n");
- if (fmap_read_from_rom(&fmap, flashctx, offset, len)) {
- msg_gerr("Failed to read fmap from ROM.\n");
- return 1;
+ int ret;
+ uint64_t fmap_offset, fmap_size;
+
+ if(flashctx == NULL || flashctx->chip == NULL || flashctx->chip->read == NULL) {
+ return 2; // UNEXPECTED_NULL_POINTER
}

- msg_gdbg("Adding fmap layout to global layout.\n");
- if (flashrom_layout_parse_fmap(layout, flashctx, fmap)) {
- msg_gerr("Failed to add fmap regions to layout.\n");
- ret = 1;
+ ret = find_fmap_in_rom(flashctx, &fmap_offset, &fmap_size);
+ if (ret != 0) {
+ return ret;
}
+ void *fmap_buffer = malloc(fmap_size);
+ if (fmap_buffer == NULL) {
+ return 1; // MEMORY_ALLOCATION_FAILED
+ }
+ // prepare flash access?
+ // use flashrom_image_read?
+ ret = flashctx->chip->read(flashctx, fmap_buffer, fmap_offset, fmap_size);
+ if (ret != 0) {
+ free(fmap_buffer);
+ return ret;
+ }
+ ret = read_layout_from_fmap(layout, fmap_buffer, fmap_size, 0);
+ free(fmap_buffer);

- free(fmap);
+ // TODO add layout to flashctx?
return ret;
-#endif
}

/**
@@ -577,33 +560,21 @@
int flashrom_layout_read_fmap_from_buffer(struct flashrom_layout **const layout,
struct flashctx *const flashctx, const uint8_t *const buf, size_t size)
{
-#ifndef __FLASHROM_LITTLE_ENDIAN__
- return 3;
-#else
- struct fmap *fmap = NULL;
- int ret = 1;
+ (void)(flashctx); // Unused
+ int ret;
+ uint64_t fmap_offset;

- if (!buf || !size)
- goto _ret;
-
- msg_gdbg("Attempting to read fmap from buffer.\n");
- if (fmap_read_from_buffer(&fmap, buf, size)) {
- msg_gerr("Failed to read fmap from buffer.\n");
- goto _ret;
+ ret = find_fmap_in_buffer(buf, size, &fmap_offset);
+ if (ret != 0) {
+ return ret;
+ }
+ ret = read_layout_from_fmap(layout, buf, size, fmap_offset);
+ if( ret != 0) {
+ return ret;
}

- msg_gdbg("Adding fmap layout to global layout.\n");
- if (flashrom_layout_parse_fmap(layout, flashctx, fmap)) {
- msg_gerr("Failed to add fmap regions to layout.\n");
- goto _free_ret;
- }
-
- ret = 0;
-_free_ret:
- free(fmap);
-_ret:
- return ret;
-#endif
+ // TODO add layout to flashctx?
+ return 0;
}

/**

To view, visit change 57265. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: flashrom
Gerrit-Branch: master
Gerrit-Change-Id: I6d38649327d2021a9917d1294f1541376ed6717d
Gerrit-Change-Number: 57265
Gerrit-PatchSet: 1
Gerrit-Owner: Thomas Heijligen <src@posteo.de>
Gerrit-MessageType: newchange