#! /usr/bin/python
# SPDX-License-Identifier: CC0-1.0
import argparse
import errno
import io
import itertools
import os
import re
import shutil
import struct
import sys
import tempfile
from subprocess import PIPE, Popen, STDOUT
# Python 3 shims
try:
from functools import reduce
except:
pass
try:
from itertools import zip_longest as izip_longest
except:
from itertools import izip_longest
# revs:
# [ { "path", "cpuid", "pf", "rev", "date" } ]
# artifacts:
# * content summary (per-file)
# * overlay summary (per-fms/pf)
# * changelog (per-file?)
# * discrepancies (per-fms/pf)
log_level = 0
print_date = False
def log_status(msg, level=0):
global log_level
if log_level >= level:
sys.stderr.write(msg + "\n")
def log_info(msg, level=2):
global log_level
if log_level >= level:
sys.stderr.write("INFO: " + msg + "\n")
def log_warn(msg, level=1):
global log_level
if log_level >= level:
sys.stderr.write("WARNING: " + msg + "\n")
def log_error(msg, level=-1):
global log_level
if log_level >= level:
sys.stderr.write("ERROR: " + msg + "\n")
def remove_prefix(text, prefix):
if isinstance(prefix, str):
prefix = [prefix, ]
for p in prefix:
pfx = p if p.endswith(os.sep) else p + os.sep
if text.startswith(pfx):
return text[len(pfx):]
return text
def file_walk(args, yield_dirs=False):
for content in args:
if os.path.isdir(content):
if yield_dirs:
yield ("", content)
for root, dirs, files in os.walk(content):
if yield_dirs:
for f in dirs:
p = os.path.join(root, f)
yield (remove_prefix(p, content), p)
for f in files:
p = os.path.join(root, f)
yield (remove_prefix(p, content), p)
elif os.path.exists(content):
yield ("", content)
else:
raise IOError(errno.ENOENT, os.strerror(errno.ENOENT), content)
def cpuid_fname(c):
return "%02x-%02x-%02x" % (
((c >> 16) & 0xff0) + ((c >> 8) & 0xf),
((c >> 12) & 0xf0) + ((c >> 4) & 0xf),
c & 0xf)
def read_revs_dir(path, src=None, ret=None):
if ret is None:
ret = []
ucode_re = re.compile('[0-9a-f]{2}-[0-9a-f]{2}-0[0-9a-f]$')
ucode_dat_re = re.compile('microcode.*\.dat$')
for rp, ap in file_walk([path, ]):
rp_fname = os.path.basename(rp)
if not ucode_re.match(rp_fname) and not ucode_dat_re.match(rp_fname):
continue
# Text-based format
data = None
if ucode_dat_re.match(rp_fname):
data = io.BytesIO()
with open(ap, "r") as f:
for line in f:
if line.startswith("/"):
continue
vals = line.split(",")
for val in vals:
val = val.strip()
if not val:
continue
data.write(struct.pack("<I", int(val, 16)))
sz = data.seek(0, os.SEEK_CUR)
data.seek(0, os.SEEK_SET)
else:
sz = os.stat(ap).st_size
try:
with data or open(ap, "rb") as f:
log_info("Processing %s" % ap)
offs = 0
while offs < sz:
f.seek(offs, os.SEEK_SET)
hdr = struct.unpack("IiIIIIIIIIII", f.read(48))
ret.append({"path": rp, "src": src or path,
"cpuid": hdr[3], "pf": hdr[6], "rev": hdr[1],
"date": hdr[2], "offs": offs, "cksum": hdr[4],
"data_size": hdr[7], "total_size": hdr[8]})
if hdr[8] and hdr[8] - hdr[7] > 48:
f.seek(hdr[7], os.SEEK_CUR)
ext_tbl = struct.unpack("IIIII", f.read(20))
log_status("Found %u extended signatures for %s:%#x" %
(ext_tbl[0], rp, offs), level=1)
cur_offs = offs + hdr[7] + 48 + 20
ext_sig_cnt = 0
while cur_offs < offs + hdr[8] \
and ext_sig_cnt <= ext_tbl[0]:
ext_sig = struct.unpack("III", f.read(12))
ret.append({"path": rp, "src": src or path,
"cpuid": ext_sig[0], "pf": ext_sig[1],
"rev": hdr[1], "date": hdr[2],
"offs": offs, "ext_offs": cur_offs,
"cksum": hdr[4],
"ext_cksum": ext_sig[2],
"data_size": hdr[7],
"total_size": hdr[8]})
log_status(("Got ext sig %#x/%#x for " +
"%s:%#x:%#x/%#x") %
(ext_sig[0], ext_sig[1], rp, offs,
hdr[3], hdr[6]), level=2)
cur_offs += 12
ext_sig_cnt += 1
offs += hdr[8] or 2048
except Exception as e:
log_error("a problem occurred while processing %s: %s" % (ap, e),
level=1)
return ret
def read_revs_rpm(path, ret=None):
if ret is None:
ret = []
dir_tmp = tempfile.mkdtemp()
log_status("Trying to extract files from RPM \"%s\"..." % path,
level=1)
rpm2cpio = Popen(args=["rpm2cpio", path], stdout=PIPE, stderr=PIPE,
close_fds=True)
cpio = Popen(args=["cpio", "-idmv", "*??-??-??", "*microcode*.dat"],
cwd=dir_tmp, stdin=rpm2cpio.stdout,
stdout=PIPE, stderr=STDOUT)
out, cpio_stderr = cpio.communicate()
rpm2cpio_out, rpm2cpio_err = rpm2cpio.communicate()
rpm2cpio_ret = rpm2cpio.returncode
cpio_ret = cpio.returncode
log_info("rpm2cpio exit code: %d, cpio exit code: %d" %
(rpm2cpio_ret, cpio_ret))
if rpm2cpio_err:
log_info("rpm2cpio stderr:\n%s" % rpm2cpio_err, level=3)
if out:
log_info("cpio output:\n%s" % out, level=3)
if cpio_stderr:
log_info("cpio stderr:\n%s" % cpio_stderr, level=3)
if rpm2cpio_ret == 0 and cpio_ret == 0:
ret = read_revs_dir(dir_tmp, path)
shutil.rmtree(dir_tmp)
return ret
def read_revs(path, ret=None):
if ret is None:
ret = []
if os.path.isdir(path):
return read_revs_dir(path, ret)
else:
return read_revs_rpm(path, ret)
def gen_mc_map(mc_data, merge=False, merge_path=False):
"""
Converts an array of microcode file information to a map with path/sig/pf
as a key.
merge: whether to leave only the newest mc variant in the map or leave all
possible variants.
"""
res = dict()
for mc in mc_data:
key = (None if merge_path else mc["path"], mc["cpuid"], mc["pf"])
if key not in res:
res[key] = dict()
cpuid = mc["cpuid"]
cur_pf = mc["pf"]
pid = 1
while cur_pf > 0:
if cur_pf & 1 and not (merge and pid in res[key]
and res[key][pid]["rev"][0] >= mc["rev"]):
if pid not in res[cpuid] or merge:
res[cpuid][pid] = []
res[cpuid][pid].append(mc)
cur_pf = cur_pf / 2
pid = pid * 2
return res
def gen_fn_map(mc_data, merge=False, merge_path=False):
res = dict()
for mc in mc_data:
key = (None if merge_path else mc["path"], mc["cpuid"], mc["pf"])
if key in res:
log_warn("Duplicate path/cpuid/pf: %s/%#x/%#x" % key)
else:
res[key] = []
if merge and len(res[key]):
if mc["rev"] > res[key][0]["rev"]:
res[key][0] = mc
else:
res[key].append(mc)
return res
def revcmp(a, b):
return b["rev"] - a["rev"]
class ChangeLogEntry:
ADDED = 0
REMOVED = 1
UPDATED = 2
DOWNGRADED = 3
OTHER = 4
def mc_stripped_path(mc):
paths = ("usr/share/microcode_ctl/ucode_with_caveats/intel",
"usr/share/microcode_ctl/ucode_with_caveats",
"usr/share/microcode_ctl",
"lib/firmware",
"etc/firmware",
)
return remove_prefix(mc["path"], paths)
class mcnm:
MCNM_ABBREV = 0
MCNM_FAMILIES = 1
MCNM_MODELS = 2
MCNM_FAMILIES_MODELS = 3
MCNM_CODENAME = 4
def get_mc_cnames(mc, cmap, mode=mcnm.MCNM_ABBREV):
if not isinstance(mc, dict):
mc = mc_from_mc_key(mc)
sig = mc["cpuid"]
pf = mc["pf"]
res = []
if not cmap:
return None
if sig not in cmap:
log_info("No codename information for sig %#x" % sig)
return None
cnames = cmap[sig]
if mode in (mcnm.MCNM_FAMILIES, mcnm.MCNM_MODELS,
mcnm.MCNM_FAMILIES_MODELS):
for c in cnames:
if not (pf & c["pf_mask"]):
continue
for m, f in ((mcnm.MCNM_FAMILIES, "families"),
(mcnm.MCNM_MODELS, "models")):
if m & mode == 0:
continue
if f not in c or not c[f]:
log_info("No %s for sig %#x in %r" % (f, sig, c))
continue
res.append(c[f])
return ", ".join(res) or None
steppings = dict()
suffices = dict()
for c in cnames:
if pf and not (pf & c["pf_mask"]):
continue
if mode == mcnm.MCNM_ABBREV and "abbrev" in c and c["abbrev"]:
cname = c["abbrev"]
else:
cname = c["codename"]
if cname not in suffices:
suffices[cname] = set()
if "variant" in c and c["variant"]:
suffices[cname] |= set(c["variant"])
if cname not in steppings:
steppings[cname] = set()
if c["stepping"]:
steppings[cname] |= set(c["stepping"])
for cname in sorted(steppings.keys()):
cname_str = cname
if len(suffices[cname]):
cname_str += "-" + "/".join(sorted(suffices[cname]))
if len(steppings[cname]):
cname_str += " " + "/".join(sorted(steppings[cname]))
res.append(cname_str)
return ", ".join(res) or None
def mc_from_mc_key(k):
return dict(zip(("path", "cpuid", "pf"), k))
def mc_path(mc, pf_sfx=True, midword=None, cmap=None):
if not isinstance(mc, dict):
mc = mc_from_mc_key(mc)
path = mc_stripped_path(mc) if mc["path"] is not None else None
cpuid_fn = cpuid_fname(mc["cpuid"])
fname = os.path.basename(mc["path"] or cpuid_fn)
midword = "" if midword is None else " " + midword
cname = get_mc_cnames(mc, cmap)
cname_str = " (" + cname + ")" if cname else ""
if pf_sfx:
sfx = "/0x%02x" % mc["pf"]
else:
sfx = ""
if not path or path == os.path.join("intel-ucode", cpuid_fn):
return "%s%s%s%s" % (fname, sfx, cname_str, midword)
else:
return "%s%s%s%s (in %s)" % (cpuid_fn, sfx, cname_str, midword, path)
def gen_changelog_file(old, new):
pass
def mc_cmp(old_mc, new_mc):
res = []
old_mc_revs = [x["rev"] for x in old_mc]
new_mc_revs = [x["rev"] for x in new_mc]
common = set(old_mc_revs) & set(new_mc_revs)
old_rev_list = [x for x in sorted(old_mc_revs) if x not in common]
new_rev_list = [x for x in sorted(new_mc_revs) if x not in common]
if len(old_rev_list) != 1 or len(new_rev_list) != 1:
for i in new_mc:
if i["rev"] in new_rev_list:
res.append((ChangeLogEntry.ADDED, None, i))
for i in old_mc:
if i["rev"] in old_rev_list:
res.append((ChangeLogEntry.REMOVED, i, None))
else:
for old in old_mc:
if old["rev"] == old_rev_list[0]:
break
for new in new_mc:
if new["rev"] == new_rev_list[0]:
break
if new["rev"] > old["rev"]:
res.append((ChangeLogEntry.UPDATED, old, new))
elif new["rev"] < old["rev"]:
res.append((ChangeLogEntry.DOWNGRADED, old, new))
return res
def gen_changelog(old, new):
res = []
old_map = gen_fn_map(old)
new_map = gen_fn_map(new)
old_files = set(old_map.keys())
new_files = set(new_map.keys())
both = old_files & new_files
added = new_files - old_files
removed = old_files - new_files
for f in sorted(added):
p = mc_path(new_map[f][0])
for old_f in sorted(removed):
old_p = mc_path(old_map[old_f][0])
if p == old_p and f[1] == old_f[1] and f[2] == old_f[2]:
log_info("Matched %s (%s and %s)" %
(p, old_map[old_f][0]["path"], new_map[f][0]["path"]))
added.remove(f)
removed.remove(old_f)
res += mc_cmp(old_map[old_f], new_map[f])
for f in sorted(added):
for i in new_map[f]:
res.append((ChangeLogEntry.ADDED, None, i))
for f in sorted(removed):
for i in old_map[f]:
res.append((ChangeLogEntry.REMOVED, i, None))
for f in sorted(both):
res += mc_cmp(old_map[f], new_map[f])
return res
def mc_date(mc):
if isinstance(mc, dict):
mc = mc["date"]
return "%04x-%02x-%02x" % (mc & 0xffff, mc >> 24, (mc >> 16) & 0xff)
def mc_rev(mc, date=None):
'''
While revision is signed for comparison purposes, historically
it is printed as unsigned, Oh well.
'''
global print_date
if mc["rev"] < 0:
rev = 2**32 + mc["rev"]
else:
rev = mc["rev"]
if date if date is not None else print_date:
return "%#x (%s)" % (rev, mc_date(mc))
else:
return "%#x" % rev
def print_changelog(clog, cmap, args):
for e, old, new in sorted(clog):
if e == ChangeLogEntry.ADDED:
print("Addition of %s at revision %s" %
(mc_path(new, midword="microcode", cmap=cmap), mc_rev(new)))
elif e == ChangeLogEntry.REMOVED:
print("Removal of %s at revision %s" %
(mc_path(old, midword="microcode", cmap=cmap), mc_rev(old)))
elif e == ChangeLogEntry.UPDATED:
print("Update of %s from revision %s up to %s" %
(mc_path(old, midword="microcode", cmap=cmap),
mc_rev(old), mc_rev(new)))
elif e == ChangeLogEntry.DOWNGRADED:
print("Downgrade of %s from revision %s down to %s" %
(mc_path(old, midword="microcode", cmap=cmap),
mc_rev(old), mc_rev(new)))
elif e == ChangeLogEntry.OTHER:
print("Other change in %s:" % old["path"])
print(" old: %#x/%#x: rev %s (offs %#x)" %
(old["cpuid"], old["pf"], mc_rev(old), old["offs"]))
print(" new: %#x/%#x: rev %s (offs %#x)" %
(new["cpuid"], new["pf"], mc_rev(new), new["offs"]))
class TableStyles:
TS_CSV = 0
TS_FANCY = 1
def print_line(line, column_sz):
print(" | ".join([str(x).ljust(column_sz[i])
for i, x in zip(itertools.count(),
itertools.chain(line,
[""] * (len(column_sz) -
len(line))))]).rstrip())
def print_table(items, header=[], style=TableStyles.TS_CSV):
if style == TableStyles.TS_CSV:
for i in items:
print(";".join(i))
elif style == TableStyles.TS_FANCY:
column_sz = list(reduce(lambda x, y:
map(max, izip_longest(x, y, fillvalue=0)),
[[len(x) for x in i]
for i in itertools.chain(header, items)]))
for i in header:
print_line(i, column_sz)
if header:
print("-+-".join(["-" * x for x in column_sz]))
for i in items:
print_line(i, column_sz)
def print_summary(revs, cmap, args):
m = gen_fn_map(revs)
cnames_mode = mcnm.MCNM_ABBREV if args.abbrev else mcnm.MCNM_CODENAME
header = []
if args.header:
header.append(["Path", "Offset", "Ext. Offset", "CPUID",
"Platform ID Mask", "Revision", "Date", "Checksum",
"Codenames"] +
(["Models"] if args.models else []))
tbl = []
for k in sorted(m.keys()):
for mc in m[k]:
tbl.append([mc_stripped_path(mc),
"0x%x" % mc["offs"],
"0x%x" % mc["ext_offs"] if "ext_offs" in mc else "-",
"0x%05x" % mc["cpuid"],
"0x%02x" % mc["pf"],
mc_rev(mc, date=False),
mc_date(mc),
"0x%08x" % mc["cksum"],
get_mc_cnames(mc, cmap, cnames_mode) or ""] +
([get_mc_cnames(mc, cmap,
mcnm.MCNM_FAMILIES_MODELS)]
if args.models else []))
print_table(tbl, header, style=TableStyles.TS_FANCY)
def read_codenames_file(path):
'''
Supports two formats: new and old
* old: tab-separated. Field order:
Segment, (unused), Codename, (dash-separated) Stepping,
Platform ID mask, CPUID, (unused) Update link, (unused) Specs link
* new: semicolon-separated; support comments. Distinguished
by the first line that starts with octothorp. Field order:
Segment, Unused, Codename, Stepping, Platform ID mask, CPUID,
Abbreviation, Variant(s), Families, Models
'''
old_fields = ["segment", "_", "codename", "stepping", "pf_mask", "sig",
"_update", "_specs"]
new_fields = ["segment", "_", "codename", "stepping", "pf_mask", "sig",
"abbrev", "variant", "families", "models"]
new_fmt = False
field_names = old_fields
res = dict()
try:
with open(path, "r") as f:
for line in f:
line = line.strip()
if len(line) == 0:
continue
if line[0] == '#':
new_fmt = True
field_names = new_fields
continue
fields = line.split(";" if new_fmt else "\t",
1 + len(field_names))
fields = dict(zip(field_names, fields))
if "sig" not in fields:
log_warn("Skipping %r (from \"%s\")" % (fields, line))
continue
sig = fields["sig"] = int(fields["sig"], 16)
fields["pf_mask"] = int(fields["pf_mask"], 16)
fields["stepping"] = fields["stepping"].split(",")
if "variant" in fields:
if fields["variant"]:
fields["variant"] = fields["variant"].split(",")
else:
fields["variant"] = []
if sig not in res:
res[sig] = list()
res[sig].append(fields)
except Exception as e:
log_error("a problem occurred while reading code names: %s" % e)
return res
def print_discrepancies(rev_map, deps, cmap, args):
"""
rev_map: dict "name": revs
deps: list of tuples (name, parent/None)
"""
sigs = set()
for p, r in rev_map.items():
sigs |= set(r.keys())
if args.header:
header1 = ["sig"]
if args.print_vs:
header2 = [""]
for p, n, d in deps:
header1.append(n)
if args.print_vs:
add = ""
if d:
for pd, nd, dd in deps:
if pd == d:
add = "(vs. %s)" % nd
break
header2.append(add)
if args.models:
header1.append("Model names")
if args.print_vs:
header2.append("")
header = [header1] + ([header2] if args.print_vs else [])
tbl = []
for s in sorted(sigs):
out = [mc_path(s)]
print_out = not args.print_filter
print_date = args.min_date is None
for p, n, d in deps:
cur = dict([(x["rev"], x) for x in rev_map[p][s]]) \
if s in rev_map[p] else []
v = "/".join([mc_rev(y) for x, y in sorted(cur.items())]) \
if cur else "-"
if d is not None:
prev = [x["rev"] for x in rev_map[d][s]] if s in rev_map[d] \
else []
if [x for x in cur if x not in prev]:
v += " (*)"
print_out = True
if args.min_date is not None and s in rev_map[p]:
for x in rev_map[p][s]:
print_date |= mc_date(x) > args.min_date
out.append(v)
if print_out and print_date:
if args.models:
out.append(get_mc_cnames(s, cmap) or "")
tbl.append(out)
print_table(tbl, header, style=TableStyles.TS_FANCY)
def cmd_summary(args):
revs = []
for p in args.filelist:
revs = read_revs(p, ret=revs)
codenames_map = read_codenames_file(args.codenames)
print_summary(revs, codenames_map, args)
return 0
def cmd_changelog(args):
codenames_map = read_codenames_file(args.codenames)
base_path = args.filelist[0]
upd_path = args.filelist[1]
base = read_revs(base_path)
upd = read_revs(upd_path)
print_changelog(gen_changelog(base, upd), codenames_map, args)
return 0
def cmd_discrepancies(args):
"""
filenames:
* "<" prefix (possibly multiple times) to refer to a previous entry
to compare against
* "[name]" prefix is a name reference
"""
codenames_map = read_codenames_file(args.codenames)
rev_map = dict()
deps = list()
cur = -1
for path in args.filelist:
orig_path = path
name = None
cur += 1
dep = None
while True:
if path[0] == '<':
path = path[1:]
dep = cur - 1 if dep is None else dep - 1
elif path[0] == '[' and path.find(']') > 0:
pos = path.find(']')
name = path[1:pos]
path = path[pos + 1:]
else:
break
if name is None:
name = path
if dep is not None and dep < 0:
log_error("Incorrect dep reference for '%s' (points to index %d)" %
(orig_path, dep))
return 1
deps.append((path, name, deps[dep][0] if dep is not None else None))
rev_map[path] = gen_fn_map(read_revs(path), merge=args.merge,
merge_path=True)
print_discrepancies(rev_map, deps, codenames_map, args)
return 0
def parse_cli():
root_parser = argparse.ArgumentParser(prog="gen_updates",
description="Intel CPU Microcode " +
"parser")
root_parser.add_argument("-C", "--codenames", default='codenames',
help="Code names file")
root_parser.add_argument("-v", "--verbose", action="count", default=0,
help="Increase output verbosity")
cmdparsers = root_parser.add_subparsers(title="Commands",
help="main gen_updates commands")
parser_s = cmdparsers.add_parser("summary",
help="Generate microcode summary")
parser_s.add_argument("-a", "--abbreviate", action="store_const",
dest="abbrev", const=True, default=True,
help="Abbreviate code names")
parser_s.add_argument("-A", "--no-abbreviate", action="store_const",
dest="abbrev", const=False,
help="Do not abbreviate code names")
parser_s.add_argument("-m", "--print-models", action="store_const",
dest="models", const=True, default=False,
help="Print models")
parser_s.add_argument("-M", "--no-print-models",
action="store_const", dest="models",
const=False, help="Do not print models")
parser_s.add_argument("-H", "--no-print-header",
action="store_const", dest="header",
const=False, default=True,
help="Do not print hader")
parser_s.add_argument("filelist", nargs="*", default=[],
help="List or RPMs/directories to process")
parser_s.set_defaults(func=cmd_summary)
parser_c = cmdparsers.add_parser("changelog",
help="Generate changelog")
parser_c.add_argument("filelist", nargs=2,
help="RPMs/directories to compare")
parser_c.set_defaults(func=cmd_changelog)
parser_d = cmdparsers.add_parser("discrepancies",
help="Generate discrepancies")
parser_d.add_argument("-s", "--merge-revs", action="store_const",
dest="merge", const=True, default=False,
help="Merge revisions that come" +
" from different files")
parser_d.add_argument("-S", "--no-merge-revs", action="store_const",
dest="merge", const=False,
help="Do not Merge revisions that come" +
" from different files")
parser_d.add_argument("-v", "--print-vs", action="store_const",
dest="print_vs", const=True, default=False,
help="Print base version ")
parser_d.add_argument("-V", "--no-print-vs", action="store_const",
dest="print_vs", const=False,
help="Do not Merge revisions that come" +
" from different files")
parser_d.add_argument("-m", "--print-models", action="store_const",
dest="models", const=True, default=True,
help="Print model names")
parser_d.add_argument("-M", "--no-print-models", action="store_const",
dest="models", const=False,
help="Do not print model names")
parser_d.add_argument("-H", "--no-print-header", action="store_const",
dest="header", const=False, default=True,
help="Do not print hader")
parser_d.add_argument("-a", "--print-all-files", action="store_const",
dest="print_filter", const=False, default=True,
help="Print all files")
parser_d.add_argument("-c", "--print-changed-files", action="store_const",
dest="print_filter", const=True,
help="Print only changed files")
parser_d.add_argument("-d", "--min-date", action="store",
help="Minimum date filter")
parser_d.add_argument("filelist", nargs='*',
help="RPMs/directories to compare")
parser_d.set_defaults(func=cmd_discrepancies)
args = root_parser.parse_args()
if not hasattr(args, "func"):
root_parser.print_help()
return None
return args
def main():
args = parse_cli()
if args is None:
return 1
return args.func(args)
if __name__ == "__main__":
sys.exit(main())