binman: Support updating section contents

Implement this feature since it is useful for updating FITs within an
image.

Signed-off-by: Simon Glass <sjg@chromium.org>
This commit is contained in:
Simon Glass
2023-03-08 13:15:14 -08:00
parent 033828cf34
commit 7caa372a5e
10 changed files with 287 additions and 15 deletions
+16
View File
@@ -1347,6 +1347,22 @@ You can also replace just a selection of entries::
$ binman replace -i image.bin "*u-boot*" -I indir
It is possible to replace whole sections as well, but in that case any
information about entries within the section may become outdated. This is
because Binman cannot know whether things have moved around or resized within
the section, once you have updated its data.
Technical note: With 'allow-repack', Binman writes information about the
original offset and size properties of each entry, if any were specified, in
the 'orig-offset' and 'orig-size' properties. This allows Binman to distinguish
between an entry which ended up being packed at an offset (or assigned a size)
and an entry which had a particular offset / size requested in the Binman
configuration. Where are particular offset / size was requested, this is treated
as set in stone, so Binman will ensure it doesn't change. Without this feature,
repacking an entry might cause it to disobey the original constraints provided
when it was created.
Repacking an image involves
.. _`BinmanLogging`:
+2
View File
@@ -403,6 +403,8 @@ def ReplaceEntries(image_fname, input_fname, indir, entry_paths,
image_fname = os.path.abspath(image_fname)
image = Image.FromFile(image_fname)
image.mark_build_done()
# Replace an entry from a single file, as a special case
if input_fname:
if not entry_paths:
+22
View File
@@ -104,6 +104,10 @@ class Entry(object):
firmware. This means that it will not be changed by the update.
This is just a signal: enforcement of this is up to the updater.
This flag does not automatically propagate down to child entries.
build_done (bool): Indicates that the entry data has been built and does
not need to be done again. This is only used with 'binman replace',
to stop sections from being rebuilt if their entries have not been
replaced
"""
fake_dir = None
@@ -153,6 +157,7 @@ class Entry(object):
self.elf_base_sym = None
self.offset_from_elf = None
self.preserve = False
self.build_done = False
@staticmethod
def FindEntryClass(etype, expanded):
@@ -1013,6 +1018,7 @@ features to produce new behaviours.
else:
self.contents_size = self.pre_reset_size
ok = self.ProcessContentsUpdate(data)
self.build_done = False
self.Detail('WriteData: size=%x, ok=%s' % (len(data), ok))
section_ok = self.section.WriteChildData(self)
return ok and section_ok
@@ -1034,6 +1040,14 @@ features to produce new behaviours.
True if the section could be updated successfully, False if the
data is such that the section could not update
"""
self.build_done = False
entry = self.section
# Now we must rebuild all sections above this one
while entry and entry != entry.section:
self.build_done = False
entry = entry.section
return True
def GetSiblingOrder(self):
@@ -1356,3 +1370,11 @@ features to produce new behaviours.
val = elf.GetSymbolOffset(entry.elf_fname, sym_name,
entry.elf_base_sym)
return val + offset
def mark_build_done(self):
"""Mark an entry as already built"""
self.build_done = True
entries = self.GetEntries()
if entries:
for entry in entries.values():
entry.mark_build_done()
+1 -1
View File
@@ -270,4 +270,4 @@ class Entry_atf_fip(Entry_section):
# Recreate the data structure, leaving the data for this child alone,
# so that child.data is used to pack into the FIP.
self.ObtainContents(skip_entry=child)
return True
return super().WriteChildData(child)
+1 -1
View File
@@ -295,7 +295,7 @@ class Entry_cbfs(Entry):
# Recreate the data structure, leaving the data for this child alone,
# so that child.data is used to pack into the FIP.
self.ObtainContents(skip_entry=child)
return True
return super().WriteChildData(child)
def AddBintools(self, btools):
super().AddBintools(btools)
+5
View File
@@ -777,6 +777,8 @@ class Entry_fit(Entry_section):
Args:
image_pos (int): Position of this entry in the image
"""
if self.build_done:
return
super().SetImagePos(image_pos)
# If mkimage is missing we'll have empty data,
@@ -830,3 +832,6 @@ class Entry_fit(Entry_section):
# missing
for entry in self._priv_entries.values():
entry.CheckMissing(missing_list)
def CheckEntries(self):
pass
+22 -8
View File
@@ -397,10 +397,13 @@ class Entry_section(Entry):
This excludes any padding. If the section is compressed, the
compressed data is returned
"""
data = self.BuildSectionData(required)
if data is None:
return None
self.SetContents(data)
if not self.build_done:
data = self.BuildSectionData(required)
if data is None:
return None
self.SetContents(data)
else:
data = self.data
if self._filename:
tools.write_file(tools.get_output_filename(self._filename), data)
return data
@@ -427,8 +430,11 @@ class Entry_section(Entry):
self._SortEntries()
self._extend_entries()
data = self.BuildSectionData(True)
self.SetContents(data)
if self.build_done:
self.size = None
else:
data = self.BuildSectionData(True)
self.SetContents(data)
self.CheckSize()
@@ -810,6 +816,9 @@ class Entry_section(Entry):
def LoadData(self, decomp=True):
for entry in self._entries.values():
entry.LoadData(decomp)
data = self.ReadData(decomp)
self.contents_size = len(data)
self.ProcessContentsUpdate(data)
self.Detail('Loaded data')
def GetImage(self):
@@ -866,10 +875,15 @@ class Entry_section(Entry):
return data
def WriteData(self, data, decomp=True):
self.Raise("Replacing sections is not implemented yet")
ok = super().WriteData(data, decomp)
# The section contents are now fixed and cannot be rebuilt from the
# containing entries.
self.mark_build_done()
return ok
def WriteChildData(self, child):
return True
return super().WriteChildData(child)
def SetAllowMissing(self, allow_missing):
"""Set whether a section allows missing external blobs
+132 -5
View File
@@ -5819,13 +5819,61 @@ fdt fdtmap Extract the devicetree blob from the fdtmap
self.assertEqual(expected_fdtmap, fdtmap)
def testReplaceSectionSimple(self):
"""Test replacing a simple section with arbitrary data"""
"""Test replacing a simple section with same-sized data"""
new_data = b'w' * len(COMPRESS_DATA + U_BOOT_DATA)
with self.assertRaises(ValueError) as exc:
self._RunReplaceCmd('section', new_data,
dts='241_replace_section_simple.dts')
data, expected_fdtmap, image = self._RunReplaceCmd('section',
new_data, dts='241_replace_section_simple.dts')
self.assertEqual(new_data, data)
entries = image.GetEntries()
self.assertIn('section', entries)
entry = entries['section']
self.assertEqual(len(new_data), entry.size)
def testReplaceSectionLarger(self):
"""Test replacing a simple section with larger data"""
new_data = b'w' * (len(COMPRESS_DATA + U_BOOT_DATA) + 1)
data, expected_fdtmap, image = self._RunReplaceCmd('section',
new_data, dts='241_replace_section_simple.dts')
self.assertEqual(new_data, data)
entries = image.GetEntries()
self.assertIn('section', entries)
entry = entries['section']
self.assertEqual(len(new_data), entry.size)
fentry = entries['fdtmap']
self.assertEqual(entry.offset + entry.size, fentry.offset)
def testReplaceSectionSmaller(self):
"""Test replacing a simple section with smaller data"""
new_data = b'w' * (len(COMPRESS_DATA + U_BOOT_DATA) - 1) + b'\0'
data, expected_fdtmap, image = self._RunReplaceCmd('section',
new_data, dts='241_replace_section_simple.dts')
self.assertEqual(new_data, data)
# The new size is the same as the old, just with a pad byte at the end
entries = image.GetEntries()
self.assertIn('section', entries)
entry = entries['section']
self.assertEqual(len(new_data), entry.size)
def testReplaceSectionSmallerAllow(self):
"""Test failing to replace a simple section with smaller data"""
new_data = b'w' * (len(COMPRESS_DATA + U_BOOT_DATA) - 1)
try:
state.SetAllowEntryContraction(True)
with self.assertRaises(ValueError) as exc:
self._RunReplaceCmd('section', new_data,
dts='241_replace_section_simple.dts')
finally:
state.SetAllowEntryContraction(False)
# Since we have no information about the position of things within the
# section, we cannot adjust the position of /section-u-boot so it ends
# up outside the section
self.assertIn(
"Node '/section': Replacing sections is not implemented yet",
"Node '/section/u-boot': Offset 0x24 (36) size 0x4 (4) is outside "
"the section '/section' starting at 0x0 (0) of size 0x27 (39)",
str(exc.exception))
def testMkimageImagename(self):
@@ -6412,6 +6460,85 @@ fdt fdtmap Extract the devicetree blob from the fdtmap
'tool', '-l'))
self.assertEqual(['mary', 'anna', 'fred'], tools.tool_search_paths)
def testReplaceSectionEntry(self):
"""Test replacing an entry in a section"""
expect_data = b'w' * len(U_BOOT_DATA + COMPRESS_DATA)
entry_data, expected_fdtmap, image = self._RunReplaceCmd('section/blob',
expect_data, dts='241_replace_section_simple.dts')
self.assertEqual(expect_data, entry_data)
entries = image.GetEntries()
self.assertIn('section', entries)
section = entries['section']
sect_entries = section.GetEntries()
self.assertIn('blob', sect_entries)
entry = sect_entries['blob']
self.assertEqual(len(expect_data), entry.size)
fname = tools.get_output_filename('image-updated.bin')
data = tools.read_file(fname)
new_blob_data = data[entry.image_pos:entry.image_pos + len(expect_data)]
self.assertEqual(expect_data, new_blob_data)
self.assertEqual(U_BOOT_DATA,
data[entry.image_pos + len(expect_data):]
[:len(U_BOOT_DATA)])
def testReplaceSectionDeep(self):
"""Test replacing an entry in two levels of sections"""
expect_data = b'w' * len(U_BOOT_DATA + COMPRESS_DATA)
entry_data, expected_fdtmap, image = self._RunReplaceCmd(
'section/section/blob', expect_data,
dts='278_replace_section_deep.dts')
self.assertEqual(expect_data, entry_data)
entries = image.GetEntries()
self.assertIn('section', entries)
section = entries['section']
subentries = section.GetEntries()
self.assertIn('section', subentries)
section = subentries['section']
sect_entries = section.GetEntries()
self.assertIn('blob', sect_entries)
entry = sect_entries['blob']
self.assertEqual(len(expect_data), entry.size)
fname = tools.get_output_filename('image-updated.bin')
data = tools.read_file(fname)
new_blob_data = data[entry.image_pos:entry.image_pos + len(expect_data)]
self.assertEqual(expect_data, new_blob_data)
self.assertEqual(U_BOOT_DATA,
data[entry.image_pos + len(expect_data):]
[:len(U_BOOT_DATA)])
def testReplaceFitSibling(self):
"""Test an image with a FIT inside where we replace its sibling"""
fname = TestFunctional._MakeInputFile('once', b'available once')
self._DoReadFileRealDtb('277_replace_fit_sibling.dts')
os.remove(fname)
try:
tmpdir, updated_fname = self._SetupImageInTmpdir()
fname = os.path.join(tmpdir, 'update-blob')
expected = b'w' * (len(COMPRESS_DATA + U_BOOT_DATA) + 1)
tools.write_file(fname, expected)
self._DoBinman('replace', '-i', updated_fname, 'blob', '-f', fname)
data = tools.read_file(updated_fname)
start = len(U_BOOT_DTB_DATA)
self.assertEqual(expected, data[start:start + len(expected)])
map_fname = os.path.join(tmpdir, 'image-updated.map')
self.assertFalse(os.path.exists(map_fname))
finally:
shutil.rmtree(tmpdir)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: GPL-2.0+
/dts-v1/;
/ {
binman {
allow-repack;
u-boot {
};
blob {
filename = "compress";
};
fit {
description = "test-desc";
#address-cells = <1>;
images {
kernel {
description = "Vanilla Linux kernel";
type = "kernel";
arch = "ppc";
os = "linux";
compression = "gzip";
load = <00000000>;
entry = <00000000>;
hash-1 {
algo = "crc32";
};
blob-ext {
filename = "once";
};
};
fdt-1 {
description = "Flattened Device Tree blob";
type = "flat_dt";
arch = "ppc";
compression = "none";
hash-1 {
algo = "crc32";
};
u-boot-spl-dtb {
};
};
};
configurations {
default = "conf-1";
conf-1 {
description = "Boot Linux kernel with FDT blob";
kernel = "kernel";
fdt = "fdt-1";
};
};
};
fdtmap {
};
};
};
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-2.0+
/dts-v1/;
/ {
binman {
allow-repack;
u-boot-dtb {
};
section {
section {
blob {
filename = "compress";
};
};
u-boot {
};
};
fdtmap {
};
};
};