diff --git a/opencore_legacy_patcher/support/arguments.py b/opencore_legacy_patcher/support/arguments.py index 809d4528d..40e2fef01 100644 --- a/opencore_legacy_patcher/support/arguments.py +++ b/opencore_legacy_patcher/support/arguments.py @@ -17,15 +17,14 @@ from .. import constants from ..wx_gui import gui_entry from ..efi_builder import build +from ..sys_patch import sys_patch +from ..sys_patch.auto_patcher import StartAutomaticPatching from ..datasets import ( model_array, os_data ) -from ..sys_patch import ( - sys_patch, - sys_patch_auto -) + from . import ( utilities, defaults, @@ -118,7 +117,7 @@ class arguments: """ logging.info("Set Auto patching") - sys_patch_auto.AutomaticSysPatch(self.constants).start_auto_patch() + StartAutomaticPatching(self.constants).start_auto_patch() def _prepare_for_update_handler(self) -> None: diff --git a/opencore_legacy_patcher/sys_patch/auto_patcher/__init__.py b/opencore_legacy_patcher/sys_patch/auto_patcher/__init__.py new file mode 100644 index 000000000..31da484bd --- /dev/null +++ b/opencore_legacy_patcher/sys_patch/auto_patcher/__init__.py @@ -0,0 +1,17 @@ +""" +auto_patcher: Automatic system volume patching after updates, etc. + +Usage: + +>>> # Installing launch services +>>> from auto_patcher import InstallAutomaticPatchingServices +>>> InstallAutomaticPatchingServices(self.constants).install_auto_patcher_launch_agent() + + +>>> # When patching the system volume (ex. launch service) +>>> from auto_patcher import StartAutomaticPatching +>>> StartAutomaticPatching(self.constants).start_auto_patch() +""" + +from .install import InstallAutomaticPatchingServices +from .start import StartAutomaticPatching \ No newline at end of file diff --git a/opencore_legacy_patcher/sys_patch/auto_patcher/install.py b/opencore_legacy_patcher/sys_patch/auto_patcher/install.py new file mode 100644 index 000000000..879e4caeb --- /dev/null +++ b/opencore_legacy_patcher/sys_patch/auto_patcher/install.py @@ -0,0 +1,116 @@ +""" +install.py: Install the auto patcher launch services +""" + +import hashlib +import logging +import plistlib +import subprocess + +from pathlib import Path + +from ... import constants + +from ...volume import generate_copy_arguments + +from ...support import ( + utilities, + subprocess_wrapper +) + + +class InstallAutomaticPatchingServices: + """ + Install the auto patcher launch services + """ + + def __init__(self, global_constants: constants.Constants): + self.constants: constants.Constants = global_constants + + + def install_auto_patcher_launch_agent(self, kdk_caching_needed: bool = False): + """ + Install patcher launch services + + 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 + + services = { + self.constants.auto_patch_launch_agent_path: "/Library/LaunchAgents/com.dortania.opencore-legacy-patcher.auto-patch.plist", + self.constants.update_launch_daemon_path: "/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.macos-update.plist", + **({ self.constants.rsr_monitor_launch_daemon_path: "/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.rsr-monitor.plist" } if self._create_rsr_monitor_daemon() else {}), + **({ self.constants.kdk_launch_daemon_path: "/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.os-caching.plist" } if kdk_caching_needed is True else {} ), + } + + for service in services: + name = Path(service).name + logging.info(f"- Installing {name}") + if Path(services[service]).exists(): + if hashlib.sha256(open(service, "rb").read()).hexdigest() == hashlib.sha256(open(services[service], "rb").read()).hexdigest(): + logging.info(f" - {name} checksums match, skipping") + continue + logging.info(f" - Existing service found, removing") + subprocess_wrapper.run_as_root_and_verify(["/bin/rm", services[service]], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + # Create parent directories + if not Path(services[service]).parent.exists(): + logging.info(f" - Creating {Path(services[service]).parent} directory") + subprocess_wrapper.run_as_root_and_verify(["/bin/mkdir", "-p", Path(services[service]).parent], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + subprocess_wrapper.run_as_root_and_verify(generate_copy_arguments(service, services[service]), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + # Set the permissions on the service + subprocess_wrapper.run_as_root_and_verify(["/bin/chmod", "644", services[service]], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + subprocess_wrapper.run_as_root_and_verify(["/usr/sbin/chown", "root:wheel", services[service]], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + + def _create_rsr_monitor_daemon(self) -> bool: + # Get kext list in /Library/Extensions that have the 'GPUCompanionBundles' property + # This is used to determine if we need to run the RSRMonitor + logging.info("- Checking if RSRMonitor is needed") + + cryptex_path = f"/System/Volumes/Preboot/{utilities.get_preboot_uuid()}/cryptex1/current/OS.dmg" + if not Path(cryptex_path).exists(): + logging.info("- No OS.dmg, skipping RSRMonitor") + return False + + kexts = [] + for kext in Path("/Library/Extensions").glob("*.kext"): + if not Path(f"{kext}/Contents/Info.plist").exists(): + continue + try: + kext_plist = plistlib.load(open(f"{kext}/Contents/Info.plist", "rb")) + except Exception as e: + logging.info(f" - Failed to load plist for {kext.name}: {e}") + continue + if "GPUCompanionBundles" not in kext_plist: + continue + logging.info(f" - Found kext with GPUCompanionBundles: {kext.name}") + kexts.append(kext.name) + + # If we have no kexts, we don't need to run the RSRMonitor + if not kexts: + logging.info("- No kexts found with GPUCompanionBundles, skipping RSRMonitor") + return False + + # Load the RSRMonitor plist + rsr_monitor_plist = plistlib.load(open(self.constants.rsr_monitor_launch_daemon_path, "rb")) + + arguments = ["/bin/rm", "-Rfv"] + arguments += [f"/Library/Extensions/{kext}" for kext in kexts] + + # Add the arguments to the RSRMonitor plist + rsr_monitor_plist["ProgramArguments"] = arguments + + # Next add monitoring for '/System/Volumes/Preboot/{UUID}/cryptex1/OS.dmg' + logging.info(f" - Adding monitor: {cryptex_path}") + rsr_monitor_plist["WatchPaths"] = [ + cryptex_path, + ] + + # Write the RSRMonitor plist + plistlib.dump(rsr_monitor_plist, Path(self.constants.rsr_monitor_launch_daemon_path).open("wb")) + + return True diff --git a/opencore_legacy_patcher/sys_patch/sys_patch_auto.py b/opencore_legacy_patcher/sys_patch/auto_patcher/start.py similarity index 72% rename from opencore_legacy_patcher/sys_patch/sys_patch_auto.py rename to opencore_legacy_patcher/sys_patch/auto_patcher/start.py index 1141ad448..bd2469c2a 100644 --- a/opencore_legacy_patcher/sys_patch/sys_patch_auto.py +++ b/opencore_legacy_patcher/sys_patch/auto_patcher/start.py @@ -1,11 +1,10 @@ """ -sys_patch_auto.py: Library of functions for launch services, including automatic patching +start.py: Start automatic patching of host """ import wx import wx.html2 -import hashlib import logging import plistlib import requests @@ -13,31 +12,27 @@ import markdown2 import subprocess import webbrowser -from pathlib import Path +from .. import sys_patch_detect -from . import sys_patch_detect +from ... import constants -from .. import constants +from ...datasets import css_data -from ..datasets import css_data -from ..volume import generate_copy_arguments - -from ..wx_gui import ( +from ...wx_gui import ( gui_entry, gui_support ) -from ..support import ( +from ...support import ( utilities, updates, global_settings, network_handler, - subprocess_wrapper ) -class AutomaticSysPatch: +class StartAutomaticPatching: """ - Library of functions for launch agent, including automatic patching + Start automatic patching of host """ def __init__(self, global_constants: constants.Constants): @@ -317,92 +312,4 @@ Please check the Github page for more information about this release.""" gui_entry.EntryPoint(self.constants).start(entry=gui_entry.SupportedEntryPoints.BUILD_OC) except KeyError: - logging.info("- Unable to determine if boot disk is removable, skipping prompt") - - - def install_auto_patcher_launch_agent(self, kdk_caching_needed: bool = False): - """ - Install patcher launch services - - 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 - - services = { - self.constants.auto_patch_launch_agent_path: "/Library/LaunchAgents/com.dortania.opencore-legacy-patcher.auto-patch.plist", - self.constants.update_launch_daemon_path: "/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.macos-update.plist", - **({ self.constants.rsr_monitor_launch_daemon_path: "/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.rsr-monitor.plist" } if self._create_rsr_monitor_daemon() else {}), - **({ self.constants.kdk_launch_daemon_path: "/Library/LaunchDaemons/com.dortania.opencore-legacy-patcher.os-caching.plist" } if kdk_caching_needed is True else {} ), - } - - for service in services: - name = Path(service).name - logging.info(f"- Installing {name}") - if Path(services[service]).exists(): - if hashlib.sha256(open(service, "rb").read()).hexdigest() == hashlib.sha256(open(services[service], "rb").read()).hexdigest(): - logging.info(f" - {name} checksums match, skipping") - continue - logging.info(f" - Existing service found, removing") - subprocess_wrapper.run_as_root_and_verify(["/bin/rm", services[service]], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - # Create parent directories - if not Path(services[service]).parent.exists(): - logging.info(f" - Creating {Path(services[service]).parent} directory") - subprocess_wrapper.run_as_root_and_verify(["/bin/mkdir", "-p", Path(services[service]).parent], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - subprocess_wrapper.run_as_root_and_verify(generate_copy_arguments(service, services[service]), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - - # Set the permissions on the service - subprocess_wrapper.run_as_root_and_verify(["/bin/chmod", "644", services[service]], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - subprocess_wrapper.run_as_root_and_verify(["/usr/sbin/chown", "root:wheel", services[service]], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - - - def _create_rsr_monitor_daemon(self) -> bool: - # Get kext list in /Library/Extensions that have the 'GPUCompanionBundles' property - # This is used to determine if we need to run the RSRMonitor - logging.info("- Checking if RSRMonitor is needed") - - cryptex_path = f"/System/Volumes/Preboot/{utilities.get_preboot_uuid()}/cryptex1/current/OS.dmg" - if not Path(cryptex_path).exists(): - logging.info("- No OS.dmg, skipping RSRMonitor") - return False - - kexts = [] - for kext in Path("/Library/Extensions").glob("*.kext"): - if not Path(f"{kext}/Contents/Info.plist").exists(): - continue - try: - kext_plist = plistlib.load(open(f"{kext}/Contents/Info.plist", "rb")) - except Exception as e: - logging.info(f" - Failed to load plist for {kext.name}: {e}") - continue - if "GPUCompanionBundles" not in kext_plist: - continue - logging.info(f" - Found kext with GPUCompanionBundles: {kext.name}") - kexts.append(kext.name) - - # If we have no kexts, we don't need to run the RSRMonitor - if not kexts: - logging.info("- No kexts found with GPUCompanionBundles, skipping RSRMonitor") - return False - - # Load the RSRMonitor plist - rsr_monitor_plist = plistlib.load(open(self.constants.rsr_monitor_launch_daemon_path, "rb")) - - arguments = ["/bin/rm", "-Rfv"] - arguments += [f"/Library/Extensions/{kext}" for kext in kexts] - - # Add the arguments to the RSRMonitor plist - rsr_monitor_plist["ProgramArguments"] = arguments - - # Next add monitoring for '/System/Volumes/Preboot/{UUID}/cryptex1/OS.dmg' - logging.info(f" - Adding monitor: {cryptex_path}") - rsr_monitor_plist["WatchPaths"] = [ - cryptex_path, - ] - - # Write the RSRMonitor plist - plistlib.dump(rsr_monitor_plist, Path(self.constants.rsr_monitor_launch_daemon_path).open("wb")) - - return True + logging.info("- Unable to determine if boot disk is removable, skipping prompt") \ No newline at end of file diff --git a/opencore_legacy_patcher/sys_patch/kernelcache/__init__.py b/opencore_legacy_patcher/sys_patch/kernelcache/__init__.py new file mode 100644 index 000000000..495ee58b1 --- /dev/null +++ b/opencore_legacy_patcher/sys_patch/kernelcache/__init__.py @@ -0,0 +1,11 @@ +""" +kernelcache: Library for rebuilding macOS kernelcache files. + +Usage: + +>>> from kernelcache import RebuildKernelCache +>>> RebuildKernelCache(os_version, mount_location, auxiliary_cache, auxiliary_cache_only).rebuild() +""" + +from .rebuild import RebuildKernelCache +from .kernel_collection.support import KernelCacheSupport \ No newline at end of file diff --git a/opencore_legacy_patcher/sys_patch/kernelcache/base/cache.py b/opencore_legacy_patcher/sys_patch/kernelcache/base/cache.py new file mode 100644 index 000000000..b2bdc4aca --- /dev/null +++ b/opencore_legacy_patcher/sys_patch/kernelcache/base/cache.py @@ -0,0 +1,8 @@ +""" +cache.py: Base class for kernel cache management +""" + +class BaseKernelCache: + + def rebuild(self) -> None: + raise NotImplementedError("To be implemented in subclass") \ No newline at end of file diff --git a/opencore_legacy_patcher/sys_patch/kernelcache/kernel_collection/auxiliary.py b/opencore_legacy_patcher/sys_patch/kernelcache/kernel_collection/auxiliary.py new file mode 100644 index 000000000..61ad64917 --- /dev/null +++ b/opencore_legacy_patcher/sys_patch/kernelcache/kernel_collection/auxiliary.py @@ -0,0 +1,72 @@ +""" +auxiliary.py: Auxiliary Kernel Collection management +""" + +import logging +import subprocess + +from ..base.cache import BaseKernelCache +from ....support import subprocess_wrapper + + +class AuxiliaryKernelCollection(BaseKernelCache): + + def __init__(self, mount_location: str) -> None: + self.mount_location = mount_location + + + def _kmutil_arguments(self) -> list[str]: + args = ["/usr/bin/kmutil", "create", "--allow-missing-kdk"] + + args.append("--new") + args.append("aux") + + args.append("--boot-path") + args.append(f"{self.mount_location}/System/Library/KernelCollections/BootKernelExtensions.kc") + + args.append("--system-path") + args.append(f"{self.mount_location}/System/Library/KernelCollections/SystemKernelExtensions.kc") + + return args + + + def _force_auxiliary_usage(self) -> bool: + """ + Force the auxiliary kernel collection to be used. + + This is required as Apple doesn't offer a public way + to rebuild the auxiliary kernel collection. Instead deleting + necessary files and directories will force the newly built + collection to be used. + """ + + print("- Forcing Auxiliary Kernel Collection usage") + result = subprocess_wrapper.run_as_root(["/usr/bin/killall", "syspolicyd", "kernelmanagerd"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + if result.returncode != 0: + logging.info("- Unable to kill syspolicyd and kernelmanagerd") + subprocess_wrapper.log(result) + return False + + for file in ["KextPolicy", "KextPolicy-shm", "KextPolicy-wal"]: + result = subprocess_wrapper.run_as_root(["/bin/rm", f"/private/var/db/SystemPolicyConfiguration/{file}"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + if result.returncode != 0: + logging.info(f"- Unable to remove {file}") + subprocess_wrapper.log(result) + return False + + return True + + + def rebuild(self) -> None: + logging.info("- Building new Auxiliary Kernel Collection") + result = subprocess_wrapper.run_as_root(self._kmutil_arguments(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + if result.returncode != 0: + logging.info("- Unable to build Auxiliary Kernel Collection") + subprocess_wrapper.log(result) + return False + + if self._force_auxiliary_usage() is False: + return False + + return True + diff --git a/opencore_legacy_patcher/sys_patch/kernelcache/kernel_collection/boot_system.py b/opencore_legacy_patcher/sys_patch/kernelcache/kernel_collection/boot_system.py new file mode 100644 index 000000000..b40172d68 --- /dev/null +++ b/opencore_legacy_patcher/sys_patch/kernelcache/kernel_collection/boot_system.py @@ -0,0 +1,62 @@ +""" +boot_system.py: Boot and System Kernel Collection management +""" + +import logging +import subprocess + +from ..base.cache import BaseKernelCache +from ....support import subprocess_wrapper +from ....datasets import os_data + + +class BootSystemKernelCollections(BaseKernelCache): + + def __init__(self, mount_location: str, detected_os: int, auxiliary_kc: bool) -> None: + self.mount_location = mount_location + self.detected_os = detected_os + self.auxiliary_kc = auxiliary_kc + + + def _kmutil_arguments(self) -> list[str]: + """ + Generate kmutil arguments for creating or updating + the boot, system and auxiliary kernel collections + """ + + args = ["/usr/bin/kmutil"] + + if self.detected_os >= os_data.os_data.ventura: + args.append("create") + args.append("--allow-missing-kdk") + else: + args.append("install") + + args.append("--volume-root") + args.append(self.mount_location) + + args.append("--update-all") + + args.append("--variant-suffix") + args.append("release") + + if self.auxiliary_kc is True: + # Following arguments are supposed to skip kext consent + # prompts when creating auxiliary KCs with SIP disabled + args.append("--no-authentication") + args.append("--no-authorization") + + return args + + + def rebuild(self) -> bool: + logging.info(f"- Rebuilding {'Boot and System' if self.auxiliary_kc is False else 'Boot, System and Auxiliary'} Kernel Collections") + if self.auxiliary_kc is True: + logging.info(" (You will get a prompt by System Preferences, ignore for now)") + + result = subprocess_wrapper.run_as_root(self._kmutil_arguments(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + if result.returncode != 0: + subprocess_wrapper.log(result) + return False + + return True diff --git a/opencore_legacy_patcher/sys_patch/kernelcache/kernel_collection/support.py b/opencore_legacy_patcher/sys_patch/kernelcache/kernel_collection/support.py new file mode 100644 index 000000000..06a34f5cb --- /dev/null +++ b/opencore_legacy_patcher/sys_patch/kernelcache/kernel_collection/support.py @@ -0,0 +1,162 @@ +""" +support.py: Kernel Cache support functions +""" + +import logging +import plistlib + +from pathlib import Path +from datetime import datetime + +from ....datasets import os_data +from ....support import subprocess_wrapper + + +class KernelCacheSupport: + + def __init__(self, mount_location_data: str, detected_os: int, skip_root_kmutil_requirement: bool) -> None: + self.mount_location_data = mount_location_data + self.detected_os = detected_os + self.skip_root_kmutil_requirement = skip_root_kmutil_requirement + + + def check_kexts_needs_authentication(self, kext_name: str) -> bool: + """ + Verify whether the user needs to authenticate in System Preferences + Sets 'needs_to_open_preferences' to True if the kext is not in the AuxKC + + Logic: + Under 'private/var/db/KernelManagement/AuxKC/CurrentAuxKC/com.apple.kcgen.instructions.plist' + ["kextsToBuild"][i]: + ["bundlePathMainOS"] = /Library/Extensions/Test.kext + ["cdHash"] = Bundle's CDHash (random on ad-hoc signed, static on dev signed) + ["teamID"] = Team ID (blank on ad-hoc signed) + To grab the CDHash of a kext, run 'codesign -dvvv ' + """ + + try: + aux_cache_path = Path(self.mount_location_data) / Path("/private/var/db/KernelExtensionManagement/AuxKC/CurrentAuxKC/com.apple.kcgen.instructions.plist") + if aux_cache_path.exists(): + aux_cache_data = plistlib.load((aux_cache_path).open("rb")) + for kext in aux_cache_data["kextsToBuild"]: + if "bundlePathMainOS" in aux_cache_data["kextsToBuild"][kext]: + if aux_cache_data["kextsToBuild"][kext]["bundlePathMainOS"] == f"/Library/Extensions/{kext_name}": + return False + except PermissionError: + pass + + logging.info(f" - {kext_name} requires authentication in System Preferences") + + return True + + + def add_auxkc_support(self, install_file: str, source_folder_path: str, install_patch_directory: str, destination_folder_path: str) -> str: + """ + Patch provided Kext to support Auxiliary Kernel Collection + + Logic: + In macOS Ventura, KDKs are required to build new Boot and System KCs + However for some patch sets, we're able to use the Auxiliary KCs with '/Library/Extensions' + + kernelmanagerd determines which kext is installed by their 'OSBundleRequired' entry + If a kext is labeled as 'OSBundleRequired: Root' or 'OSBundleRequired: Safe Boot', + kernelmanagerd will require the kext to be installed in the Boot/SysKC + + Additionally, kexts starting with 'com.apple.' are not natively allowed to be installed + in the AuxKC. So we need to explicitly set our 'OSBundleRequired' to 'Auxiliary' + + Parameters: + install_file (str): Kext file name + source_folder_path (str): Source folder path + install_patch_directory (str): Patch directory + destination_folder_path (str): Destination folder path + + Returns: + str: Updated destination folder path + """ + + if self.skip_root_kmutil_requirement is False: + return destination_folder_path + if not install_file.endswith(".kext"): + return destination_folder_path + if install_patch_directory != "/System/Library/Extensions": + return destination_folder_path + if self.detected_os < os_data.os_data.ventura: + return destination_folder_path + + updated_install_location = str(self.mount_location_data) + "/Library/Extensions" + + logging.info(f" - Adding AuxKC support to {install_file}") + plist_path = Path(Path(source_folder_path) / Path(install_file) / Path("Contents/Info.plist")) + plist_data = plistlib.load((plist_path).open("rb")) + + # Check if we need to update the 'OSBundleRequired' entry + if not plist_data["CFBundleIdentifier"].startswith("com.apple."): + return updated_install_location + if "OSBundleRequired" in plist_data: + if plist_data["OSBundleRequired"] == "Auxiliary": + return updated_install_location + + plist_data["OSBundleRequired"] = "Auxiliary" + plistlib.dump(plist_data, plist_path.open("wb")) + + return updated_install_location + + + + def clean_auxiliary_kc(self) -> None: + """ + Clean the Auxiliary Kernel Collection + + Logic: + When reverting root volume patches, the AuxKC will still retain the UUID + it was built against. Thus when Boot/SysKC are reverted, Aux will break + To resolve this, delete all installed kexts in /L*/E* and rebuild the AuxKC + We can verify our binaries based off the OpenCore-Legacy-Patcher.plist file + """ + + if self.detected_os < os_data.os_data.big_sur: + return + + logging.info("- Cleaning Auxiliary Kernel Collection") + oclp_path = "/System/Library/CoreServices/OpenCore-Legacy-Patcher.plist" + if Path(oclp_path).exists(): + oclp_plist_data = plistlib.load(Path(oclp_path).open("rb")) + for key in oclp_plist_data: + if isinstance(oclp_plist_data[key], (bool, int)): + continue + for install_type in ["Install", "Install Non-Root"]: + if install_type not in oclp_plist_data[key]: + continue + for location in oclp_plist_data[key][install_type]: + if not location.endswith("Extensions"): + continue + for file in oclp_plist_data[key][install_type][location]: + if not file.endswith(".kext"): + continue + if not Path(f"/Library/Extensions/{file}").exists(): + continue + logging.info(f" - Removing {file}") + subprocess_wrapper.run_as_root(["/bin/rm", "-Rf", f"/Library/Extensions/{file}"]) + + # Handle situations where users migrated from older OSes with a lot of garbage in /L*/E* + # ex. Nvidia Web Drivers, NetUSB, dosdude1's patches, etc. + # Move if file's age is older than October 2021 (year before Ventura) + if self.detected_os < os_data.os_data.ventura: + return + + relocation_path = "/Library/Relocated Extensions" + if not Path(relocation_path).exists(): + subprocess_wrapper.run_as_root(["/bin/mkdir", relocation_path]) + + for file in Path("/Library/Extensions").glob("*.kext"): + try: + if datetime.fromtimestamp(file.stat().st_mtime) < datetime(2021, 10, 1): + logging.info(f" - Relocating {file.name} kext to {relocation_path}") + if Path(relocation_path) / Path(file.name).exists(): + subprocess_wrapper.run_as_root(["/bin/rm", "-Rf", relocation_path / Path(file.name)]) + subprocess_wrapper.run_as_root(["/bin/mv", file, relocation_path]) + except: + # Some users have the most cursed /L*/E* folders + # ex. Symlinks pointing to symlinks pointing to dead files + pass diff --git a/opencore_legacy_patcher/sys_patch/kernelcache/mkext/mkext.py b/opencore_legacy_patcher/sys_patch/kernelcache/mkext/mkext.py new file mode 100644 index 000000000..94d0ba64c --- /dev/null +++ b/opencore_legacy_patcher/sys_patch/kernelcache/mkext/mkext.py @@ -0,0 +1,32 @@ +""" +mkext.py: MKext cache management +""" + +import logging +import subprocess + +from ..base.cache import BaseKernelCache + +from ....support import subprocess_wrapper + + +class MKext(BaseKernelCache): + + def __init__(self, mount_location: str) -> None: + self.mount_location = mount_location + + + def _mkext_arguments(self) -> list[str]: + args = ["/usr/bin/touch", f"{self.mount_location}/System/Library/Extensions"] + return args + + + def rebuild(self) -> None: + logging.info("- Rebuilding MKext cache") + result = subprocess_wrapper.run_as_root(self._mkext_arguments(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + if result.returncode != 0: + subprocess_wrapper.log(result) + return False + + return True \ No newline at end of file diff --git a/opencore_legacy_patcher/sys_patch/kernelcache/prelinked/prelinked.py b/opencore_legacy_patcher/sys_patch/kernelcache/prelinked/prelinked.py new file mode 100644 index 000000000..2aa6f477b --- /dev/null +++ b/opencore_legacy_patcher/sys_patch/kernelcache/prelinked/prelinked.py @@ -0,0 +1,48 @@ +""" +prelinked.py: Prelinked Kernel cache management +""" + +import logging +import subprocess + +from pathlib import Path + +from ..base.cache import BaseKernelCache +from ....support import subprocess_wrapper + + +class PrelinkedKernel(BaseKernelCache): + + def __init__(self, mount_location: str) -> None: + self.mount_location = mount_location + + + def _kextcache_arguments(self) -> list[str]: + args = ["/usr/sbin/kextcache", "-invalidate", f"{self.mount_location}/"] + return args + + def _update_preboot_kernel_cache(self) -> bool: + """ + Ensure Preboot volume's kernel cache is updated + """ + if not Path("/usr/sbin/kcditto").exists(): + return + + logging.info("- Syncing Kernel Cache to Preboot") + subprocess_wrapper.run_as_root_and_verify(["/usr/sbin/kcditto"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + + def rebuild(self) -> None: + logging.info("- Rebuilding Prelinked Kernel") + result = subprocess_wrapper.run_as_root(self._kextcache_arguments(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + # kextcache notes: + # - kextcache always returns 0, even if it fails + # - Check the output for 'KernelCache ID' to see if the cache was successfully rebuilt + if "KernelCache ID" not in result.stdout.decode(): + subprocess_wrapper.log(result) + return False + + self._update_preboot_kernel_cache() + + return True \ No newline at end of file diff --git a/opencore_legacy_patcher/sys_patch/kernelcache/rebuild.py b/opencore_legacy_patcher/sys_patch/kernelcache/rebuild.py new file mode 100644 index 000000000..c0c75e21d --- /dev/null +++ b/opencore_legacy_patcher/sys_patch/kernelcache/rebuild.py @@ -0,0 +1,51 @@ +""" +rebuild.py: Manage kernel cache rebuilding regardless of macOS version +""" + +from .base.cache import BaseKernelCache +from ...datasets import os_data + + +class RebuildKernelCache: + """ + RebuildKernelCache: Rebuild the kernel cache + + Parameters: + - os_version: macOS version + - mount_location: Path to the mounted volume + - auxiliary_cache: Whether to create auxiliary kernel cache (Big Sur and later) + - auxiliary_cache_only: Whether to only create auxiliary kernel cache (Ventura and later) + """ + def __init__(self, os_version: os_data.os_data, mount_location: str, auxiliary_cache: bool, auxiliary_cache_only: bool) -> None: + self.os_version = os_version + self.mount_location = mount_location + self.auxiliary_cache = auxiliary_cache + self.auxiliary_cache_only = auxiliary_cache_only + + + def _rebuild_method(self) -> BaseKernelCache: + """ + Determine the correct method to rebuild the kernel cache + """ + if self.os_version >= os_data.os_data.big_sur: + if self.os_version >= os_data.os_data.ventura: + if self.auxiliary_cache_only: + from .kernel_collection.auxiliary import AuxiliaryKernelCollection + return AuxiliaryKernelCollection(self.mount_location) + + from .kernel_collection.boot_system import BootSystemKernelCollections + return BootSystemKernelCollections(self.mount_location, self.os_version, self.auxiliary_cache) + + if os_data.os_data.catalina >= self.os_version >= os_data.os_data.lion: + from .prelinked.prelinked import PrelinkedKernel + return PrelinkedKernel(self.mount_location) + + from .mkext.mkext import MKext + return MKext(self.mount_location) + + + def rebuild(self) -> bool: + """ + Rebuild the kernel cache + """ + return self._rebuild_method().rebuild() \ No newline at end of file diff --git a/opencore_legacy_patcher/sys_patch/sys_patch.py b/opencore_legacy_patcher/sys_patch/sys_patch.py index b29267404..fe141b07b 100644 --- a/opencore_legacy_patcher/sys_patch/sys_patch.py +++ b/opencore_legacy_patcher/sys_patch/sys_patch.py @@ -55,11 +55,12 @@ from ..support import ( ) from . import ( sys_patch_detect, - sys_patch_auto, sys_patch_helpers, sys_patch_generate, - sys_patch_mount + sys_patch_mount, + kernelcache ) +from .auto_patcher import InstallAutomaticPatchingServices class PatchSysVolume: @@ -270,7 +271,13 @@ class PatchSysVolume: self._clean_skylight_plugins() self._delete_nonmetal_enforcement() - self._clean_auxiliary_kc() + + kernelcache.KernelCacheSupport( + mount_location_data=self.mount_location_data, + detected_os=self.constants.detected_os, + skip_root_kmutil_requirement=self.skip_root_kmutil_requirement + ).clean_auxiliary_kc() + self.constants.root_patcher_succeeded = True logging.info("- Unpatching complete") logging.info("\nPlease reboot the machine for patches to take effect") @@ -315,93 +322,12 @@ class PatchSysVolume: bool: True if successful, False if not """ - logging.info("- Rebuilding Kernel Cache (This may take some time)") - if self.constants.detected_os > os_data.os_data.catalina: - # Base Arguments - args = ["/usr/bin/kmutil", "install"] - - if self.skip_root_kmutil_requirement is True: - # Only rebuild the Auxiliary Kernel Collection - args.append("--new") - args.append("aux") - - args.append("--boot-path") - args.append(f"{self.mount_location}/System/Library/KernelCollections/BootKernelExtensions.kc") - - args.append("--system-path") - args.append(f"{self.mount_location}/System/Library/KernelCollections/SystemKernelExtensions.kc") - else: - # Rebuild Boot, System and Auxiliary Kernel Collections - args.append("--volume-root") - args.append(self.mount_location) - - # Build Boot, Sys and Aux KC - args.append("--update-all") - - # If multiple kernels found, only build release KCs - args.append("--variant-suffix") - args.append("release") - - if self.constants.detected_os >= os_data.os_data.ventura: - # With Ventura, we're required to provide a KDK in some form - # to rebuild the Kernel Cache - # - # However since we already merged the KDK onto root with 'ditto', - # We can add '--allow-missing-kdk' to skip parsing the KDK - # - # This allows us to only delete/overwrite kexts inside of - # /System/Library/Extensions and not the entire KDK - args.append("--allow-missing-kdk") - - # 'install' and '--update-all' cannot be used together in Ventura. - # kmutil will request the usage of 'create' instead: - # Warning: kmutil install's usage of --update-all is deprecated. - # Use kmutil create --update-install instead' - args[1] = "create" - - if self.needs_kmutil_exemptions is True: - # When installing to '/Library/Extensions', following args skip kext consent - # prompt in System Preferences when SIP's disabled - logging.info(" (You will get a prompt by System Preferences, ignore for now)") - args.append("--no-authentication") - args.append("--no-authorization") - else: - args = ["/usr/sbin/kextcache", "-i", f"{self.mount_location}/"] - - result = subprocess_wrapper.run_as_root(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - - # kextcache notes: - # - kextcache always returns 0, even if it fails - # - Check the output for 'KernelCache ID' to see if the cache was successfully rebuilt - # kmutil notes: - # - will return 71 on failure to build KCs - # - will return 31 on 'No binaries or codeless kexts were provided' - # - will return -10 if the volume is missing (ie. unmounted by another process) - if result.returncode != 0 or (self.constants.detected_os < os_data.os_data.catalina and "KernelCache ID" not in result.stdout.decode()): - logging.info("- Unable to build new kernel cache") - subprocess_wrapper.log(result) - logging.info("") - logging.info("\nPlease reboot the machine to avoid potential issues rerunning the patcher") - return False - - if self.skip_root_kmutil_requirement is True: - # Force rebuild the Auxiliary KC - result = subprocess_wrapper.run_as_root(["/usr/bin/killall", "syspolicyd", "kernelmanagerd"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - if result.returncode != 0: - logging.info("- Unable to remove kernel extension policy files") - subprocess_wrapper.log(result) - logging.info("") - logging.info("\nPlease reboot the machine to avoid potential issues rerunning the patcher") - return False - - for file in ["KextPolicy", "KextPolicy-shm", "KextPolicy-wal"]: - self._remove_file("/private/var/db/SystemPolicyConfiguration/", file) - else: - # Install RSRHelper utility to handle desynced KCs - sys_patch_helpers.SysPatchHelpers(self.constants).install_rsr_repair_binary() - - logging.info("- Successfully built new kernel cache") - return True + return kernelcache.RebuildKernelCache( + os_version=self.constants.detected_os, + mount_location=self.mount_location, + auxiliary_cache=self.needs_kmutil_exemptions, + auxiliary_cache_only=self.skip_root_kmutil_requirement + ).rebuild() def _create_new_apfs_snapshot(self) -> bool: @@ -464,61 +390,6 @@ class PatchSysVolume: subprocess_wrapper.run_as_root(["/usr/bin/defaults", "delete", "/Library/Preferences/com.apple.CoreDisplay", arg]) - def _clean_auxiliary_kc(self) -> None: - """ - Clean the Auxiliary Kernel Collection - - Logic: - When reverting root volume patches, the AuxKC will still retain the UUID - it was built against. Thus when Boot/SysKC are reverted, Aux will break - To resolve this, delete all installed kexts in /L*/E* and rebuild the AuxKC - We can verify our binaries based off the OpenCore-Legacy-Patcher.plist file - """ - - if self.constants.detected_os < os_data.os_data.big_sur: - return - - logging.info("- Cleaning Auxiliary Kernel Collection") - oclp_path = "/System/Library/CoreServices/OpenCore-Legacy-Patcher.plist" - if Path(oclp_path).exists(): - oclp_plist_data = plistlib.load(Path(oclp_path).open("rb")) - for key in oclp_plist_data: - if isinstance(oclp_plist_data[key], (bool, int)): - continue - for install_type in ["Install", "Install Non-Root"]: - if install_type not in oclp_plist_data[key]: - continue - for location in oclp_plist_data[key][install_type]: - if not location.endswith("Extensions"): - continue - for file in oclp_plist_data[key][install_type][location]: - if not file.endswith(".kext"): - continue - self._remove_file("/Library/Extensions", file) - - # Handle situations where users migrated from older OSes with a lot of garbage in /L*/E* - # ex. Nvidia Web Drivers, NetUSB, dosdude1's patches, etc. - # Move if file's age is older than October 2021 (year before Ventura) - if self.constants.detected_os < os_data.os_data.ventura: - return - - relocation_path = "/Library/Relocated Extensions" - if not Path(relocation_path).exists(): - subprocess_wrapper.run_as_root(["/bin/mkdir", relocation_path]) - - for file in Path("/Library/Extensions").glob("*.kext"): - try: - if datetime.fromtimestamp(file.stat().st_mtime) < datetime(2021, 10, 1): - logging.info(f" - Relocating {file.name} kext to {relocation_path}") - if Path(relocation_path) / Path(file.name).exists(): - subprocess_wrapper.run_as_root(["/bin/rm", "-Rf", relocation_path / Path(file.name)]) - subprocess_wrapper.run_as_root(["/bin/mv", file, relocation_path]) - except: - # Some users have the most cursed /L*/E* folders - # ex. Symlinks pointing to symlinks pointing to dead files - pass - - def _write_patchset(self, patchset: dict) -> None: """ Write patchset information to Root Volume @@ -537,93 +408,6 @@ class PatchSysVolume: subprocess_wrapper.run_as_root_and_verify(generate_copy_arguments(f"{self.constants.payload_path}/{file_name}", destination_path), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - def _add_auxkc_support(self, install_file: str, source_folder_path: str, install_patch_directory: str, destination_folder_path: str) -> str: - """ - Patch provided Kext to support Auxiliary Kernel Collection - - Logic: - In macOS Ventura, KDKs are required to build new Boot and System KCs - However for some patch sets, we're able to use the Auxiliary KCs with '/Library/Extensions' - - kernelmanagerd determines which kext is installed by their 'OSBundleRequired' entry - If a kext is labeled as 'OSBundleRequired: Root' or 'OSBundleRequired: Safe Boot', - kernelmanagerd will require the kext to be installed in the Boot/SysKC - - Additionally, kexts starting with 'com.apple.' are not natively allowed to be installed - in the AuxKC. So we need to explicitly set our 'OSBundleRequired' to 'Auxiliary' - - Parameters: - install_file (str): Kext file name - source_folder_path (str): Source folder path - install_patch_directory (str): Patch directory - destination_folder_path (str): Destination folder path - - Returns: - str: Updated destination folder path - """ - - if self.skip_root_kmutil_requirement is False: - return destination_folder_path - if not install_file.endswith(".kext"): - return destination_folder_path - if install_patch_directory != "/System/Library/Extensions": - return destination_folder_path - if self.constants.detected_os < os_data.os_data.ventura: - return destination_folder_path - - updated_install_location = str(self.mount_location_data) + "/Library/Extensions" - - logging.info(f" - Adding AuxKC support to {install_file}") - plist_path = Path(Path(source_folder_path) / Path(install_file) / Path("Contents/Info.plist")) - plist_data = plistlib.load((plist_path).open("rb")) - - # Check if we need to update the 'OSBundleRequired' entry - if not plist_data["CFBundleIdentifier"].startswith("com.apple."): - return updated_install_location - if "OSBundleRequired" in plist_data: - if plist_data["OSBundleRequired"] == "Auxiliary": - return updated_install_location - - plist_data["OSBundleRequired"] = "Auxiliary" - plistlib.dump(plist_data, plist_path.open("wb")) - - self._check_kexts_needs_authentication(install_file) - - return updated_install_location - - - def _check_kexts_needs_authentication(self, kext_name: str): - """ - Verify whether the user needs to authenticate in System Preferences - Sets 'needs_to_open_preferences' to True if the kext is not in the AuxKC - - Logic: - Under 'private/var/db/KernelManagement/AuxKC/CurrentAuxKC/com.apple.kcgen.instructions.plist' - ["kextsToBuild"][i]: - ["bundlePathMainOS"] = /Library/Extensions/Test.kext - ["cdHash"] = Bundle's CDHash (random on ad-hoc signed, static on dev signed) - ["teamID"] = Team ID (blank on ad-hoc signed) - To grab the CDHash of a kext, run 'codesign -dvvv ' - - Parameters: - kext_name (str): Name of the kext to check - """ - - try: - aux_cache_path = Path(self.mount_location_data) / Path("/private/var/db/KernelExtensionManagement/AuxKC/CurrentAuxKC/com.apple.kcgen.instructions.plist") - if aux_cache_path.exists(): - aux_cache_data = plistlib.load((aux_cache_path).open("rb")) - for kext in aux_cache_data["kextsToBuild"]: - if "bundlePathMainOS" in aux_cache_data["kextsToBuild"][kext]: - if aux_cache_data["kextsToBuild"][kext]["bundlePathMainOS"] == f"/Library/Extensions/{kext_name}": - return - except PermissionError: - pass - - logging.info(f" - {kext_name} requires authentication in System Preferences") - self.constants.needs_to_open_preferences = True # Notify in GUI to open System Preferences - - def _patch_root_vol(self): """ Patch root volume @@ -639,7 +423,7 @@ class PatchSysVolume: needs_daemon = False if self.constants.detected_os >= os_data.os_data.ventura and self.skip_root_kmutil_requirement is False: needs_daemon = True - sys_patch_auto.AutomaticSysPatch(self.constants).install_auto_patcher_launch_agent(kdk_caching_needed=needs_daemon) + InstallAutomaticPatchingServices(self.constants).install_auto_patcher_launch_agent(kdk_caching_needed=needs_daemon) self._rebuild_root_volume() @@ -652,6 +436,12 @@ class PatchSysVolume: required_patches (dict): Patchset to execute (generated by sys_patch_generate.GenerateRootPatchSets) """ + kc_support_obj = kernelcache.KernelCacheSupport( + mount_location_data=self.mount_location_data, + detected_os=self.constants.detected_os, + skip_root_kmutil_requirement=self.skip_root_kmutil_requirement + ) + source_files_path = str(self.constants.payload_local_binaries_root_path) self._preflight_checks(required_patches, source_files_path) for patch in required_patches: @@ -669,31 +459,38 @@ class PatchSysVolume: for method_install in ["Install", "Install Non-Root"]: - if method_install in required_patches[patch]: - for install_patch_directory in list(required_patches[patch][method_install]): - logging.info(f"- Handling Installs in: {install_patch_directory}") - for install_file in list(required_patches[patch][method_install][install_patch_directory]): - source_folder_path = source_files_path + "/" + required_patches[patch][method_install][install_patch_directory][install_file] + install_patch_directory - if method_install == "Install": - destination_folder_path = str(self.mount_location) + install_patch_directory - else: - if install_patch_directory == "/Library/Extensions": - self.needs_kmutil_exemptions = True - self._check_kexts_needs_authentication(install_file) - destination_folder_path = str(self.mount_location_data) + install_patch_directory + if method_install not in required_patches[patch]: + continue - updated_destination_folder_path = self._add_auxkc_support(install_file, source_folder_path, install_patch_directory, destination_folder_path) + for install_patch_directory in list(required_patches[patch][method_install]): + logging.info(f"- Handling Installs in: {install_patch_directory}") + for install_file in list(required_patches[patch][method_install][install_patch_directory]): + source_folder_path = source_files_path + "/" + required_patches[patch][method_install][install_patch_directory][install_file] + install_patch_directory + if method_install == "Install": + destination_folder_path = str(self.mount_location) + install_patch_directory + else: + if install_patch_directory == "/Library/Extensions": + self.needs_kmutil_exemptions = True + if kc_support_obj.check_kexts_needs_authentication(install_file) is True: + self.constants.needs_to_open_preferences = True - if destination_folder_path != updated_destination_folder_path: - # Update required_patches to reflect the new destination folder path - if updated_destination_folder_path not in required_patches[patch][method_install]: - required_patches[patch][method_install].update({updated_destination_folder_path: {}}) - required_patches[patch][method_install][updated_destination_folder_path].update({install_file: required_patches[patch][method_install][install_patch_directory][install_file]}) - required_patches[patch][method_install][install_patch_directory].pop(install_file) + destination_folder_path = str(self.mount_location_data) + install_patch_directory - destination_folder_path = updated_destination_folder_path + updated_destination_folder_path = kc_support_obj.add_auxkc_support(install_file, source_folder_path, install_patch_directory, destination_folder_path) - self._install_new_file(source_folder_path, destination_folder_path, install_file) + if kc_support_obj.check_kexts_needs_authentication(install_file) is True: + self.constants.needs_to_open_preferences = True + + if destination_folder_path != updated_destination_folder_path: + # Update required_patches to reflect the new destination folder path + if updated_destination_folder_path not in required_patches[patch][method_install]: + required_patches[patch][method_install].update({updated_destination_folder_path: {}}) + required_patches[patch][method_install][updated_destination_folder_path].update({install_file: required_patches[patch][method_install][install_patch_directory][install_file]}) + required_patches[patch][method_install][install_patch_directory].pop(install_file) + + destination_folder_path = updated_destination_folder_path + + self._install_new_file(source_folder_path, destination_folder_path, install_file) if "Processes" in required_patches[patch]: for process in required_patches[patch]["Processes"]: @@ -729,7 +526,11 @@ class PatchSysVolume: # Make sure non-Metal Enforcement preferences are not present self._delete_nonmetal_enforcement() # Make sure we clean old kexts in /L*/E* that are not in the patchset - self._clean_auxiliary_kc() + kernelcache.KernelCacheSupport( + mount_location_data=self.mount_location_data, + detected_os=self.constants.detected_os, + skip_root_kmutil_requirement=self.skip_root_kmutil_requirement + ).clean_auxiliary_kc() # Make sure SNB kexts are compatible with the host if "Intel Sandy Bridge" in required_patches: