221 lines
14 KiB
Python
221 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""One fixed-200 native bridge import diagnostic; build/run only after review."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import xml.etree.ElementTree as ET
|
|
from pure_apple import complete_rows, save, snapshot, STAGES
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
RUNTIME = Path('/opt/homebrew/Cellar/dotnet/10.0.403/libexec/shared/Microsoft.NETCore.App/10.0.14')
|
|
BRIDGE = RUNTIME / 'libSystem.Security.Cryptography.Native.Apple.dylib'
|
|
BRIDGE_SHA = '0333994e217a3d111d36d7f31460eea69a286f9fc819e2d7c746df13d2aec46c'
|
|
CRYPTO_SHA = 'f4957d7218a004a27bf8964e45147df6a88092c6e7344582082840d62c6c579a'
|
|
DOTNET = '/opt/homebrew/Cellar/dotnet/10.0.403/libexec/dotnet'
|
|
EXPECTED_SDK = {
|
|
'Opc.Ua.Client.dll': '620da406fabdbc75d11f9827ea7e39d2d3c762471bd2264b0d11f1d6cd9247cd',
|
|
'Opc.Ua.Configuration.dll': '6ed6a594c93c3ff35d1a16539a9fe87c1e2640549cc2f48ed6bae333dc8bf0e7',
|
|
'Opc.Ua.Core.Schema.dll': '51cfad48657d95d38074bb88be29aea6f5b361a07397247d58838d066b99772e',
|
|
'Opc.Ua.Core.Types.dll': 'a7dd141b8dabe8848ff28c2fb8e16a85a0508e6a49e6b1a1c1e267175b99af96',
|
|
'Opc.Ua.Core.dll': '9d0857aa6b7f3c49037d4ea557b4da8edf07e146069464481783e66c5c9cd35a',
|
|
'Opc.Ua.Security.Certificates.dll': '7f4969aceb2f0db5328c6882cb4b117342f3b18905c38acdba600683860c4fd0',
|
|
'Opc.Ua.Server.dll': '930e714a2f6e15d393840bcdc95e1afa8012bc740c3738a655f958142721864e',
|
|
'Opc.Ua.Types.dll': 'c542f69882f4e8a4ebd5ccc28a3881d6967c7ffb6dcc3c56dc6c776f39c2ede1',
|
|
}
|
|
CHILDREN = []
|
|
REAP_OUTCOMES = []
|
|
COPY_STAGES = ['import-cert-track', 'move-outer-copy', 'move-cert-untrack', 'move-cert-track',
|
|
'move-identity-track', 'identity-cert-track', 'release-original-cert',
|
|
'release-copied-cert', 'release-identity']
|
|
|
|
|
|
def sha(path):
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def reap(process):
|
|
outcome = {'processId': process.pid, 'confirmedReaped': False}
|
|
try:
|
|
if process.poll() is None:
|
|
try:
|
|
os.killpg(process.pid, signal.SIGTERM)
|
|
except ProcessLookupError:
|
|
pass
|
|
try:
|
|
process.wait(timeout=10)
|
|
except subprocess.TimeoutExpired:
|
|
try:
|
|
os.killpg(process.pid, signal.SIGKILL)
|
|
except ProcessLookupError:
|
|
pass
|
|
process.wait(timeout=5)
|
|
outcome['exitCode'] = process.wait(timeout=0)
|
|
outcome['confirmedReaped'] = True
|
|
except Exception as error:
|
|
outcome['errorType'] = type(error).__name__
|
|
raise
|
|
finally:
|
|
REAP_OUTCOMES.append(outcome)
|
|
|
|
|
|
def bounded(command, log, timeout, env=None):
|
|
with log.open('w') as stream:
|
|
process = subprocess.Popen(command, stdout=stream, stderr=subprocess.STDOUT,
|
|
env=env, start_new_session=True)
|
|
CHILDREN.append(process)
|
|
try:
|
|
status = process.wait(timeout=timeout)
|
|
if status:
|
|
raise RuntimeError('Bounded subprocess failed: ' + log.name)
|
|
finally:
|
|
reap(process)
|
|
|
|
|
|
def wipe_owned(root):
|
|
"""Best-effort caller-owned file overwrite/unlink; no physical secure-erasure claim."""
|
|
if not root.exists():
|
|
return
|
|
files = [p for p in root.rglob('*') if p.is_file() and not p.is_symlink()]
|
|
if len(files) > 100 or any(p.stat().st_size > 1024 * 1024 or p.stat().st_nlink != 1 for p in files):
|
|
raise RuntimeError('Unexpected owned fixture cleanup size/link layout; preserve private root')
|
|
for path in files:
|
|
size = path.stat().st_size
|
|
with path.open('r+b') as stream:
|
|
stream.write(bytes(size)); stream.flush(); os.fsync(stream.fileno())
|
|
shutil.rmtree(root)
|
|
|
|
|
|
def main():
|
|
os.umask(0o077) # This collector and its children only; includes normal SDK seed PFX files.
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--references', required=True, type=Path)
|
|
parser.add_argument('--output', required=True, type=Path, help='Fresh private output directory')
|
|
args = parser.parse_args()
|
|
if sys.platform != 'darwin' or os.uname().machine != 'arm64':
|
|
parser.error('Pinned diagnostic requires macOS arm64')
|
|
if sha(BRIDGE) != BRIDGE_SHA or sha(RUNTIME / 'System.Security.Cryptography.dll') != CRYPTO_SHA:
|
|
raise RuntimeError('Installed native/managed cryptography identity changed')
|
|
references = args.references.resolve()
|
|
if {p.name: sha(p) for p in references.glob('Opc.Ua*.dll')} != EXPECTED_SDK:
|
|
raise RuntimeError('Pinned SDK2 seed binary set changed')
|
|
output = args.output.resolve(); output.mkdir(parents=True, exist_ok=False); os.chmod(output, 0o700)
|
|
binary = output / 'NativeImportControl'
|
|
command = ['clang', '-std=c11', '-O0', '-g', '-Wall', '-Wextra', str(HERE / 'NativeImportControl.c'),
|
|
'-framework', 'Security', '-framework', 'CoreFoundation', '-o', str(binary)]
|
|
provenance = {'sourcesSha256': {name: sha(HERE / name) for name in ['NativeImportControl.c', 'ImportSeed.cs', 'native_import.py', 'pure_apple.py']},
|
|
'nativeBridgeSha256': BRIDGE_SHA, 'managedCryptographySha256': CRYPTO_SHA, 'sdkSeedSha256': EXPECTED_SDK,
|
|
'compileCommand': command, 'compiler': subprocess.check_output(['clang', '--version'], text=True, timeout=10),
|
|
'os': subprocess.check_output(['sw_vers'], text=True, timeout=10), 'architecture': os.uname().machine,
|
|
'hostDotnet': subprocess.check_output([DOTNET, '--info'], text=True, timeout=10),
|
|
'repetitions': 200, 'stages': STAGES, 'idleSeconds': 5, 'fullSoakGate': False,
|
|
'managedCallerInMeasuredChild': False, 'managedPfxParsingEmulated': False,
|
|
'legacyKeyLifetime': 'Explicit CFRelease after identity disposal; managed local has no explicit using scope.',
|
|
'wrapperNativeCopyCallsPerOperation': 9, 'checkpointDeadlineSeconds': 180,
|
|
'deadlineScope': 'Checkpoint/child deadline; in-progress bounded probes and bounded reap add overhead.',
|
|
'defaultOrSearchListSetters': False, 'fixtureCreationUmask': '0077'}
|
|
save(output / 'provenance.json', provenance)
|
|
bounded(command, output / 'native-build.log', 60)
|
|
project = ET.Element('Project', Sdk='Microsoft.NET.Sdk'); properties = ET.SubElement(project, 'PropertyGroup')
|
|
for name, value in {'OutputType': 'Exe', 'TargetFramework': 'net10.0', 'ImplicitUsings': 'enable', 'Nullable': 'enable',
|
|
'EnableDefaultCompileItems': 'false', 'TreatWarningsAsErrors': 'true', 'UseSharedCompilation': 'false'}.items():
|
|
ET.SubElement(properties, name).text = value
|
|
items = ET.SubElement(project, 'ItemGroup'); ET.SubElement(items, 'FrameworkReference', Include='Microsoft.AspNetCore.App')
|
|
ET.SubElement(items, 'Compile', Include=str(HERE / 'ImportSeed.cs'))
|
|
for name in EXPECTED_SDK:
|
|
ref = ET.SubElement(items, 'Reference', Include=Path(name).stem); ET.SubElement(ref, 'HintPath').text = str(references / name)
|
|
ET.indent(project); ET.ElementTree(project).write(output / 'ImportSeed.csproj', encoding='unicode')
|
|
bounded([DOTNET, 'build', str(output / 'ImportSeed.csproj'), '-c', 'Release', '-m:1', '--disable-build-servers', '--nologo'], output / 'seed-build.log', 120)
|
|
provenance['binarySha256'] = sha(binary)
|
|
provenance['seedBinarySha256'] = sha(output / 'bin/Release/net10.0/ImportSeed.dll')
|
|
provenance['linkedLibraries'] = subprocess.check_output(['otool', '-L', str(binary)], text=True, timeout=10)
|
|
save(output / 'provenance.json', provenance)
|
|
task = output / 'task'; task.mkdir(mode=0o700)
|
|
for name in ['seed', 'seed-tmp', 'seed-data', 'tmp']:
|
|
(task / name).mkdir(mode=0o700)
|
|
try:
|
|
bounded([DOTNET, str(output / 'bin/Release/net10.0/ImportSeed.dll'), str(task / 'seed')], output / 'seed.jsonl', 75,
|
|
env=dict(os.environ, TMPDIR=str(task / 'seed-tmp'), XDG_DATA_HOME=str(task / 'seed-data')))
|
|
seeds = complete_rows(output / 'seed.jsonl')
|
|
if len(seeds) != 1 or seeds[0]['kind'] != 'seed' or seeds[0]['sdkVersion'] != '2.0.1.0' or seeds[0]['cryptographyRuntime']['sha256'] != CRYPTO_SHA:
|
|
raise RuntimeError('Seed/runtime count or identity mismatch')
|
|
if sha(task / 'seed/leaf.der') != seeds[0]['leafSha256']:
|
|
raise RuntimeError('Seed leaf file identity mismatch')
|
|
if any(p.stat().st_mode & 0o077 for p in (task / 'seed').rglob('*') if p.is_file()):
|
|
raise RuntimeError('Seed fixture file permissions are not private0600')
|
|
for name in ['pki']:
|
|
wipe_owned(task / 'seed' / name)
|
|
wipe_owned(task / 'seed-tmp'); wipe_owned(task / 'seed-data')
|
|
if sha(BRIDGE) != BRIDGE_SHA:
|
|
raise RuntimeError('Native bridge changed before measured execution')
|
|
started = time.monotonic()
|
|
with (output / 'samples.jsonl').open('w') as log:
|
|
process = subprocess.Popen([str(binary), str(task), str(BRIDGE)], stdout=log, stderr=subprocess.STDOUT,
|
|
env=dict(os.environ, TMPDIR=str(task / 'tmp')), start_new_session=True)
|
|
CHILDREN.append(process)
|
|
try:
|
|
deadline = time.monotonic() + 180; samples = []
|
|
for stage in STAGES:
|
|
while True:
|
|
rows = complete_rows(output / 'samples.jsonl')
|
|
if any(r['kind'] == 'failed' for r in rows):
|
|
raise RuntimeError('Native import failed; preserve all status/ownership rows')
|
|
checkpoints = [r for r in rows if r['kind'] == 'checkpoint' and r.get('stage') == stage]
|
|
if checkpoints:
|
|
samples.append({**checkpoints[0], **snapshot(process.pid, task, output, stage)})
|
|
(task / ('continue-' + stage)).touch(); break
|
|
if process.poll() is not None or time.monotonic() >= deadline:
|
|
raise RuntimeError('Native checkpoint exceeded bound: ' + stage)
|
|
time.sleep(.05)
|
|
status = process.wait(timeout=15)
|
|
rows = complete_rows(output / 'samples.jsonl')
|
|
operations = [r for r in rows if r['kind'] == 'operation']; fingerprints = [r for r in rows if r['kind'] == 'fingerprint']
|
|
complete = [r for r in rows if r['kind'] == 'complete']
|
|
if status or len(rows) != 207 or len(operations) != 200 or len(fingerprints) != 2 or len(complete) != 1 or [r['iteration'] for r in operations] != list(range(1, 201)):
|
|
raise RuntimeError('Native fixed operation/completion counts failed')
|
|
for row in operations:
|
|
if row['failed'] or row['fallbackCleanup'] or not row['privatePathVerified'] or not row['deleteDuringFinalIdentityUntrack'] or row['logicalOwnersRemaining'] or row['modernErrorPresent'] or row['externalErrorPresent'] or row['ownedOutputs'] != row['releasedOutputs']:
|
|
raise RuntimeError('Native ownership order/cleanup failed')
|
|
if [c['stage'] for c in row['copies']] != COPY_STAGES or any(c['status'] for c in row['copies']) or [c['returnedRef'] for c in row['copies']] != [False] * 3 + [True] * 6:
|
|
raise RuntimeError('Native wrapper-copy sequence failed')
|
|
if any(row[k] for k in ['certStatus', 'createStatus', 'pathStatus', 'neverLockStatus', 'legacyStatus', 'moveStatus', 'copyCertStatus', 'deleteStatus', 'modernErrorCode', 'externalErrorCode']) or any(row[k] != 1 for k in ['certReturn', 'modernReturn', 'externalReturn', 'legacyReturn', 'moveReturn']):
|
|
raise RuntimeError('Native bridge statuses failed')
|
|
if sha(task / 'seed/leaf.der') != seeds[0]['leafSha256'] or complete[0]['operations'] != 200 or complete[0]['bridgeSha256'] != BRIDGE_SHA or complete[0]['ownedOutputs'] != complete[0]['releasedOutputs']:
|
|
raise RuntimeError('Native identity/completion ownership guard failed')
|
|
if any(samples[-1][k] for k in ['ownedKeychainFdCount', 'ownedKeychainFileCount']):
|
|
raise RuntimeError('Private keychain files/descriptors retained')
|
|
save(output / 'result.json', {'status': 'passed', 'seed': seeds[0], 'samples': samples, 'fingerprints': fingerprints,
|
|
'complete': complete[0], 'wallSecondsIncludingProbes': time.monotonic() - started, 'fullSoakGate': False,
|
|
'nativeInventoryIsNotOpenHandlesOrExclusiveRss': True, 'measuredChildManagedRuntimePresent': False})
|
|
finally:
|
|
reap(process)
|
|
finally:
|
|
save(output / 'fixture-before-harness-cleanup.json', {'remainingOwnedKeychainFiles': sum(p.is_file() and '.keychain' in p.name for p in (task / 'tmp').rglob('*'))})
|
|
all_reaped = all(child.returncode is not None for child in CHILDREN)
|
|
cleanup = {'allLaunchedChildrenConfirmedReaped': all_reaped, 'reapOutcomes': REAP_OUTCOMES,
|
|
'privateMaterialOverwrittenThenUnlinkedAfterChildReaping': False, 'physicalSecureErasureClaimed': False}
|
|
if not all_reaped:
|
|
cleanup.update(status='failed-preserved-private-fixture', ownedFixtureRootRemoved=False)
|
|
save(output / 'fixture-cleanup.json', cleanup)
|
|
raise RuntimeError('A launched child was not reaped; private fixture preserved without overwrite/unlink')
|
|
try:
|
|
wipe_owned(task)
|
|
cleanup.update(status='passed', ownedFixtureRootRemoved=not task.exists(),
|
|
privateMaterialOverwrittenThenUnlinkedAfterChildReaping=True)
|
|
except Exception as error:
|
|
cleanup.update(status='failed', errorType=type(error).__name__, ownedFixtureRootRemoved=not task.exists())
|
|
raise
|
|
finally:
|
|
save(output / 'fixture-cleanup.json', cleanup)
|
|
print(output)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|