sys_patch.py: Add backend for MetallibSupportPkg

This commit is contained in:
Mykola Grymalyuk
2024-08-28 15:21:35 -06:00
parent a6503bcd97
commit bd51332a17
7 changed files with 448 additions and 19 deletions

View File

@@ -812,6 +812,10 @@ class Constants:
def kdk_download_path(self): def kdk_download_path(self):
return self.payload_path / Path("KDK.dmg") return self.payload_path / Path("KDK.dmg")
@property
def metallib_download_path(self):
return self.payload_path / Path("MetallibSupportPkg.pkg")
@property @property
def icons_path(self): def icons_path(self):
return [ return [

View File

@@ -2,11 +2,16 @@
sys_patch_dict.py: Dictionary defining patch sets used during Root Volume patching (sys_patch.py) sys_patch_dict.py: Dictionary defining patch sets used during Root Volume patching (sys_patch.py)
""" """
import enum
import packaging.version import packaging.version
from . import os_data from . import os_data
class DynamicPatchset(enum.Enum):
MetallibSupportPkg = "MetallibSupportPkg"
class SystemPatchDictionary(): class SystemPatchDictionary():
""" """
Library for generating patch sets for sys_patch.py and supporting modules Library for generating patch sets for sys_patch.py and supporting modules
@@ -70,8 +75,8 @@ class SystemPatchDictionary():
self.patchset_dict: dict = {} self.patchset_dict: dict = {}
self.marketing_version: str = marketing_version self.marketing_version: str = marketing_version
self.affected_by_cve_2024_23227: bool = self.__is_affect_by_cve_2024_23227() self.affected_by_cve_2024_23227: bool = self.__is_affect_by_cve_2024_23227()
self.metallib_directory: str = self.__resolve_metallibsupportpkg() self.metallib_directory: DynamicPatchset = DynamicPatchset.MetallibSupportPkg
# XNU Kernel versions # XNU Kernel versions
self.macOS_12_0_B7: float = 21.1 self.macOS_12_0_B7: float = 21.1
@@ -149,13 +154,6 @@ class SystemPatchDictionary():
return False return False
def __resolve_metallibsupportpkg(self) -> str:
"""
Temporarily hard coded
"""
return "/Library/Application Support/Dortania/MetallibSupportPkg/15.0-24A5327a"
def _generate_sys_patch_dict(self): def _generate_sys_patch_dict(self):
""" """
Generates the sys_patch_dict dictionary Generates the sys_patch_dict dictionary
@@ -804,6 +802,12 @@ class SystemPatchDictionary():
"default.metallib": self.metallib_directory, "default.metallib": self.metallib_directory,
}, },
}, },
"Remove": {
"/System/Library/PrivateFrameworks/RenderBox.framework/Versions/A/Resources": [
# For some reason Ivy Bridge can't tell the metallib lacks AIR64 support, and errors out
"archive.metallib",
],
},
}, },
# Primarily for AMD GCN GPUs # Primarily for AMD GCN GPUs

View File

@@ -0,0 +1,271 @@
"""
metallib_handler.py: Library for handling Metal libraries
"""
import logging
import requests
import subprocess
import packaging.version
from typing import cast
from pathlib import Path
from . import network_handler, subprocess_wrapper
from .. import constants
from ..datasets import os_data
METALLIB_INSTALL_PATH: str = "/Library/Application Support/Dortania/MetallibSupportPkg"
METALLIB_API_LINK: str = "https://dortania.github.io/MetallibSupportPkg/manifest.json"
METALLIB_ASSET_LIST: list = None
class MetalLibraryObject:
def __init__(self, global_constants: constants.Constants,
host_build: str, host_version: str,
ignore_installed: bool = False, passive: bool = False
) -> None:
self.constants: constants.Constants = global_constants
self.host_build: str = host_build # ex. 20A5384c
self.host_version: str = host_version # ex. 11.0.1
self.passive: bool = passive # Don't perform actions requiring elevated privileges
self.ignore_installed: bool = ignore_installed # If True, will ignore any installed MetallibSupportPkg PKGs and download the latest
self.metallib_already_installed: bool = False
self.metallib_installed_path: str = ""
self.metallib_url: str = ""
self.metallib_url_build: str = ""
self.metallib_url_version: str = ""
self.metallib_url_is_exactly_match: bool = False
self.metallib_closest_match_url: str = ""
self.metallib_closest_match_url_build: str = ""
self.metallib_closest_match_url_version: str = ""
self.success: bool = False
self.error_msg: str = ""
self._get_latest_metallib()
def _get_remote_metallibs(self) -> dict:
"""
Get the MetallibSupportPkg list from the API
"""
global METALLIB_ASSET_LIST
logging.info("Pulling metallib list from MetallibSupportPkg API")
if METALLIB_ASSET_LIST:
return METALLIB_ASSET_LIST
try:
results = network_handler.NetworkUtilities().get(
METALLIB_API_LINK,
headers={
"User-Agent": f"OCLP/{self.constants.patcher_version}"
},
timeout=5
)
except (requests.exceptions.Timeout, requests.exceptions.TooManyRedirects, requests.exceptions.ConnectionError):
logging.info("Could not contact MetallibSupportPkg API")
return None
if results.status_code != 200:
logging.info("Could not fetch Metallib list")
return None
METALLIB_ASSET_LIST = results.json()
return METALLIB_ASSET_LIST
def _get_latest_metallib(self) -> None:
"""
Get the latest MetallibSupportPkg PKG
"""
parsed_version = cast(packaging.version.Version, packaging.version.parse(self.host_version))
if os_data.os_conversion.os_to_kernel(str(parsed_version.major)) < os_data.os_data.sequoia:
self.error_msg = "MetallibSupportPkg is not required for macOS Sonoma or older"
logging.warning(f"{self.error_msg}")
return
self.metallib_installed_path = self._local_metallib_installed()
if self.metallib_installed_path:
logging.info(f"metallib already installed ({Path(self.metallib_installed_path).name}), skipping")
self.metallib_already_installed = True
self.success = True
return
remote_metallib_version = self._get_remote_metallibs()
if remote_metallib_version is None:
logging.warning("Failed to fetch metallib list, falling back to local metallib matching")
# First check if a metallib matching the current macOS version is installed
# ex. 13.0.1 vs 13.0
loose_version = f"{parsed_version.major}.{parsed_version.minor}"
logging.info(f"Checking for metallibs loosely matching {loose_version}")
self.metallib_installed_path = self._local_metallib_installed(match=loose_version, check_version=True)
if self.metallib_installed_path:
logging.info(f"Found matching metallib: {Path(self.metallib_installed_path).name}")
self.metallib_already_installed = True
self.success = True
return
older_version = f"{parsed_version.major}.{parsed_version.minor - 1 if parsed_version.minor > 0 else 0}"
logging.info(f"Checking for metallibs matching {older_version}")
self.metallib_installed_path = self._local_metallib_installed(match=older_version, check_version=True)
if self.metallib_installed_path:
logging.info(f"Found matching metallib: {Path(self.metallib_installed_path).name}")
self.metallib_already_installed = True
self.success = True
return
logging.warning(f"Couldn't find metallib matching {self.host_version} or {older_version}, please install one manually")
self.error_msg = f"Could not contact MetallibSupportPkg API, and no metallib matching {self.host_version} ({self.host_build}) or {older_version} was installed.\nPlease ensure you have a network connection or manually install a metallib."
return
# First check exact match
for metallib in remote_metallib_version:
if (metallib["build"] != self.host_build):
continue
self.metallib_url = metallib["url"]
self.metallib_url_build = metallib["build"]
self.metallib_url_version = metallib["version"]
self.metallib_url_is_exactly_match = True
break
# If no exact match, check for closest match
if self.metallib_url == "":
for metallib in remote_metallib_version:
metallib_version = cast(packaging.version.Version, packaging.version.parse(metallib["version"]))
if metallib_version > parsed_version:
continue
if metallib_version.major != parsed_version.major:
continue
if metallib_version.minor not in range(parsed_version.minor - 1, parsed_version.minor + 1):
continue
# The metallib list is already sorted by version then date, so the first match is the closest
self.metallib_closest_match_url = metallib["url"]
self.metallib_closest_match_url_build = metallib["build"]
self.metallib_closest_match_url_version = metallib["version"]
self.metallib_url_is_exactly_match = False
break
if self.metallib_url == "":
if self.metallib_closest_match_url == "":
logging.warning(f"No metallibs found for {self.host_build} ({self.host_version})")
self.error_msg = f"No metallibs found for {self.host_build} ({self.host_version})"
return
logging.info(f"No direct match found for {self.host_build}, falling back to closest match")
logging.info(f"Closest Match: {self.metallib_closest_match_url_build} ({self.metallib_closest_match_url_version})")
self.metallib_url = self.metallib_closest_match_url
self.metallib_url_build = self.metallib_closest_match_url_build
self.metallib_url_version = self.metallib_closest_match_url_version
else:
logging.info(f"Direct match found for {self.host_build} ({self.host_version})")
# Check if this metallib is already installed
self.metallib_installed_path = self._local_metallib_installed(match=self.metallib_url_build)
if self.metallib_installed_path:
logging.info(f"metallib already installed ({Path(self.metallib_installed_path).name}), skipping")
self.metallib_already_installed = True
self.success = True
return
logging.info("Following metallib is recommended:")
logging.info(f"- metallib Build: {self.metallib_url_build}")
logging.info(f"- metallib Version: {self.metallib_url_version}")
logging.info(f"- metallib URL: {self.metallib_url}")
self.success = True
def _local_metallib_installed(self, match: str = None, check_version: bool = False) -> str:
"""
Check if a metallib is already installed
"""
if not Path(METALLIB_INSTALL_PATH).exists():
return None
for metallib_folder in Path(METALLIB_INSTALL_PATH).iterdir():
if not metallib_folder.is_dir():
continue
if check_version:
if match not in metallib_folder.name:
continue
else:
if not metallib_folder.name.endswith(f"-{match}"):
continue
return metallib_folder
return None
def retrieve_download(self, override_path: str = "") -> network_handler.DownloadObject:
"""
Retrieve MetallibSupportPkg PKG download object
"""
self.success = False
self.error_msg = ""
if self.metallib_already_installed:
logging.info("No download required, metallib already installed")
self.success = True
return None
if self.metallib_url == "":
self.error_msg = "Could not retrieve metallib catalog, no metallib to download"
logging.error(self.error_msg)
return None
logging.info(f"Returning DownloadObject for metallib: {Path(self.metallib_url).name}")
self.success = True
metallib_download_path = self.constants.metallib_download_path if override_path == "" else Path(override_path)
return network_handler.DownloadObject(self.metallib_url, metallib_download_path)
def install_metallib(self, metallib: str = None) -> None:
"""
Install MetallibSupportPkg PKG
"""
if not self.success:
logging.error("Cannot install metallib, no metallib was successfully retrieved")
return False
if self.metallib_already_installed:
logging.info("No installation required, metallib already installed")
return True
result = subprocess_wrapper.run_as_root([
"/usr/sbin/installer", "-pkg", metallib if metallib else self.constants.metallib_download_path, "-target", "/"
])
if result.returncode != 0:
subprocess_wrapper.log(result)
return False
return True

View File

@@ -132,6 +132,8 @@ class PatcherValidation:
if install_type in patchset[patch_subject][patch_core]: if install_type in patchset[patch_subject][patch_core]:
for install_directory in patchset[patch_subject][patch_core][install_type]: for install_directory in patchset[patch_subject][patch_core][install_type]:
for install_file in patchset[patch_subject][patch_core][install_type][install_directory]: for install_file in patchset[patch_subject][patch_core][install_type][install_directory]:
if patchset[patch_subject][patch_core][install_type][install_directory][install_file] in sys_patch_dict.DynamicPatchset:
continue
source_file = str(self.constants.payload_local_binaries_root_path) + "/" + patchset[patch_subject][patch_core][install_type][install_directory][install_file] + install_directory + "/" + install_file source_file = str(self.constants.payload_local_binaries_root_path) + "/" + patchset[patch_subject][patch_core][install_type][install_directory][install_file] + install_directory + "/" + install_file
if not Path(source_file).exists(): if not Path(source_file).exists():
logging.info(f"File not found: {source_file}") logging.info(f"File not found: {source_file}")

View File

@@ -660,13 +660,14 @@ class DetectRootPatch:
"Settings: Requires AMFI exemption": self.amfi_must_disable, "Settings: Requires AMFI exemption": self.amfi_must_disable,
"Settings: Supports Auxiliary Cache": not self.requires_root_kc, "Settings: Supports Auxiliary Cache": not self.requires_root_kc,
"Settings: Kernel Debug Kit missing": self.missing_kdk if self.os_major >= os_data.os_data.ventura.value else False, "Settings: Kernel Debug Kit missing": self.missing_kdk if self.os_major >= os_data.os_data.ventura.value else False,
"Settings: MetallibSupportPkg missing": self.os_major >= os_data.os_data.sequoia.value and any([self.kepler_gpu, self.ivy_gpu, self.haswell_gpu]),
"Validation: Patching Possible": self.verify_patch_allowed(), "Validation: Patching Possible": self.verify_patch_allowed(),
"Validation: Unpatching Possible": self._verify_unpatch_allowed(), "Validation: Unpatching Possible": self._verify_unpatch_allowed(),
f"Validation: Unsupported Host OS": self.unsupported_os, f"Validation: Unsupported Host OS": self.unsupported_os,
f"Validation: SIP is enabled (Required: {self._check_sip()[2]} or higher)": self.sip_enabled, f"Validation: SIP is enabled (Required: {self._check_sip()[2]} or higher)": self.sip_enabled,
f"Validation: Currently Booted SIP: ({hex(py_sip_xnu.SipXnu().get_sip_status().value)})": self.sip_enabled, f"Validation: Currently Booted SIP: ({hex(py_sip_xnu.SipXnu().get_sip_status().value)})": self.sip_enabled,
"Validation: SecureBootModel is enabled": self.sbm_enabled, "Validation: SecureBootModel is enabled": self.sbm_enabled,
f"Validation: {'AMFI' if self.constants.host_is_hackintosh is True or self._get_amfi_level_needed() > 2 else 'Library Validation'} is enabled": self.amfi_enabled if self.amfi_must_disable is True else False, f"Validation: {'AMFI' if self.constants.host_is_hackintosh is True or self._get_amfi_level_needed() > 2 else 'Library Validation'} is enabled": self.amfi_enabled if self.amfi_must_disable is True else False,
"Validation: FileVault is enabled": self.fv_enabled, "Validation: FileVault is enabled": self.fv_enabled,
"Validation: System is dosdude1 patched": self.dosdude_patched, "Validation: System is dosdude1 patched": self.dosdude_patched,
"Validation: WhateverGreen.kext missing": self.missing_whatever_green if self.nvidia_web is True else False, "Validation: WhateverGreen.kext missing": self.missing_whatever_green if self.nvidia_web is True else False,
@@ -719,9 +720,7 @@ class DetectRootPatch:
return True return True
if any([self.kepler_gpu, self.ivy_gpu, self.haswell_gpu]): if any([self.kepler_gpu, self.ivy_gpu, self.haswell_gpu]):
if Path("~/.dortania_developer").expanduser().exists(): if Path("~/.dortania_developer").expanduser().exists():
# Temporarily hard coded return True
if Path("/Library/Application Support/Dortania/MetallibSupportPkg/15.0-24A5327a").exists():
return True
return False return False
return True return True

View File

@@ -39,7 +39,8 @@ import logging
import plistlib import plistlib
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from functools import cache
from .mount import ( from .mount import (
RootVolumeMount, RootVolumeMount,
@@ -54,12 +55,16 @@ from .utilities import (
from .. import constants from .. import constants
from ..datasets import os_data
from ..volume import generate_copy_arguments from ..volume import generate_copy_arguments
from ..datasets import (
os_data,
sys_patch_dict
)
from ..support import ( from ..support import (
utilities, utilities,
subprocess_wrapper subprocess_wrapper,
metallib_handler
) )
from . import ( from . import (
sys_patch_helpers, sys_patch_helpers,
@@ -357,7 +362,7 @@ class PatchSysVolume:
) )
source_files_path = str(self.constants.payload_local_binaries_root_path) source_files_path = str(self.constants.payload_local_binaries_root_path)
self._preflight_checks(required_patches, source_files_path) required_patches = self._preflight_checks(required_patches, source_files_path)
for patch in required_patches: for patch in required_patches:
logging.info("- Installing Patchset: " + patch) logging.info("- Installing Patchset: " + patch)
for method_remove in ["Remove", "Remove Non-Root"]: for method_remove in ["Remove", "Remove Non-Root"]:
@@ -429,13 +434,56 @@ class PatchSysVolume:
self._write_patchset(required_patches) self._write_patchset(required_patches)
def _preflight_checks(self, required_patches: dict, source_files_path: Path) -> None: def _resolve_metallib_support_pkg(self) -> str:
"""
Resolves MetalLibSupportPkg
"""
metallib_obj = metallib_handler.MetalLibraryObject(self.constants, self.constants.detected_os_build, self.constants.detected_os_version)
if metallib_obj.success is False:
logging.error(f"Failed to find MetalLibSupportPkg: {metallib_obj.error_msg}")
raise Exception(f"Failed to find MetalLibSupportPkg: {metallib_obj.error_msg}")
metallib_download_obj = metallib_obj.retrieve_download()
if not metallib_download_obj:
# Already downloaded, return path
logging.info(f"Using MetalLibSupportPkg: {metallib_obj.metallib_installed_path}")
return str(metallib_obj.metallib_installed_path)
metallib_download_obj.download(spawn_thread=False)
if metallib_download_obj.download_complete is False:
error_msg = metallib_download_obj.error_msg
logging.error(f"Could not download MetalLibSupportPkg: {error_msg}")
raise Exception(f"Could not download MetalLibSupportPkg: {error_msg}")
if metallib_obj.install_metallib() is False:
logging.error("Failed to install MetalLibSupportPkg")
raise Exception("Failed to install MetalLibSupportPkg")
# After install, check if it's present
return self._resolve_metallib_support_pkg()
@cache
def _resolve_dynamic_patchset(self, variant: sys_patch_dict.DynamicPatchset) -> str:
"""
Resolves dynamic patchset to a path
"""
if variant == sys_patch_dict.DynamicPatchset.MetallibSupportPkg:
return self._resolve_metallib_support_pkg()
raise Exception(f"Unknown Dynamic Patchset: {variant}")
def _preflight_checks(self, required_patches: dict, source_files_path: Path) -> dict:
""" """
Runs preflight checks before patching Runs preflight checks before patching
Parameters: Parameters:
required_patches (dict): Patchset dictionary (from sys_patch_generate.GenerateRootPatchSets) required_patches (dict): Patchset dictionary (from sys_patch_generate.GenerateRootPatchSets)
source_files_path (Path): Path to the source files (PatcherSupportPkg) source_files_path (Path): Path to the source files (PatcherSupportPkg)
Returns:
dict: Updated patchset dictionary
""" """
logging.info("- Running Preflight Checks before patching") logging.info("- Running Preflight Checks before patching")
@@ -447,6 +495,9 @@ class PatchSysVolume:
continue continue
for install_patch_directory in required_patches[patch][method_type]: for install_patch_directory in required_patches[patch][method_type]:
for install_file in required_patches[patch][method_type][install_patch_directory]: for install_file in required_patches[patch][method_type][install_patch_directory]:
if required_patches[patch][method_type][install_patch_directory][install_file] in sys_patch_dict.DynamicPatchset:
required_patches[patch][method_type][install_patch_directory][install_file] = self._resolve_dynamic_patchset(required_patches[patch][method_type][install_patch_directory][install_file])
source_file = required_patches[patch][method_type][install_patch_directory][install_file] + install_patch_directory + "/" + install_file source_file = required_patches[patch][method_type][install_patch_directory][install_file] + install_patch_directory + "/" + install_file
# Check whether to source from root # Check whether to source from root
@@ -477,6 +528,8 @@ class PatchSysVolume:
logging.info("- Finished Preflight, starting patching") logging.info("- Finished Preflight, starting patching")
return required_patches
# Entry Function # Entry Function
def start_patch(self): def start_patch(self):

View File

@@ -16,8 +16,11 @@ from pathlib import Path
from .. import constants from .. import constants
from ..datasets import os_data from ..datasets import os_data
from ..support import kdk_handler
from ..support import (
kdk_handler,
metallib_handler
)
from ..sys_patch import ( from ..sys_patch import (
sys_patch, sys_patch,
) )
@@ -137,6 +140,95 @@ class SysPatchStartFrame(wx.Frame):
return True return True
def _metallib_download(self, frame: wx.Frame = None) -> bool:
frame = self if not frame else frame
logging.info("MetallibSupportPkg missing, generating Metallib download frame")
header = wx.StaticText(frame, label="Downloading Metal Libraries", pos=(-1,5))
header.SetFont(gui_support.font_factory(19, wx.FONTWEIGHT_BOLD))
header.Centre(wx.HORIZONTAL)
subheader = wx.StaticText(frame, label="Fetching MetallibSupportPkg database...", pos=(-1, header.GetPosition()[1] + header.GetSize()[1] + 5))
subheader.SetFont(gui_support.font_factory(13, wx.FONTWEIGHT_NORMAL))
subheader.Centre(wx.HORIZONTAL)
progress_bar = wx.Gauge(frame, range=100, pos=(-1, subheader.GetPosition()[1] + subheader.GetSize()[1] + 5), size=(250, 20))
progress_bar.Centre(wx.HORIZONTAL)
progress_bar_animation = gui_support.GaugePulseCallback(self.constants, progress_bar)
progress_bar_animation.start_pulse()
# Set size of frame
frame.SetSize((-1, progress_bar.GetPosition()[1] + progress_bar.GetSize()[1] + 35))
frame.Show()
self.metallib_obj: metallib_handler.MetalLibraryObject = None
def _metallib_thread_spawn():
self.metallib_obj = metallib_handler.MetalLibraryObject(self.constants, self.constants.detected_os_build, self.constants.detected_os_version)
metallib_thread = threading.Thread(target=_metallib_thread_spawn)
metallib_thread.start()
while metallib_thread.is_alive():
wx.Yield()
if self.metallib_obj.success is False:
progress_bar_animation.stop_pulse()
progress_bar.SetValue(0)
wx.MessageBox(f"Metallib download failed: {self.metallib_obj.error_msg}", "Error", wx.OK | wx.ICON_ERROR)
return False
self.metallib_download_obj = self.metallib_obj.retrieve_download()
if not self.metallib_download_obj:
# Metallib is already downloaded
return True
gui_download.DownloadFrame(
self,
title=self.title,
global_constants=self.constants,
download_obj=self.metallib_download_obj,
item_name=f"Metallib Build {self.metallib_obj.metallib_url_build}"
)
if self.metallib_download_obj.download_complete is False:
return False
logging.info("Metallib download complete, installing Metallib PKG")
header.SetLabel(f"Installing Metallib: {self.metallib_obj.metallib_url_build}")
header.Centre(wx.HORIZONTAL)
subheader.SetLabel("Installing MetallibSupportPkg PKG...")
subheader.Centre(wx.HORIZONTAL)
self.result = False
def _install_metallib():
self.result = self.metallib_obj.install_metallib()
install_thread = threading.Thread(target=_install_metallib)
install_thread.start()
while install_thread.is_alive():
wx.Yield()
if self.result is False:
progress_bar_animation.stop_pulse()
progress_bar.SetValue(0)
wx.MessageBox(f"Metallib installation failed: {self.metallib_obj.error_msg}", "Error", wx.OK | wx.ICON_ERROR)
return False
progress_bar_animation.stop_pulse()
progress_bar.SetValue(100)
logging.info("Metallib installation complete")
for child in frame.GetChildren():
child.Destroy()
return True
def _generate_modal(self, patches: dict = {}, variant: str = "Root Patching"): def _generate_modal(self, patches: dict = {}, variant: str = "Root Patching"):
""" """
Create UI for root patching/unpatching Create UI for root patching/unpatching
@@ -227,6 +319,10 @@ class SysPatchStartFrame(wx.Frame):
if self._kdk_download(self) is False: if self._kdk_download(self) is False:
sys.exit(1) sys.exit(1)
if self.patches["Settings: MetallibSupportPkg missing"] is True:
if self._metallib_download(self) is False:
sys.exit(1)
self._generate_modal(self.patches, "Root Patching") self._generate_modal(self.patches, "Root Patching")
self.return_button.Disable() self.return_button.Disable()