From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from phobos.denx.de (phobos.denx.de [85.214.62.61]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.lore.kernel.org (Postfix) with ESMTPS id 4E645C54EBC for ; Sat, 7 Jan 2023 21:10:13 +0000 (UTC) Received: from h2850616.stratoserver.net (localhost [IPv6:::1]) by phobos.denx.de (Postfix) with ESMTP id 743CE855FF; Sat, 7 Jan 2023 22:08:40 +0100 (CET) Authentication-Results: phobos.denx.de; dmarc=pass (p=none dis=none) header.from=chromium.org Authentication-Results: phobos.denx.de; spf=pass smtp.mailfrom=u-boot-bounces@lists.denx.de Authentication-Results: phobos.denx.de; dkim=pass (1024-bit key; unprotected) header.d=chromium.org header.i=@chromium.org header.b="cyVnI7vN"; dkim-atps=neutral Received: by phobos.denx.de (Postfix, from userid 109) id 2A6F8853C0; Sat, 7 Jan 2023 22:08:01 +0100 (CET) Received: from mail-il1-x131.google.com (mail-il1-x131.google.com [IPv6:2607:f8b0:4864:20::131]) (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits)) (No client certificate requested) by phobos.denx.de (Postfix) with ESMTPS id BC17A85514 for ; Sat, 7 Jan 2023 22:07:39 +0100 (CET) Authentication-Results: phobos.denx.de; dmarc=pass (p=none dis=none) header.from=chromium.org Authentication-Results: phobos.denx.de; spf=pass smtp.mailfrom=sjg@chromium.org Received: by mail-il1-x131.google.com with SMTP id g2so2819286ila.4 for ; Sat, 07 Jan 2023 13:07:39 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=chromium.org; s=google; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=t+oCPD6brljvq9fdpmlNJtg0C8RjPqz56nH9PUDaUQw=; b=cyVnI7vNu2jGLaZuiU5U9oCqqjXi5XujdOf1Iax3DvPzBKpMn1QTjLNyU5/C9h+Lcc pxeT9N5GqcKf+1LZToMOz61F0gRWwbnmuLVCjNYayWIn8ecIys93I8UlFHhBtupnMlun P1YoAiL+KMl+ECKqVwUkIxuqeAJ3UWlUtz4TY= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=t+oCPD6brljvq9fdpmlNJtg0C8RjPqz56nH9PUDaUQw=; b=zLqGYJvyMD9hNsxiGzhSuTQI8lBW1n69tEJvp9czoKiGBeBI9aKpn1+bxEvnjYYVmv zEBZcfCCX0KsxwFkRGdP5uSlpA5BL90HEBsvby3XJhw9TfAwJ8KeZEABLorfFfuleVNJ bUJHWxJRwvty5tkUmboaoJDfnrQLnpmdYslBjSKX/TFAMWcE3vj1Cw8mUWYoUhgB7Rme wbwreQ4BbQRxnqEkq+2gF2IQ3vkbS5SH7gYbehIkNecuY8G05ooK2zyqjf2DzVXY2lUy OfWVyB/OPSNSMiEJaG9wTnFgSLt3k/dQTb6Z+wLf2mp3OZIuYA6xyNT0I8/RJfeBelMa QLCg== X-Gm-Message-State: AFqh2kor4fKGHnefVXg/haldEI9kZHWvuAch0IWErLl5vT6iXT8pwgiv YAaOvZIaq9insAzeKn16YSMGkGzjWvGY59ZH X-Google-Smtp-Source: AMrXdXuSsjNX2gbgZ9QIzHmqmF3Chs7Hg+wapSDxEJdyfCWl3UmV+JW1gkQf+4Qsa5WElf/oyjBi3g== X-Received: by 2002:a92:dccb:0:b0:30d:76f3:ad5e with SMTP id b11-20020a92dccb000000b0030d76f3ad5emr9811005ilr.25.1673125657977; Sat, 07 Jan 2023 13:07:37 -0800 (PST) Received: from sjg1.roam.corp.google.com (c-71-56-217-229.hsd1.co.comcast.net. [71.56.217.229]) by smtp.gmail.com with ESMTPSA id l10-20020a92d94a000000b0030c00897497sm1465825ilq.19.2023.01.07.13.07.37 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sat, 07 Jan 2023 13:07:37 -0800 (PST) From: Simon Glass To: U-Boot Mailing List Cc: Roger Quadros , Tom Rini , Alper Nebi Yasak , Peter Geis , Jerome Forissier , huang lin , Jeffy Chen , Simon Glass , Kever Yang , Philipp Tomsich , Ivan Mikhaylov , Quentin Schulz , Philippe Reynes Subject: [PATCH v9 07/14] binman: Support new op-tee binary format Date: Sat, 7 Jan 2023 14:07:14 -0700 Message-Id: <20230107210721.478062-8-sjg@chromium.org> X-Mailer: git-send-email 2.39.0.314.g84b9a713c41-goog In-Reply-To: <20230107210721.478062-1-sjg@chromium.org> References: <20230107210721.478062-1-sjg@chromium.org> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-BeenThere: u-boot@lists.denx.de X-Mailman-Version: 2.1.39 Precedence: list List-Id: U-Boot discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: u-boot-bounces@lists.denx.de Sender: "U-Boot" X-Virus-Scanned: clamav-milter 0.103.6 at phobos.denx.de X-Virus-Status: Clean OP-TEE has a format with a binary header that can be used instead of the ELF file. With newer versions of OP-TEE this may be required on some platforms. Add support for this in binman. First, add a method to obtain the ELF sections from an entry, then use that in the FIT support. We then end up with the ability to support both types of OP-TEE files, depending on which one is passed in with the entry argument (TEE=xxx in the U-Boot build). Signed-off-by: Simon Glass --- (no changes since v8) Changes in v8: - Fix 'anb', 'provide' and 'second' typos - Don't silence the return code from tee_os.ObtainContents() - Rename is_optee_bin() - Improve various comments Changes in v7: - Correct missing test coverage Changes in v6: - Update op-tee to support new v1 binary header tools/binman/entries.rst | 37 ++++++++- tools/binman/entry.py | 13 +++ tools/binman/etype/fit.py | 72 +++++++++-------- tools/binman/etype/section.py | 9 +++ tools/binman/etype/tee_os.py | 76 +++++++++++++++++- tools/binman/ftest.py | 83 ++++++++++++++++++++ tools/binman/test/263_tee_os_opt.dts | 22 ++++++ tools/binman/test/264_tee_os_opt_fit.dts | 33 ++++++++ tools/binman/test/265_tee_os_opt_fit_bad.dts | 40 ++++++++++ 9 files changed, 352 insertions(+), 33 deletions(-) create mode 100644 tools/binman/test/263_tee_os_opt.dts create mode 100644 tools/binman/test/264_tee_os_opt_fit.dts create mode 100644 tools/binman/test/265_tee_os_opt_fit_bad.dts diff --git a/tools/binman/entries.rst b/tools/binman/entries.rst index 5b9eb8b82c7..f6cc8003853 100644 --- a/tools/binman/entries.rst +++ b/tools/binman/entries.rst @@ -1508,12 +1508,47 @@ Entry: tee-os: Entry containing an OP-TEE Trusted OS (TEE) blob Properties / Entry arguments: - tee-os-path: Filename of file to read into entry. This is typically - called tee-pager.bin + called tee.bin or tee.elf This entry holds the run-time firmware, typically started by U-Boot SPL. See the U-Boot README for your architecture or board for how to use it. See https://github.com/OP-TEE/optee_os for more information about OP-TEE. +Note that if the file is in ELF format, it must go in a FIT. In that case, +this entry will mark itself as absent, providing the data only through the +read_elf_segments() method. + +Marking this entry as absent means that it if is used in the wrong context +it can be automatically dropped. Thus it is possible to add an OP-TEE entry +like this:: + + binman { + tee-os { + }; + }; + +and pass either an ELF or plain binary in with -a tee-os-path +and have binman do the right thing: + + - include the entry if tee.bin is provided and it does NOT have the v1 + header + - drop it otherwise + +When used within a FIT, we can do:: + + binman { + fit { + tee-os { + }; + }; + }; + +which will split the ELF into separate nodes for each segment, if an ELF +file is provided (see :ref:`etype_fit`), or produce a single node if the +OP-TEE binary v1 format is provided (see optee_doc_) . + +.. _optee_doc: https://optee.readthedocs.io/en/latest/architecture/core.html#partitioning-of-the-binary + .. _etype_text: diff --git a/tools/binman/entry.py b/tools/binman/entry.py index 637aece3705..de51d295891 100644 --- a/tools/binman/entry.py +++ b/tools/binman/entry.py @@ -1290,3 +1290,16 @@ features to produce new behaviours. def mark_absent(self, msg): tout.info("Entry '%s' marked absent: %s" % (self._node.path, msg)) self.absent = True + + def read_elf_segments(self): + """Read segments from an entry that can generate an ELF file + + Returns: + tuple: + list of segments, each: + int: Segment number (0 = first) + int: Start address of segment in memory + bytes: Contents of segment + int: entry address of ELF file + """ + return None diff --git a/tools/binman/etype/fit.py b/tools/binman/etype/fit.py index 8ad4f3a8a83..fea3adcc68d 100644 --- a/tools/binman/etype/fit.py +++ b/tools/binman/etype/fit.py @@ -540,41 +540,35 @@ class Entry_fit(Entry_section): else: self.Raise("Generator node requires 'fit,fdt-list' property") - def _gen_split_elf(base_node, node, elf_data, missing): + def _gen_split_elf(base_node, node, segments, entry_addr): """Add nodes for the ELF file, one per group of contiguous segments Args: base_node (Node): Template node from the binman definition node (Node): Node to replace (in the FIT being built) - data (bytes): ELF-format data to process (may be empty) - missing (bool): True if any of the data is missing - + segments (list): list of segments, each: + int: Segment number (0 = first) + int: Start address of segment in memory + bytes: Contents of segment + entry_addr (int): entry address of ELF file """ - # If any pieces are missing, skip this. The missing entries will - # show an error - if not missing: - try: - segments, entry = elf.read_loadable_segments(elf_data) - except ValueError as exc: - self._raise_subnode(node, - f'Failed to read ELF file: {str(exc)}') - for (seq, start, data) in segments: - node_name = node.name[1:].replace('SEQ', str(seq + 1)) - with fsw.add_node(node_name): - loadables.append(node_name) - for pname, prop in node.props.items(): - if not pname.startswith('fit,'): - fsw.property(pname, prop.bytes) - elif pname == 'fit,load': - fsw.property_u32('load', start) - elif pname == 'fit,entry': - if seq == 0: - fsw.property_u32('entry', entry) - elif pname == 'fit,data': - fsw.property('data', bytes(data)) - elif pname != 'fit,operation': - self._raise_subnode( - node, f"Unknown directive '{pname}'") + for (seq, start, data) in segments: + node_name = node.name[1:].replace('SEQ', str(seq + 1)) + with fsw.add_node(node_name): + loadables.append(node_name) + for pname, prop in node.props.items(): + if not pname.startswith('fit,'): + fsw.property(pname, prop.bytes) + elif pname == 'fit,load': + fsw.property_u32('load', start) + elif pname == 'fit,entry': + if seq == 0: + fsw.property_u32('entry', entry_addr) + elif pname == 'fit,data': + fsw.property('data', bytes(data)) + elif pname != 'fit,operation': + self._raise_subnode( + node, f"Unknown directive '{pname}'") def _gen_node(base_node, node, depth, in_images, entry): """Generate nodes from a template @@ -598,6 +592,8 @@ class Entry_fit(Entry_section): depth (int): Current node depth (0 is the base 'fit' node) in_images (bool): True if this is inside the 'images' node, so that 'data' properties should be generated + entry (entry_Section): Entry for the section containing the + contents of this node """ oper = self._get_operation(base_node, node) if oper == OP_GEN_FDT_NODES: @@ -609,10 +605,24 @@ class Entry_fit(Entry_section): missing_list = [] entry.ObtainContents() entry.Pack(0) - data = entry.GetData() entry.CheckMissing(missing_list) - _gen_split_elf(base_node, node, data, bool(missing_list)) + # If any pieces are missing, skip this. The missing entries will + # show an error + if not missing_list: + segs = entry.read_elf_segments() + if segs: + segments, entry_addr = segs + else: + elf_data = entry.GetData() + try: + segments, entry_addr = ( + elf.read_loadable_segments(elf_data)) + except ValueError as exc: + self._raise_subnode( + node, f'Failed to read ELF file: {str(exc)}') + + _gen_split_elf(base_node, node, segments, entry_addr) def _add_node(base_node, depth, node): """Add nodes to the output FIT diff --git a/tools/binman/etype/section.py b/tools/binman/etype/section.py index dcb7a062047..57bfee0b286 100644 --- a/tools/binman/etype/section.py +++ b/tools/binman/etype/section.py @@ -948,3 +948,12 @@ class Entry_section(Entry): super().AddBintools(btools) for entry in self._entries.values(): entry.AddBintools(btools) + + def read_elf_segments(self): + entries = self.GetEntries() + + # If the section only has one entry, see if it can provide ELF segments + if len(entries) == 1: + for entry in entries.values(): + return entry.read_elf_segments() + return None diff --git a/tools/binman/etype/tee_os.py b/tools/binman/etype/tee_os.py index 6ce4b672de4..5529727e833 100644 --- a/tools/binman/etype/tee_os.py +++ b/tools/binman/etype/tee_os.py @@ -4,19 +4,93 @@ # Entry-type module for OP-TEE Trusted OS firmware blob # +import struct + from binman.etype.blob_named_by_arg import Entry_blob_named_by_arg +from binman import elf class Entry_tee_os(Entry_blob_named_by_arg): """Entry containing an OP-TEE Trusted OS (TEE) blob Properties / Entry arguments: - tee-os-path: Filename of file to read into entry. This is typically - called tee-pager.bin + called tee.bin or tee.elf This entry holds the run-time firmware, typically started by U-Boot SPL. See the U-Boot README for your architecture or board for how to use it. See https://github.com/OP-TEE/optee_os for more information about OP-TEE. + + Note that if the file is in ELF format, it must go in a FIT. In that case, + this entry will mark itself as absent, providing the data only through the + read_elf_segments() method. + + Marking this entry as absent means that it if is used in the wrong context + it can be automatically dropped. Thus it is possible to add an OP-TEE entry + like this:: + + binman { + tee-os { + }; + }; + + and pass either an ELF or plain binary in with -a tee-os-path + and have binman do the right thing: + + - include the entry if tee.bin is provided and it does NOT have the v1 + header + - drop it otherwise + + When used within a FIT, we can do:: + + binman { + fit { + tee-os { + }; + }; + }; + + which will split the ELF into separate nodes for each segment, if an ELF + file is provided (see :ref:`etype_fit`), or produce a single node if the + OP-TEE binary v1 format is provided (see optee_doc_) . + + .. _optee_doc: https://optee.readthedocs.io/en/latest/architecture/core.html#partitioning-of-the-binary """ def __init__(self, section, etype, node): super().__init__(section, etype, node, 'tee-os') self.external = True + + @staticmethod + def is_optee_bin_v1(data): + return len(data) >= 8 and data[0:5] == b'OPTE\x01' + + def ObtainContents(self, fake_size=0): + result = super().ObtainContents(fake_size) + if not self.missing: + # If using the flat binary (without the OP-TEE header), then it is + # just included as a blob. But if it is an ELF or usees the v1 + # binary header, then the FIT implementation will call + # read_elf_segments() to get the segment information + if elf.is_valid(self.data): + self.mark_absent('uses Elf format which must be in a FIT') + elif self.is_optee_bin_v1(self.data): + # The FIT implementation will call read_elf_segments() to get + # the segment information + self.mark_absent('uses v1 format which must be in a FIT') + return result + + def read_elf_segments(self): + data = self.GetData() + if self.is_optee_bin_v1(data): + # OP-TEE v1 format (tee.bin) + init_sz, start_hi, start_lo, _, paged_sz = ( + struct.unpack_from('<5I', data, 0x8)) + if paged_sz != 0: + self.Raise("OP-TEE paged mode not supported") + e_entry = (start_hi << 32) + start_lo + p_addr = e_entry + p_data = data[0x1c:] + if len(p_data) != init_sz: + self.Raise("Invalid OP-TEE file: size mismatch (expected %#x, have %#x)" % + (init_sz, len(p_data))) + return [[0, p_addr, p_data]], e_entry + return None diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py index f47a745f1e1..f893050e706 100644 --- a/tools/binman/ftest.py +++ b/tools/binman/ftest.py @@ -112,6 +112,8 @@ REPACK_DTB_PROPS = ['orig-offset', 'orig-size'] # Supported compression bintools COMP_BINTOOLS = ['bzip2', 'gzip', 'lz4', 'lzma_alone', 'lzop', 'xz', 'zstd'] +TEE_ADDR = 0x5678 + class TestFunctional(unittest.TestCase): """Functional tests for binman @@ -219,6 +221,9 @@ class TestFunctional(unittest.TestCase): TestFunctional._MakeInputFile('tee.elf', tools.read_file(cls.ElfTestFile('elf_sections'))) + # Newer OP_TEE file in v1 binary format + cls.make_tee_bin('tee.bin') + cls.comp_bintools = {} for name in COMP_BINTOOLS: cls.comp_bintools[name] = bintool.Bintool.create(name) @@ -644,6 +649,14 @@ class TestFunctional(unittest.TestCase): def ElfTestFile(cls, fname): return os.path.join(cls._elf_testdir, fname) + @classmethod + def make_tee_bin(cls, fname, paged_sz=0, extra_data=b''): + init_sz, start_hi, start_lo, dummy = (len(U_BOOT_DATA), 0, TEE_ADDR, 0) + data = b'OPTE\x01xxx' + struct.pack('<5I', init_sz, start_hi, start_lo, + dummy, paged_sz) + U_BOOT_DATA + data += extra_data + TestFunctional._MakeInputFile(fname, data) + def AssertInList(self, grep_list, target): """Assert that at least one of a list of things is in a target @@ -6095,6 +6108,76 @@ fdt fdtmap Extract the devicetree blob from the fdtmap data = self._DoReadFile('262_absent.dts') self.assertEqual(U_BOOT_DATA + U_BOOT_IMG_DATA, data) + def testPackTeeOsOptional(self): + """Test that an image with an optional TEE binary can be created""" + entry_args = { + 'tee-os-path': 'tee.elf', + } + data = self._DoReadFileDtb('263_tee_os_opt.dts', + entry_args=entry_args)[0] + self.assertEqual(U_BOOT_DATA + U_BOOT_IMG_DATA, data) + + def checkFitTee(self, dts, tee_fname): + """Check that a tee-os entry works and returns data + + Args: + dts (str): Device tree filename to use + tee_fname (str): filename containing tee-os + + Returns: + bytes: Image contents + """ + if not elf.ELF_TOOLS: + self.skipTest('Python elftools not available') + entry_args = { + 'of-list': 'test-fdt1 test-fdt2', + 'default-dt': 'test-fdt2', + 'tee-os-path': tee_fname, + } + test_subdir = os.path.join(self._indir, TEST_FDT_SUBDIR) + data = self._DoReadFileDtb(dts, entry_args=entry_args, + extra_indirs=[test_subdir])[0] + return data + + def testFitTeeOsOptionalFit(self): + """Test an image with a FIT with an optional OP-TEE binary""" + data = self.checkFitTee('264_tee_os_opt_fit.dts', 'tee.bin') + + # There should be only one node, holding the data set up in SetUpClass() + # for tee.bin + dtb = fdt.Fdt.FromData(data) + dtb.Scan() + node = dtb.GetNode('/images/tee-1') + self.assertEqual(TEE_ADDR, + fdt_util.fdt32_to_cpu(node.props['load'].value)) + self.assertEqual(TEE_ADDR, + fdt_util.fdt32_to_cpu(node.props['entry'].value)) + self.assertEqual(U_BOOT_DATA, node.props['data'].bytes) + + def testFitTeeOsOptionalFitBad(self): + """Test an image with a FIT with an optional OP-TEE binary""" + with self.assertRaises(ValueError) as exc: + self.checkFitTee('265_tee_os_opt_fit_bad.dts', 'tee.bin') + self.assertIn( + "Node '/binman/fit': subnode 'images/@tee-SEQ': Failed to read ELF file: Magic number does not match", + str(exc.exception)) + + def testFitTeeOsBad(self): + """Test an OP-TEE binary with wrong formats""" + self.make_tee_bin('tee.bad1', 123) + with self.assertRaises(ValueError) as exc: + self.checkFitTee('264_tee_os_opt_fit.dts', 'tee.bad1') + self.assertIn( + "Node '/binman/fit/images/@tee-SEQ/tee-os': OP-TEE paged mode not supported", + str(exc.exception)) + + self.make_tee_bin('tee.bad2', 0, b'extra data') + with self.assertRaises(ValueError) as exc: + self.checkFitTee('264_tee_os_opt_fit.dts', 'tee.bad2') + self.assertIn( + "Node '/binman/fit/images/@tee-SEQ/tee-os': Invalid OP-TEE file: size mismatch (expected 0x4, have 0xe)", + str(exc.exception)) + if __name__ == "__main__": unittest.main() diff --git a/tools/binman/test/263_tee_os_opt.dts b/tools/binman/test/263_tee_os_opt.dts new file mode 100644 index 00000000000..2e4ec24ac2c --- /dev/null +++ b/tools/binman/test/263_tee_os_opt.dts @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-2.0+ + +/dts-v1/; + +/ { + #address-cells = <1>; + #size-cells = <1>; + + binman { + u-boot { + }; + tee-os { + /* + * this results in nothing being added since only the + * .bin format is supported by this etype, unless it is + * part of a FIT + */ + }; + u-boot-img { + }; + }; +}; diff --git a/tools/binman/test/264_tee_os_opt_fit.dts b/tools/binman/test/264_tee_os_opt_fit.dts new file mode 100644 index 00000000000..ae44b433edf --- /dev/null +++ b/tools/binman/test/264_tee_os_opt_fit.dts @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0+ + +/dts-v1/; + +/ { + #address-cells = <1>; + #size-cells = <1>; + + binman { + fit { + description = "test-desc"; + #address-cells = <1>; + fit,fdt-list = "of-list"; + + images { + @tee-SEQ { + fit,operation = "split-elf"; + description = "TEE"; + type = "tee"; + arch = "arm64"; + os = "tee"; + compression = "none"; + fit,load; + fit,entry; + fit,data; + + tee-os { + }; + }; + }; + }; + }; +}; diff --git a/tools/binman/test/265_tee_os_opt_fit_bad.dts b/tools/binman/test/265_tee_os_opt_fit_bad.dts new file mode 100644 index 00000000000..7fa363cc199 --- /dev/null +++ b/tools/binman/test/265_tee_os_opt_fit_bad.dts @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0+ + +/dts-v1/; + +/ { + #address-cells = <1>; + #size-cells = <1>; + + binman { + fit { + description = "test-desc"; + #address-cells = <1>; + fit,fdt-list = "of-list"; + + images { + @tee-SEQ { + fit,operation = "split-elf"; + description = "TEE"; + type = "tee"; + arch = "arm64"; + os = "tee"; + compression = "none"; + fit,load; + fit,entry; + fit,data; + + tee-os { + }; + + /* + * mess up the ELF data by adding + * another bit of data at the end + */ + u-boot { + }; + }; + }; + }; + }; +}; -- 2.39.0.314.g84b9a713c41-goog