1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
#!/usr/bin/env python3
"""Check the selected default hosts' linked code and direct dependencies.
This is a Linux/GNU release-build gate, separate from sanitizer and core checks.
It does not estimate process RAM or the cost of optional UI/network hosts.
"""
import argparse
import hashlib
import json
from pathlib import Path
import platform
import re
import subprocess
ROOT = Path(__file__).resolve().parent
NEEDED = re.compile(r"\(NEEDED\).*Shared library: \[([^]]+)\]")
def run(*args):
return subprocess.check_output([str(arg) for arg in args], text=True)
def inspect(path):
rows = [row.split() for row in run('size', path).splitlines()]
if len(rows) != 2 or len(rows[1]) < 6 or not all(
rows[1][i].isdigit() for i in range(3)):
raise ValueError(f'unsupported size output for {path}')
sections = dict(zip(('text', 'data', 'bss'), map(int, rows[1][:3])))
needed = sorted(NEEDED.findall(run('readelf', '-d', path)))
return {'sections': sections, 'needed': needed,
'sha256': hashlib.sha256(path.read_bytes()).hexdigest()}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--build', type=Path, required=True)
parser.add_argument('--budget', type=Path, default=ROOT / 'profile-budget.json')
parser.add_argument('--only', action='append', metavar='BINARY',
help='check only this selected binary (repeatable)')
parser.add_argument('--output', type=Path)
args = parser.parse_args()
if args.output and args.output.exists():
parser.error('evidence file already exists')
budget = json.loads(args.budget.read_text())
selected = args.only or list(budget['binaries'])
unknown = set(selected) - set(budget['binaries'])
if unknown:
parser.error(f'unknown budget binary: {sorted(unknown)}')
binaries = {}
failures = []
for name in selected:
limits = budget['binaries'][name]
path = args.build / name
try:
observed = inspect(path)
except (OSError, subprocess.CalledProcessError, ValueError) as error:
failures.append(f'{name}: cannot inspect: {error}')
continue
binaries[name] = observed
for section, value in observed['sections'].items():
limit = limits[f'max_{section}_bytes']
if value > limit:
failures.append(f'{name} {section}: {value} > {limit}')
if observed['needed'] != sorted(limits['needed']):
failures.append(f'{name} dependencies: {observed["needed"]} '
f'!= {sorted(limits["needed"])}')
result = {'passed': not failures, 'failures': failures,
'profile': budget['profile'], 'scope': budget['scope'],
'budget_sha256': hashlib.sha256(args.budget.read_bytes()).hexdigest(),
'platform': platform.platform(), 'binaries': binaries}
output = json.dumps(result, indent=2) + '\n'
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
with args.output.open('x') as destination:
destination.write(output)
print(output, end='')
if failures:
raise SystemExit(1)
if __name__ == '__main__':
main()
|