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