Files
lmxopcua/scripts/validation/opcua-lifecycle-memory/native-retention/native_import.py
T
2026-11-10 17:31:50 -05:00

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()