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 
7 import ast
8 import os, struct, mmap
9 
10 class NotELFFileError(Exception):
11     pass
12 
13 class 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 
161 def 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 
183 def 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 
190 def 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 
204 def 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
216 handle_error.visitorcode = handle_error_visitorcode
217 
218 def 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 
223 def exit_if_errors(d):
224     exit_with_message_if_errors("Fatal QA errors were found, failing task.", d)
225 
226 def 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 
243 if __name__ == "__main__":
244     import sys
245 
246     with ELFFile(sys.argv[1]) as elf:
247         elf.open()
248         print(elf.isDynamic())
249