test: record partial native bridge import control
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user