From 1843da377239f84d4d2eb5dd9511373339b80fdc Mon Sep 17 00:00:00 2001 From: Jamie <2119834+jamieQ@users.noreply.github.com> Date: Mon, 9 Nov 2026 08:48:14 -0600 Subject: [PATCH 1/2] perf(apple): Release signature parser after executable validation Keep the Mach-O parser local to executable validation and pass it explicitly when checking the signature. This releases the native binary graph before resource validation and subsequent analysis. --- .../utils/apple/code_signature_validator.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/launchpad/utils/apple/code_signature_validator.py b/src/launchpad/utils/apple/code_signature_validator.py index c8a81923..72c92030 100644 --- a/src/launchpad/utils/apple/code_signature_validator.py +++ b/src/launchpad/utils/apple/code_signature_validator.py @@ -86,7 +86,6 @@ def __init__(self, archive: ZippedXCArchive) -> None: self.plist = self.archive.get_plist() self.executable_name: str = self.archive.get_plist().get("CFBundleExecutable", "") self.app_root: SafeDirectory = self.archive.get_app_bundle_path() - self.macho_parser: Optional[MachOParser] = None def validate(self) -> Tuple[bool, List[str]]: """Validate the code signature. @@ -126,11 +125,10 @@ def _validate_executable(self) -> BinaryCheckResult: if fat_binary is None: raise ValueError("Failed to parse binary") - self.macho_parser = MachOParser(fat_binary.at(0)) - if self.macho_parser.is_encrypted(): + macho_parser = MachOParser(fat_binary.at(0)) + if macho_parser.is_encrypted(): return BinaryCheckResult(valid=False) - - return self._check_binary() + return self._check_binary(macho_parser) def _validate_info_plist(self, binary_hashes: BinaryCheckResult) -> bool: """Validate the Info.plist.""" @@ -254,12 +252,9 @@ def _check_file( logger.debug(f"File {file_path} is not in the files2 array") errors.append(f"file added: {file_path}") - def _check_binary(self) -> BinaryCheckResult: + def _check_binary(self, macho_parser: MachOParser) -> BinaryCheckResult: """Check the binary.""" - if not self.macho_parser: - raise ValueError("MachO parser not initialized") - - code_signature = self.macho_parser.parse_code_signature() + code_signature = macho_parser.parse_code_signature() if not code_signature: logger.info("No code signature found") return BinaryCheckResult(valid=False, error_message="No code signature found") From e62e94e298dd898e4bb1deaf924def13befd51b3 Mon Sep 17 00:00:00 2001 From: Jamie <2119834+jamieQ@users.noreply.github.com> Date: Mon, 9 Nov 2026 08:48:24 -0600 Subject: [PATCH 2/2] perf(apple): Release dSYM parser before symbol grouping Extract Python-owned symbols in a helper so native parser references leave scope before grouping and demangling. Materialize the results rather than yielding to avoid retaining the parser in a suspended generator. --- src/launchpad/size/analyzers/apple.py | 86 +++++++++++++++------------ 1 file changed, 49 insertions(+), 37 deletions(-) diff --git a/src/launchpad/size/analyzers/apple.py b/src/launchpad/size/analyzers/apple.py index 02a1aeb3..35b7218b 100644 --- a/src/launchpad/size/analyzers/apple.py +++ b/src/launchpad/size/analyzers/apple.py @@ -42,7 +42,7 @@ from launchpad.size.insights.insight import InsightsInput from launchpad.size.models.common import APPLE_ANALYSIS_VERSION from launchpad.size.models.treemap import FlaggedInsight -from launchpad.size.symbols.macho_symbol_sizes import MachOSymbolSizes +from launchpad.size.symbols.macho_symbol_sizes import MachOSymbolSizes, SymbolSize from launchpad.size.treemap.treemap_builder import TreemapBuilder from launchpad.size.utils.apple_bundle_size import calculate_bundle_sizes from launchpad.size.utils.file_analysis import analyze_apple_files @@ -591,42 +591,11 @@ def _analyze_binary( dwarf_relocations = None if dwarf_binary_path: - dsym_config = lief.MachO.ParserConfig() - dsym_config.parse_dyld_exports = False - dsym_config.parse_dyld_bindings = False - dsym_config.parse_dyld_rebases = False - - with open(dwarf_binary_path, "rb") as f: - dwarf_fat_binary = lief.MachO.parse(f, dsym_config) # type: ignore - if dwarf_fat_binary: - # Build a map of architecture name -> dSYM slice index - dsym_arch_map: Dict[str, int] = {} - for i, dsym_slice in enumerate(dwarf_fat_binary): - dsym_arch_name = get_cpu_type_name(dsym_slice.header.cpu_type) - dsym_arch_map[dsym_arch_name] = i - - # Symbolicate each architecture slice - for arch_slice in architecture_slices: - if arch_slice.arch_name in dsym_arch_map: - dsym_index = dsym_arch_map[arch_slice.arch_name] - dsym_binary = dwarf_fat_binary.at(dsym_index) - slice_symbol_sizes = MachOSymbolSizes(dsym_binary).get_symbol_sizes() - arch_slice.symbol_info = SymbolInfo.from_symbol_sizes( - symbol_sizes=slice_symbol_sizes, - ) - logger.debug( - f"Symbolicated {arch_slice.arch_name} slice with {len(slice_symbol_sizes)} symbols" - ) - else: - logger.warning(f"No dSYM slice found for architecture {arch_slice.arch_name}") - - else: - logger.error( - "size.apple.skip_symbol_analysis.dwarf_binary_parse_failed", - extra={ - "binary_name": binary_info.name, - }, - ) + for arch_slice, slice_symbol_sizes in self._extract_dsym_symbols( + dwarf_binary_path, binary_info.name, architecture_slices + ): + arch_slice.symbol_info = SymbolInfo.from_symbol_sizes(symbol_sizes=slice_symbol_sizes) + logger.debug(f"Symbolicated {arch_slice.arch_name} slice with {len(slice_symbol_sizes)} symbols") if binary_info.relocations_path and os.getenv("LAUNCHPAD_ENV") == "development": with sentry_sdk.traces.start_span(name="dwarf_relocations.parse", attributes={"sentry.op": "parse"}): @@ -653,6 +622,49 @@ def _analyze_binary( strippable_symbols_size=strippable_symbols_size, ) + def _extract_dsym_symbols( + self, dwarf_binary_path: Path, binary_name: str, architecture_slices: list[ArchitectureSlice] + ) -> list[tuple[ArchitectureSlice, list[SymbolSize]]]: + """Extract symbols without retaining the native dSYM parser after return. + + Return a list rather than yielding so parser references leave scope + when this method returns. + """ + dsym_config = lief.MachO.ParserConfig() + dsym_config.parse_dyld_exports = False + dsym_config.parse_dyld_bindings = False + dsym_config.parse_dyld_rebases = False + + with open(dwarf_binary_path, "rb") as f: + dwarf_fat_binary = lief.MachO.parse(f, dsym_config) # type: ignore + symbols_by_arch: list[tuple[ArchitectureSlice, list[SymbolSize]]] = [] + if dwarf_fat_binary: + # Build a map of architecture name -> dSYM slice index + dsym_arch_map: Dict[str, int] = {} + for i, dsym_slice in enumerate(dwarf_fat_binary): + dsym_arch_name = get_cpu_type_name(dsym_slice.header.cpu_type) + dsym_arch_map[dsym_arch_name] = i + + # Extract symbols for each architecture slice + for arch_slice in architecture_slices: + if arch_slice.arch_name in dsym_arch_map: + dsym_index = dsym_arch_map[arch_slice.arch_name] + dsym_binary = dwarf_fat_binary.at(dsym_index) + slice_symbol_sizes = MachOSymbolSizes(dsym_binary).get_symbol_sizes() + symbols_by_arch.append((arch_slice, slice_symbol_sizes)) + else: + logger.warning(f"No dSYM slice found for architecture {arch_slice.arch_name}") + + else: + logger.error( + "size.apple.skip_symbol_analysis.dwarf_binary_parse_failed", + extra={ + "binary_name": binary_name, + }, + ) + + return symbols_by_arch + def _extract_binary_metadata(self, binary_path: Path) -> _BinaryMetadata: with open(binary_path, "rb") as f: config = lief.MachO.ParserConfig()