1# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2017 Google, Inc
3# Written by Simon Glass <sjg@chromium.org>
4#
5# Test for the elf module
6
7import os
8import shutil
9import struct
10import sys
11import tempfile
12import unittest
13
14from binman import elf
15from u_boot_pylib import command
16from u_boot_pylib import terminal
17from u_boot_pylib import test_util
18from u_boot_pylib import tools
19from u_boot_pylib import tout
20
21binman_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
22
23
24class FakeEntry:
25    """A fake Entry object, usedfor testing
26
27    This supports an entry with a given size.
28    """
29    def __init__(self, contents_size):
30        self.contents_size = contents_size
31        self.data = tools.get_bytes(ord('a'), contents_size)
32
33    def GetPath(self):
34        return 'entry_path'
35
36
37class FakeSection:
38    """A fake Section object, used for testing
39
40    This has the minimum feature set needed to support testing elf functions.
41    A GetSymbolValue() function is provided which returns a fake value for any
42    symbol requested.
43    """
44    def __init__(self, sym_value=1):
45        self.sym_value = sym_value
46
47    def GetPath(self):
48        return 'section_path'
49
50    def GetImageSymbolValue(self, name, weak, msg, base_addr):
51        """Fake implementation which returns the same value for all symbols"""
52        return self.sym_value
53
54    def GetImage(self):
55        return self
56
57def BuildElfTestFiles(target_dir):
58    """Build ELF files used for testing in binman
59
60    This compiles and links the test files into the specified directory. It uses
61    the Makefile and source files in the binman test/ directory.
62
63    Args:
64        target_dir: Directory to put the files into
65    """
66    if not os.path.exists(target_dir):
67        os.mkdir(target_dir)
68    testdir = os.path.join(binman_dir, 'test')
69
70    # If binman is involved from the main U-Boot Makefile the -r and -R
71    # flags are set in MAKEFLAGS. This prevents this Makefile from working
72    # correctly. So drop any make flags here.
73    if 'MAKEFLAGS' in os.environ:
74        del os.environ['MAKEFLAGS']
75    try:
76        tools.run('make', '-C', target_dir, '-f',
77                  os.path.join(testdir, 'Makefile'), 'SRC=%s/' % testdir)
78    except ValueError as e:
79        # The test system seems to suppress this in a strange way
80        print(e)
81
82
83class TestElf(unittest.TestCase):
84    @classmethod
85    def setUpClass(cls):
86        cls._indir = tempfile.mkdtemp(prefix='elf.')
87        tools.set_input_dirs(['.'])
88        BuildElfTestFiles(cls._indir)
89
90    @classmethod
91    def tearDownClass(cls):
92        if cls._indir:
93            shutil.rmtree(cls._indir)
94
95    @classmethod
96    def ElfTestFile(cls, fname):
97        return os.path.join(cls._indir, fname)
98
99    def testAllSymbols(self):
100        """Test that we can obtain a symbol from the ELF file"""
101        fname = self.ElfTestFile('u_boot_ucode_ptr')
102        syms = elf.GetSymbols(fname, [])
103        self.assertIn('_dt_ucode_base_size', syms)
104
105    def testRegexSymbols(self):
106        """Test that we can obtain from the ELF file by regular expression"""
107        fname = self.ElfTestFile('u_boot_ucode_ptr')
108        syms = elf.GetSymbols(fname, ['ucode'])
109        self.assertIn('_dt_ucode_base_size', syms)
110        syms = elf.GetSymbols(fname, ['missing'])
111        self.assertNotIn('_dt_ucode_base_size', syms)
112        syms = elf.GetSymbols(fname, ['missing', 'ucode'])
113        self.assertIn('_dt_ucode_base_size', syms)
114
115    def testMissingFile(self):
116        """Test that a missing file is detected"""
117        entry = FakeEntry(10)
118        section = FakeSection()
119        with self.assertRaises(ValueError) as e:
120            elf.LookupAndWriteSymbols('missing-file', entry, section)
121        self.assertIn("Filename 'missing-file' not found in input path",
122                      str(e.exception))
123
124    def testOutsideFile(self):
125        """Test a symbol which extends outside the entry area is detected"""
126        if not elf.ELF_TOOLS:
127            self.skipTest('Python elftools not available')
128        entry = FakeEntry(10)
129        section = FakeSection()
130        elf_fname = self.ElfTestFile('u_boot_binman_syms')
131        with self.assertRaises(ValueError) as e:
132            elf.LookupAndWriteSymbols(elf_fname, entry, section)
133        self.assertIn('entry_path has offset 8 (size 8) but the contents size '
134                      'is a', str(e.exception))
135
136    def testMissingImageStart(self):
137        """Test that we detect a missing __image_copy_start symbol
138
139        This is needed to mark the start of the image. Without it we cannot
140        locate the offset of a binman symbol within the image.
141        """
142        entry = FakeEntry(10)
143        section = FakeSection()
144        elf_fname = self.ElfTestFile('u_boot_binman_syms_bad')
145        count = elf.LookupAndWriteSymbols(elf_fname, entry, section)
146        self.assertEqual(0, count)
147
148    def testBadSymbolSize(self):
149        """Test that an attempt to use an 8-bit symbol are detected
150
151        Only 32 and 64 bits are supported, since we need to store an offset
152        into the image.
153        """
154        if not elf.ELF_TOOLS:
155            self.skipTest('Python elftools not available')
156        entry = FakeEntry(10)
157        section = FakeSection()
158        elf_fname =self.ElfTestFile('u_boot_binman_syms_size')
159        with self.assertRaises(ValueError) as e:
160            elf.LookupAndWriteSymbols(elf_fname, entry, section)
161        self.assertIn('has size 1: only 4 and 8 are supported',
162                      str(e.exception))
163
164    def testNoValue(self):
165        """Test the case where we have no value for the symbol
166
167        This should produce -1 values for all three symbols, taking up the
168        first 16 bytes of the image.
169        """
170        if not elf.ELF_TOOLS:
171            self.skipTest('Python elftools not available')
172        entry = FakeEntry(28)
173        section = FakeSection(sym_value=None)
174        elf_fname = self.ElfTestFile('u_boot_binman_syms')
175        count = elf.LookupAndWriteSymbols(elf_fname, entry, section)
176        self.assertEqual(5, count)
177        expected = (struct.pack('<L', elf.BINMAN_SYM_MAGIC_VALUE) +
178                    tools.get_bytes(255, 20) +
179                    tools.get_bytes(ord('a'), 4))
180        self.assertEqual(expected, entry.data)
181
182    def testDebug(self):
183        """Check that enabling debug in the elf module produced debug output"""
184        if not elf.ELF_TOOLS:
185            self.skipTest('Python elftools not available')
186        try:
187            tout.init(tout.DEBUG)
188            entry = FakeEntry(24)
189            section = FakeSection()
190            elf_fname = self.ElfTestFile('u_boot_binman_syms')
191            with terminal.capture() as (stdout, stderr):
192                elf.LookupAndWriteSymbols(elf_fname, entry, section)
193            self.assertTrue(len(stdout.getvalue()) > 0)
194        finally:
195            tout.init(tout.WARNING)
196
197    def testMakeElf(self):
198        """Test for the MakeElf function"""
199        outdir = tempfile.mkdtemp(prefix='elf.')
200        expected_text = b'1234'
201        expected_data = b'wxyz'
202        elf_fname = os.path.join(outdir, 'elf')
203        bin_fname = os.path.join(outdir, 'bin')
204
205        # Make an Elf file and then convert it to a fkat binary file. This
206        # should produce the original data.
207        elf.MakeElf(elf_fname, expected_text, expected_data)
208        objcopy, args = tools.get_target_compile_tool('objcopy')
209        args += ['-O', 'binary', elf_fname, bin_fname]
210        stdout = command.output(objcopy, *args)
211        with open(bin_fname, 'rb') as fd:
212            data = fd.read()
213        self.assertEqual(expected_text + expected_data, data)
214        shutil.rmtree(outdir)
215
216    def testDecodeElf(self):
217        """Test for the MakeElf function"""
218        if not elf.ELF_TOOLS:
219            self.skipTest('Python elftools not available')
220        outdir = tempfile.mkdtemp(prefix='elf.')
221        expected_text = b'1234'
222        expected_data = b'wxyz'
223        elf_fname = os.path.join(outdir, 'elf')
224        elf.MakeElf(elf_fname, expected_text, expected_data)
225        data = tools.read_file(elf_fname)
226
227        load = 0xfef20000
228        entry = load + 2
229        expected = expected_text + expected_data
230        self.assertEqual(elf.ElfInfo(expected, load, entry, len(expected)),
231                         elf.DecodeElf(data, 0))
232        self.assertEqual(elf.ElfInfo(b'\0\0' + expected[2:],
233                                     load, entry, len(expected)),
234                         elf.DecodeElf(data, load + 2))
235        shutil.rmtree(outdir)
236
237    def testEmbedData(self):
238        """Test for the GetSymbolFileOffset() function"""
239        if not elf.ELF_TOOLS:
240            self.skipTest('Python elftools not available')
241
242        fname = self.ElfTestFile('embed_data')
243        offset = elf.GetSymbolFileOffset(fname, ['embed_start', 'embed_end'])
244        start = offset['embed_start'].offset
245        end = offset['embed_end'].offset
246        data = tools.read_file(fname)
247        embed_data = data[start:end]
248        expect = struct.pack('<IIIII', 2, 3, 0x1234, 0x5678, 0)
249        self.assertEqual(expect, embed_data)
250
251    def testEmbedFail(self):
252        """Test calling GetSymbolFileOffset() without elftools"""
253        old_val = elf.ELF_TOOLS
254        try:
255            elf.ELF_TOOLS = False
256            fname = self.ElfTestFile('embed_data')
257            with self.assertRaises(ValueError) as e:
258                elf.GetSymbolFileOffset(fname, ['embed_start', 'embed_end'])
259            with self.assertRaises(ValueError) as e:
260                elf.DecodeElf(tools.read_file(fname), 0xdeadbeef)
261            with self.assertRaises(ValueError) as e:
262                elf.GetFileOffset(fname, 0xdeadbeef)
263            with self.assertRaises(ValueError) as e:
264                elf.GetSymbolFromAddress(fname, 0xdeadbeef)
265            with self.assertRaises(ValueError) as e:
266                entry = FakeEntry(10)
267                section = FakeSection()
268                elf.LookupAndWriteSymbols(fname, entry, section, True)
269
270            self.assertIn(
271                "Section 'section_path': entry 'entry_path': Cannot write symbols to an ELF file without Python elftools",
272                str(e.exception))
273        finally:
274            elf.ELF_TOOLS = old_val
275
276    def testEmbedDataNoSym(self):
277        """Test for GetSymbolFileOffset() getting no symbols"""
278        if not elf.ELF_TOOLS:
279            self.skipTest('Python elftools not available')
280
281        fname = self.ElfTestFile('embed_data')
282        offset = elf.GetSymbolFileOffset(fname, ['missing_sym'])
283        self.assertEqual({}, offset)
284
285    def test_read_loadable_segments(self):
286        """Test for read_loadable_segments()"""
287        if not elf.ELF_TOOLS:
288            self.skipTest('Python elftools not available')
289        fname = self.ElfTestFile('embed_data')
290        segments, entry = elf.read_loadable_segments(tools.read_file(fname))
291
292    def test_read_segments_fail(self):
293        """Test for read_loadable_segments() without elftools"""
294        old_val = elf.ELF_TOOLS
295        try:
296            elf.ELF_TOOLS = False
297            fname = self.ElfTestFile('embed_data')
298            with self.assertRaises(ValueError) as e:
299                elf.read_loadable_segments(tools.read_file(fname))
300            self.assertIn("Python: No module named 'elftools'",
301                          str(e.exception))
302        finally:
303            elf.ELF_TOOLS = old_val
304
305    def test_read_segments_bad_data(self):
306        """Test for read_loadable_segments() with an invalid ELF file"""
307        if not elf.ELF_TOOLS:
308            self.skipTest('Python elftools not available')
309        fname = self.ElfTestFile('embed_data')
310        with self.assertRaises(ValueError) as e:
311            elf.read_loadable_segments(tools.get_bytes(100, 100))
312        self.assertIn('Magic number does not match', str(e.exception))
313
314    def test_get_file_offset(self):
315        """Test GetFileOffset() gives the correct file offset for a symbol"""
316        if not elf.ELF_TOOLS:
317            self.skipTest('Python elftools not available')
318        fname = self.ElfTestFile('embed_data')
319        syms = elf.GetSymbols(fname, ['embed'])
320        addr = syms['embed'].address
321        offset = elf.GetFileOffset(fname, addr)
322        data = tools.read_file(fname)
323
324        # Just use the first 4 bytes and assume it is little endian
325        embed_data = data[offset:offset + 4]
326        embed_value = struct.unpack('<I', embed_data)[0]
327        self.assertEqual(0x1234, embed_value)
328
329    def test_get_file_offset_fail(self):
330        """Test calling GetFileOffset() without elftools"""
331        old_val = elf.ELF_TOOLS
332        try:
333            elf.ELF_TOOLS = False
334            fname = self.ElfTestFile('embed_data')
335            with self.assertRaises(ValueError) as e:
336                elf.GetFileOffset(fname, 0)
337            self.assertIn("Python: No module named 'elftools'",
338                      str(e.exception))
339        finally:
340            elf.ELF_TOOLS = old_val
341
342    def test_get_symbol_from_address(self):
343        """Test GetSymbolFromAddress()"""
344        if not elf.ELF_TOOLS:
345            self.skipTest('Python elftools not available')
346        fname = self.ElfTestFile('elf_sections')
347        sym_name = 'calculate'
348        syms = elf.GetSymbols(fname, [sym_name])
349        addr = syms[sym_name].address
350        sym = elf.GetSymbolFromAddress(fname, addr)
351        self.assertEqual(sym_name, sym)
352
353    def test_get_symbol_from_address_fail(self):
354        """Test calling GetSymbolFromAddress() without elftools"""
355        old_val = elf.ELF_TOOLS
356        try:
357            elf.ELF_TOOLS = False
358            fname = self.ElfTestFile('embed_data')
359            with self.assertRaises(ValueError) as e:
360                elf.GetSymbolFromAddress(fname, 0x1000)
361            self.assertIn("Python: No module named 'elftools'",
362                          str(e.exception))
363        finally:
364            elf.ELF_TOOLS = old_val
365
366    def test_is_valid(self):
367        """Test is_valid()"""
368        self.assertEqual(False, elf.is_valid(b''))
369        self.assertEqual(False, elf.is_valid(b'1234'))
370
371        fname = self.ElfTestFile('elf_sections')
372        data = tools.read_file(fname)
373        self.assertEqual(True, elf.is_valid(data))
374        self.assertEqual(False, elf.is_valid(data[4:]))
375
376    def test_get_symbol_offset(self):
377        fname = self.ElfTestFile('embed_data')
378        syms = elf.GetSymbols(fname, ['embed_start', 'embed'])
379        expected = syms['embed'].address - syms['embed_start'].address
380        val = elf.GetSymbolOffset(fname, 'embed', 'embed_start')
381        self.assertEqual(expected, val)
382
383        with self.assertRaises(KeyError) as e:
384            elf.GetSymbolOffset(fname, 'embed')
385        self.assertIn('__image_copy_start', str(e.exception))
386
387    def test_get_symbol_address(self):
388        fname = self.ElfTestFile('embed_data')
389        addr = elf.GetSymbolAddress(fname, 'region_size')
390        self.assertEqual(0, addr)
391
392
393if __name__ == '__main__':
394    unittest.main()
395