1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0+
3#
4# Copyright (c) 2014 Google, Inc
5#
6# Intel microcode update tool
7
8from optparse import OptionParser
9import os
10import re
11import struct
12import sys
13
14MICROCODE_DIR = 'arch/x86/dts/microcode'
15
16class Microcode:
17    """Holds information about the microcode for a particular model of CPU.
18
19    Attributes:
20        name:  Name of the CPU this microcode is for, including any version
21                   information (e.g. 'm12206a7_00000029')
22        model: Model code string (this is cpuid(1).eax, e.g. '206a7')
23        words: List of hex words containing the microcode. The first 16 words
24                   are the public header.
25    """
26    def __init__(self, name, data):
27        self.name = name
28        # Convert data into a list of hex words
29        self.words = []
30        for value in ''.join(data).split(','):
31            hexval = value.strip()
32            if hexval:
33                self.words.append(int(hexval, 0))
34
35        # The model is in the 4rd hex word
36        self.model = '%x' % self.words[3]
37
38def ParseFile(fname):
39    """Parse a micrcode.dat file and return the component parts
40
41    Args:
42        fname: Filename to parse
43    Returns:
44        3-Tuple:
45            date:         String containing date from the file's header
46            license_text: List of text lines for the license file
47            microcodes:   List of Microcode objects from the file
48    """
49    re_date = re.compile('/\* *(.* [0-9]{4}) *\*/$')
50    re_license = re.compile('/[^-*+] *(.*)$')
51    re_name = re.compile('/\* *(.*)\.inc *\*/', re.IGNORECASE)
52    microcodes = {}
53    license_text = []
54    date = ''
55    data = []
56    name = None
57    with open(fname) as fd:
58        for line in fd:
59            line = line.rstrip()
60            m_date = re_date.match(line)
61            m_license = re_license.match(line)
62            m_name = re_name.match(line)
63            if m_name:
64                if name:
65                    microcodes[name] = Microcode(name, data)
66                name = m_name.group(1).lower()
67                data = []
68            elif m_license:
69                license_text.append(m_license.group(1))
70            elif m_date:
71                date = m_date.group(1)
72            else:
73                data.append(line)
74    if name:
75        microcodes[name] = Microcode(name, data)
76    return date, license_text, microcodes
77
78def ParseHeaderFiles(fname_list):
79    """Parse a list of header files and return the component parts
80
81    Args:
82        fname_list: List of files to parse
83    Returns:
84            date:         String containing date from the file's header
85            license_text: List of text lines for the license file
86            microcodes:   List of Microcode objects from the file
87    """
88    microcodes = {}
89    license_text = []
90    date = ''
91    name = None
92    for fname in fname_list:
93        name = os.path.basename(fname).lower()
94        name = os.path.splitext(name)[0]
95        data = []
96        with open(fname) as fd:
97            license_start = False
98            license_end = False
99            for line in fd:
100                line = line.rstrip()
101
102                if len(line) >= 2:
103                    if line[0] == '/' and line[1] == '*':
104                        license_start = True
105                        continue
106                    if line[0] == '*' and line[1] == '/':
107                        license_end = True
108                        continue
109                if license_start and not license_end:
110                    # Ignore blank line
111                    if len(line) > 0:
112                        license_text.append(line)
113                    continue
114                # Omit anything after the last comma
115                words = line.split(',')[:-1]
116                data += [word + ',' for word in words]
117        microcodes[name] = Microcode(name, data)
118    return date, license_text, microcodes
119
120
121def List(date, microcodes, model):
122    """List the available microcode chunks
123
124    Args:
125        date:           Date of the microcode file
126        microcodes:     Dict of Microcode objects indexed by name
127        model:          Model string to search for, or None
128    """
129    print('Date: %s' % date)
130    if model:
131        mcode_list, tried = FindMicrocode(microcodes, model.lower())
132        print('Matching models %s:' % (', '.join(tried)))
133    else:
134        print('All models:')
135        mcode_list = [microcodes[m] for m in list(microcodes.keys())]
136    for mcode in mcode_list:
137        print('%-20s: model %s' % (mcode.name, mcode.model))
138
139def FindMicrocode(microcodes, model):
140    """Find all the microcode chunks which match the given model.
141
142    This model is something like 306a9 (the value returned in eax from
143    cpuid(1) when running on Intel CPUs). But we allow a partial match,
144    omitting the last 1 or two characters to allow many families to have the
145    same microcode.
146
147    If the model name is ambiguous we return a list of matches.
148
149    Args:
150        microcodes: Dict of Microcode objects indexed by name
151        model:      String containing model name to find
152    Returns:
153        Tuple:
154            List of matching Microcode objects
155            List of abbreviations we tried
156    """
157    # Allow a full name to be used
158    mcode = microcodes.get(model)
159    if mcode:
160        return [mcode], []
161
162    tried = []
163    found = []
164    for i in range(3):
165        abbrev = model[:-i] if i else model
166        tried.append(abbrev)
167        for mcode in list(microcodes.values()):
168            if mcode.model.startswith(abbrev):
169                found.append(mcode)
170        if found:
171            break
172    return found, tried
173
174def CreateFile(date, license_text, mcodes, outfile):
175    """Create a microcode file in U-Boot's .dtsi format
176
177    Args:
178        date:       String containing date of original microcode file
179        license:    List of text lines for the license file
180        mcodes:      Microcode objects to write (normally only 1)
181        outfile:    Filename to write to ('-' for stdout)
182    """
183    out = '''/*%s
184 * ---
185 * This is a device tree fragment. Use #include to add these properties to a
186 * node.
187 *
188 * Date: %s
189 */
190
191compatible = "intel,microcode";
192intel,header-version = <%d>;
193intel,update-revision = <%#x>;
194intel,date-code = <%#x>;
195intel,processor-signature = <%#x>;
196intel,checksum = <%#x>;
197intel,loader-revision = <%d>;
198intel,processor-flags = <%#x>;
199
200/* The first 48-bytes are the public header which repeats the above data */
201data = <%s
202\t>;'''
203    words = ''
204    add_comments = len(mcodes) > 1
205    for mcode in mcodes:
206        if add_comments:
207            words += '\n/* %s */' % mcode.name
208        for i in range(len(mcode.words)):
209            if not (i & 3):
210                words += '\n'
211            val = mcode.words[i]
212            # Change each word so it will be little-endian in the FDT
213            # This data is needed before RAM is available on some platforms so
214            # we cannot do an endianness swap on boot.
215            val = struct.unpack("<I", struct.pack(">I", val))[0]
216            words += '\t%#010x' % val
217
218    # Use the first microcode for the headers
219    mcode = mcodes[0]
220
221    # Take care to avoid adding a space before a tab
222    text = ''
223    for line in license_text:
224        if line[0] == '\t':
225            text += '\n *' + line
226        else:
227            text += '\n * ' + line
228    args = [text, date]
229    args += [mcode.words[i] for i in range(7)]
230    args.append(words)
231    if outfile == '-':
232        print(out % tuple(args))
233    else:
234        if not outfile:
235            if not os.path.exists(MICROCODE_DIR):
236                print("Creating directory '%s'" % MICROCODE_DIR, file=sys.stderr)
237                os.makedirs(MICROCODE_DIR)
238            outfile = os.path.join(MICROCODE_DIR, mcode.name + '.dtsi')
239        print("Writing microcode for '%s' to '%s'" % (
240                ', '.join([mcode.name for mcode in mcodes]), outfile), file=sys.stderr)
241        with open(outfile, 'w') as fd:
242            print(out % tuple(args), file=fd)
243
244def MicrocodeTool():
245    """Run the microcode tool"""
246    commands = 'create,license,list'.split(',')
247    parser = OptionParser()
248    parser.add_option('-d', '--mcfile', type='string', action='store',
249                    help='Name of microcode.dat file')
250    parser.add_option('-H', '--headerfile', type='string', action='append',
251                    help='Name of .h file containing microcode')
252    parser.add_option('-m', '--model', type='string', action='store',
253                    help="Model name to extract ('all' for all)")
254    parser.add_option('-M', '--multiple', type='string', action='store',
255                    help="Allow output of multiple models")
256    parser.add_option('-o', '--outfile', type='string', action='store',
257                    help='Filename to use for output (- for stdout), default is'
258                    ' %s/<name>.dtsi' % MICROCODE_DIR)
259    parser.usage += """ command
260
261    Process an Intel microcode file (use -h for help). Commands:
262
263       create     Create microcode .dtsi file for a model
264       list       List available models in microcode file
265       license    Print the license
266
267    Typical usage:
268
269       ./tools/microcode-tool -d microcode.dat -m 306a create
270
271    This will find the appropriate file and write it to %s.""" % MICROCODE_DIR
272
273    (options, args) = parser.parse_args()
274    if not args:
275        parser.error('Please specify a command')
276    cmd = args[0]
277    if cmd not in commands:
278        parser.error("Unknown command '%s'" % cmd)
279
280    if (not not options.mcfile) != (not not options.mcfile):
281        parser.error("You must specify either header files or a microcode file, not both")
282    if options.headerfile:
283        date, license_text, microcodes = ParseHeaderFiles(options.headerfile)
284    elif options.mcfile:
285        date, license_text, microcodes = ParseFile(options.mcfile)
286    else:
287        parser.error('You must specify a microcode file (or header files)')
288
289    if cmd == 'list':
290        List(date, microcodes, options.model)
291    elif cmd == 'license':
292        print('\n'.join(license_text))
293    elif cmd == 'create':
294        if not options.model:
295            parser.error('You must specify a model to create')
296        model = options.model.lower()
297        if options.model == 'all':
298            options.multiple = True
299            mcode_list = list(microcodes.values())
300            tried = []
301        else:
302            mcode_list, tried = FindMicrocode(microcodes, model)
303        if not mcode_list:
304            parser.error("Unknown model '%s' (%s) - try 'list' to list" %
305                        (model, ', '.join(tried)))
306        if not options.multiple and len(mcode_list) > 1:
307            parser.error("Ambiguous model '%s' (%s) matched %s - try 'list' "
308                        "to list or specify a particular file" %
309                        (model, ', '.join(tried),
310                        ', '.join([m.name for m in mcode_list])))
311        CreateFile(date, license_text, mcode_list, options.outfile)
312    else:
313        parser.error("Unknown command '%s'" % cmd)
314
315if __name__ == "__main__":
316    MicrocodeTool()
317