145 lines
7.6 KiB
Python
Executable File
145 lines
7.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Compile the pinned 2023 crypto provider internally for ABI verification.
|
|
|
|
Apple source, generated headers and objects remain ignored. This does not
|
|
register the provider, produce a certified module or demonstrate a kernel boot.
|
|
"""
|
|
import concurrent.futures
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import platform
|
|
import subprocess
|
|
import sys
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / 'scripts'))
|
|
import pbxproj
|
|
from kernel_target import TRIPLE, check_object
|
|
from xnu_symbols import kernel_names
|
|
SOURCE = ROOT / 'refer/corecrypto-2023'
|
|
REVISION = '11fba65b12d8830630e248dfa8344caf8b065f26'
|
|
OUT = ROOT / 'build/corecrypto-2023'
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--namespace', action='store_true', help='Isolate provider globals from XNU crypto wrappers')
|
|
args = parser.parse_args()
|
|
|
|
|
|
def names(path, mode):
|
|
text = subprocess.check_output(['xcrun', 'nm', '-g', mode, '-j', path], text=True)
|
|
return {line.strip() for line in text.splitlines() if line.startswith('_')}
|
|
|
|
|
|
if subprocess.check_output(['git', '-C', SOURCE, 'rev-parse', 'HEAD'], text=True).strip() != REVISION:
|
|
raise SystemExit('Unexpected corecrypto source revision')
|
|
if subprocess.check_output(['git', '-C', SOURCE, 'status', '--porcelain'], text=True):
|
|
raise SystemExit('The crypto reference must stay immutable')
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
if platform.system() == 'Darwin':
|
|
subprocess.run(['plutil', '-convert', 'json', '-o', OUT / 'project.json',
|
|
SOURCE / 'corecrypto.xcodeproj/project.pbxproj'], check=True)
|
|
else:
|
|
(OUT / 'project.json').write_text(json.dumps(pbxproj.load(SOURCE / 'corecrypto.xcodeproj/project.pbxproj')))
|
|
project = json.loads((OUT / 'project.json').read_text())
|
|
objects = project['objects']
|
|
parents = {child: key for key, value in objects.items() for child in value.get('children', [])}
|
|
|
|
|
|
def resolve(key):
|
|
entry = objects[key]
|
|
path = entry.get('path', '')
|
|
if entry.get('sourceTree') == 'SOURCE_ROOT' or key not in parents:
|
|
return SOURCE / path
|
|
if entry.get('sourceTree') != '<group>':
|
|
raise ValueError(f'Unsupported source tree: {entry}')
|
|
return resolve(parents[key]) / path
|
|
|
|
|
|
target = next(value for value in objects.values() if value.get('isa') == 'PBXNativeTarget'
|
|
and value.get('name') == 'corecrypto_kext')
|
|
phase = next(objects[key] for key in target['buildPhases'] if objects[key]['isa'] == 'PBXSourcesBuildPhase')
|
|
sources = [resolve(objects[key]['fileRef']) for key in phase['files']
|
|
if objects[objects[key]['fileRef']].get('path', '').endswith('.c')]
|
|
for source in sources:
|
|
if not source.is_file() or not source.resolve().is_relative_to(SOURCE.resolve()):
|
|
raise SystemExit(f'Missing or unexpected source path: {source}')
|
|
include = OUT / 'include/corecrypto'
|
|
include.mkdir(parents=True, exist_ok=True)
|
|
for header in SOURCE.glob('*/corecrypto/*.h'):
|
|
link = include / header.name
|
|
if link.exists() and link.resolve() != header.resolve():
|
|
raise SystemExit(f'Conflicting header: {header.name}')
|
|
if not link.exists():
|
|
link.symlink_to(header)
|
|
sdk = subprocess.check_output(['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'], text=True).strip()
|
|
framework = ROOT / '.cache/fakeroot/System/Library/Frameworks/Kernel.framework/Versions/A'
|
|
command = ['xcrun', '--sdk', 'iphoneos', 'clang', '-target', TRIPLE, '-isysroot', sdk,
|
|
'-mkernel', '-ffreestanding', '-fno-builtin', '-fno-stack-check', '-std=gnu99', '-fblocks', '-O2',
|
|
'-fno-vectorize', '-fno-slp-vectorize', '-mno-implicit-float',
|
|
'-DKERNEL=1', '-DKERNEL_PRIVATE=1', '-DBUILDKERNEL=1', '-DCC_USE_ASM=0', '-DNDEBUG=1',
|
|
'-DCC_DISABLE_RSAKEYGEN=1', '-Wno-deprecated-declarations', '-Wno-incompatible-library-redeclaration',
|
|
'-I' + str(include.parent), '-I' + str(framework / 'PrivateHeaders'), '-I' + str(framework / 'Headers')]
|
|
for folder in sorted({h.parent for h in SOURCE.rglob('*.h') if 'test' not in str(h.relative_to(SOURCE)).lower()}):
|
|
command.extend(['-I', str(folder)])
|
|
variant = '-namespaced' if args.namespace else ''
|
|
if args.namespace:
|
|
plain = OUT / 'libcorecrypto_kernel_noasm.a'
|
|
if not plain.is_file():
|
|
raise SystemExit('Build the plain provider once before creating its namespace')
|
|
collisions = sorted(names(plain, '-U') & kernel_names(ROOT))
|
|
if any(not name[1:].isidentifier() for name in collisions):
|
|
raise SystemExit('A colliding symbol cannot be namespaced with a C identifier')
|
|
header = OUT / 'namespace.h'
|
|
header.write_text(''.join(f'#define {name[1:]} cc14_{name[1:]}\n' for name in collisions))
|
|
command.extend(['-include', str(header)])
|
|
(OUT / f'compile-command{variant}.json').write_text(json.dumps(command, indent=2) + '\n')
|
|
object_dir = OUT / f'objects{variant}'
|
|
object_dir.mkdir(exist_ok=True)
|
|
|
|
|
|
def compile_one(source):
|
|
relative = str(source.relative_to(SOURCE))
|
|
name = hashlib.sha256(relative.encode()).hexdigest()[:12] + '-' + source.stem
|
|
destination = object_dir / (name + '.o')
|
|
result = subprocess.run([*command, '-c', source, '-o', destination], stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT, text=True)
|
|
(object_dir / (name + '.log')).write_text(result.stdout)
|
|
return {'source': relative, 'object': destination.name, 'exit_code': result.returncode,
|
|
'diagnostic': result.stdout[-2500:] if result.returncode else None}
|
|
|
|
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
|
|
results = list(executor.map(compile_one, sources))
|
|
failed = [result for result in results if result['exit_code']]
|
|
report = {'source_url': 'https://github.com/ONLYA/corecrypto-2023', 'source_revision': REVISION,
|
|
'provenance': 'Third-party source snapshot; not authenticated against an Apple archive',
|
|
'module_version': '14.0', 'target': TRIPLE, 'assembly_enabled': False,
|
|
'compiled_sources': len(results), 'failures': len(failed), 'objects': results,
|
|
'kernel_provider_registered': False, 'crypto_selftests_passed': False}
|
|
(OUT / f'build-report{variant}.json').write_text(json.dumps(report, indent=2) + '\n')
|
|
print(f'{len(results)} kernel C sources; {len(failed)} failures', flush=True)
|
|
for result in failed[:8]:
|
|
print(result['source'], result['diagnostic'], flush=True)
|
|
if failed:
|
|
sys.exit(1)
|
|
for result in results:
|
|
check_object((object_dir / result['object']).read_bytes(), result['object'])
|
|
archive = OUT / f'libcorecrypto_kernel_noasm{variant}.a'
|
|
archive.unlink(missing_ok=True)
|
|
subprocess.run(['xcrun', 'ar', 'rcs', archive, *[object_dir / result['object'] for result in results]], check=True)
|
|
defined = names(archive, '-U')
|
|
undefined = names(archive, '-u') - defined
|
|
kernel_exports = kernel_names(ROOT)
|
|
survey = {'unresolved_external_names': sorted(undefined), 'missing_kernel_names': sorted(undefined - kernel_exports),
|
|
'duplicate_kernel_names': sorted(defined & kernel_exports), 'provider_registered': False,
|
|
'abi_compatibility_proven': False}
|
|
(OUT / f'link-survey{variant}.json').write_text(json.dumps(survey, indent=2) + '\n')
|
|
report['object_architecture_and_deployment_audited'] = len(results)
|
|
report['archive_sha256'] = hashlib.sha256(archive.read_bytes()).hexdigest()
|
|
report['namespace_isolated'] = args.namespace
|
|
(OUT / f'build-report{variant}.json').write_text(json.dumps(report, indent=2) + '\n')
|
|
print(f'Internal unregistered provider archive: {archive.name}')
|
|
print(f'External names absent from kernel: {survey["missing_kernel_names"]}')
|
|
print(f'Globals shared with kernel: {len(survey["duplicate_kernel_names"])}; initialization and intact selftests are still required')
|