xref: /openbmc/openbmc/poky/meta/lib/oe/qa.py (revision 220dafdb)
1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6
7import os, struct, mmap
8
9class NotELFFileError(Exception):
10    pass
11
12class ELFFile:
13    EI_NIDENT = 16
14
15    EI_CLASS      = 4
16    EI_DATA       = 5
17    EI_VERSION    = 6
18    EI_OSABI      = 7
19    EI_ABIVERSION = 8
20
21    E_MACHINE    = 0x12
22
23    # possible values for EI_CLASS
24    ELFCLASSNONE = 0
25    ELFCLASS32   = 1
26    ELFCLASS64   = 2
27
28    # possible value for EI_VERSION
29    EV_CURRENT   = 1
30
31    # possible values for EI_DATA
32    EI_DATA_NONE  = 0
33    EI_DATA_LSB  = 1
34    EI_DATA_MSB  = 2
35
36    PT_INTERP = 3
37
38    def my_assert(self, expectation, result):
39        if not expectation == result:
40            #print "'%x','%x' %s" % (ord(expectation), ord(result), self.name)
41            raise NotELFFileError("%s is not an ELF" % self.name)
42
43    def __init__(self, name):
44        self.name = name
45        self.objdump_output = {}
46        self.data = None
47
48    # Context Manager functions to close the mmap explicitly
49    def __enter__(self):
50        return self
51
52    def __exit__(self, exc_type, exc_value, traceback):
53        self.close()
54
55    def close(self):
56        if self.data:
57            self.data.close()
58
59    def open(self):
60        with open(self.name, "rb") as f:
61            try:
62                self.data = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
63            except ValueError:
64                # This means the file is empty
65                raise NotELFFileError("%s is empty" % self.name)
66
67        # Check the file has the minimum number of ELF table entries
68        if len(self.data) < ELFFile.EI_NIDENT + 4:
69            raise NotELFFileError("%s is not an ELF" % self.name)
70
71        # ELF header
72        self.my_assert(self.data[0], 0x7f)
73        self.my_assert(self.data[1], ord('E'))
74        self.my_assert(self.data[2], ord('L'))
75        self.my_assert(self.data[3], ord('F'))
76        if self.data[ELFFile.EI_CLASS] == ELFFile.ELFCLASS32:
77            self.bits = 32
78        elif self.data[ELFFile.EI_CLASS] == ELFFile.ELFCLASS64:
79            self.bits = 64
80        else:
81            # Not 32-bit or 64.. lets assert
82            raise NotELFFileError("ELF but not 32 or 64 bit.")
83        self.my_assert(self.data[ELFFile.EI_VERSION], ELFFile.EV_CURRENT)
84
85        self.endian = self.data[ELFFile.EI_DATA]
86        if self.endian not in (ELFFile.EI_DATA_LSB, ELFFile.EI_DATA_MSB):
87            raise NotELFFileError("Unexpected EI_DATA %x" % self.endian)
88
89    def osAbi(self):
90        return self.data[ELFFile.EI_OSABI]
91
92    def abiVersion(self):
93        return self.data[ELFFile.EI_ABIVERSION]
94
95    def abiSize(self):
96        return self.bits
97
98    def isLittleEndian(self):
99        return self.endian == ELFFile.EI_DATA_LSB
100
101    def isBigEndian(self):
102        return self.endian == ELFFile.EI_DATA_MSB
103
104    def getStructEndian(self):
105        return {ELFFile.EI_DATA_LSB: "<",
106                ELFFile.EI_DATA_MSB: ">"}[self.endian]
107
108    def getShort(self, offset):
109        return struct.unpack_from(self.getStructEndian() + "H", self.data, offset)[0]
110
111    def getWord(self, offset):
112        return struct.unpack_from(self.getStructEndian() + "i", self.data, offset)[0]
113
114    def isDynamic(self):
115        """
116        Return True if there is a .interp segment (therefore dynamically
117        linked), otherwise False (statically linked).
118        """
119        offset = self.getWord(self.bits == 32 and 0x1C or 0x20)
120        size = self.getShort(self.bits == 32 and 0x2A or 0x36)
121        count = self.getShort(self.bits == 32 and 0x2C or 0x38)
122
123        for i in range(0, count):
124            p_type = self.getWord(offset + i * size)
125            if p_type == ELFFile.PT_INTERP:
126                return True
127        return False
128
129    def machine(self):
130        """
131        We know the endian stored in self.endian and we
132        know the position
133        """
134        return self.getShort(ELFFile.E_MACHINE)
135
136    def set_objdump(self, cmd, output):
137        self.objdump_output[cmd] = output
138
139    def run_objdump(self, cmd, d):
140        import bb.process
141        import sys
142
143        if cmd in self.objdump_output:
144            return self.objdump_output[cmd]
145
146        objdump = d.getVar('OBJDUMP')
147
148        env = os.environ.copy()
149        env["LC_ALL"] = "C"
150        env["PATH"] = d.getVar('PATH')
151
152        try:
153            bb.note("%s %s %s" % (objdump, cmd, self.name))
154            self.objdump_output[cmd] = bb.process.run([objdump, cmd, self.name], env=env, shell=False)[0]
155            return self.objdump_output[cmd]
156        except Exception as e:
157            bb.note("%s %s %s failed: %s" % (objdump, cmd, self.name, e))
158            return ""
159
160def elf_machine_to_string(machine):
161    """
162    Return the name of a given ELF e_machine field or the hex value as a string
163    if it isn't recognised.
164    """
165    try:
166        return {
167            0x00: "Unset",
168            0x02: "SPARC",
169            0x03: "x86",
170            0x08: "MIPS",
171            0x14: "PowerPC",
172            0x28: "ARM",
173            0x2A: "SuperH",
174            0x32: "IA-64",
175            0x3E: "x86-64",
176            0xB7: "AArch64",
177            0xF7: "BPF"
178        }[machine]
179    except:
180        return "Unknown (%s)" % repr(machine)
181
182def write_error(type, error, d):
183    logfile = d.getVar('QA_LOGFILE')
184    if logfile:
185        p = d.getVar('P')
186        with open(logfile, "a+") as f:
187            f.write("%s: %s [%s]\n" % (p, error, type))
188
189def handle_error(error_class, error_msg, d):
190    if error_class in (d.getVar("ERROR_QA") or "").split():
191        write_error(error_class, error_msg, d)
192        bb.error("QA Issue: %s [%s]" % (error_msg, error_class))
193        d.setVar("QA_ERRORS_FOUND", "True")
194        return False
195    elif error_class in (d.getVar("WARN_QA") or "").split():
196        write_error(error_class, error_msg, d)
197        bb.warn("QA Issue: %s [%s]" % (error_msg, error_class))
198    else:
199        bb.note("QA Issue: %s [%s]" % (error_msg, error_class))
200    return True
201
202def add_message(messages, section, new_msg):
203    if section not in messages:
204        messages[section] = new_msg
205    else:
206        messages[section] = messages[section] + "\n" + new_msg
207
208def exit_with_message_if_errors(message, d):
209    qa_fatal_errors = bb.utils.to_boolean(d.getVar("QA_ERRORS_FOUND"), False)
210    if qa_fatal_errors:
211        bb.fatal(message)
212
213def exit_if_errors(d):
214    exit_with_message_if_errors("Fatal QA errors were found, failing task.", d)
215
216def check_upstream_status(fullpath):
217    import re
218    kinda_status_re = re.compile(r"^.*upstream.*status.*$", re.IGNORECASE | re.MULTILINE)
219    strict_status_re = re.compile(r"^Upstream-Status: (Pending|Submitted|Denied|Inappropriate|Backport|Inactive-Upstream)( .+)?$", re.MULTILINE)
220    guidelines = "https://docs.yoctoproject.org/contributor-guide/recipe-style-guide.html#patch-upstream-status"
221
222    with open(fullpath, encoding='utf-8', errors='ignore') as f:
223        file_content = f.read()
224        match_kinda = kinda_status_re.search(file_content)
225        match_strict = strict_status_re.search(file_content)
226
227        if not match_strict:
228            if match_kinda:
229                return "Malformed Upstream-Status in patch\n%s\nPlease correct according to %s :\n%s" % (fullpath, guidelines, match_kinda.group(0))
230            else:
231                return "Missing Upstream-Status in patch\n%s\nPlease add according to %s ." % (fullpath, guidelines)
232
233if __name__ == "__main__":
234    import sys
235
236    with ELFFile(sys.argv[1]) as elf:
237        elf.open()
238        print(elf.isDynamic())
239