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