blob: 7376f674b3237375fec1cb1a180fed9a64458c2f [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Verify the locked run-iket NativeDump patch from retained trace artifacts.
Run ``run-iket`` with ``--log-level trace --keep`` so its per-kernel patched
and unpatched images are retained. This helper reconstructs a disassemblable
patched CUBIN outside the repository and emits only a normalized JSON report.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import shutil
import subprocess
from pathlib import Path
INSTRUCTION = re.compile(r"/\*([0-9a-f]+)\*/\s+(.*?)\s*;")
PATCH_OFFSET = re.compile(
r"Patching SASS => name: (?P<kernel>.*?), .*?driver_patch_offset: (?P<offset>\d+)"
)
STORE_REGISTER = re.compile(r"\bSTS\s+\[[^]]+\],\s+(R\d+)$")
GLOBAL_STORE_REGISTER = re.compile(r"\bSTG\.E\s+\[RZ\.U32\+UR0\],\s+(R\d+)$")
POINTER_INCREMENT = re.compile(r"^UIADD3\.64\s+UR0,\s+UPT,\s+UPT,\s+UR0,\s+0x4,\s+URZ$")
NORMALIZED_SIGNATURE = [
"GLOBALTIMERLO",
"ENCODE_EVENT_ID",
"STORE_GLOBAL_32",
"ADD_WRITE_PTR_64_4",
]
def _sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _single(paths, description: str) -> Path:
paths = list(paths)
if len(paths) != 1:
raise RuntimeError(f"expected one {description}, found {len(paths)}")
return paths[0]
def _discover_artifacts(run_dir: Path, kernel: str | None):
unpatched_paths = list(run_dir.glob("iket/pid_*/unpatched_*"))
if kernel is not None:
unpatched_paths = [path for path in unpatched_paths if path.name == f"unpatched_{kernel}"]
unpatched = _single(unpatched_paths, "unpatched kernel image")
kernel = unpatched.name.removeprefix("unpatched_")
patched = unpatched.with_name(kernel)
if not patched.is_file():
raise RuntimeError("patched kernel image is missing; rerun with --log-level trace --keep")
injection_log = unpatched.with_name("smodel.injection.log")
offsets = [
int(match.group("offset"))
for match in PATCH_OFFSET.finditer(injection_log.read_text(encoding="utf-8"))
if match.group("kernel") == kernel
]
if len(offsets) != 1:
raise RuntimeError(f"expected one driver patch offset for {kernel}, found {offsets}")
unpatched_bytes = unpatched.read_bytes()
probe = unpatched_bytes[offsets[0] : offsets[0] + 256]
matching_cubins = []
for cubin in run_dir.glob("tracker/pid_*/module_*.cubin"):
data = cubin.read_bytes()
position = data.find(probe)
if position >= 0 and data.find(probe, position + 1) < 0:
matching_cubins.append((cubin, position))
if len(matching_cubins) != 1:
raise RuntimeError(
f"expected one tracker CUBIN containing {kernel}, found {len(matching_cubins)}"
)
cubin, cubin_offset = matching_cubins[0]
return kernel, cubin, unpatched, patched, offsets[0], cubin_offset
def _disassemble(nvdisasm: str, cubin: Path) -> str:
return subprocess.run(
[nvdisasm, "-c", str(cubin)], check=True, capture_output=True, text=True
).stdout
def _function_instructions(disassembly: str, kernel: str) -> dict[int, str]:
marker = f"\n.text.{kernel}:\n"
if marker not in disassembly:
raise RuntimeError(f"nvdisasm output does not contain .text.{kernel}")
function = disassembly.split(marker, 1)[1].split("\n//---------------------", 1)[0]
return {int(address, 16): text.strip() for address, text in INSTRUCTION.findall(function)}
def _without_predicate(instruction: str) -> str:
return re.sub(r"^@!?[A-Z0-9]+\s+", "", instruction)
def _verify_patch(unpatched_sass: str, patched_sass: str, kernel: str) -> int:
before = _function_instructions(unpatched_sass, kernel)
after = _function_instructions(patched_sass, kernel)
if before.keys() != after.keys():
raise RuntimeError("patched kernel changed the instruction address layout")
changed = [address for address in before if before[address] != after[address]]
if not changed or len(changed) % 2:
raise RuntimeError(f"unexpected changed instruction count: {len(changed)}")
addresses = sorted(before)
address_index = {address: index for index, address in enumerate(addresses)}
sites = []
for index in range(0, len(changed), 2):
store_address, increment_address = changed[index : index + 2]
if increment_address not in (store_address + 16, store_address + 32):
raise RuntimeError(
f"patch changes are not a NativeDump store/increment pair at {store_address:#x}"
)
if increment_address == store_address + 32:
middle = store_address + 16
if before[middle] != after[middle]:
raise RuntimeError(f"interleaved instruction changed at {middle:#x}")
old_store = _without_predicate(before[store_address])
new_store = _without_predicate(after[store_address])
old_register = STORE_REGISTER.search(old_store)
new_register = GLOBAL_STORE_REGISTER.search(new_store)
if old_register is None or new_register is None:
raise RuntimeError(f"unexpected store rewrite at {store_address:#x}")
if old_register.group(1) != new_register.group(1):
raise RuntimeError(f"store source register changed at {store_address:#x}")
if not _without_predicate(before[increment_address]).startswith("PMTRIG "):
raise RuntimeError(f"expected PMTRIG placeholder at {increment_address:#x}")
if POINTER_INCREMENT.fullmatch(_without_predicate(after[increment_address])) is None:
raise RuntimeError(f"unexpected pointer increment at {increment_address:#x}")
position = address_index[store_address]
prefix = [after[address] for address in addresses[max(0, position - 12) : position]]
if not any("SR_GLOBALTIMERLO" in instruction for instruction in prefix):
raise RuntimeError(f"event site lacks GLOBALTIMERLO at {store_address:#x}")
if not any("LOP3.LUT" in instruction for instruction in prefix):
raise RuntimeError(f"event site lacks event-id encoding at {store_address:#x}")
sites.append(store_address)
placeholder_count = sum("PMTRIG " in instruction for instruction in before.values())
if placeholder_count != len(sites):
raise RuntimeError(
f"patched {len(sites)} of {placeholder_count} NativeDump placeholder sites"
)
if any("PMTRIG " in instruction for instruction in after.values()):
raise RuntimeError("patched kernel still contains PMTRIG placeholders")
return len(sites)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--run-dir", type=Path, required=True)
parser.add_argument("--kernel")
parser.add_argument("--nvdisasm", default=shutil.which("nvdisasm"))
parser.add_argument("--output-dir", type=Path, required=True)
args = parser.parse_args()
if args.nvdisasm is None:
parser.error("nvdisasm is required")
kernel, cubin, unpatched, patched, patch_offset, cubin_offset = _discover_artifacts(
args.run_dir, args.kernel
)
unpatched_bytes = unpatched.read_bytes()
patched_bytes = patched.read_bytes()
if len(unpatched_bytes) != len(patched_bytes) or unpatched_bytes == patched_bytes:
raise RuntimeError("patched and unpatched kernel images have invalid sizes or contents")
cubin_bytes = bytearray(cubin.read_bytes())
patched_code = patched_bytes[patch_offset:]
end = cubin_offset + len(patched_code)
if end > len(cubin_bytes):
raise RuntimeError("patched kernel image extends beyond the tracker CUBIN")
cubin_bytes[cubin_offset:end] = patched_code
args.output_dir.mkdir(parents=True, exist_ok=True)
patched_cubin = args.output_dir / "patched.cubin"
patched_cubin.write_bytes(cubin_bytes)
unpatched_sass = _disassemble(args.nvdisasm, cubin)
patched_sass = _disassemble(args.nvdisasm, patched_cubin)
(args.output_dir / "unpatched.sass").write_text(unpatched_sass, encoding="utf-8")
(args.output_dir / "patched.sass").write_text(patched_sass, encoding="utf-8")
site_count = _verify_patch(unpatched_sass, patched_sass, kernel)
report = {
"schema_version": 1,
"kernel": kernel,
"site_count": site_count,
"driver_patch_offset": patch_offset,
"cubin_code_offset": cubin_offset,
"normalized_signature": NORMALIZED_SIGNATURE,
"sha256": {
"tracker_cubin": _sha256(cubin),
"unpatched_kernel": _sha256(unpatched),
"patched_kernel": _sha256(patched),
"unpatched_disassembly": hashlib.sha256(unpatched_sass.encode()).hexdigest(),
"patched_disassembly": hashlib.sha256(patched_sass.encode()).hexdigest(),
},
}
report_path = args.output_dir / "verification.json"
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(report_path)
if __name__ == "__main__":
main()