diff --git a/resources/sys_patch/sys_patch.py b/resources/sys_patch/sys_patch.py index 584849375..5b2036e13 100644 --- a/resources/sys_patch/sys_patch.py +++ b/resources/sys_patch/sys_patch.py @@ -340,7 +340,7 @@ class PatchSysVolume: self._remove_file("/private/var/db/SystemPolicyConfiguration/", file) else: # Install RSRHelper utility to handle desynced KCs - sys_patch_helpers.sys_patch_helpers(self.constants).install_rsr_repair_binary() + sys_patch_helpers.SysPatchHelpers(self.constants).install_rsr_repair_binary() logging.info("- Successfully built new kernel cache") return True @@ -445,7 +445,7 @@ class PatchSysVolume: destination_path = f"{self.mount_location}/System/Library/CoreServices" file_name = "OpenCore-Legacy-Patcher.plist" destination_path_file = f"{destination_path}/{file_name}" - if sys_patch_helpers.sys_patch_helpers(self.constants).generate_patchset_plist(patchset, file_name, self.kdk_path): + if sys_patch_helpers.SysPatchHelpers(self.constants).generate_patchset_plist(patchset, file_name, self.kdk_path): logging.info("- Writing patchset information to Root Volume") if Path(destination_path_file).exists(): utilities.process_status(utilities.elevated(["rm", destination_path_file], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)) @@ -576,9 +576,9 @@ class PatchSysVolume: logging.info(f"- Running Process:\n{process}") utilities.process_status(subprocess.run(process, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)) if any(x in required_patches for x in ["AMD Legacy GCN", "AMD Legacy Polaris", "AMD Legacy Vega"]): - sys_patch_helpers.sys_patch_helpers(self.constants).disable_window_server_caching() + sys_patch_helpers.SysPatchHelpers(self.constants).disable_window_server_caching() if any(x in required_patches for x in ["Intel Ivy Bridge", "Intel Haswell"]): - sys_patch_helpers.sys_patch_helpers(self.constants).remove_news_widgets() + sys_patch_helpers.SysPatchHelpers(self.constants).remove_news_widgets() self._write_patchset(required_patches) def _preflight_checks(self, required_patches, source_files_path): @@ -593,7 +593,7 @@ class PatchSysVolume: # Make sure SNB kexts are compatible with the host if "Intel Sandy Bridge" in required_patches: - sys_patch_helpers.sys_patch_helpers(self.constants).snb_board_id_patch(source_files_path) + sys_patch_helpers.SysPatchHelpers(self.constants).snb_board_id_patch(source_files_path) for patch in required_patches: # Check if all files are present diff --git a/resources/sys_patch/sys_patch_auto.py b/resources/sys_patch/sys_patch_auto.py index 364354b4b..e71d235cc 100644 --- a/resources/sys_patch/sys_patch_auto.py +++ b/resources/sys_patch/sys_patch_auto.py @@ -1,29 +1,42 @@ -# Auto Patching's main purpose is to try and tell the user they're missing root patches -# New users may not realize OS updates remove our patches, so we try and run when nessasary -# Conditions for running: -# - Verify running GUI (TUI users can write their own scripts) -# - Verify the Snapshot Seal is intact (if not, assume user is running patches) -# - Verify this model needs patching (if not, assume user upgraded hardware and OCLP was not removed) -# - Verify there are no updates for OCLP (ensure we have the latest patch sets) -# If all these tests pass, start Root Patcher # Copyright (C) 2022, Mykola Grymalyuk -from pathlib import Path import plistlib import subprocess import webbrowser import logging +from pathlib import Path + from resources import utilities, updates, global_settings, network_handler, constants from resources.sys_patch import sys_patch_detect from resources.gui import gui_main + class AutomaticSysPatch: + """ + Library of functions for launch agent, including automatic patching + """ def __init__(self, global_constants: constants.Constants): self.constants: constants.Constants = global_constants def start_auto_patch(self): + """ + Initiates automatic patching + + Auto Patching's main purpose is to try and tell the user they're missing root patches + New users may not realize OS updates remove our patches, so we try and run when nessasary + + Conditions for running: + - Verify running GUI (TUI users can write their own scripts) + - Verify the Snapshot Seal is intact (if not, assume user is running patches) + - Verify this model needs patching (if not, assume user upgraded hardware and OCLP was not removed) + - Verify there are no updates for OCLP (ensure we have the latest patch sets) + + If all these tests pass, start Root Patcher + + """ + logging.info("- Starting Automatic Patching") if self.constants.wxpython_variant is False: logging.info("- Auto Patch option is not supported on TUI, please use GUI") @@ -113,26 +126,35 @@ class AutomaticSysPatch: else: logging.info("- Detected Snapshot seal not intact, skipping") - if self.determine_if_versions_match() is False: - self.determine_if_boot_matches() + if self._determine_if_versions_match(): + self._determine_if_boot_matches() - def determine_if_versions_match(self): + def _determine_if_versions_match(self): + """ + Determine if the booted version of OCLP matches the installed version + + ie. Installed app is 0.2.0, but EFI version is 0.1.0 + + Returns: + bool: True if versions match, False if not + """ + logging.info("- Checking booted vs installed OCLP Build") if self.constants.computer.oclp_version is None: logging.info("- Booted version not found") - return False + return True if self.constants.computer.oclp_version == self.constants.patcher_version: logging.info("- Versions match") - return False + return True # Check if installed version is newer than booted version if updates.CheckBinaryUpdates(self.constants)._check_if_build_newer( self.constants.computer.oclp_version.split("."), self.constants.patcher_version.split(".") ) is True: logging.info("- Installed version is newer than booted version") - return False + return True args = [ "osascript", @@ -150,17 +172,24 @@ class AutomaticSysPatch: self.constants.start_build_install = True gui_main.wx_python_gui(self.constants).main_menu(None) - return True + return False - def determine_if_boot_matches(self): - # Goal of this function is to determine whether the user - # is using a USB drive to Boot OpenCore but macOS does not - # reside on the same drive as the USB. - # If we determine them to be mismatched, notify the user - # and ask if they want to install to install to disk + def _determine_if_boot_matches(self): + """ + Determine if the boot drive matches the macOS drive + ie. Booted from USB, but macOS is on internal disk + + Goal of this function is to determine whether the user + is using a USB drive to Boot OpenCore but macOS does not + reside on the same drive as the USB. + + If we determine them to be mismatched, notify the user + and ask if they want to install to install to disk. + """ logging.info("- Determining if macOS drive matches boot drive") + should_notify = global_settings.GlobalEnviromentSettings().read_property("AutoPatch_Notify_Mismatched_Disks") if should_notify is False: logging.info("- Skipping due to user preference") @@ -223,9 +252,16 @@ class AutomaticSysPatch: def install_auto_patcher_launch_agent(self): - # Installs the following: - # - OpenCore-Patcher.app in /Library/Application Support/Dortania/ - # - com.dortania.opencore-legacy-patcher.auto-patch.plist in /Library/LaunchAgents/ + """ + Install the Auto Patcher Launch Agent + + Installs the following: + - OpenCore-Patcher.app in /Library/Application Support/Dortania/ + - com.dortania.opencore-legacy-patcher.auto-patch.plist in /Library/LaunchAgents/ + + See start_auto_patch() comments for more info + """ + if self.constants.launcher_script is not None: logging.info("- Skipping Auto Patcher Launch Agent, not supported when running from source") return diff --git a/resources/sys_patch/sys_patch_helpers.py b/resources/sys_patch/sys_patch_helpers.py index a256d54a4..ad4e4ef2c 100644 --- a/resources/sys_patch/sys_patch_helpers.py +++ b/resources/sys_patch/sys_patch_helpers.py @@ -12,46 +12,75 @@ from data import os_data from resources import bplist, constants, generate_smbios, utilities -class sys_patch_helpers: +class SysPatchHelpers: + """ + Library of helper functions for sys_patch.py and related libraries + """ def __init__(self, global_constants: constants.Constants): self.constants: constants.Constants = global_constants - def snb_board_id_patch(self, source_files_path): - # AppleIntelSNBGraphicsFB hard codes the supported Board IDs for Sandy Bridge iGPUs - # Because of this, the kext errors out on unsupported systems - # This function simply patches in a supported Board ID, using 'determine_best_board_id_for_sandy()' - # to supplement the ideal Board ID + def snb_board_id_patch(self, source_files_path: str): + """ + Patch AppleIntelSNBGraphicsFB.kext to support unsupported Board IDs + + AppleIntelSNBGraphicsFB hard codes the supported Board IDs for Sandy Bridge iGPUs + Because of this, the kext errors out on unsupported systems + This function simply patches in a supported Board ID, using 'determine_best_board_id_for_sandy()' + to supplement the ideal Board ID + + Parameters: + source_files_path (str): Path to the source files + + """ + source_files_path = str(source_files_path) - if self.constants.computer.reported_board_id not in self.constants.sandy_board_id_stock: - logging.info(f"- Found unsupported Board ID {self.constants.computer.reported_board_id}, performing AppleIntelSNBGraphicsFB bin patching") - board_to_patch = generate_smbios.determine_best_board_id_for_sandy(self.constants.computer.reported_board_id, self.constants.computer.gpus) - logging.info(f"- Replacing {board_to_patch} with {self.constants.computer.reported_board_id}") - board_to_patch_hex = bytes.fromhex(board_to_patch.encode('utf-8').hex()) - reported_board_hex = bytes.fromhex(self.constants.computer.reported_board_id.encode('utf-8').hex()) + if self.constants.computer.reported_board_id in self.constants.sandy_board_id_stock: + return - if len(board_to_patch_hex) > len(reported_board_hex): - # Pad the reported Board ID with zeros to match the length of the board to patch - reported_board_hex = reported_board_hex + bytes(len(board_to_patch_hex) - len(reported_board_hex)) - elif len(board_to_patch_hex) < len(reported_board_hex): - logging.info(f"- Error: Board ID {self.constants.computer.reported_board_id} is longer than {board_to_patch}") - raise Exception("Host's Board ID is longer than the kext's Board ID, cannot patch!!!") + logging.info(f"- Found unsupported Board ID {self.constants.computer.reported_board_id}, performing AppleIntelSNBGraphicsFB bin patching") - path = source_files_path + "/10.13.6/System/Library/Extensions/AppleIntelSNBGraphicsFB.kext/Contents/MacOS/AppleIntelSNBGraphicsFB" - if Path(path).exists(): - with open(path, 'rb') as f: - data = f.read() - data = data.replace(board_to_patch_hex, reported_board_hex) - with open(path, 'wb') as f: - f.write(data) - else: - logging.info(f"- Error: Could not find {path}") - raise Exception("Failed to find AppleIntelSNBGraphicsFB.kext, cannot patch!!!") + board_to_patch = generate_smbios.determine_best_board_id_for_sandy(self.constants.computer.reported_board_id, self.constants.computer.gpus) + logging.info(f"- Replacing {board_to_patch} with {self.constants.computer.reported_board_id}") + + board_to_patch_hex = bytes.fromhex(board_to_patch.encode('utf-8').hex()) + reported_board_hex = bytes.fromhex(self.constants.computer.reported_board_id.encode('utf-8').hex()) + + if len(board_to_patch_hex) > len(reported_board_hex): + # Pad the reported Board ID with zeros to match the length of the board to patch + reported_board_hex = reported_board_hex + bytes(len(board_to_patch_hex) - len(reported_board_hex)) + elif len(board_to_patch_hex) < len(reported_board_hex): + logging.info(f"- Error: Board ID {self.constants.computer.reported_board_id} is longer than {board_to_patch}") + raise Exception("Host's Board ID is longer than the kext's Board ID, cannot patch!!!") + + path = source_files_path + "/10.13.6/System/Library/Extensions/AppleIntelSNBGraphicsFB.kext/Contents/MacOS/AppleIntelSNBGraphicsFB" + if not Path(path).exists(): + logging.info(f"- Error: Could not find {path}") + raise Exception("Failed to find AppleIntelSNBGraphicsFB.kext, cannot patch!!!") + + with open(path, 'rb') as f: + data = f.read() + data = data.replace(board_to_patch_hex, reported_board_hex) + with open(path, 'wb') as f: + f.write(data) - def generate_patchset_plist(self, patchset, file_name, kdk_used): + def generate_patchset_plist(self, patchset: dict, file_name: str, kdk_used: Path): + """ + Generate patchset file for user reference + + Parameters: + patchset (dict): Dictionary of patchset, see sys_patch_detect.py and sys_patch_dict.py + file_name (str): Name of the file to write to + kdk_used (Path): Path to the KDK used, if any + + Returns: + bool: True if successful, False if not + + """ + source_path = f"{self.constants.payload_path}" source_path_file = f"{source_path}/{file_name}" @@ -67,23 +96,35 @@ class sys_patch_helpers: "Kernel Debug Kit Used": f"{kdk_string}", "OS Version": f"{self.constants.detected_os}.{self.constants.detected_os_minor} ({self.constants.detected_os_build})", } + data.update(patchset) + if Path(source_path_file).exists(): os.remove(source_path_file) + # Need to write to a safe location plistlib.dump(data, Path(source_path_file).open("wb"), sort_keys=False) + if Path(source_path_file).exists(): return True + return False def disable_window_server_caching(self): - # On legacy GCN GPUs, the WindowServer cache generated creates - # corrupted Opaque shaders. - # To work-around this, we disable WindowServer caching - # And force macOS into properly generating the Opaque shaders + """ + Disable WindowServer's asset caching + + On legacy GCN GPUs, the WindowServer cache generated creates + corrupted Opaque shaders. + + To work-around this, we disable WindowServer caching + And force macOS into properly generating the Opaque shaders + """ + if self.constants.detected_os < os_data.os_data.ventura: return + logging.info("- Disabling WindowServer Caching") # Invoke via 'bash -c' to resolve pathing utilities.elevated(["bash", "-c", "rm -rf /private/var/folders/*/*/*/WindowServer/com.apple.WindowServer"]) @@ -95,11 +136,17 @@ class sys_patch_helpers: def remove_news_widgets(self): - # On Ivy Bridge and Haswell iGPUs, RenderBox will crash the News Widgets in - # Notification Centre. To ensure users can access Notifications normally, - # we manually remove all News Widgets + """ + Remove News Widgets from Notification Centre + + On Ivy Bridge and Haswell iGPUs, RenderBox will crash the News Widgets in + Notification Centre. To ensure users can access Notifications normally, + we manually remove all News Widgets + """ + if self.constants.detected_os < os_data.os_data.ventura: return + logging.info("- Parsing Notification Centre Widgets") file_path = "~/Library/Containers/com.apple.notificationcenterui/Data/Library/Preferences/com.apple.notificationcenterui.plist" file_path = Path(file_path).expanduser() @@ -111,22 +158,27 @@ class sys_patch_helpers: did_find = False with open(file_path, "rb") as f: data = plistlib.load(f) - if "widgets" in data: - if "instances" in data["widgets"]: - for widget in list(data["widgets"]["instances"]): - widget_data = bplist.BPListReader(widget).parse() - for entry in widget_data: - if not 'widget' in entry: - continue - sub_data = bplist.BPListReader(widget_data[entry]).parse() - for sub_entry in sub_data: - if not '$object' in sub_entry: - continue - if not b'com.apple.news' in sub_data[sub_entry][2]: - continue - logging.info(f" - Found News Widget to remove: {sub_data[sub_entry][2].decode('ascii')}") - data["widgets"]["instances"].remove(widget) - did_find = True + if "widgets" not in data: + return + + if "instances" not in data["widgets"]: + return + + for widget in list(data["widgets"]["instances"]): + widget_data = bplist.BPListReader(widget).parse() + for entry in widget_data: + if 'widget' not in entry: + continue + sub_data = bplist.BPListReader(widget_data[entry]).parse() + for sub_entry in sub_data: + if not '$object' in sub_entry: + continue + if not b'com.apple.news' in sub_data[sub_entry][2]: + continue + logging.info(f" - Found News Widget to remove: {sub_data[sub_entry][2].decode('ascii')}") + data["widgets"]["instances"].remove(widget) + did_find = True + if did_find: with open(file_path, "wb") as f: plistlib.dump(data, f, sort_keys=False) @@ -134,16 +186,23 @@ class sys_patch_helpers: def install_rsr_repair_binary(self): - # With macOS 13.2, Apple implemented the Rapid Security Response System - # However Apple added a half baked snapshot reversion system if seal was broken, - # which forgets to handle Preboot BootKC syncing + """ + Installs RSRRepair - # Thus this application will try to re-sync the BootKC with SysKC in the event of a panic - # Reference: https://github.com/dortania/OpenCore-Legacy-Patcher/issues/1019 + RSRRepair is a utility that will sync the SysKC and BootKC in the event of a panic - # This is a (hopefully) temporary work-around, however likely to stay. - # RSRRepair has the added bonus of fixing desynced KCs from 'bless', so useful in Big Sur+ - # https://github.com/flagersgit/RSRRepair + With macOS 13.2, Apple implemented the Rapid Security Response System + However Apple added a half baked snapshot reversion system if seal was broken, + which forgets to handle Preboot BootKC syncing. + + Thus this application will try to re-sync the BootKC with SysKC in the event of a panic + Reference: https://github.com/dortania/OpenCore-Legacy-Patcher/issues/1019 + + This is a (hopefully) temporary work-around, however likely to stay. + RSRRepair has the added bonus of fixing desynced KCs from 'bless', so useful in Big Sur+ + Source: https://github.com/flagersgit/RSRRepair + + """ if self.constants.detected_os < os_data.os_data.big_sur: return diff --git a/resources/validation.py b/resources/validation.py index 337152c13..709117972 100644 --- a/resources/validation.py +++ b/resources/validation.py @@ -117,7 +117,7 @@ class PatcherValidation: raise Exception(f"Failed to find {source_file}") logging.info(f"- Validating against Darwin {major_kernel}.{minor_kernel}") - if not sys_patch_helpers.sys_patch_helpers(self.constants).generate_patchset_plist(patchset, f"OpenCore-Legacy-Patcher-{major_kernel}.{minor_kernel}.plist", None): + if not sys_patch_helpers.SysPatchHelpers(self.constants).generate_patchset_plist(patchset, f"OpenCore-Legacy-Patcher-{major_kernel}.{minor_kernel}.plist", None): raise Exception("Failed to generate patchset plist") # Remove the plist file after validation @@ -146,7 +146,7 @@ class PatcherValidation: self._validate_root_patch_files(supported_os, i) logging.info("Validating SNB Board ID patcher") self.constants.computer.reported_board_id = "Mac-7BA5B2DFE22DDD8C" - sys_patch_helpers.sys_patch_helpers(self.constants).snb_board_id_patch(self.constants.payload_local_binaries_root_path) + sys_patch_helpers.SysPatchHelpers(self.constants).snb_board_id_patch(self.constants.payload_local_binaries_root_path) # Clean up subprocess.run(