Add complete guide and all config variants

This commit is contained in:
renato97
2026-02-05 14:06:25 +00:00
parent 239ee0e593
commit b40c76762c
1053 changed files with 167761 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,96 @@
#!/bin/bash
# create_vault.sh
#
#
# Created by Rodion Shingarev on 13.04.19.
#
OCPath="$1"
if [ "${OCPath}" = "" ]; then
echo "Usage ./create_vault.sh path/to/EFI/OC"
exit 1
fi
if [ ! -d "${OCPath}" ]; then
echo "Path $OCPath is missing!"
exit 1
fi
if [ ! -x /usr/bin/env ] || [ ! -x /usr/bin/find ] || [ ! -x /bin/rm ] || [ ! -x /usr/bin/sed ] || [ ! -x /usr/bin/openssl ] || [ ! -x /usr/bin/awk ] || [ ! -x /usr/bin/sort ] || [ ! -x /usr/bin/xxd ]; then
echo "Unix environment is broken!"
exit 1
fi
abort() {
/bin/rm -rf vault.plist vault.sig /tmp/vault_hash
echo "Fatal error: ${1}!"
exit 1
}
# plist output functions so we don't need PlistBuddy
write_header() {
cat <<EOF > "$1"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Files</key>
<dict>
EOF
}
write_file_name_and_hash() {
{
echo -e "\t\t<key>${2}</key>"
echo -e "\t\t<data>"
echo -e -n "\t\t"
cat "$3"
echo -e "\t\t</data>"
} >> "$1"
}
write_footer() {
cat <<EOF >> "$1"
</dict>
<key>Version</key>
<integer>1</integer>
</dict>
</plist>
EOF
}
echo "Chose ${OCPath} for hashing..."
cd "${OCPath}" || abort "Failed to reach ${OCPath}"
/bin/rm -rf vault.plist vault.sig || abort "Failed to cleanup"
echo "Hashing files in ${OCPath}..."
write_header vault.plist
/usr/bin/find . -not -path '*/\.*' -type f \
\( ! -iname ".*" \) \
\( ! -iname "vault.*" \) \
\( ! -iname "MemTest86.log" \) \
\( ! -iname "MemTest86-Report-*.html" \) \
\( ! -iname "OpenCore.efi" \) | env LC_COLLATE=POSIX /usr/bin/sort | while read -r fname; do
fname="${fname#"./"}"
wname="${fname//\//\\\\}"
sha=$(/usr/bin/openssl sha256 "${fname}" | /usr/bin/awk '{print $2}') || abort "Failed to hash ${fname}"
if [ "${#sha}" != 64 ] || [ "$(echo "$sha"| /usr/bin/sed 's/^[a-f0-9]*$//')" ]; then
abort "Got invalid hash: ${sha}!"
fi
echo "${wname}: ${sha}"
echo "${sha}" | /usr/bin/xxd -r -p | /usr/bin/openssl base64 > /tmp/vault_hash || abort "Hashing failure"
write_file_name_and_hash vault.plist "${wname}" /tmp/vault_hash
done
/bin/rm -rf /tmp/vault_hash
write_footer vault.plist
echo "All done!"
exit 0

View File

@@ -0,0 +1,72 @@
#!/bin/sh
abort() {
echo "Fatal error: ${1}!"
exit 1
}
# shellcheck disable=SC2317,SC2329
cleanup() {
echo "Cleaning up key"
rm -rf "${KeyPath}"
}
if [ ! -x /usr/bin/dirname ] || [ ! -x /bin/chmod ] || [ ! -x /bin/mkdir ] || [ ! -x /bin/rm ] || [ ! -x /usr/bin/strings ] || [ ! -x /usr/bin/grep ] || [ ! -x /usr/bin/awk ] || [ ! -x /bin/dd ] || [ ! -x /usr/bin/uuidgen ] ; then
abort "Unix environment is broken!"
fi
cd "$(/usr/bin/dirname "$0")" || abort "Failed to enter working directory!"
OCPath="$1"
if [ "$OCPath" = "" ]; then
OCPath=../../EFI/OC
fi
KeyPath="/tmp/$(/usr/bin/uuidgen)"
OCBin="${OCPath}/OpenCore.efi"
PubKey="${KeyPath}/vault.pub"
if [ ! -d "${OCPath}" ]; then
abort "Path ${OCPath} is missing!"
fi
if [ ! -f "${OCBin}" ]; then
abort "OpenCore.efi is missing!"
fi
if [ ! -x ./RsaTool ] || [ ! -x ./create_vault.sh ]; then
if [ -f ./RsaTool ]; then
/bin/chmod a+x ./RsaTool || abort "Failed to set permission for RsaTool"
else
abort "Failed to find RsaTool!"
fi
if [ -f ./create_vault.sh ]; then
/bin/chmod a+x ./create_vault.sh || abort "Failed to set permission for create_vault.sh"
else
abort "Failed to find create_vault.sh!"
fi
fi
trap cleanup EXIT INT TERM
if [ ! -d "${KeyPath}" ]; then
/bin/mkdir -p "${KeyPath}" || abort "Failed to create path ${KeyPath}"
fi
./create_vault.sh "${OCPath}" || abort "create_vault.sh returns errors!"
echo "Signing ${OCBin}..."
./RsaTool -sign "${OCPath}/vault.plist" "${OCPath}/vault.sig" "${PubKey}" || abort "Failed to patch ${PubKey}"
echo "Bin-patching ${OCBin}..."
off=$((0x$(/usr/bin/hexdump -C "${OCBin}" | /usr/bin/grep "=BEGIN OC VAULT=" | /usr/bin/awk '{print $1}') + 16))
if [ "${off}" -le 16 ]; then
abort "${OCBin} is borked"
fi
/bin/dd of="${OCBin}" if="${PubKey}" bs=1 seek="${off}" count=528 conv=notrunc || abort "Failed to bin-patch ${OCBin}"
echo "All done!"
exit 0

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,184 @@
# Provides standalone GOP driver for EFI era Mac Pro and iMac
## Releases
EnableGop version (OpenCore version)
### 1.4 (0.9.3)
- Incorporates recent updates to OpenCore console control code, but no difference in behaviour compared
to version 1.3 is expected on any supported systems.
### 1.3 (0.9.2)
- Included fix to GopBurstMode for non-standard frame buffer information on AMD Radeon HD 7970 and similar
- Applied GopBurstMode even on natively supported cards, as it can provide a noticable speed up
### 1.2 (0.9.1)
- Added GopBurstMode support
*Note 1*: This should provide faster GOP rendering on all EnableGopDirect systems; and rendering at least at
the same speed as before, and on some systems noticeably faster than before, on almost all EnableGop systems.
*Note 2*: The compressed driver for version 1.2 is 1KB larger than for version 1.1, so for AMD GPU firmware which is
tight on space version 1.1 may be used instead to avoid the need for VGA stripping to make additional space.
### 1.1 (0.9.0)
- Fixed early verbose boot lines appearing over picker
- Added EnableGop version number to UI section
### 1.0 (0.8.9)
- Initial public release
## Status
**Current status: Beta release.**
This driver has been tested and is working on several iMac models
with several different GPUs, and on several MacPro4,1/5,1 machines with several different GPUs. However, in the worst
case (and still possible) scenario, an incompatible or incorrectly installed driver
in firmware may brick your hardware.
*In all cases take a backup of the main firmware or GPU firmware which you are modifying, and confirm that
you can successfully restore from this, before starting.*
## Recovery from bricked hardware
- If attempting main firmware insertion on a MacPro4,1/5,1, for recovery from a bricked device you will either
need a Matt card (which may breach intellectual property laws in some jurisdictions) or the ability to
desolder and reprogram your own SPI flash chip.
- If testing via main firmware insertion on an iMac, you will need the ability to disassemble your iMac and
reprogram its SPI flash chip using a SOIC clip attached to a CH341A controller running on another computer.
- If testing via GPU firmware insertion (iMac or Mac Pro), you will need the ability to disassemble your system,
likely remove the heat sink from the graphics card, and then reprogram its SPI flash chip using a SOIC
clip attached to a CH341A controller running on another computer.
- If testing via GPU firmware insertion, in some cases it may also be possible
to use physical electrical connection to your GPU in order to enable booting with no graphics even though the GPU
is present, then connect to your machine with `ssh` (which must have been enabled beforehand) and reprogram the GPU
firmware. Advice on this headless boot approach is not provided here, but may be found for instance on the iMac GPU
related forum threads listed below.
*If you are not familiar with the above procedures, you are strongly recommended to wait for further testing by
users who are. No further help can be provided here, and you proceed entirely at your own risk.*
## Summary
Targetting EFI-era (~2009-2012) MacPro4,1/5,1 and iMac firmware, this driver gathers and injects the parts of
OpenCore needed for pre-boot graphics support with non-natively supported GPUs.
The requirements for using this driver are:
- EFI-era (~2009-2012) MacPro4,1/5,1 or iMac with most recent main firmware.
- A GPU which does not produce native pre-boot graphics (such as native picker when pressing ALT key during boot)
before OpenCore starts (otherwise, you do not need it).
- A GPU which produces graphics when using OpenCore (this must include successfully showing the native Apple boot
picker when started via the latest version of OpenCore tool `BootKicker.efi`) (otherwise, the driver will not work).
- *Note*: If your OpenCore installation includes a required GOP driver for your graphics card, then you would
also need to burn that driver to the firmware of your graphics card in order to obtain pre-OpenCore graphics;
instructions for this are outside the scope of this tutorial, although the procedures required for modifying
GPU firmware are similar to what is covered here.
Note that such a driver is added by the OCLP **Enable AMD GOP** option, which is enabled automatically on some
systems by recent versions of OpenCore Legacy Patcher, as a way to enable the OpenCore menu in cards such as ex-mining GPUs.
When installed, the driver should enable:
- Native boot picker via ALT key
- Firmware password UI
- Target disk mode UI
- macOS boot progress screen
- etc.
Compiled versions of the driver files and these instructions may be found in the `Utilities/EnableGop`
directory of the OpenCore release package.
For GPUs needing `DirectGopRendering` in OpenCore configuration, use `EnableGopDirect.efi`, otherwise use `EnableGop.efi`
as it renders faster on most other systems.
The driver may be installed to GPU or main motherboard firmware. It is expected that most Mac Pro users will use main firmware insertion
and most iMac users will chose GPU firmware insertion, however both techniques work on both systems (but it is harder to modify the
iMac main firmware, since there is no simple way to enable writing to it).
Further discussion and community support for this driver is available at:
- https://forums.macrumors.com/threads/pre-opencore-gop-support-for-efi-era-imacs-and-mac-pros.2378942/
## Usage
## Install to main firmware
For reading and writing to main firmware on the Mac Pro, @Macschrauber's [Rom Dump](https://github.com/Macschrauber/Macschrauber-s-Rom-Dump) works
well. Alternatively the kexts and executables which this uses can be sourced individually (or extracted from the Rom Dump app) and
run from the command line.
The main firmware on the iMac cannot be updated without an initial hardware flash (SOIC clip plus CH341A controller), therefore
the recommended approach on iMac systems is [GPU firmware injection](#install-to-gpu-firmware). However, the below instructions for firmware
injection do work, if you are willing to do a hardware flash of the resulting firmware file, or if you have already
[unprotected your iMac firmware](https://forums.macrumors.com/threads/imac-2011-see-more-uefi-firmware-mod.2257435/page-3?post=31087001#post-31087001) -
which reduces security, and is only recommended for those actively developing firmware modifications.
The `.ffs` file provided in this directory can be manually added to the extracted firmware file using [`UEFITool`](https://github.com/LongSoft/UEFITool),
or automatically added using @dosdude1's [`DXEInject`](https://dosdude1.com/apps/). Once more, if you are not familiar with these procedures,
you are recommended to proceed with extreme caution.
### Using DXEInject
To install the driver via `DXEInject`, the command is:
- `DXEInject {original}.rom {modified}.rom EnableGop.ffs`
The file `{modifed}.rom` is ready for burning, although the result can be checked using UEFITool, if required.
> *Note*: If only reading a file with UEFITool, the latest version is recommended, as it provides the most information.
For writing, the older version 0.25.1 must be used, as described below.
### Using UEFITool
The `.ffs` file may be inserted anywhere within the same firmware volume which contains `DuetBds`
(file GUID `A6F691AC-31C8-4444-854C-E2C1A6950F92`). However, for simplicity, these instructions
will insert it in the same place that `DXEInject` does:
- Use UEFITool 0.25.1 (it must be that old version, not the newer NE or new engine versions, which
cannot yet edit)
- Perform a header-only GUID search for `BAE7599F-3C6B-43B7-BDF0-9CE07AA91AA6`
- Double-click on the search result
- Right-click on the DXE driver with that GUID which should then appear
- Choose "Insert after..." and select `EnableGop.ffs`
- Save the modified firmware
The end result, after saving and re-loading, should look like this:
<img src="UEFITool_Inserted_Screenshot.png">
## Install to GPU firmware
Instructions and a script for inserting the driver into Nvidia or AMD GPU firmware (aka VBIOS) are provided.
Please note all the cautions already given above about the difficulty of recovering, unless you are familiar with
the procedures necessary, if this process fails.
To use the provided `vBiosInsert.sh` script:
- Locate an appropriate version of the `nvflash` tool (Nvidia) or `amdvbflash` tool (AMD) (both are available for
Linux and Windows), which can be used to read from and write to the GPU firmware.
- Use that tool to read a copy of the GPU firmware.
- Run `./vBiosInsert.sh [-a|-n] {original}.rom EnableGop.efi {modified}.rom`, with `-a` for AMD and `-n` for Nvidia.
- If you have any problems with `vBiosInsert.sh` from a specific release
of EnableGop, please try the version included with the latest release of OpenCore
before reporting any issues.
The script receives updates to support additional graphics cards independently
of any bumps to the release version of EnableGop. If you need to, you can use
the latest version of `vBiosInsert.sh` to inject older versions of EnableGop.
- The new file `{modified}.rom` may be burnt to the GPU firmware.
In the case of AMD, considerably less space is normally available, due to a strict limit of 128k for legacy and EFI
parts of the larger ROM image. If there is not enough space (i.e. script reports
data would be truncated) then it is necessary to [strip some legacy VGA parts of the
GPU firmware](https://github.com/Ausdauersportler/IMAC-EFI-BOOT-SCREEN/wiki/Deleting-the-VGA). This is beyond the scope
of these instructions.
If required to manually detect the GOP offset (this should normally be autodetected):
> Using a hex editor, search in the GPU firmware dump for the byte sequence `F1 0E 00 00` with the byte sequence `55 AA` coming
close before it; the start address of the `55 AA` is the GOP offset value needed.
For further information on GPU firmware modification, see:
- https://forums.macrumors.com/threads/2011-imac-graphics-card-upgrade.1596614/
- https://forums.macrumors.com/threads/imac-2011-maxwell-and-pascal-gpu-upgrade.2300989/
- https://github.com/Ausdauersportler/IMAC-EFI-BOOT-SCREEN/wiki
- https://winraid.level1techs.com/t/amd-and-nvidia-gop-update-no-requests-diy/30917

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 KiB

View File

@@ -0,0 +1,334 @@
#!/bin/bash
#
# Copyright © 2023 Mike Beaton. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# Insert EFI into AMD or Nvidia VBIOS.
# Tested back to Mac OS X 10.11 El Capitan.
#
usage() {
echo "Usage: ./${SELFNAME} [args] {rom-file} {efi-file} {out-file}"
echo "Args:"
echo " -a : AMD"
echo " -n : Nvidia"
echo " -o {GOP offset} : GOP offset (auto-detected if Homebrew grep is installed)"
echo " Can specify 0x{hex} or {decimal}"
echo " -t {temp dir} : Specify temporary directory, and keep temp files"
echo " -m {max size} : Specify VBIOS max size (Nvidia only)"
echo " (For AMD first 128KB is modified, any remainder is kept)"
echo "Examples:"
echo " ./${SELFNAME} -n -o 0xFC00 nv.rom EnableGop.efi nv_mod.rom"
echo " ./${SELFNAME} -n nv.rom EnableGop.efi nv_mod.rom"
echo " ./${SELFNAME} -a amd.rom EnableGop.efi amd_mod.rom"
echo ""
}
SELFNAME="$(/usr/bin/basename "${0}")"
commands=(
"EfiRom"
"UEFIRomExtract"
"hexdump"
"grep"
)
FOUND=1
for command in "${commands[@]}"; do
if ! command -v "$command" 1>/dev/null ; then
echo "${command} not available!"
FOUND=0
fi
done
if [ "$FOUND" -eq 0 ] ; then
exit 1
fi
AMD=0
AMD_SAFE_SIZE="0x20000"
NVIDIA_SAFE_SIZE="0x40000"
GOP_OFFSET="-"
NVIDIA=0
POS=0
TRUNCATE=0
while true; do
if [ "$1" = "-a" ] ; then
AMD=1
NVIDIA=0
shift
elif [ "$1" = "-n" ] ; then
AMD=0
NVIDIA=1
shift
elif [ "$1" = "-o" ] ; then
shift
if [ "$1" != "" ] && ! [ "${1:0:1}" = "-" ] ; then
GOP_OFFSET=$1
shift
else
echo "No GOP offset specified" && exit 1
fi
elif [ "$1" = "-s" ] ; then # semi-secret option to modify AMD safe size
shift
if [ "$1" != "" ] && ! [ "${1:0:1}" = "-" ] ; then
AMD_SAFE_SIZE=$1
shift
else
echo "No AMD safe size specified" && exit 1
fi
elif [ "$1" = "-m" ] ; then
shift
if [ "$1" != "" ] && ! [ "${1:0:1}" = "-" ] ; then
TRUNCATE=1
TRUNCATE_SIZE=$1
NVIDIA_SAFE_SIZE=$TRUNCATE_SIZE
shift
else
echo "No max size specified" && exit 1
fi
elif [ "$1" = "-t" ] ; then
shift
if [ "$1" != "" ] && ! [ "${1:0:1}" = "-" ] ; then
TEMP_DIR=$1
shift
else
echo "No temp dir specified" && exit 1
fi
elif [ "${1:0:1}" = "-" ] ; then
echo "Unknown option: ${1}" && exit 1
elif [ "$1" != "" ] ; then
case "$POS" in
0 )
ROM_FILE="$1"
;;
1 )
EFI_FILE="$1"
;;
2 )
OUT_FILE="$1"
;;
* )
echo "Too many filenames specified" && exit 1
;;
esac
POS=$(($POS+1))
shift
else
break
fi
done
if [ "$ROM_FILE" = "" ] ||
[ "$EFI_FILE" = "" ] ||
[ "$OUT_FILE" = "" ] ; then
usage
exit 0
fi
if [ "$AMD" -eq 0 ] && [ "$NVIDIA" -eq 0 ] ; then
echo "Must specify -a or -n" && exit 1
fi
if [ "$AMD" -eq 1 ] && [ "$TRUNCATE" -eq 1 ] ; then
echo "-m is not valid with -a" && exit 1
fi
if [ "$TEMP_DIR" != "" ] ; then
mkdir -p "$TEMP_DIR" || exit 1
tmpdir="$TEMP_DIR"
else
# https://unix.stackexchange.com/a/84980/340732
tmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'vbios') || exit 1
fi
ORIGINAL_SIZE=$(stat -f%z "$ROM_FILE") || exit 1
if [ "$AMD" -eq 1 ] ; then
# For AMD we can only modify the first 128KB, anything above 128KB
# should not be moved, and is not mapped to memory visible by the CPU
# for loading EFI drivers.
TRUNCATE=1
TRUNCATE_SIZE="$AMD_SAFE_SIZE"
# Also works, with empty keep_part.rom, in the atypical case where we
# are provided with only the used part of the ROM below 128KB.
dd bs=1 if="$ROM_FILE" of="$tmpdir/modify_part.rom" count="$AMD_SAFE_SIZE" 2>/dev/null || exit 1
dd bs=1 if="$ROM_FILE" of="$tmpdir/keep_part.rom" skip="$AMD_SAFE_SIZE" 2>/dev/null || exit 1
else
if [ "$TRUNCATE" -eq 0 ] ; then
# If original size is a plausible ROM size (exact power of two, 64KB or
# larger; 64KB chosen partly for neat regexp) treat it as the full available
# size of the VBIOS chip unless overridden with -m.
printf '%x' "$ORIGINAL_SIZE" | grep -Eq "^(1|2|4|8)0000+$" && TRUNCATE=1
if [ "$TRUNCATE" -eq 1 ] ; then
echo "Detected standard ROM size."
TRUNCATE_SIZE="$ORIGINAL_SIZE"
else
if [ "$ORIGINAL_SIZE" -gt "$((NVIDIA_SAFE_SIZE))" ] ; then
echo " - File size of ${ORIGINAL_SIZE} bytes must be no more than $((NVIDIA_SAFE_SIZE)) bytes; use -m or check file" && exit 1
fi
TRUNCATE=1
TRUNCATE_SIZE="$NVIDIA_SAFE_SIZE"
fi
fi
cp "$ROM_FILE" "$tmpdir/modify_part.rom" || exit 1
fi
if [ "$GOP_OFFSET" = "-" ] ; then
echo "Auto-detecting GOP offset..."
# nicer techniques which do not assume nice alignment of what is being searched for do not work on older Mac OS X
OUTPUT=$(hexdump -C "$tmpdir/modify_part.rom" | grep '55 aa .. .. f1 0e 00 00' | head -1)
# Make macOS bash to split as expected:
# shellcheck disable=SC2206
GOP_ARRAY=($OUTPUT)
GOP_OFFSET=${GOP_ARRAY[0]}
if [ "$GOP_OFFSET" != "" ] ; then
GOP_OFFSET="0x${GOP_OFFSET}"
GOP_OFFSET=$(($GOP_OFFSET))
else
GOP_OFFSET=-1
fi
if [ "$GOP_OFFSET" -eq -1 ] ; then
echo " - No GOP found in ROM!" && exit 1
fi
fi
dd bs=1 if="$tmpdir/modify_part.rom" of="$tmpdir/original_first_part.rom" count=$(($GOP_OFFSET)) 2>/dev/null || exit 1
dd bs=1 if="$tmpdir/modify_part.rom" of="$tmpdir/original_last_part.rom" skip=$(($GOP_OFFSET)) 2>/dev/null || exit 1
echo "Compressing EFI using EfiRom..."
if [ "$AMD" -eq 1 ] ; then
EfiRom -o "$tmpdir/insert.rom" -ec "$EFI_FILE" -f 0xAAAA -i 0xBBBB -l 0x30000 -p || exit 1
else
EfiRom -o "$tmpdir/insert.rom" -ec "$EFI_FILE" -f 0xAAAA -i 0xBBBB -l 0x30000 || exit 1
fi
if [ "$NVIDIA" -eq 1 ] ; then
dd bs=1 if="$tmpdir/insert.rom" of="$tmpdir/insert_first_part" count=$((0x38)) 2>/dev/null || exit 1
dd bs=1 if="$tmpdir/insert.rom" of="$tmpdir/insert_last_part" skip=$((0x38)) 2>/dev/null || exit 1
# TODO: truncation logic should be fixed for when there is not enough spare padding in output of EfiRom;
# we currently assume without checking that there is enough space to fit in the NPDE header (if not,
# script will report failure to verify with UEFIRomExtract below).
INSERT_SIZE=$(stat -f%z "$tmpdir/insert.rom") || exit 1
# Calculate NPDE size from original GOP and add to new image
EfiImageOffset=$(dd if="$tmpdir/original_last_part.rom" ibs=1 skip=$((0x16)) count=2 2>/dev/null | od -t u4 -An | xargs)
if [ "$EfiImageOffset" -eq $((0x50)) ] ; then
NpdeSize=$((0x18))
elif [ "$EfiImageOffset" -eq $((0x4C)) ] ; then
NpdeSize=$((0x14))
elif [ "$EfiImageOffset" -eq $((0x38)) ] ; then
NpdeSize=0
else
# Note: We need at least 0x14 NPDE size for the patch-ups we do below for size and end-marker to make sense
printf "Unsupported EFI Image Offset 0x%x, cannot calculate NPDE Size!\n" "$EfiImageOffset"
exit 1
fi
fi
if [ "$NVIDIA" -eq 1 ] && [ "$NpdeSize" -ne 0 ] ; then
echo "Adding Nvidia header..."
dd bs=1 if="$tmpdir/original_last_part.rom" of="$tmpdir/insert_first_part" skip=$((0x38)) seek=$((0x38)) count="$NpdeSize" 2>/dev/null || exit 1
cat "$tmpdir/insert_first_part" "$tmpdir/insert_last_part" > "$tmpdir/insert_oversize.rom" || exit 1
# Note: `truncate` command is not present by default on macOS
dd bs=1 if="$tmpdir/insert_oversize.rom" of="$tmpdir/insert_fixed.rom" count="$INSERT_SIZE" 2>/dev/null || exit 1
# Patch size in NPDE
dd bs=1 if="$tmpdir/insert.rom" of="$tmpdir/insert_fixed.rom" skip=$((0x2)) seek=$((0x48)) count=1 conv=notrunc 2>/dev/null || exit 1
else
cp "$tmpdir/insert.rom" "$tmpdir/insert_fixed.rom" || exit 1
fi
# patch with vendor and device id from original GOP
dd bs=1 if="$tmpdir/original_last_part.rom" of="$tmpdir/insert_fixed.rom" skip=$((0x20)) seek=$((0x20)) count=4 conv=notrunc 2>/dev/null || exit 1
if [ "$NVIDIA" -eq 1 ] && [ "$NpdeSize" -ne 0 ] ; then
# Patch EFI image offset in PCIR
dd bs=1 if="$tmpdir/original_last_part.rom" of="$tmpdir/insert_fixed.rom" skip=$((0x16)) seek=$((0x16)) count=1 conv=notrunc 2>/dev/null || exit 1
# Patch end marker in NPDE in fixed ROM (leave PCIR correct and EFI extractable from fixed ROM)
echo -n -e '\x00' | dd bs=1 of="$tmpdir/insert_fixed.rom" seek=$((0x4A)) conv=notrunc 2>/dev/null || exit 1
fi
echo "Combining..."
cat "$tmpdir/original_first_part.rom" "$tmpdir/insert_fixed.rom" "$tmpdir/original_last_part.rom" > "$tmpdir/combined.rom" || exit 1
# If new ROM is larger than truncate size, determine overflow by seeing
# whether truncated ROM still has padding.
# For Nvidia we would need to parse and navigate NVGI and NPDE headers
# to calculate true occupied VBIOS space. To calcuate AMD occupation below
# 128KB limit, we could navigate normal PCI expansion ROM headers.
COMBINED_SIZE=$(stat -f%z "$tmpdir/combined.rom") || exit 1
if [ "$COMBINED_SIZE" -le "$(($TRUNCATE_SIZE))" ] ; then
TRUNCATE=0
fi
if [ "$TRUNCATE" -eq 1 ] ; then
echo "Truncating to original size..."
dd bs=1 if="$tmpdir/combined.rom" of="$tmpdir/truncated.rom" count="$TRUNCATE_SIZE" 2>/dev/null || exit 1
COUNT=$(hexdump -v -e '1/8 " %016X\n"' "$tmpdir/truncated.rom" | tail -n 8 | grep "FFFFFFFFFFFFFFFF" | wc -l)
if [ "$COUNT" -ne 8 ] ; then
# Some Nvidia ROMs, at least, incorrectly have 00000000 padding after active contents
# (it is incorrect, since writing only active contents using nvflash resets the rest to ffffffff).
# May also be relevant if we ever have any truly 00000000 default ROM images.
COUNT=$(hexdump -v -e '1/8 " %016X\n"' "$tmpdir/truncated.rom" | tail -n 8 | grep "0000000000000000" | wc -l)
fi
if [ "$COUNT" -ne 8 ] ; then
echo " - Not enough space within $((TRUNCATE_SIZE / 1024))k limit - aborting!" && exit 1
fi
if [ "$AMD" -eq 1 ] ; then
cat "$tmpdir/truncated.rom" "$tmpdir/keep_part.rom" > "$OUT_FILE" || exit 1
else
cp "$tmpdir/truncated.rom" "$OUT_FILE" || exit 1
fi
else
cp "$tmpdir/combined.rom" "$OUT_FILE" || exit 1
fi
# patch end marker in PCIR in out file
echo -n -e '\x00' | dd bs=1 of="$OUT_FILE" seek=$(($GOP_OFFSET + 0x31)) conv=notrunc 2>/dev/null || exit 1
printf "Verifying (starting at 0x%X)...\n" "$GOP_OFFSET"
dd bs=1 if="$OUT_FILE" of="$tmpdir/out_efi_part.rom" skip=$(($GOP_OFFSET)) 2>/dev/null || exit 1
# UEFIRomExtract error messages are on stdout, so we cannot suppress unwanted normal output here
UEFIRomExtract "$tmpdir/out_efi_part.rom" "$tmpdir/extracted.efi" || exit 1
ERROR=0
diff "$tmpdir/extracted.efi" "$EFI_FILE" 1>/dev/null || ERROR=1
if [ "$ERROR" -ne 0 ] ; then
echo " - Failure comparing extracted EFI to original!"
fi
OLD_EFI_COUNT=$(EfiRom -d "$tmpdir/original_last_part.rom" | grep "0x0EF1" | wc -l) || exit 1
OLD_EFI_COUNT=$(($OLD_EFI_COUNT)) || exit 1
NEW_EFI_COUNT=$(EfiRom -d "$tmpdir/out_efi_part.rom" | grep "0x0EF1" | wc -l) || exit 1
NEW_EFI_COUNT=$(($NEW_EFI_COUNT)) || exit 1
if [ "$NEW_EFI_COUNT" -ne $(($OLD_EFI_COUNT + 1)) ] ; then
echo " - ${OLD_EFI_COUNT} EFI parts in original ROM, and detected ${NEW_EFI_COUNT} EFI parts in modified ROM, expected $(($OLD_EFI_COUNT + 1))!"
ERROR=1
fi
if [ "$ERROR" -eq 0 ] ; then
echo "SUCCESS."
else
echo "*** WARNING - FAIL ***"
fi
if [ "$TEMP_DIR" = "" ] ; then
rm -rf "$tmpdir" || exit 1
fi
echo "Done."

View File

@@ -0,0 +1,44 @@
#!/bin/sh
## @file
# Copyright (c) 2022, joevt. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
##
serial_dev=$(ioreg -p IODeviceTree -lw0 | perl -e '
$ioregpath=""; $pcipath=""; while (<>) {
if ( /^([ |]*)\+\-o (.+) <class (\w+)/ ) {
$indent = (length $1) / 2; $name = $2; $class = $3;
$ioregpath =~ s|^((/[^/]*){$indent}).*|$1/$name|;
$pcipath =~ s|^((/[^/]*){$indent}).*|$1/|;
$name = ""; $uid = 0; $classcode = "";
}
if ( $class eq "IOACPIPlatformDevice" ) { if ( /^[ |]*"name" = <"([^\"]*)">/ ) { $name = $1; }; if ( /^[ |]*"_UID" = "(\d+)"/ ) { $uid = $1; } }
elsif ( $class eq "IOPCIDevice" ) { if ( /^[ |]*"pcidebug" = "\d+:(\d+):(\d+).*"/ ) { $name = sprintf("Pci(0x%x,0x%x)", $1, $2); } if ( /^[ |]*"class-code" = <(\w+)>/ ) { $classcode = $1; } }
if ( /^[ |]*}/ && $name ) {
if ( $class eq "IOACPIPlatformDevice" ) {
if ($name eq "PNP0A03") { $name = sprintf ("PciRoot(0x%x)", $uid); }
elsif ($name eq "PNP0A08") { $name = sprintf ("PciRoot(0x%x)", $uid); }
elsif ($name =~ /PNP..../) { $name = sprintf ("Acpi(%s,0x%x)", $name, $uid); }
# not translating all ACPI types since we only care about PCI devices
}
$pcipath .= $name;
if ( $classcode eq "02000700" ) {
$serialdevicepath = $pcipath =~ s/.*PciRoot\(0x0\)//r =~ s|/Pci\(||gr =~ s|\)| 00 00 |gr =~ s|0x||gr =~ s|,| |gr =~ s|\b(\w)\b|0\1|gr =~ s|$|FF|r;
print $ioregpath =~ s|/Root/||r . "\n";
print $pcipath =~ s|/*||r . "\n";
print "xxd -p -r <<< \"" . $serialdevicepath . "\" | base64\n";
print "\n";
}
}
}
')
if [ "$serial_dev" != "" ]; then
echo "${serial_dev}"
else
echo "No serial device found!"
fi
exit 0

View File

@@ -0,0 +1,4 @@
FindSerialPort
================
This script finds PCIe serial ports and outputs their paths. Thanks [joevt](https://github.com/joevt) for writing it.

View File

@@ -0,0 +1,131 @@
#!/bin/bash
# Install booter on physical disk.
cd "$(dirname "$0")" || exit 1
if [ ! -f "boot${ARCHS}${DUET_SUFFIX}" ] || [ ! -f boot0 ] || [ ! -f boot1f32 ]; then
echo "Boot files are missing from this package!"
echo "You probably forgot to build DuetPkg first."
exit 1
fi
if [ "$(uname)" = "Linux" ]; then
if [ "$EUID" -ne 0 ]
then echo "Please run this script as root"
exit
fi
if [ "$(which lsblk)" = "" ]; then
echo "lsblk tool is missing! Try installing util-linux package"
exit 1
fi
if [ "$(which fdisk)" = "" ]; then
echo "fdisk tool is missing!"
exit 1
fi
rm -f newbs origbs
echo "Select the disk where you want to install boot files (${ARCHS}${DUET_SUFFIX}):"
lsblk -d | tail -n+2 | cut -d" " -f1
echo "Example: sda"
read -r DRIVE
DRIVE="/dev/${DRIVE}"
if ! lsblk "$DRIVE"; then
echo Disk "${DRIVE}" not found
exit 1
fi
echo "Choose EFI partition on selected disk:"
lsblk -f "${DRIVE}"
echo "Example: sda1"
read -r EFI_PART
EFI_PART="/dev/${EFI_PART}"
if ! lsblk -f "$EFI_PART" | grep -q -e FAT32 -e vfat; then
echo "No FAT32 partition to install"
exit 1
fi
# Write MBR
dd if=boot0 of="$DRIVE" bs=1 count=446 conv=notrunc || exit 1
umount "${EFI_PART}"
dd if="${EFI_PART}" count=1 of=origbs
cp -v boot1f32 newbs
dd if=origbs of=newbs skip=3 seek=3 bs=1 count=87 conv=notrunc
dd if=/dev/random of=newbs skip=496 seek=496 bs=1 count=14 conv=notrunc
dd if=newbs of="${EFI_PART}"
p=/tmp/$(uuidgen)/EFI
mkdir -p "${p}" || exit 1
mount -t vfat "${EFI_PART}" "${p}" -o rw,noatime,uid="$(id -u)",gid="$(id -g)" || exit 1
cp -v "boot${ARCHS}${DUET_SUFFIX}" "${p}/boot" || exit 1
echo Check "${p}" boot drive EFI folder to install OpenCorePkg
DISK_SCHEME=$(fdisk -l "${DRIVE}" | sed -n 's/.*Disklabel type: *//p')
if [ "$DISK_SCHEME" != "gpt" ]; then
BOOT_FLAG=$(dd if="$DRIVE" bs=1 count=1 status=none skip=$((0x1BE)) | od -t x1 -A n | tr -d ' ')
if [ "$BOOT_FLAG" != "80" ]; then
fdisk "$DRIVE" <<END
p
a
1
w
END
fi
fi
else
rm -f newbs origbs
diskutil list
echo "Disable SIP in the case of any problems with installation!!!"
echo "Enter disk number to install OpenDuet (${ARCHS}${DUET_SUFFIX}) to:"
read -r N
if ! diskutil info disk"${N}" | grep -q "/dev/disk"; then
echo Disk "$N" not found
exit 1
fi
if ! diskutil info disk"${N}"s1 | grep -q -e FAT_32 -e EFI; then
echo "No FAT32 partition to install"
exit 1
fi
# Write MBR
sudo fdisk -uy -f boot0 /dev/rdisk"${N}" || exit 1
diskutil umount disk"${N}"s1
sudo dd if=/dev/rdisk"${N}"s1 count=1 of=origbs
cp -v boot1f32 newbs
sudo dd if=origbs of=newbs skip=3 seek=3 bs=1 count=87 conv=notrunc
dd if=/dev/random of=newbs skip=496 seek=496 bs=1 count=14 conv=notrunc
sudo dd if=newbs of=/dev/rdisk"${N}"s1
#if [[ "$(sudo diskutil mount disk"${N}"s1)" == *"mounted" ]]
if sudo diskutil mount disk"${N}"s1 | grep -q mounted; then
cp -v "boot${ARCHS}${DUET_SUFFIX}" "$(diskutil info disk"${N}"s1 | sed -n 's/.*Mount Point: *//p')/boot"
else
p=/tmp/$(uuidgen)/EFI
mkdir -p "${p}" || exit 1
sudo mount_msdos /dev/disk"${N}"s1 "${p}" || exit 1
cp -v "boot${ARCHS}${DUET_SUFFIX}" "${p}/boot" || exit 1
open "${p}"
fi
if diskutil info disk"${N}" | grep -q FDisk_partition_scheme; then
sudo fdisk -e /dev/rdisk"$N" <<-MAKEACTIVE
p
f 1
w
y
q
MAKEACTIVE
fi
fi

View File

@@ -0,0 +1,6 @@
#!/bin/bash
cd "$(dirname "$0")" || exit 1
export ARCHS=IA32
export DUET_SUFFIX=""
source BootInstallBase.sh

View File

@@ -0,0 +1,6 @@
#!/bin/bash
cd "$(dirname "$0")" || exit 1
export ARCHS=IA32
export DUET_SUFFIX="-blockio"
source BootInstallBase.sh

View File

@@ -0,0 +1,6 @@
#!/bin/bash
cd "$(dirname "$0")" || exit 1
export ARCHS=X64
export DUET_SUFFIX=""
source BootInstallBase.sh

View File

@@ -0,0 +1,6 @@
#!/bin/bash
cd "$(dirname "$0")" || exit 1
export ARCHS=X64
export DUET_SUFFIX="-blockio"
source BootInstallBase.sh

View File

@@ -0,0 +1,186 @@
#!/bin/bash
# Build QEMU image, example:
# qemu-system-x86_64 -drive file=$QEMU_IMAGE/OpenCore.RO.raw -serial stdio \
# -usb -device usb-kbd -device usb-mouse -s -m 8192
cd "$(dirname "$0")" || exit 1
usage () {
echo "Usage: $(basename "$0") [X64|IA32|EFI]"
exit 1
}
missing_files () {
echo "Boot files are missing from this package!"
echo "You probably forgot to build DuetPkg first."
exit 1
}
if [ $# -gt 1 ]; then
usage
elif [ $# -eq 1 ]; then
if [ "$1" != "X64" ] && [ "$1" != "IA32" ] && [ "$1" != "EFI" ]; then
usage
fi
ARCHS=$1
elif [ "${ARCHS}" = "" ]; then
usage
fi
if [ "$ARCHS" != "EFI" ]; then
if [ ! -f "boot${ARCHS}" ] || [ ! -f boot0 ] || [ ! -f boot1f32 ]; then
missing_files
fi
fi
if [ "$(which qemu-img)" = "" ]; then
echo "QEMU installation missing"
exit 1
fi
if [ ! -d ROOT ]; then
echo "No ROOT directory with ESP partition contents"
exit 1
fi
if [ "$(uname)" = "Linux" ]; then
if [ "$EUID" -ne 0 ]
then echo "Please run this script as root"
exit
fi
if [ "$(which qemu-nbd)" = "" ]; then
echo "Your QEMU installation doesn't contain qemu-nbd tool!"
exit 1
fi
if [ "$(which mkfs.vfat)" = "" ]; then
echo "mkfs.vfat not found, dosfstools package is missing?"
exit 1
fi
if [ "$(which fdisk)" = "" ]; then
echo "fdisk tool is missing!"
exit 1
fi
IMAGE=OpenCore.raw
DIR="$IMAGE.d"
NBD=/dev/nbd0
rm -rf "$DIR"
rm -f "$IMAGE" newbs origbs
mkdir -p "$DIR"
# Create 200M MS-DOS bootable disk image with 1 FAT32 partition
modprobe nbd
qemu-img create -f raw "$IMAGE" 200M
fdisk "$IMAGE" << END
o
n
p
1
2048
409599
t
b
a
w
END
qemu-nbd -f raw --connect="$NBD" "$IMAGE"
# Wait a it after mounting
sleep 2
mkfs.vfat -F32 ${NBD}p1
if [ "$ARCHS" != "EFI" ]; then
# Copy boot1f32 into FAT32 Boot Record
dd if=${NBD}p1 of=origbs count=1
cp -v boot1f32 newbs
dd if=origbs of=newbs skip=3 seek=3 bs=1 count=87 conv=notrunc
dd if=/dev/random of=newbs skip=496 seek=496 bs=1 count=14 conv=notrunc
dd if=newbs of=${NBD}p1 conv=notrunc
fi
mount -t vfat ${NBD}p1 "$DIR" -o rw,noatime,uid="$(id -u)",gid="$(id -g)"
sleep 2
if [ "$ARCHS" != "EFI" ]; then
# Copy boot file into FAT32 file system
cp -v "boot${ARCHS}" "$DIR/boot"
fi
# Copy ESP contents into FAT32 file system
cp -rv ROOT/* "$DIR"
# Remove temporary files
sleep 2
umount -R "$DIR"
qemu-nbd -d "$NBD"
rm -r "$DIR"
rm newbs origbs
if [ "$ARCHS" != "EFI" ]; then
# Copy boot0 into MBR
dd if=boot0 of="$IMAGE" bs=1 count=446 conv=notrunc
fi
chown "$(whoami)" "$IMAGE"
elif [ "$(uname)" = "Darwin" ]; then
rm -f OpenCore.dmg.sparseimage OpenCore.RO.raw OpenCore.RO.dmg
hdiutil create -size 200m -layout "UNIVERSAL HD" -type SPARSE -o OpenCore.dmg
newDevice=$(hdiutil attach -nomount OpenCore.dmg.sparseimage |head -n 1 | awk '{print $1}')
echo newdevice "$newDevice"
diskutil partitionDisk "${newDevice}" 1 MBR fat32 TEST R
# boot install script
diskutil list
N=$(echo "$newDevice" | tr -dc '0-9')
echo "Will be installed to Disk ${N}"
if [[ ! $(diskutil info disk"${N}" | sed -n 's/.*Device Node: *//p') ]]
then
echo Disk "$N" not found
exit 1
fi
FS=$(diskutil info disk"${N}"s1 | sed -n 's/.*File System Personality: *//p')
echo "$FS"
if [ "$FS" != "MS-DOS FAT32" ]
then
echo "No FAT32 partition to install"
exit 1
fi
if [ "$ARCHS" != "EFI" ]; then
# Write MBR
sudo fdisk -f boot0 -u /dev/rdisk"${N}"
# Write boot1f32
diskutil umount disk"${N}"s1
sudo dd if=/dev/rdisk"${N}"s1 count=1 of=origbs
cp -v boot1f32 newbs
sudo dd if=origbs of=newbs skip=3 seek=3 bs=1 count=87 conv=notrunc
dd if=/dev/random of=newbs skip=496 seek=496 bs=1 count=14 conv=notrunc
sudo dd if=newbs of=/dev/rdisk"${N}"s1
diskutil mount disk"${N}"s1
# Copy boot
cp -v "boot${ARCHS}" "$(diskutil info disk"${N}"s1 | sed -n 's/.*Mount Point: *//p')/boot"
fi
cp -rv ROOT/* "$(diskutil info disk"${N}"s1 | sed -n 's/.*Mount Point: *//p')"
if [ "$ARCHS" != "EFI" ] && [ "$(diskutil info disk"${N}" | sed -n 's/.*Content (IOContent): *//p')" == "FDisk_partition_scheme" ]; then
sudo fdisk -e /dev/rdisk"$N" <<-MAKEACTIVE
p
f 1
w
y
q
MAKEACTIVE
fi
hdiutil detach "$newDevice"
hdiutil convert -format UDRO OpenCore.dmg.sparseimage -o OpenCore.RO.dmg
qemu-img convert -f dmg -O raw OpenCore.RO.dmg OpenCore.RO.raw
fi

View File

@@ -0,0 +1,7 @@
BootInstall
===========
This tool installs legacy DuetPkg environment on GPT-formatted disk
to enable UEFI environment on BIOS-based systems.
Source code: https://github.com/acidanthera/DuetPkg

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,767 @@
#!/bin/bash
#
# Copyright © 2022 Mike Beaton. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# Examine log at /var/log/org.acidanthera.nvramhook.launchd/launchd.log.
#
# Script can run immediately or be installed as daemon or logout hook. Installs as
# launch daemon on Yosemite and above, which is the only supported approach that on
# shutdown reads NVRAM after macOS installer vars are set, in more recent macOS
# installers (e.g. Monterey).
#
# Non-installed script can be run directly as 'daemon', 'agent' or 'logout' hook by
# specifying one of those options on the command line. For 'logout' this saves NVRAM
# immediately, for 'daemon' or 'agent' this waits to be terminated e.g. CTRL+C before
# saving.
#
# Credits:
# - Rodion Shingarev (with additional contributions by vit9696 and PMheart) for
# original LogoutHook.command
# - Rodion Shingarev for nvramdump command
# - Mike Beaton for this script
#
usage() {
echo "Usage: ${SELFNAME} [install|uninstall|status] [logout|daemon|agent|both]"
echo " - [install|uninstall] without type uses recommended type for macOS version"
echo " - 'status' shows status of agent, daemon and logout hook"
echo " - 'status daemon' or 'status agent' give additional detail"
echo ""
}
# Non-sudo log only if agent will use it, so logfile has been chmod-ed at install.
# If only daemon or logout hook will use it, explicit sudo required at install
# time, but harmless when running.
doLog() {
if [ ! "$AGENT" = "1" ] ; then
# macOS recreates this for daemon at reboot, but preferable
# to continue logging immediately after any log cleardown,
# also never recreated except for this with logout hook
sudo mkdir -p "${LOGDIR}"
# 'sudo tee' to write to log file as root (could also 'sh -c') for installation steps;
# will be root anyway when running installed as daemon or logout hook
if [ ! "${LOG_PREFIX}" = "" ] ; then
sudo tee -a "${LOGFILE}" > /dev/null << EOF
$(date +"${DATEFORMAT}") (${LOG_PREFIX}) ${1}
EOF
else
sudo tee -a "${LOGFILE}" > /dev/null << EOF
${1}
EOF
fi
else
# non-sudo copy, could simplify
if [ ! "${LOG_PREFIX}" = "" ] ; then
tee -a "${LOGFILE}" > /dev/null << EOF
$(date +"${DATEFORMAT}") (${LOG_PREFIX}) ${1}
EOF
else
tee -a "${LOGFILE}" > /dev/null << EOF
${1}
EOF
fi
fi
if [ ! "$INSTALLED" = "1" ] ; then
echo "${1}"
fi
}
abort() {
doLog "Fatal error: ${1}"
exit 1
}
# abort without logging or requiring sudo
earlyAbort() {
echo "Fatal error: ${1}" > /dev/stderr
exit 1
}
getDarwinMajorVersion() {
darwin_ver=$(uname -r)
# Cannot add -r in earlier macOS
# shellcheck disable=SC2162
IFS="." read -a darwin_ver_array <<< "$darwin_ver"
echo "${darwin_ver_array[0]}"
}
installLog() {
if [ ! -d "${LOGDIR}" ] ; then
# We intentionally don't use -p as something is probably
# weird if parent dirs don't exist here
sudo mkdir "${LOGDIR}" || abort "Failed to mkdir for logfile!"
fi
if [ ! -f "${LOGFILE}" ] ; then
sudo touch "${LOGFILE}" || abort "Failed to touch logfile!"
fi
if [ "$AGENT" = "1" ] ; then
sudo chmod 666 "${LOGFILE}" || abort "Failed to chmod logfile!"
fi
}
install() {
FAIL="Failed to install!"
if [ ! -d "${PRIVILEGED_HELPER_TOOLS}" ] ; then
sudo mkdir "${PRIVILEGED_HELPER_TOOLS}" || abort "${FAIL}"
fi
# Copy executable files into place.
sudo cp "${SELFNAME}" "${HELPER}" || abort "${FAIL}"
sudo cp nvramdump "${NVRAMDUMP}" || abort "${FAIL}"
# Install logout hook.
if [ "$LOGOUT" = "1" ] ; then
sudo defaults write com.apple.loginwindow LogoutHook "${HELPER}" || abort "${FAIL}"
fi
if [ "$DAEMON" = "1" ] ; then
# Make customised Launchd.command.plist for daemon.
if [ "$BOTH" = "1" ] ; then
LAUNCHFILE_DAEMON="${LAUNCHFILE_DAEMON} (1 of 2)"
fi
if [ -f "$LAUNCHFILE_DAEMON" ] ; then
sudo rm "$LAUNCHFILE_DAEMON" || abort "${FAIL}"
fi
sudo ln -s "$(which sh)" "$LAUNCHFILE_DAEMON" || abort "${FAIL}"
sed "s/\$LABEL/${ORG}.daemon/g;s/\$HELPER/$(sed 's/\//\\\//g' <<< "$HELPER")/g;s/\$PARAM/${DAEMON_PARAMS}/g;s/\$LOGFILE/$(sed 's/\//\\\//g' <<< "$LOGFILE")/g;s/\$LAUNCHFILE/$(sed 's/\//\\\//g' <<< "$LAUNCHFILE_DAEMON")/g" "Launchd.command.plist" > "/tmp/Launchd.command.plist" || abort "${FAIL}"
sudo cp "/tmp/Launchd.command.plist" "${DAEMON_PLIST}" || abort "${FAIL}"
rm -f /tmp/Launchd.command.plist
# Launch already installed daemon.
sudo launchctl load "${DAEMON_PLIST}" || abort "${FAIL}"
fi
if [ "$AGENT" = "1" ] ; then
# Make customised Launchd.command.plist for agent.
if [ "$BOTH" = "1" ] ; then
LAUNCHFILE_AGENT="${LAUNCHFILE_AGENT} (2 of 2)"
fi
if [ -f "$LAUNCHFILE_AGENT" ] ; then
sudo rm "$LAUNCHFILE_AGENT" || abort "${FAIL}"
fi
sudo ln -s "$(which sh)" "$LAUNCHFILE_AGENT" || abort "${FAIL}"
sed "s/\$LABEL/${ORG}.agent/g;s/\$HELPER/$(sed 's/\//\\\//g' <<< "$HELPER")/g;s/\$PARAM/${AGENT_PARAMS}/g;s/\$LOGFILE/$(sed 's/\//\\\//g' <<< "$LOGFILE")/g;s/\$LAUNCHFILE/$(sed 's/\//\\\//g' <<< "$LAUNCHFILE_AGENT")/g" "Launchd.command.plist" > "/tmp/Launchd.command.plist" || abort "${FAIL}"
sudo cp "/tmp/Launchd.command.plist" "${AGENT_PLIST}" || abort "${FAIL}"
rm -f /tmp/Launchd.command.plist
# Launch already installed agent.
sudo launchctl bootstrap gui/501 "${AGENT_PLIST}" || abort "${FAIL}"
fi
echo "Installed."
}
uninstall() {
UNINSTALLED=1
if [ "$DAEMON" = "1" ] || [ "$AGENT" = "1" ] ; then
if [ "$DAEMON_PID" = "" ] ; then
UNINSTALLED=2
if [ ! "$INSTALL" = "1" ] && [ "$DAEMON" = "1" ] ; then
echo "Daemon was not installed!" > /dev/stderr
fi
else
# Place special value in saved device node so that nvram.plist is not updated at uninstall
sudo /usr/sbin/nvram "${BOOT_NODE}=null" || abort "Failed to save null boot device!"
sudo launchctl unload "${DAEMON_PLIST}" || UNINSTALLED=0
DAEMON_PID=
fi
sudo rm -f "${DAEMON_PLIST}"
sudo rm -f "${LAUNCHFILE_DAEMON}"*
fi
if [ "$AGENT" = "1" ] || [ "$DAEMON" = "1" ] ; then
if [ "$AGENT_PID" = "" ] ; then
UNINSTALLED=2
if [ ! "$INSTALL" = "1" ] && [ "$AGENT" = "1" ] ; then
echo "Agent was not installed!" > /dev/stderr
fi
else
# Place special value in saved device node so that nvram.plist is not updated at uninstall
sudo /usr/sbin/nvram "${BOOT_NODE}=null" || abort "Failed to save null boot device!"
sudo launchctl bootout "${AGENT_ID}" || UNINSTALLED=0
AGENT_PID=
fi
sudo rm -f "${AGENT_PLIST}"
sudo rm -f "${LAUNCHFILE_AGENT}"*
fi
if [ "$LOGOUT" = "1" ] ; then
if [ "$LOGOUT_HOOK" = "" ] ; then
UNINSTALLED=2
echo "Logout hook was not installed!" > /dev/stderr
else
sudo defaults delete com.apple.loginwindow LogoutHook || UNINSTALLED=0
LOGOUT_HOOK=
fi
fi
if [ "$DAEMON_PID" = "" ] &&
[ "$AGENT_PID" = "" ] &&
[ "$LOGOUT_HOOK" = "" ] ; then
sudo rm -f "${HELPER}"
sudo rm -f "${NVRAMDUMP}"
fi
if [ ! "$INSTALL" = "1" ] ; then
if [ "$UNINSTALLED" = "0" ] ; then
echo "Could not uninstall!"
elif [ "$UNINSTALLED" = "1" ] ; then
echo "Uninstalled."
fi
fi
}
status() {
if [ "$DAEMON" = "1" ] ; then
# Detailed info on daemon.
sudo launchctl print "$DAEMON_ID"
elif [ "$AGENT" = "1" ] ; then
# Detailed info on agent.
sudo launchctl print "$AGENT_ID"
else
echo "Daemon pid = ${DAEMON_PID}"
echo "Agent pid = ${AGENT_PID}"
if [ ! "$BOTH" = "1" ] ; then
echo "LogoutHook = ${LOGOUT_HOOK}"
fi
fi
}
# On earlier macOS (at least Mojave in VMWare) there is an intermittent
# problem where msdos/FAT kext is occasionally not available when we try
# to mount the drive on daemon exit; mounting and unmounting on daemon
# start here fixes this.
# Doing this introduces a different problem, since this early mount sometimes
# fails initially, however (as with 'diskutil info' above) in that case the
# script gets restarted by launchd after ~5s, and eventually either succeeds or
# finds the drive already mounted (when applicable, i.e. not marked as ESP).
#
# On later macOS (Sonoma) this problem recurs, and the strategy of co-ordinated
# daemon and user agent was introduced to handle this (see saveNvram()):
#
# At startup:
# - daemon waits to see agent
# - daemon mounts ESP
# - daemon kills agent, then waits 1s
# - agent:
# o traps kill
# o touches nvram.plist (which forces kext to be loaded)
# o exits
# - after 1s wait daemon unmounts ESP
#
# At shutdown:
# - daemon mounts ESP and writes nvram.plist
# o this only works if the above palaver at startup has happened recently
#
earlyMount() {
mount_path=$(mount | sed -n "s:${node} on \(.*\) (.*$:\1:p")
if [ ! "${mount_path}" = "" ] ; then
doLog "Early mount not needed, already mounted at ${mount_path}"
else
sudo /usr/sbin/diskutil mount "${node}" 1>/dev/null || abort "Early mount failed!"
sleep 1 & wait
sudo /usr/sbin/diskutil unmount "${node}" 1>/dev/null || abort "Early unmount failed!"
doLog "Early mount/unmount succeeded"
fi
}
# Save some diskutil info in emulated NVRAM for use at daemon shutdown:
# - While we can access diskutil normally at agent startup and at logout hook;
# - We cannot use diskutil at daemon shutdown, because:
# "Unable to run because unable to use the DiskManagement framework.
# Common reasons include, but are not limited to, the DiskArbitration
# framework being unavailable due to being booted in single-user mode."
# - At daemon startup, diskutil works but the device may not be ready
# immediately, but macOS restarts us quickly (~5s) and then we can run.
# Note that saving any info for use at process shutdown if not running as
# daemon (sudo) would have to go into e.g. a file not nvram.
#
# $1 = 0 - Set node var, but do not save in emulated NVRAM
# $1 = 1 - Set node var and save in emulated NVRAM
#
saveMount() {
UUID="$(/usr/sbin/nvram "${BOOT_PATH}" | sed 's/.*GPT,\([^,]*\),.*/\1/')"
if [ "$(printf '%s' "${UUID}" | /usr/bin/wc -c)" -eq 36 ] && [ -z "$(echo "${UUID}" | sed 's/[-0-9A-F]//g')" ] ; then
node="$(/usr/sbin/diskutil info "${UUID}" | sed -n 's/.*Device Node: *//p')"
# This may randomly fail initially, if so the script gets restarted by
# launchd and eventually succeeds.
if [ "${node}" = "" ] ; then
abort "Cannot access device node!"
fi
doLog "Found boot device at ${node}"
if [ "${1}" = "1" ] ; then
if [ "$BOTH" = "1" ] &&
[ "$DAEMON" = "1" ]; then
doLog "Using both agent and daemon, delaying early mount"
else
earlyMount
fi
# Use hopefully emulated NVRAM as temporary storage for the boot
# device node discovered with diskutil.
# If we are in emulated NVRAM, should not appear at next boot as
# nvramdump does not write values from OC GUID back to nvram.plist.
sudo /usr/sbin/nvram "${BOOT_NODE}=${node}" || abort "Failed to store boot device!"
fi
else
abort "Missing or invalid ${BOOT_PATH} value!"
fi
}
# $1 = 0 - Use existing node var, do not fetch from emulated NVRAM
# $1 = 1 - Use node var saved in emulated NVRAM
#
# $2 = 0 - Just touch nvram.plist
# $2 = 1 - Really save NVRAM state to nvram.plist
# $2 = 2 - Just pre-mount
#
saveNvram() {
if [ "${1}" = "1" ] ; then
# . matches tab, note that \t for tab cannot be used in earlier macOS (e.g Mojave)
node=$(/usr/sbin/nvram "$BOOT_NODE" | sed -n "s/${BOOT_NODE}.//p")
if [ "$2" = "1" ]; then
if [ ! "$INSTALLED" = "1" ] ; then
# don't trash saved value if daemon or agent is live
launchctl print "$DAEMON_ID" 2>/dev/null 1>/dev/null || \
launchctl print "$AGENT_ID" 2>/dev/null 1>/dev/null || \
sudo /usr/sbin/nvram -d "$BOOT_NODE"
else
sudo /usr/sbin/nvram -d "$BOOT_NODE"
fi
fi
fi
if [ "${node}" = "" ] ; then
abort "Cannot access saved device node!"
elif [ "${node}" = "null" ] ; then
if [ ! "$AGENT" = "1" ] ; then
sudo /usr/sbin/nvram "${BOOT_NODE}=" || abort "Failed to remove boot node variable!"
fi
doLog "Uninstalling…"
return
fi
if [ "$INSTALLED" = "1" ] &&
[ "$AGENT" = "1" ] ; then
if /usr/sbin/nvram "${DAEMON_WAITING}" 1>/dev/null 2>/dev/null ; then
echo -n
else
doLog "Daemon is not waiting, stopping..."
return
fi
fi
mount_path=$(mount | sed -n "s:${node} on \(.*\) (.*$:\1:p")
if [ ! "${mount_path}" = "" ] ; then
doLog "Found mounted at ${mount_path}"
if [ "$DAEMON" = "1" ] &&
[ "$BOTH" = "1" ] &&
[ ! "$2" = "2" ] &&
[ "$(getDarwinMajorVersion)" -ge ${LAUNCHD_BOTH} ] ; then
doLog "WARNING: User-mounted ESP may not save NVRAM successfully"
fi
else
if [ "$INSTALLED" = "1" ] &&
[ "$AGENT" = "1" ] ; then
# This should have been pre-mounted for us by daemon
abort "Agent cannot mount ESP!"
fi
# use reasonably assumed unique path
mount_path="/Volumes/${UNIQUE_DIR}"
sudo mkdir -p "${mount_path}" || abort "Failed to make directory!"
sudo mount -t msdos "${node}" "${mount_path}" || abort "Failed to mount!"
doLog "Mounted at ${mount_path}"
MOUNTED=1
fi
if [ "${NVRAM_DIR}" = "" ] ; then
nvram_dir="${mount_path}"
else
nvram_dir="${mount_path}/${NVRAM_DIR}"
if [ ! -d "${nvram_dir}" ] ; then
mkdir "${nvram_dir}" || abort "Failed to make directory ${nvram_dir}"
fi
fi
if [ "${2}" = "2" ] ; then
doLog "Killing agent…"
sudo /usr/sbin/nvram "${DAEMON_WAITING}=1" || abort "Failed to set daemon-waiting variable!"
kill "$3" || abort "Cannot kill agent!"
if [ "$BOTH" = "1" ] ; then
sleep 1 & wait
sudo /usr/sbin/nvram "${DAEMON_WAITING}=" || abort "Failed to remove daemon-waiting variable!"
fi
elif [ ! "${2}" = "1" ] ; then
# Just touch nvram.plist
touch "${nvram_dir}/nvram.plist" || abort "Failed to touch nvram.plist!"
doLog "Touched nvram.plist"
else
# Really save NVRAM
rm -f /tmp/nvram.plist
${USE_NVRAMDUMP} || abort "failed to save nvram.plist!"
if [ -f "${nvram_dir}/nvram.plist" ] ; then
cp "${nvram_dir}/nvram.plist" "${nvram_dir}/nvram.fallback" || abort "Failed to create nvram.fallback!"
doLog "Copied nvram.fallback"
fi
cp /tmp/nvram.plist "${nvram_dir}/nvram.plist" || abort "Failed to copy nvram.plist!"
doLog "Saved nvram.plist"
rm -f /tmp/nvram.plist
if [ -f "${nvram_dir}/nvram.used" ] ; then
rm "${nvram_dir}/nvram.used" || abort "Failed to delete nvram.used!"
doLog "Deleted nvram.used"
fi
fi
# We would like to always unmount here if we made the mount point, but at exit of
# installed daemon umount fails with "Resource busy" and diskutil is not available.
# This should not cause any problem except that the boot drive will be left mounted
# at the unique path if the daemon process gets killed (the process would then be
# restarted by macOS and NVRAM should still be saved at exit).
while [ "$MOUNTED" = "1" ] ;
do
# For logout hook or if running in immediate mode, we can clean up.
if [ "$LOGOUT" = "1" ] ||
[ ! "$INSTALLED" = "1" ] ||
[ "$2" = "2" ] ; then
# Sometimes needed after directory access...
# # if [ ! "$2" = "2" ] ; then
# # sleep 1 & wait
# # fi
# Depending on how installed and macOS version, either unmount may be needed.
# (On Lion failure of 'diskutil unmount' may be to say volume is already unmounted when it is not.)
MOUNTED=0
sudo diskutil unmount "${node}" 1>/dev/null 2>/dev/null || MOUNTED=1
if [ "$MOUNTED" = "0" ] ; then
doLog "Unmounted with diskutil"
else
sudo umount "${node}" && MOUNTED=0
if [ "$MOUNTED" = "0" ] ; then
sudo rmdir "${mount_path}" || abort "Failed to remove directory!"
doLog "Unmounted with umount"
else
if [ "$INSTALLED" = "1" ] ; then
abort "Failed to unmount!"
else
doLog "Retrying…"
fi
fi
fi
fi
done
}
onComplete() {
doLog "Trap ${1}"
if [ "$DAEMON" = "1" ] ; then
saveNvram 1 1
elif [ "$AGENT" = "1" ] ; then
saveNvram 1 0
fi
doLog "Ended."
# Required if running directly (launchd kills any orphaned child processes by default).
# ShellCheck ref: https://github.com/koalaman/shellcheck/issues/648#issuecomment-208306771
# shellcheck disable=SC2046
kill $(jobs -p)
exit 0
}
if [ ! -x /usr/bin/dirname ] ||
[ ! -x /usr/bin/basename ] ||
[ ! -x /usr/bin/wc ] ||
[ ! -x /usr/sbin/diskutil ] ||
[ ! -x /usr/sbin/nvram ] ; then
earlyAbort "Unix environment is broken!"
fi
NVRAM_DIR="NVRAM"
OCNVRAMGUID="4D1FDA02-38C7-4A6A-9CC6-4BCCA8B30102"
# Darwin version from which we can wake a sleeping daemon, corresponds to Yosemite and upwards.
LAUNCHD_DARWIN=14
# Darwin version for which we need both agent and daemon, corresponds to Sonoma and upwards.
LAUNCHD_BOTH=23
# OC generated NVRAM var
BOOT_PATH="${OCNVRAMGUID}:boot-path"
# temp storage vars for this script
BOOT_NODE="${OCNVRAMGUID}:boot-node"
DAEMON_WAITING="${OCNVRAMGUID}:daemon-waiting"
# re-use as unique directory name for mount point when needed
UNIQUE_DIR="${BOOT_NODE}"
PRIVILEGED_HELPER_TOOLS="/Library/PrivilegedHelperTools"
ORG="org.acidanthera.nvramhook"
NVRAMDUMP="${PRIVILEGED_HELPER_TOOLS}/${ORG}.nvramdump.helper"
DAEMON_ID="system/${ORG}.daemon"
DAEMON_PLIST="/Library/LaunchDaemons/${ORG}.daemon.plist"
DAEMON_PARAMS="daemon"
AGENT_ID="gui/501/${ORG}.agent"
AGENT_PLIST="/Library/LaunchAgents/${ORG}.agent.plist"
AGENT_PARAMS="agent"
HELPER="${PRIVILEGED_HELPER_TOOLS}/${ORG}.helper"
LAUNCHFILE_DAEMON="${PRIVILEGED_HELPER_TOOLS}/${ORG} System Daemon"
LAUNCHFILE_AGENT="${PRIVILEGED_HELPER_TOOLS}/${ORG} User Agent"
LOGDIR="/var/log/${ORG}.launchd"
# launchd .plist configuration is set in install() to redirect stdout and stderr (useful) to LOGFILE anyway.
LOGFILE="${LOGDIR}/launchd.log"
SELFDIR="$(/usr/bin/dirname "${0}")"
SELFNAME="$(/usr/bin/basename "${0}")"
# -R sets this, on newer macOS only; date format is required because root from user sudo and root
# when running daemons or logout hooks are picking up different date formats.
DATEFORMAT="%a, %d %b %Y %T %z"
# Detect whether we're running renamed, i.e. installed copy, or else presumably from distribution directory.
if [ ! "$SELFNAME" = "Launchd.command" ] ; then
USE_NVRAMDUMP="${NVRAMDUMP}"
INSTALLED=1
else
cd "${SELFDIR}" || earlyAbort "Failed to enter working directory!"
if [ ! -x ./nvramdump ] ; then
earlyAbort "Compiled nvramdump is not found!"
fi
USE_NVRAMDUMP="./nvramdump"
fi
for arg;
do
case $arg in
install )
INSTALL=1
;;
uninstall )
UNINSTALL=1
;;
daemon )
DAEMON=1
;;
"daemon both" )
DAEMON=1
BOTH=1
;;
agent )
AGENT=1
;;
"agent both" )
AGENT=1
BOTH=1
;;
both )
BOTH=1
;;
logout )
LOGOUT=1
;;
status )
STATUS=1
;;
# Usage for invalid param.
# Note script called as logout hook gets passed name of user logging out as a param.
* )
if [ ! "$INSTALLED" = "1" ] ; then
usage
exit 0
fi
;;
esac
done
# Get root permissions early if immediate mode.
if [ "$INSTALL" = "1" ] ||
[ "$UNINSTALL" = "1" ] ||
[ "$STATUS" = "1" ] ; then
if [ "$(whoami)" = "root" ] ; then
earlyAbort "Do not run this script as root!"
fi
sudo echo -n || earlyAbort "Could not obtain sudo!"
if [ "$INSTALL" = "1" ] ; then
UNINSTALL=1
fi
if [ "$UNINSTALL" = "1" ] ||
[ "$STATUS" = "1" ] ; then
# For agent/daemon pid prefer 'launchctl list' as it works on older macOS where 'launchtl print' does not.
DAEMON_PID="$(sudo launchctl list | sed -n "s/\([0-9\-]*\).*${ORG}.daemon/\1/p" | sed 's/-/Failed to start!/')"
AGENT_PID="$(launchctl list | sed -n "s/\([0-9\-]*\).*${ORG}.agent/\1/p" | sed 's/-/Failed to start!/')"
LOGOUT_HOOK="$(sudo defaults read com.apple.loginwindow LogoutHook 2>/dev/null)"
fi
fi
# If not told what to do, work it out.
# When running as daemon, 'daemon' is specified as a param.
if [ ! "$DAEMON" = "1" ] &&
[ ! "$AGENT" = "1" ] &&
[ ! "$BOTH" = "1" ] &&
[ ! "$LOGOUT" = "1" ] ; then
if [ "$INSTALL" = "1" ] ||
[ "$UNINSTALL" = "1" ] ; then
# When not specified, choose to (un)install daemon or logout hook depending on macOS version.
if [ "$(getDarwinMajorVersion)" -ge ${LAUNCHD_BOTH} ] ; then
echo "Darwin $(uname -r) >= ${LAUNCHD_BOTH}, using both agent and daemon"
BOTH=1
AGENT=1
DAEMON=1
elif [ "$(getDarwinMajorVersion)" -ge ${LAUNCHD_DARWIN} ] ; then
echo "Darwin $(uname -r) >= ${LAUNCHD_DARWIN}, using daemon"
DAEMON=1
else
echo "Darwin $(uname -r) < ${LAUNCHD_DARWIN}, using logout hook."
LOGOUT=1
fi
else
if [ "$INSTALLED" = "1" ] ; then
# If installed with no type as a param, we are installed as logout hook.
LOGOUT=1
elif [ ! "$STATUS" = "1" ] ; then
# Usage for no params.
usage
exit 0
fi
fi
fi
if [ ! "$INSTALLED" = "1" ] &&
[ "$BOTH" = "1" ] ; then
LOG_PREFIX="Both"
AGENT=1
DAEMON=1
AGENT_PARAMS="${AGENT_PARAMS} both"
DAEMON_PARAMS="${DAEMON_PARAMS} both"
elif [ "$DAEMON" = "1" ] ; then
LOG_PREFIX="Daemon"
elif [ "$AGENT" = "1" ] ; then
LOG_PREFIX="Agent"
elif [ "$LOGOUT" = "1" ] ; then
LOG_PREFIX="Logout"
fi
if [ ! "$INSTALLED" = "1" ] ; then
LOG_PREFIX="${LOG_PREFIX}-Immediate"
fi
if [ "$INSTALL" = "1" ] &&
[ "$AGENT" = "1" ] &&
[ ! "$DAEMON" = "1" ] ; then
earlyAbort "Standalone agent install is not supported!"
fi
if [ "$UNINSTALL" = "1" ] ; then
msg="$(uninstall)"
if [ ! "${msg}" = "" ] ; then
doLog "${msg}"
fi
if [ ! "$INSTALL" = "1" ] ; then
exit 0
fi
fi
if [ "$INSTALL" = "1" ] ; then
installLog
# Save NVRAM immediately, this will become the fallback NVRAM after the first shutdown.
# Do not install if this fails, since this indicates that required boot path from OC is
# not available, or other fatal error.
if [ "$DAEMON" = "1" ] ||
[ "$LOGOUT" = "1" ] ; then
doLog "Installing…"
doLog "Saving initial nvram.plist…"
saveMount 0
if [ "$DAEMON" = "1" ] ; then
# daemon
saveNvram 0 1
else
# logout hook
saveNvram 0 1
fi
fi
install
exit 0
fi
if [ "$STATUS" = "1" ] ; then
status
exit 0
fi
if [ "${LOGOUT}" = "1" ] ; then
saveMount 0
saveNvram 0 1
exit 0
fi
# Useful for trapping all signals to see what we get.
#for s in {1..31} ;do trap "onComplete $s" $s ;done
# Trap CTRL+C for testing for immediate mode, and trap normal or forced daemon
# or agent termination. Separate trap commands so we can log which was caught.
trap "onComplete SIGINT" SIGINT
trap "onComplete SIGTERM" SIGTERM
doLog "Starting…"
if [ "$DAEMON" = "1" ] ; then
saveMount 1
fi
if [ "$DAEMON" = "1" ] &&
[ "$BOTH" = "1" ] ; then
agent_pid=""
doLog "Waiting for agent…"
while [ "$agent_pid" = "" ] ;
do
agent_pid="$(pgrep -f "$(echo "${HELPER} agent" | sed "s/\./\\\./g")")"
if [ "$agent_pid" = "" ] ; then
# https://apple.stackexchange.com/a/126066/113758
# Only works from Yosemite upwards.
sleep 5 & wait
fi
done
doLog "Found agent…"
saveNvram 1 2 "$agent_pid"
fi
while true
do
doLog "Running…"
# https://apple.stackexchange.com/a/126066/113758
# Only works from Yosemite upwards.
sleep $RANDOM & wait
done

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>$LABEL</string>
<key>ProgramArguments</key>
<array>
<string>$LAUNCHFILE</string>
<string>$HELPER</string>
<string>$PARAM</string>
</array>
<key>StandardOutPath</key>
<string>$LOGFILE</string>
<key>StandardErrorPath</key>
<string>$LOGFILE</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnableGlobbing</key>
<false/>
<key>EnableTransactions</key>
<false/>
<key>LowPriorityIO</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,17 @@
# LogoutHook
## Script
### Usage
```./Launchd.command```
### Installation
```./Launchd.command install```
### Status
```./Launchd.command status```
Shows non-empty daemon pid only, if installed with default settings.
### Log
```/var/log/org.acidanthera.nvramhook.launchd/launchd.log```

Binary file not shown.

View File

@@ -0,0 +1,107 @@
## OpenCore + OpenLinuxBoot + Secure Boot
If you want to use OpenCore + OpenLinuxBoot + Secure Boot it is possible to sign everything
manually yourself, including any new Linux kernels after updates. This is possible since most
standard distros leave at least the previous kernel bootable (and OpenLinuxBoot exposes
this, via the Auxiliary menu), so you can boot into the old kernel, then sign the new
kernel yourself.
More convenient may be to trust the signing keys of the specific distros which you
want to boot, which are bundled into the `shimx64.efi` file installed with each distro.
You can extract these with `shim-to-cert.tool` distributed with OpenCore, then install
them in your system Secure Boot `db` variable. Best practice would be to install the deny
list (`vendor.dbx`) from `shimx64.efi`, if any, into your system `dbx` variable, as well.
(Otherwise you are ignoring any revocations which the vendor has made.)
Recently, Shim has added SBAT support, as a more efficient way to revoke unsafe
binaries. Unfortunately, the SBAT enforcement code is part of Shim, and is not
something you can extract and add to your system Secure Boot database.
To work round this, the new recommended way to boot OpenCore + OpenLinuxBoot +
Secure Boot is to make a user build of Shim. The vendor certificates
and revocation lists extracted from the distro `shimx64.efi` files are combined
and signed by you, into your own build of Shim; in this approach, these vendor
certificates should NOT also be included in the system Secure Boot database,
and should be removed if you added them previously. Including them in both places
will still boot under Secure Boot, but will effectively disable SBAT revocation.
> If you are signing everything yourself, including Linux kernels after updates, that
will still work as before and the below is not needed. Equally, if you are not
using Secure Boot the below is not needed.
The advantages of using a user build of Shim are:
- No need to sign every kernel after updates (same as previous method)
- Linux SBAT integration (new)
- Linux MOK integration (new)
- No need to include the Windows intermediate CA - you are trusting whichever distro
keys you choose to include in your own Shim, directly (new)
Disadvantages are:
- Need to update when distro keys or distro revocation lists within Shim are updated
(same as previous method)
- Need to udpate when Shim SBAT level is updated (new)
### Method
`Utilities/ShimUtils` includes a script `shim-make.tool` which will download the
current Shim source and build it for you, on macOS (using Ubuntu multipass) or on
Linux (Ubuntu and Fedora supported, others may work).
- Extract `vendor.db` and `vendor.dbx` files from the `shimx64.efi` file of each distro
which you want to load (using `shim-to-cert.tool`)
- For non-GRUB distros, the required public keys for this process cannot be extracted
from `shimx64.efi` and so must be found by additional user research
- Concatentate these (e.g. `cat fedora/vendor.db ubuntu/vendor.db > combined/vendor.db`
and `cat fedora/vendor.dbx ubuntu/vendor.dbx > combined/vendor.dbx`)
- Do not concatenate `.der` files directly, it will not work
- If you have a single distro with a single `.der` file, you can use `VENDOR_CERT_FILE`
instead of `VENDOR_DB_FILE` in the `make` options below; otherwise, you will need to use
`cert-to-efi-sig-list` from `efitools` to convert the `.der` file to a sig list - this
is done automatically by `shim-to-cert.tool` when `efitools` are available (in
Linux; or from within Ubuntu multipass on macOS, e.g. `multipass shell oc-shim`)
- Build a version of Shim which includes these concatenated signature lists (and
launches OpenCore.efi directly):
- `./shim-make.tool setup`
- `./shim-make.tool clean` (only needed if remaking after the initial make)
- `./shim-make.tool make VENDOR_DB_FILE={full-path-to}/vendor.db VENDOR_DBX_FILE={full-path-to}/vendor.dbx`
- On macOS, the paths to these files must either be within the multipass VM, or
within a subdirectory visible to macOS and the VM on the same path, such as
`/Users/{username}/shim_root` when using `shim-make.tool` default settings
- Copy the relevant files (`shimx64.efi` and `mmx64.efi` as well as `BOOTX64.CSV`) to your mounted ESP volume, e.g.:
- `./shim-make.tool install /Volumes/EFI` (macOS)
- `sudo ./shim-make.tool install /boot/efi` (Linux)
- Sign the newly built `shimx64.efi` and `mmx64.efi` with your own ISK (see e.g.
https://habr.com/en/articles/273497/ - Google translate is your friend)
- If you do not copy and sign `mmx64.efi` as well as `shimx64.efi`, your system will hang if any MOK operations are attempted
- `BOOTX64.CSV` is not required and is for information only
As before you need to sign `OpenCore.efi` and any drivers it loads with your ISK.
You now also need to add an empty SBAT section to `OpenCore.efi` before signing it.
> An empty SBAT section means: 'I'm not part of the system which allocates SBAT names
and signs them into boot files, and I don't want this boot file to be revoked by any
future SBAT revocations'. Of course, you can still revoke boot files you signed yourself
by rotating your own signing keys.
As noted [here](https://github.com/fwupd/fwupd/issues/2910) and
[here](https://github.com/rhboot/shim/issues/376),
the [documented](https://github.com/rhboot/shim/blob/main/SBAT.md) method for adding an
SBAT section to an already-linked `.efi` file does not work correctly (GNU `objcopy`
corrupts the executable). This
[third party python script](https://github.com/rhboot/shim/issues/376#issuecomment-1628004034)
does work. A suitable command is:
`pe-add-sections.py -s .sbat <(echo -n) -z .sbat -i OpenCore.efi -o OpenCore_empty_sbat.efi`
This file then needs to be signed and copied back into place, e.g.:
`sbsign --key {path-to}/ISK.key --cert {path-to}/ISK.pem OpenCore_empty_sbat.efi --output OpenCore.efi`
Finally, in order for OpenCore integration with Shim to work correctly
`UEFI/Quirks/ShimRetainProtocol` must be enabled in `config.plist`, and
`LauncherPath` should be set to `\EFI\OC\shimx64.efi`.
> Using Ubuntu multipass, it is now possible to operate entirely within macOS for signing,
key generation, etc. Note that the `~/shim_root` directory is already shared between
macOS and the `oc-shim` multipass VM (under its macOS path, e.g. `/Users/username/shim_root`),
and other macOS folders and volumes can be mounted if you wish, e.g.
`multipass mount /Volumes/EFI oc-shim:/Volumes/EFI`.

View File

@@ -0,0 +1,132 @@
#!/bin/bash
# sbat-info.tool - Dump SBAT information from .efi executable, with additional
# version and SBAT enforcement level information if the executable is Shim.
#
# Copyright (c) 2023, Michael Beaton. All rights reserved.<BR>
# SPDX-License-Identifier: BSD-3-Clause
#
LIGHT_GREEN='\033[1;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Note: binutils can simply be placed last on the path in macOS, to provide objcopy but avoid hiding native objdump (this script parses LLVM or GNU objdump output).
# Alternatively BINUTILS_PREFIX environment variable can be set to enabled prefixed co-existing GNU tools.
command -v "${BINUTILS_PREFIX}"objcopy >/dev/null 2>&1 || { echo >&2 "objcopy not found - please install binutils package or set BINUTILS_PREFIX environment variable."; exit 1; }
usage () {
echo "Usage: $(basename "$0") <efifile>"
echo " Display SBAT information, if present, for any .efi executable."
echo " If the file is a version of Shim, also dislay Shim version info and SBAT enforcement level."
}
has_section () {
"${BINUTILS_PREFIX}"objdump -h "$1" | sed -nr 's/^ *[0-9]+ ([^ ]+).*/\1/p' | grep "$2" 1>/dev/null
}
if [ -z "$1" ]; then
usage
exit 1
fi
# https://unix.stackexchange.com/a/84980/340732
op_dir=$(mktemp -d 2>/dev/null || mktemp -d -t 'shim_info') || exit 1
# objcopy and objdump do not like writing to stdout when part of a pipe, so just write to temp files
# Note: these get created as empty files even if the section does not exist
"${BINUTILS_PREFIX}"objcopy -O binary -j ".sbat" "$1" "${op_dir}/sbat" || { rm -rf "${op_dir}"; exit 1; }
"${BINUTILS_PREFIX}"objcopy -O binary -j ".sbatlevel" "$1" "${op_dir}/sbatlevel" || { rm -rf "${op_dir}"; exit 1; }
"${BINUTILS_PREFIX}"objcopy -O binary -j ".data.ident" "$1" "${op_dir}/data.ident" || { rm -rf "${op_dir}"; exit 1; }
if ! has_section "$1" ".data.ident" ; then
IS_SHIM=0
echo -e "${YELLOW}Shim version info not present (probably not Shim)${NC}"
else
# Treat this section's presence as enough for the warning colours below
IS_SHIM=1
FILE_HEADER="$(head -n 1 "${op_dir}/data.ident")"
SHIM_HEADER="UEFI SHIM"
if [ ! "$FILE_HEADER" = "$SHIM_HEADER" ] ; then
echo -e "${RED}!!!Version header found '${FILE_HEADER}' expected '${SHIM_HEADER}'${NC}"
else
echo -e "${YELLOW}Found ${SHIM_HEADER} version header${NC}"
fi
# shellcheck disable=SC2016
VERSION="$(grep -a 'Version' "${op_dir}/data.ident" | sed -n 's/^\$Version: *\([^[:space:]]*\) *\$$/\1/p')"
if [ "$VERSION" = "" ] ; then
echo -e "${RED}Expected version number not present${NC}"
else
echo -e "Version: ${LIGHT_GREEN}${VERSION}${NC} (${VERSION}+, if user build)"
fi
# shellcheck disable=SC2016
COMMIT="$(grep -a 'Commit' "${op_dir}/data.ident" | sed -n 's/^\$Commit: *\([^[:space:]]*\) *\$$/\1/p')"
if [ "$COMMIT" = "" ] ; then
echo -e "${RED}Expected commit id not present${NC}"
else
echo "Commit: ${COMMIT}"
fi
fi
echo
echo -e "${YELLOW}SBAT (when tested by other files or by self):${NC}"
if ! has_section "$1" ".sbat" ; then
echo
echo -e "${RED}No .sbat section${NC}"
else
echo
echo -n -e "$LIGHT_GREEN"
cat "${op_dir}/sbat"
echo -n -e "$NC"
if ! cat "${op_dir}/sbat" | grep -E "[a-z]|[A-Z]" 1>/dev/null ; then
# Red for shim; yellow for other file
if [ "$IS_SHIM" -eq 1 ] ; then
echo -n -e "$RED"
else
echo -n -e "$LIGHT_GREEN"
fi
echo -e "Present but empty .sbat section"
fi
fi
echo
echo -e "${YELLOW}SBAT Level (when testing other files):${NC}"
if ! has_section "$1" ".sbatlevel" ; then
echo
# Yellow for shim (not present in earlier shims with .sbat, ~15.6); green for other file
if [ "$IS_SHIM" -eq 1 ] ; then
echo -n -e "$YELLOW"
else
echo -n -e "$LIGHT_GREEN"
fi
echo -n "No .sbatlevel section"
echo -e "$NC"
else
sbat_level_version=$(dd if="${op_dir}/sbatlevel" ibs=1 skip=0 count=4 2>/dev/null | od -t u4 -An | xargs) || { rm -rf "${op_dir}"; exit 1; }
sbat_level_previous=$(dd if="${op_dir}/sbatlevel" ibs=1 skip=4 count=4 2>/dev/null | od -t u4 -An | xargs) || { rm -rf "${op_dir}"; exit 1; }
sbat_level_latest=$(dd if="${op_dir}/sbatlevel" ibs=1 skip=8 count=4 2>/dev/null | od -t u4 -An | xargs) || { rm -rf "${op_dir}"; exit 1; }
if [ "$sbat_level_version" -ne 0 ] ; then
echo -e "${RED}!!! Unexpected .sbatlevel version $sbat_level_version != 0 !!!${NC}"
fi
echo
echo -e "${YELLOW}Previous (more permissive boot, default):${NC}"
echo -n -e "$LIGHT_GREEN"
dd if="${op_dir}/sbatlevel" ibs=1 skip="$(($sbat_level_previous + 4))" count="$(($sbat_level_latest - $sbat_level_previous))" 2>/dev/null
echo -e "$NC"
echo -e "${YELLOW}Latest (stricter boot, optional):${NC}"
echo -n -e "$LIGHT_GREEN"
dd if="${op_dir}/sbatlevel" ibs=1 skip="$(($sbat_level_latest + 4))" 2>/dev/null
echo -e "$NC"
fi
rm -rf "${op_dir}"
exit 0

View File

@@ -0,0 +1,339 @@
#!/bin/bash
#
# Copyright (c) 2023 Mike Beaton. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# Makes user build of shim which launches OpenCore.efi and includes specified vendor certificate lists.
# Builds on macOS using Ubuntu multipass or on Linux directly (Ubuntu & Fedora tested).
#
# To debug shim (e.g. within OVMF) make with
# ./shim-make.tool make OPTIMIZATIONS="-O0"
# and set 605dab50-e046-4300-abb6-3dd810dd8b23:SHIM_DEBUG to (UINT8)1 (<data>AQ==</data>) in NVRAM
#
ROOT=~/shim_root
SHIM=~/shim_source
OC_SHIM=oc-shim
unamer() {
NAME="$(uname)"
if [ "$(echo "${NAME}" | grep MINGW)" != "" ] || [ "$(echo "${NAME}" | grep MSYS)" != "" ]; then
echo "Windows"
else
echo "${NAME}"
fi
}
shim_command () {
DIR=$1
shift
if [ $DARWIN -eq 1 ] ; then
if [ $ECHO -eq 1 ] ; then
# shellcheck disable=SC2068
echo multipass exec ${OC_SHIM} --working-directory "'${DIR}'" -- $@ 1>/dev/stderr
fi
# shellcheck disable=SC2068,SC2294
eval multipass exec ${OC_SHIM} --working-directory "'${DIR}'" -- $@
else
if [ $ECHO -eq 1 ] ; then
# shellcheck disable=SC2068
echo "[${DIR}]" $@ 1>/dev/stderr
fi
pushd "${DIR}" 1>/dev/null || exit 1
# shellcheck disable=SC2068,SC2294
eval $@
retval=$?
popd 1>/dev/null || exit 1
return $retval
fi
}
shim_command_v () {
FOUND="$(shim_command . command -v "'${1}'" | wc -l)"
return $((1 - $FOUND))
}
usage() {
echo "Usage:"
echo -n " ./${SELFNAME} [args] [setup|clean|make [options]|install [esp-root-path]"
if [ $DARWIN -eq 1 ] ; then
echo -n "|mount [multipass-path]]"
fi
echo "]"
echo
echo "Args:"
echo -n " -r : Specify shim output root, default '${ROOT}'"
if [ $DARWIN -eq 1 ] ; then
echo -n " (shared)"
fi
echo
echo -n " -s : Specify shim source location, default '${SHIM}'"
if [ $DARWIN -eq 1 ] ; then
echo -n " (VM only, unless 'shim-make mount' used for development/debugging)"
fi
echo
echo "If used, -r/-s:"
echo " - Should be specified on every call, they are not remembered from 'setup'"
echo " - Should come before the chosen command"
echo
echo "Examples:"
echo " ./${SELFNAME} setup (sets up directories and installs rhboot/shim source)"
echo " ./${SELFNAME} clean (cleans after previous make)"
LOCATION="."
if [ $DARWIN -eq 1 ] ; then
LOCATION="${ROOT}"
fi
echo -n " ./${SELFNAME} make VENDOR_DB_FILE='${LOCATION}/combined/vendor.db' VENDOR_DBX_FILE='${LOCATION}/combined/vendor.dbx' (makes shim with db and dbx contents"
if [ $DARWIN -eq 1 ] ; then
echo -n "; note VENDOR_DB_FILE and VENDOR_DBX_FILE are inside a directory shared with VM"
fi
echo ")"
echo " ./${SELFNAME} install '${EXAMPLE_ESP}' (installs made files to ESP mounted at '${EXAMPLE_ESP}')"
echo
echo "After installation shimx64.efi and mmx64.efi must be signed by user ISK; OpenCore.efi must have an .sbat section added and be signed by user ISK."
}
mount_path () {
SRC=$1
if [ "${SRC}" = "" ] ; then
echo "Incorrect call to mount_path!"
exit 1
fi
DEST=$2
if [ "${DEST}" = "" ] ; then
DEST=${SRC}
fi
if [ ! -d "${SRC}" ] ; then
echo "Adding ${SRC}..."
mkdir -p "${SRC}" || exit 1
else
echo "${SRC} already present..."
fi
if [ $DARWIN -eq 1 ] ; then
if ! multipass info ${OC_SHIM} | grep "${DEST}" 1>/dev/null ; then
echo "Mounting ${SRC}..."
multipass mount "${SRC}" "${OC_SHIM}:${DEST}" || exit 1
else
echo "${SRC} already mounted..."
fi
fi
}
get_ready () {
if [ $DARWIN -eq 1 ] ; then
if ! command -v multipass 1>/dev/null ; then
echo "Installing Ubuntu multipass..."
brew install --cask multipass || exit 1
else
echo "Ubuntu multipass already installed..."
fi
if ! multipass info ${OC_SHIM} &>/dev/null ; then
echo "Launching ${OC_SHIM} multipass instance..."
multipass launch -n ${OC_SHIM} || exit 1
else
echo "${OC_SHIM} multipass instance already launched..."
fi
fi
mount_path "${ROOT}"
if ! shim_command_v gcc || ! shim_command_v make || ! shim_command_v git || ! shim_command_v sbsign || ! shim_command_v sbsiglist ; then
echo "Installing dependencies..."
if shim_command_v dnf ; then
shim_command . sudo dnf install -y gcc make git elfutils-libelf-devel sbsigntools efitools || exit 1
else
shim_command . sudo apt-get update || exit 1
shim_command . sudo apt install -y gcc make git libelf-dev sbsigntool efitools || exit 1
fi
else
echo "Dependencies already installed..."
fi
#
# For debug/develop purposes on Darwin it would be nicer to keep the source code in
# macOS, just mounted and built in multipass, but the build is about 1/3 the speed of
# building in a native multipass directory.
# For the purposes of having a fast build but code which can be opened e.g.
# within an IDE within macOS, sshfs can be used to mount out from multipass
# to macOS using ./shim-make mount:
# - https://github.com/canonical/multipass/issues/1070
# - https://osxfuse.github.io/
#
if ! shim_command . test -d "'${SHIM}'" ; then
# TODO: Once there is a stable version of Shim containing the commits we need
# - 1f38cb30a5e1dcea97b8d48915515b866ec13c32
# - a8b0b600ddcf02605da8582b4eac1932a3bb13fa
# (i.e. 15.8. hopefully) we should probably check out at a fixed, stable tag, starting
# at 15.8 and then manually updated in this script when a new stable version is released,
# but which can be overridden with any commit or branch with a script option.
echo "Cloning rhboot/shim..."
shim_command . git clone https://github.com/rhboot/shim.git "'${SHIM}'" || exit 1
shim_command "${SHIM}" git submodule update --init || exit 1
else
if ! shim_command "${SHIM}" git remote -v | grep "rhboot/shim" 1>/dev/null ; then
echo "FATAL: VM subdirectory ${SHIM} is already present, but does not contain rhboot/shim!"
exit 1
fi
echo "rhboot/shim already cloned..."
fi
echo "Make.defaults:"
# These two modifications to Make.defaults only required for debugging
FOUND=$(shim_command "${SHIM}" grep "gdwarf" Make.defaults | wc -l)
if [ "$FOUND" -eq 0 ] ; then
echo " Updating gdwarf flags..."
shim_command "${SHIM}" sed -i "'s^-ggdb \\\\^-ggdb -gdwarf-4 -gstrict-dwarf \\\\^g'" Make.defaults
else
echo " gdwarf flags already updated..."
fi
FOUND=$(shim_command "${SHIM}" grep "'${ROOT}'" Make.defaults | wc -l)
if [ "$FOUND" -eq 0 ] ; then
echo " Updating debug directory..."
shim_command "${SHIM}" sed -i "\"s^-DDEBUGDIR='L\\\"/usr/lib/debug/usr/share/shim/\\\$(ARCH_SUFFIX)-\\\$(VERSION)\\\$(DASHRELEASE)/\\\"'^-DDEBUGDIR='L\\\"${ROOT}/usr/lib/debug/boot/efi/EFI/OC/\\\"'^g\"" Make.defaults
else
echo " Debug directory already updated..."
fi
# Work-around for https://github.com/rhboot/shim/issues/596
FOUND=$(shim_command "${SHIM}" grep "'export DEFINES'" Make.defaults | wc -l)
if [ "$FOUND" -eq 0 ] ; then
echo " Updating exports..."
# var assignment to make output piping work normally
# shellcheck disable=SC2034
temp=$(echo "export DEFINES" | shim_command "${SHIM}" tee -a Make.defaults) 1>/dev/null
else
echo " Exports already updated..."
fi
}
SELFNAME="$(/usr/bin/basename "${0}")"
ECHO=0
if [ "$(unamer)" = "Darwin" ] ; then
DARWIN=1
EXAMPLE_ESP='/Volumes/EFI'
else
DARWIN=0
EXAMPLE_ESP='/boot/efi'
fi
OPTS=0
while [ "${1:0:1}" = "-" ] ; do
OPTS=1
if [ "$1" = "-r" ] ; then
shift
if [ "$1" != "" ] && ! [ "${1:0:1}" = "-" ] ; then
ROOT=$1
shift
else
echo "No root directory specified" && exit 1
fi
elif [ "$1" = "-s" ] ; then
shift
if [ "$1" != "" ] && ! [ "${1:0:1}" = "-" ] ; then
SHIM=$1
shift
else
echo "No shim directory specified" && exit 1
fi
elif [ "$1" = "--echo" ] ; then
ECHO=1
shift
else
echo "Unknown option: $1"
exit 1
fi
done
if [ "$1" = "setup" ] ; then
get_ready
echo "Installation complete."
exit 0
elif [ "$1" = "clean" ] ; then
echo "Cleaning..."
shim_command "${SHIM}" make clean
exit 0
elif [ "$1" = "make" ] ; then
echo "Making..."
shift
shim_command "${SHIM}" make DEFAULT_LOADER="\\\\\\\\OpenCore.efi" OVERRIDE_SECURITY_POLICY=1 "$@"
exit 0
elif [ "$1" = "install" ] ; then
echo "Installing..."
rm -rf "${ROOT:?}"/usr
shim_command "${SHIM}" DESTDIR="'${ROOT}'" EFIDIR='OC' OSLABEL='OpenCore' make install
if [ ! "$2" = "" ] ; then
echo "Installing to ESP ${2}..."
cp "${ROOT}"/boot/efi/EFI/OC/* "${2}"/EFI/OC || exit 1
fi
exit 0
elif [ $DARWIN -eq 1 ] && [ "$1" = "mount" ] ; then
MOUNT="$2"
if [ "${MOUNT}" = "" ] ; then
MOUNT=$SHIM
fi
#
# Useful for devel/debug only.
# Note: We are only mounting in the reverse direction because we get much faster build speeds.
#
if ! command -v sshfs 1>/dev/null ; then
echo "sshfs (https://osxfuse.github.io/) is required for mounting directories from multipass into macOS (https://github.com/canonical/multipass/issues/1070)"
exit 1
fi
if [ ! -d "${MOUNT}" ] ; then
echo "Making local directory ${MOUNT}..."
mkdir -p "${MOUNT}" || exit 1
fi
ls "${MOUNT}" 1>/dev/null
if [ $? -ne 0 ] ; then
echo "Directory may be mounted but not ready (no authorized key?)"
echo "Try: umount ${MOUNT}"
exit 1
fi
if mount | grep ":${MOUNT}" ; then
echo "Already mounted at ${MOUNT}"
exit 0
fi
# shellcheck disable=SC2012
if [ "$(ls -A -1 "${MOUNT}" | wc -l)" -ne 0 ] ; then
echo "Directory ${MOUNT} is not empty!"
exit 1
fi
IP=$(multipass info ${OC_SHIM} | grep IPv4 | cut -d ":" -f 2 | sed 's/ //g')
if [ "${IP}" = "" ] ; then
echo "Cannot obtain IPv4 for ${OC_SHIM}"
exit 1
fi
if sshfs "ubuntu@${IP}:$(realpath "${MOUNT}")" "${MOUNT}" ; then
echo "Mounted at ${MOUNT}"
exit 0
else
umount "${MOUNT}"
echo "Directory cannot be mounted, append your ssh public key to .ssh/authorized_keys in the VM and try again."
exit 1
fi
elif [ "$1" = "" ] ; then
if [ $OPTS -eq 0 ] ; then
usage
else
echo "No command specified."
fi
exit 1
else
echo "Unkown command: $1"
exit 1
fi

View File

@@ -0,0 +1,93 @@
#!/bin/bash
# shim-to-cert.tool - Extract OEM signing certificate public key (and full db, dbx if present) from GRUB shim file.
#
# Copyright (c) 2021-2023, Michael Beaton. All rights reserved.<BR>
# SPDX-License-Identifier: BSD-3-Clause
#
LIGHT_GREEN='\033[1;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Note: binutils can be placed last on path in macOS, to provide objcopy but avoid overwriting native tools.
command -v "${BINUTILS_PREFIX}"objcopy >/dev/null 2>&1 || { echo >&2 "objcopy not found - please install binutils package or set BINUTILS_PREFIX environment variable."; exit 1; }
command -v openssl >/dev/null 2>&1 || { echo >&2 "openssl not found - please install openssl package."; exit 1; }
if [ -z "$1" ]; then
echo "Usage: $(basename "$0") <shimfile>"
exit 1
fi
sectfile=$(mktemp) || exit 1
# make certain we have output file name, as objcopy will trash input file without it
if [ "x$sectfile" = "x" ]; then
echo >&2 "Error creating tempfile!"
exit 1
fi
# extract .vendor_cert section
"${BINUTILS_PREFIX}"objcopy -O binary -j .vendor_cert "$1" "$sectfile" || exit 1
if [ ! -s "$sectfile" ] ; then
echo >&2 "No .vendor_cert section in $1."
rm "$sectfile"
exit 1
fi
# xargs trims white space
vendor_authorized_size=$(dd if="$sectfile" ibs=1 skip=0 count=4 2>/dev/null | od -t u4 -An | xargs) || { rm "$sectfile"; exit 1; }
vendor_deauthorized_size=$(dd if="$sectfile" ibs=1 skip=4 count=4 2>/dev/null | od -t u4 -An | xargs) || { rm "$sectfile"; exit 1; }
vendor_authorized_offset=$(dd if="$sectfile" ibs=1 skip=8 count=4 2>/dev/null | od -t u4 -An | xargs) || { rm "$sectfile"; exit 1; }
vendor_deauthorized_offset=$(dd if="$sectfile" ibs=1 skip=12 count=4 2>/dev/null | od -t u4 -An | xargs) || { rm "$sectfile"; exit 1; }
# extract cert or db
certfile=$(mktemp) || { rm "$sectfile"; exit 1; }
# extract db
if [ "$vendor_authorized_size" -ne "0" ]; then
dd if="$sectfile" ibs=1 skip="$vendor_authorized_offset" count="$vendor_authorized_size" 2>/dev/null > "$certfile" || { rm "$sectfile"; rm "$certfile"; exit 1; }
fi
# extract dbx
if [ "$vendor_deauthorized_size" -ne "0" ]; then
dd if="$sectfile" ibs=1 skip="$vendor_deauthorized_offset" count="$vendor_deauthorized_size" 2>/dev/null > "vendor.dbx" || { rm "$sectfile"; rm "$certfile"; exit 1; }
echo -e " - Secure Boot revocation list saved as ${LIGHT_GREEN}vendor.dbx${NC}"
else
echo " - No secure Boot revocation list"
fi
rm "$sectfile"
if [ "$vendor_authorized_size" -eq "0" ]; then
echo "!!! Empty vendor_authorized section (no secure boot signing certificates present)."
rm "$certfile"
exit 1
fi
# valid as single cert?
openssl x509 -noout -inform der -in "$certfile" 2>/dev/null
if [ $? -ne 0 ]; then
cp "$certfile" vendor.db
echo -e " - Secure Boot signing list saved as ${LIGHT_GREEN}vendor.db${NC}"
else
# outfile name from cert CN
certname=$(openssl x509 -noout -subject -inform der -in "$certfile" | sed 's/^subject=.*CN *=[ \"]*//' | sed 's/[,\/].*//' | sed 's/ *//g') || { rm "$certfile"; exit 1; }
outfile="${certname}.der"
cp "$certfile" "$outfile" || { rm "$certfile"; exit 1; }
echo -e " - Secure Boot certificate saved as ${LIGHT_GREEN}${outfile}${NC}"
if ! command -v cert-to-efi-sig-list >/dev/null ; then
echo -e " o To convert certificate to vendor.db use:"
echo -e " ${YELLOW}" 'cert-to-efi-sig-list <(openssl x509 -in' "'${outfile}'" '-outform PEM) vendor.db' "${NC}"
else
cert-to-efi-sig-list <(openssl x509 -in "${outfile}" -outform PEM) vendor.db || { rm "$certfile"; exit 1; }
echo -e " o Certificate has also been saved as single cert signing list ${LIGHT_GREEN}vendor.db${NC}"
fi
fi
rm "$certfile"

View File

@@ -0,0 +1,92 @@
#!/bin/bash
#
# unsign-efi-sig-list.tool
# Removes signing key from signed certificate list,
# also saves signing section and prints CNs found in,
# thereby helping to indicate who signed it originally.
#
# Copyright (c) 2023, Michael Beaton. All rights reserved.<BR>
# SPDX-License-Identifier: BSD-3-Clause
#
if [ $# -lt 1 ] || [ $# -gt 3 ] ; then
echo "Usage:"
echo " $(basename "$0") <signed-sig-list> [<outfile> [<sigfile>]]"
echo ""
echo " - Uses infile name with .unsigned appended if outfile name not specified"
echo " - Uses infile name with .pkcs7 appended if sigfile name not specified"
echo ""
exit 0
fi
if [ ! -f "$1" ] ; then
echo "File not found: $1"
exit 1
fi
infile="$1"
shift
if [ "$1" != "" ] ; then
outfile="$1"
shift
else
outfile="${infile}.unsigned"
fi
EfiTimeSize=16
WinCertHdrSize=8
GuidSize=16
ExpectedRevision=$((0x0200))
ExpectedCertificateType=$((0x0EF1))
WinCertificateSize=$(dd if="$infile" ibs=1 skip="$EfiTimeSize" count=4 2>/dev/null | od -t u4 -An | xargs)
Revision=$(dd if="$infile" ibs=1 skip="$(($EfiTimeSize + 4))" count=2 2>/dev/null | od -t u4 -An | xargs)
CertificateType=$(dd if="$infile" ibs=1 skip="$(($EfiTimeSize + 6))" count=2 2>/dev/null | od -t u4 -An | xargs)
if [ "$CertificateType" -ne "$ExpectedCertificateType" ] ; then
printf "Not a signed certificate file (signature 0x%04X found, 0x%04X expected) in: %s\n" "$CertificateType" "$ExpectedCertificateType" "$infile"
exit 1
fi
if [ "$Revision" -ne "$ExpectedRevision" ] ; then
printf "Unexpected revision (0x%04X found, 0x%04X expected) in signed certificate file: %s\n" "$Revision" "$ExpectedRevision" "$infile"
exit 1
fi
dd if="$infile" of="$outfile" ibs=1 skip=$(($EfiTimeSize + $WinCertificateSize)) 2>/dev/null
echo "Unsigned certificate list saved to '$outfile'"
# Total size is header+guid+sig
if [ "$WinCertificateSize" -le "$(($WinCertHdrSize + $GuidSize))" ] ; then
echo "!!! Certificate size smaller than expected header size"
exit 1
fi
# In little-endian byte order
EFI_CERT_TYPE_PKCS7_GUID="9dd2af4adf68ee498aa9347d375665a7"
# Expect pkcs7 sig (diff piping requires bash not sh)
if ! diff <(dd if="$infile" ibs=1 skip=$(($EfiTimeSize + $WinCertHdrSize)) count=$(($GuidSize)) 2>/dev/null | xxd -p) <(echo $EFI_CERT_TYPE_PKCS7_GUID) 1>/dev/null ; then
echo "!!! EFI_CERT_TYPE_PKCS7_GUID not found"
ext=".sig"
else
ext=".pkcs7"
fi
if [ "$1" != "" ] ; then
sigfile="$1"
shift
else
sigfile="${infile}${ext}"
fi
dd if="$infile" of="$sigfile" ibs=1 skip=$(($EfiTimeSize + $WinCertHdrSize + $GuidSize)) count=$(($WinCertificateSize - $WinCertHdrSize - $GuidSize)) 2>/dev/null
echo "Signing data saved to '$sigfile'"
# TODO: Figure out which openssl command parses this file at the correct level
echo "Signing CNs:"
openssl asn1parse -inform der -in "$sigfile" | grep -A1 commonName | grep STRING

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,219 @@
#!/bin/bash
#
# kpdescribe.sh (formerly kernel_diagreport2text.sh)
#
# Prints the stack trace from an OS X kernel panic diagnostic report, along
# with as much symbol translation as your mach_kernel version provides.
# By default, this is some, but with the Kernel Debug Kit, it should be a lot
# more. This is not an official Apple tool.
#
# USAGE:
# ./kpdescribe.sh [-f kernel_file] [-k kext_dir1;kext_dir2] Kernel_report.panic [...]
#
# Note: The Kernel Debug Kit currently requires an Apple ID to download. It
# would be great if this was not necessary.
#
# This script calls atos(1) for symbol translation, and also some sed/awk
# to decorate remaining untranslated symbols with kernel extension names,
# if the ranges match.
#
# This uses your current kernel, /mach_kernel, to translate symbols. If you run
# this on kernel diag reports from a different kernel version, it will print
# a "kernel version mismatch" warning, as the translation may be incorrect. Find
# a matching mach_kernel file and use the -f option to point to it.
#
# Updated in 2018 by vit9696 to support recent macOS versions, KEXT symbol solving,
# register printing, and other stuff to work in bash.
#
# Copyright 2014 Brendan Gregg. All rights reserved.
# Copyright 2018 vit9696. All rights reserved.
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at docs/cddl1.txt or
# http://opensource.org/licenses/CDDL-1.0.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at docs/cddl1.txt.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
kernel=/mach_kernel
kextdirs=$(echo "/System/Library/Extensions;/Library/Extensions" | tr ";" "\n")
if [ ! -f "$kernel" ]; then
kernel=/System/Library/Kernels/kernel
fi
function usage {
echo "USAGE: $0 [-f kernel_file] [-k kext_dir1;kext_dir2] Kernel_diag_report.panic [...]"
echo " eg, $0 /Library/Logs/DiagnosticReports/Kernel_2014-05-26-124827_bgregg.panic"
exit
}
(( $# == 0 )) && usage
[[ $1 == "-h" || $1 == "--help" ]] && usage
while true; do
if [[ $1 == "-f" ]]; then
kernel=$2
if [[ ! -e $kernel ]]; then
echo "ERROR: Kernel $kernel not found. Quitting."
exit 2
fi
shift 2
elif [[ $1 == "-k" ]]; then
kextdirs=$(echo "$2" | tr ";" "\n")
shift 2
else
break
fi
done
if [[ ! -x /usr/bin/atos ]]; then
echo "ERROR: Couldn't find, and need, /usr/bin/atos. Is this part of Xcode? Quitting..."
exit 2
fi
kexts=()
# Expansion is intentional here
# shellcheck disable=SC2068
for kextdir in ${kextdirs[@]}; do
if [ -d "$kextdir" ]; then
while IFS='' read -r kext; do kexts+=("$kext"); done < <(find "$kextdir" -name Info.plist)
fi
done
while (( $# != 0 )); do
if [[ "$file" != "" ]]; then print; fi
file=$1
shift
echo "File ${file/$HOME/~}"
if [[ ! -e "$file" ]]; then
print "ERROR: File ""$file"" not found. Skipping."
continue
fi
# Find slide address
slide=$(awk '/^Kernel slide:.*0x/ { print $3 }' "$file")
if [[ "$slide" == "" ]]; then
echo -n "ERROR: Missing \"Kernel slide:\" line, so can't process ""$file"". "
echo "This is needed for atos -s. Is this really a Kernel diag panic file?"
continue
fi
# Print panic line
(grep -E -A 50 '^panic' | grep -E -B 50 '^Backtrace') < "$file" | grep -vE '^Backtrace'
# Check kernel version match (uname -v string)
kernel_ver=$(strings -a "$kernel" | grep 'Darwin Kernel Version' | grep -v '@(#)')
panic_ver=$(grep 'Darwin Kernel Version' "$file")
warn=""
if [[ "$kernel_ver" != "$panic_ver" ]]; then
echo "WARNING: kernel version mismatch (use -f):"
printf "%14s: %s\n" "$kernel" "$kernel_ver"
printf "%14s: %s\n" "panic file" "$panic_ver"
warn=" (may be incorrect due to mismatch)"
fi
# Find kernel extension ranges
ranges=$(awk 'ext == 1 && /0x.*->.*0x/ {
gsub(/\(/, " "); gsub(/\)/, ""); gsub(/\[.*\]/, ""); gsub(/@/, " "); gsub(/->/, " ")
print $0
}
/Kernel Extensions in backtrace/ { ext = 1 }
/^$/ { ext = 0 }
' < "$file" | while read -r n v s e; do
# the awk gsub's convert this line:
# com.apple.driver.AppleUSBHub(666.4)[CD9B71FF-2FDD-3BC4-9C39-5E066F66D158]@0xffffff7f84ed2000->0xffffff7f84ee9fff
# into this:
# com.apple.driver.AppleUSBHub 666.4 0xffffff7f84ed2000 0xffffff7f84ee9fff
# which can then be read as three fields
echo "$n" "$v" "$s" "$e"
done)
i=0
unset name version start end kfile
while (( i < ${#ranges[@]} )); do
read -r n v s e <<< "${ranges[$i]}"
name[i]=$n
start[i]=$s
end[i]=$e
for kext in "${kexts[@]}"; do
if [ ! -f "$kext" ]; then
continue
fi
kname=$(/usr/libexec/PlistBuddy -c 'Print CFBundleIdentifier' "$kext" 2>&1)
if [ "$kname" != "$n" ]; then
continue
fi
kver=$(/usr/libexec/PlistBuddy -c 'Print CFBundleVersion' "$kext" 2>&1)
if [[ "$kver" =~ $v ]] || [ "$(echo "$v" | grep "$kver")" != "" ]; then
path="$(dirname "$kext")/MacOS/$(/usr/libexec/PlistBuddy -c 'Print CFBundleExecutable' "$kext" 2>&1)"
if [ -f "$path" ]; then
kfile[i]="$path"
fi
else
echo "Version mismatch for $kname ($kver vs $v)"
fi
done
(( i++ ))
done
# Print and translate stack
echo "Slide: $slide"
echo "Backtrace [addr unslid symbol]$warn:"
awk 'backtrace == 1 && /^[^ ]/ { print $3 }
/Backtrace.*Return Address/ { backtrace = 1 }
/^$/ { backtrace = 0 }
' < "$file" | while read -r addr; do
line=""
# Check extensions
if [[ $addr =~ 0x* ]]; then
i=0
while (( i <= ${#name[@]} )); do
[[ "${start[i]}" == "" ]] && break
# Assuming fixed width addresses, use string comparison:
if [[ $addr > ${start[$i]} && $addr < ${end[$i]} ]]; then
unslid=$((addr-${start[$i]}))
if [ "${kfile[$i]}" != "" ]; then
line=$(atos -o "${kfile[$i]}" -l "${start[$i]}" "$addr")
else
line="(in ${name[$i]} at ${start[$i]})"
fi
break
fi
(( i++ ))
done
fi
# Fallback to kernel
if [ "$line" = "" ] ; then
line=$(atos -o "$kernel" -s "$slide" "$addr")
unslid=$((addr-slide))
fi
printf "0x%016llx 0x%016llx %s\n" "$addr" "$unslid" "$line"
done
# Print other key details
awk '/^BSD process name/ { gsub(/ corresponding to current thread/, ""); print $0 }
ver == 1 { print "Mac OS version:", $0; ver = 0 }
/^Mac OS version/ { ver = 1 }
/^Boot args:/ { print $0 }
' < "$file"
done
echo ""
echo "ALWAYS include the original panic log as well!"
echo ""

View File

@@ -0,0 +1,8 @@
## macrecovery
macrecovery is a tool that helps to automate recovery interaction. It can be used to download diagnostics and recovery as well as analyse MLB.
Requires python3 to run. Run with `-h` argument to see all available arguments.
To create a disk image for a virtual machine installation use `build-image.sh`.

View File

@@ -0,0 +1,79 @@
{
"Mac-EE2EBD4B90B839A8": "13.7.7",
"Mac-BE0E8AC46FE800CC": "11.7.10",
"Mac-9AE82516C7C6B903": "12.7.6",
"Mac-942452F5819B1C1B": "10.13.6",
"Mac-942C5DF58193131B": "10.13.6",
"Mac-C08A6BB70A942AC2": "10.13.6",
"Mac-742912EFDBEE19B3": "10.13.6",
"Mac-66F35F19FE2A0D05": "10.15.7",
"Mac-2E6FAB96566FE58C": "10.15.7",
"Mac-35C1E88140C3E6CF": "11.7.10",
"Mac-7DF21CB3ED6977E5": "11.7.10",
"Mac-9F18E312C5C2BF0B": "12.7.6",
"Mac-937CB26E2E02BB01": "12.7.6",
"Mac-827FAC58A8FDFA22": "14.7.7",
"Mac-226CB3C6A851A671": "14.7.7",
"Mac-0CFF9C7C2B63DF8D": "15.6",
"Mac-C3EC7CD22292981F": "10.15.7",
"Mac-AFD8A9D944EA4843": "10.15.7",
"Mac-189A3D4F975D5FFC": "11.7.10",
"Mac-3CBD00234E554E41": "11.7.10",
"Mac-2BD1B31983FE1663": "11.7.10",
"Mac-06F11FD93F0323C5": "12.7.6",
"Mac-06F11F11946D27C5": "12.7.6",
"Mac-E43C1C25D4880AD6": "12.7.6",
"Mac-473D31EABEB93F9B": "12.7.6",
"Mac-66E35819EE2D0D05": "12.7.6",
"Mac-A5C67F76ED83108C": "12.7.6",
"Mac-B4831CEBD52A0C4C": "13.7.7",
"Mac-CAD6701F7CEA0921": "13.7.7",
"Mac-551B86E5744E2388": "13.7.7",
"Mac-937A206F2EE63C01": "15.6",
"Mac-827FB448E656EC26": "15.6",
"Mac-1E7E29AD0135F9BC": "15.6",
"Mac-53FDB3D8DB8CA971": "15.6",
"Mac-E1008331FDC96864": "latest",
"Mac-5F9802EFE386AA28": "latest",
"Mac-E7203C0F68AA0004": "15.6",
"Mac-A61BADE1FDAD7B05": "latest",
"Mac-F22589C8": "10.13.6",
"Mac-94245B3640C91C81": "10.13.6",
"Mac-94245A3940C91C80": "10.13.6",
"Mac-942459F5819B171B": "10.13.6",
"Mac-4B7AC7E43945597E": "10.15.7",
"Mac-6F01561E16C75D06": "10.15.7",
"Mac-F60DEB81FF30ACF6": "12.7.6",
"Mac-27AD2F918AE68F61": "latest",
"Mac-F2208EC8": "10.13.6",
"Mac-8ED6AF5B48C039E1": "10.13.6",
"Mac-4BC72D62AD45599E": "10.13.6",
"Mac-7BA5B2794B2CDB12": "10.13.6",
"Mac-031AEE4D24BFF0B1": "10.15.7",
"Mac-F65AE981FFA204ED": "10.15.7",
"Mac-35C5E08120C7EEAF": "12.7.6",
"Mac-7BA5B2DFE22DDD8C": "15.6",
"Mac-942B5BF58194151B": "10.13.6",
"Mac-942B59F58194171B": "10.13.6",
"Mac-00BE6ED71E35EB86": "10.15.7",
"Mac-FC02E91DDD3FA6A4": "10.15.7",
"Mac-7DF2A3B5E5D671ED": "10.15.7",
"Mac-031B6874CF7F642A": "10.15.7",
"Mac-27ADBB7B4CEE8E61": "10.15.7",
"Mac-77EB7D7DAF985301": "10.15.7",
"Mac-81E3E92DD6088272": "11.7.10",
"Mac-42FD25EABCABB274": "11.7.10",
"Mac-A369DDC4E67F1C45": "12.7.6",
"Mac-FFE5EF870D7BA81A": "12.7.6",
"Mac-DB15BD556843C820": "12.7.6",
"Mac-65CE76090165799A": "12.7.6",
"Mac-B809C3757DA9BB8D": "12.7.6",
"Mac-4B682C642B45593E": "13.7.7",
"Mac-77F17D7DA9285301": "13.7.7",
"Mac-BE088AF8C5EB4FA2": "13.7.7",
"Mac-AA95B1DDAB278B95": "15.6",
"Mac-63001698E7A34814": "15.6",
"Mac-CFF7D910A743CAAF": "latest",
"Mac-AF89B6D9451A490B": "latest",
"Mac-7BA5B2D9E42DDD94": "15.6"
}

View File

@@ -0,0 +1,24 @@
#!/bin/bash -e
if [ -z "$1" ]; then
FORMAT="raw"
else
FORMAT="${1}"
fi
rm -rf Recovery.RO.dmg Recovery.raw "Recovery.${FORMAT}" Recovery.dmg.sparseimage
hdiutil create -size 900m -layout "UNIVERSAL HD" -type SPARSE -o Recovery.dmg
newDevice=$(hdiutil attach -nomount Recovery.dmg.sparseimage | head -n 1 | awk '{print $1}')
echo newdevice "$newDevice"
diskutil partitionDisk "${newDevice}" 1 MBR fat32 RECOVERY R
N=$(echo "$newDevice" | tr -dc '0-9')
diskutil mount disk"${N}"s1
MOUNT="$(diskutil info disk"${N}"s1 | sed -n 's/.*Mount Point: *//p')"
mkdir -p "$MOUNT/com.apple.recovery.boot"
cp ./*.dmg ./*.chunklist "$MOUNT/com.apple.recovery.boot/"
diskutil umount disk"${N}"s1
hdiutil detach "$newDevice"
hdiutil convert -format UDZO Recovery.dmg.sparseimage -o Recovery.RO.dmg
rm Recovery.dmg.sparseimage
qemu-img convert -f dmg -O "${FORMAT}" Recovery.RO.dmg "Recovery.${FORMAT}"
rm Recovery.RO.dmg

View File

@@ -0,0 +1,519 @@
#!/usr/bin/env python3
"""
Gather recovery information for Macs.
Copyright (c) 2019, vit9696
"""
import argparse
import hashlib
import json
import linecache
import os
import random
import struct
import string
import sys
try:
from urllib.request import Request, HTTPError, urlopen
from urllib.parse import urlparse
except ImportError:
print('ERROR: Python 2 is not supported, please use Python 3')
sys.exit(1)
SELF_DIR = os.path.dirname(os.path.realpath(__file__))
# MacPro7,1
RECENT_MAC = 'Mac-27AD2F918AE68F61'
MLB_ZERO = '00000000000000000'
MLB_VALID = 'F5K105303J9K3F71M'
MLB_PRODUCT = 'F5K00000000K3F700'
TYPE_SID = 16
TYPE_K = 64
TYPE_FG = 64
INFO_PRODUCT = 'AP'
INFO_IMAGE_LINK = 'AU'
INFO_IMAGE_HASH = 'AH'
INFO_IMAGE_SESS = 'AT'
INFO_SIGN_LINK = 'CU'
INFO_SIGN_HASH = 'CH'
INFO_SIGN_SESS = 'CT'
INFO_REQURED = [INFO_PRODUCT, INFO_IMAGE_LINK, INFO_IMAGE_HASH, INFO_IMAGE_SESS, INFO_SIGN_LINK, INFO_SIGN_HASH, INFO_SIGN_SESS]
# Use -2 for better resize stability on Windows
TERMINAL_MARGIN = 2
def run_query(url, headers, post=None, raw=False):
if post is not None:
data = '\n'.join(entry + '=' + post[entry] for entry in post).encode()
else:
data = None
req = Request(url=url, headers=headers, data=data)
try:
response = urlopen(req)
if raw:
return response
return dict(response.info()), response.read()
except HTTPError as e:
print(f'ERROR: "{e}" when connecting to {url}')
sys.exit(1)
def generate_id(id_type, id_value=None):
return id_value or ''.join(random.choices(string.hexdigits[:16].upper(), k=id_type))
def product_mlb(mlb):
return '00000000000' + mlb[11:15] + '00'
def mlb_from_eeee(eeee):
if len(eeee) != 4:
print('ERROR: Invalid EEEE code length!')
sys.exit(1)
return f'00000000000{eeee}00'
# zhangyoufu https://gist.github.com/MCJack123/943eaca762730ca4b7ae460b731b68e7#gistcomment-3061078 2021-10-08
Apple_EFI_ROM_public_key_1 = 0xC3E748CAD9CD384329E10E25A91E43E1A762FF529ADE578C935BDDF9B13F2179D4855E6FC89E9E29CA12517D17DFA1EDCE0BEBF0EA7B461FFE61D94E2BDF72C196F89ACD3536B644064014DAE25A15DB6BB0852ECBD120916318D1CCDEA3C84C92ED743FC176D0BACA920D3FCF3158AFF731F88CE0623182A8ED67E650515F75745909F07D415F55FC15A35654D118C55A462D37A3ACDA08612F3F3F6571761EFCCBCC299AEE99B3A4FD6212CCFFF5EF37A2C334E871191F7E1C31960E010A54E86FA3F62E6D6905E1CD57732410A3EB0C6B4DEFDABE9F59BF1618758C751CD56CEF851D1C0EAA1C558E37AC108DA9089863D20E2E7E4BF475EC66FE6B3EFDCF
ChunkListHeader = struct.Struct('<4sIBBBxQQQ')
assert ChunkListHeader.size == 0x24
Chunk = struct.Struct('<I32s')
assert Chunk.size == 0x24
def verify_chunklist(cnkpath):
with open(cnkpath, 'rb') as f:
hash_ctx = hashlib.sha256()
data = f.read(ChunkListHeader.size)
hash_ctx.update(data)
magic, header_size, file_version, chunk_method, signature_method, chunk_count, chunk_offset, signature_offset = ChunkListHeader.unpack(data)
assert magic == b'CNKL'
assert header_size == ChunkListHeader.size
assert file_version == 1
assert chunk_method == 1
assert signature_method in [1, 2]
assert chunk_count > 0
assert chunk_offset == 0x24
assert signature_offset == chunk_offset + Chunk.size * chunk_count
for _ in range(chunk_count):
data = f.read(Chunk.size)
hash_ctx.update(data)
chunk_size, chunk_sha256 = Chunk.unpack(data)
yield chunk_size, chunk_sha256
digest = hash_ctx.digest()
if signature_method == 1:
data = f.read(256)
assert len(data) == 256
signature = int.from_bytes(data, 'little')
plaintext = int(f'0x1{"f"*404}003031300d060960864801650304020105000420{"0"*64}', 16) | int.from_bytes(digest, 'big')
assert pow(signature, 0x10001, Apple_EFI_ROM_public_key_1) == plaintext
elif signature_method == 2:
data = f.read(32)
assert data == digest
raise RuntimeError('Chunklist missing digital signature')
else:
raise NotImplementedError
assert f.read(1) == b''
def get_session(args):
headers = {
'Host': 'osrecovery.apple.com',
'Connection': 'close',
'User-Agent': 'InternetRecovery/1.0',
}
headers, _ = run_query('http://osrecovery.apple.com/', headers)
if args.verbose:
print('Session headers:')
for header in headers:
print(f'{header}: {headers[header]}')
for header in headers:
if header.lower() == 'set-cookie':
cookies = headers[header].split('; ')
for cookie in cookies:
return cookie if cookie.startswith('session=') else ...
raise RuntimeError('No session in headers ' + str(headers))
def get_image_info(session, bid, mlb=MLB_ZERO, diag=False, os_type='default', cid=None):
headers = {
'Host': 'osrecovery.apple.com',
'Connection': 'close',
'User-Agent': 'InternetRecovery/1.0',
'Cookie': session,
'Content-Type': 'text/plain',
}
post = {
'cid': generate_id(TYPE_SID, cid),
'sn': mlb,
'bid': bid,
'k': generate_id(TYPE_K),
'fg': generate_id(TYPE_FG)
}
if diag:
url = 'http://osrecovery.apple.com/InstallationPayload/Diagnostics'
else:
url = 'http://osrecovery.apple.com/InstallationPayload/RecoveryImage'
post['os'] = os_type
headers, output = run_query(url, headers, post)
output = output.decode('utf-8')
info = {}
for line in output.split('\n'):
try:
key, value = line.split(': ')
info[key] = value
except KeyError:
continue
except ValueError:
continue
for k in INFO_REQURED:
if k not in info:
raise RuntimeError(f'Missing key {k}')
return info
def save_image(url, sess, filename='', directory=''):
purl = urlparse(url)
headers = {
'Host': purl.hostname,
'Connection': 'close',
'User-Agent': 'InternetRecovery/1.0',
'Cookie': '='.join(['AssetToken', sess])
}
if not os.path.exists(directory):
os.makedirs(directory)
if filename == '':
filename = os.path.basename(purl.path)
if filename.find(os.sep) >= 0 or filename == '':
raise RuntimeError('Invalid save path ' + filename)
print(f'Saving {url} to {directory}{os.sep}{filename}...')
with open(os.path.join(directory, filename), 'wb') as fh:
response = run_query(url, headers, raw=True)
headers = dict(response.headers)
totalsize = -1
for header in headers:
if header.lower() == 'content-length':
totalsize = int(headers[header])
break
size = 0
oldterminalsize = 0
while True:
chunk = response.read(2**20)
if not chunk:
break
fh.write(chunk)
size += len(chunk)
try:
terminalsize = max(os.get_terminal_size().columns - TERMINAL_MARGIN, 0)
except OSError:
terminalsize = 80
if oldterminalsize != terminalsize:
print(f'\r{"":<{terminalsize}}', end='')
oldterminalsize = terminalsize
if totalsize > 0:
progress = size / totalsize
barwidth = terminalsize // 3
print(f'\r{size / (2**20):.1f}/{totalsize / (2**20):.1f} MB ', end='')
if terminalsize > 55:
print(f'|{"=" * int(barwidth * progress):<{barwidth}}|', end='')
print(f' {progress*100:.1f}% downloaded', end='')
else:
# Fallback if Content-Length isn't available
print(f'\r{size / (2**20)} MB downloaded...', end='')
sys.stdout.flush()
print('\nDownload complete!')
return os.path.join(directory, os.path.basename(filename))
def verify_image(dmgpath, cnkpath):
print('Verifying image with chunklist...')
with open(dmgpath, 'rb') as dmgf:
for cnkcount, (cnksize, cnkhash) in enumerate(verify_chunklist(cnkpath), 1):
terminalsize = max(os.get_terminal_size().columns - TERMINAL_MARGIN, 0)
print(f'\r{f"Chunk {cnkcount} ({cnksize} bytes)":<{terminalsize}}', end='')
sys.stdout.flush()
cnk = dmgf.read(cnksize)
if len(cnk) != cnksize:
raise RuntimeError(f'Invalid chunk {cnkcount} size: expected {cnksize}, read {len(cnk)}')
if hashlib.sha256(cnk).digest() != cnkhash:
raise RuntimeError(f'Invalid chunk {cnkcount}: hash mismatch')
if dmgf.read(1) != b'':
raise RuntimeError('Invalid image: larger than chunklist')
print('\nImage verification complete!')
def action_download(args):
"""
Reference information for queries:
Recovery latest:
cid=3076CE439155BA14
sn=...
bid=Mac-E43C1C25D4880AD6
k=4BE523BB136EB12B1758C70DB43BDD485EBCB6A457854245F9E9FF0587FB790C
os=latest
fg=B2E6AA07DB9088BE5BDB38DB2EA824FDDFB6C3AC5272203B32D89F9D8E3528DC
Recovery default:
cid=4A35CB95FF396EE7
sn=...
bid=Mac-E43C1C25D4880AD6
k=0A385E6FFC3DDD990A8A1F4EC8B98C92CA5E19C9FF1DD26508C54936D8523121
os=default
fg=B2E6AA07DB9088BE5BDB38DB2EA824FDDFB6C3AC5272203B32D89F9D8E3528DC
Diagnostics:
cid=050C59B51497CEC8
sn=...
bid=Mac-E43C1C25D4880AD6
k=37D42A8282FE04A12A7D946304F403E56A2155B9622B385F3EB959A2FBAB8C93
fg=B2E6AA07DB9088BE5BDB38DB2EA824FDDFB6C3AC5272203B32D89F9D8E3528DC
"""
session = get_session(args)
info = get_image_info(session, bid=args.board_id, mlb=args.mlb, diag=args.diagnostics, os_type=args.os_type)
if args.verbose:
print(info)
print(f'Downloading {info[INFO_PRODUCT]}...')
cnkname = '' if args.basename == '' else args.basename + '.chunklist'
cnkpath = save_image(info[INFO_SIGN_LINK], info[INFO_SIGN_SESS], cnkname, args.outdir)
dmgname = '' if args.basename == '' else args.basename + '.dmg'
dmgpath = save_image(info[INFO_IMAGE_LINK], info[INFO_IMAGE_SESS], dmgname, args.outdir)
try:
verify_image(dmgpath, cnkpath)
return 0
except Exception as err:
if isinstance(err, AssertionError) and str(err) == '':
try:
tb = sys.exc_info()[2]
while tb.tb_next:
tb = tb.tb_next
err = linecache.getline(tb.tb_frame.f_code.co_filename, tb.tb_lineno, tb.tb_frame.f_globals).strip()
except Exception:
err = "Invalid chunklist"
print(f'\rImage verification failed. ({err})')
return 1
def action_selfcheck(args):
"""
Sanity check server logic for recovery:
if not valid(bid):
return error()
ppp = get_ppp(sn)
if not valid(ppp):
return latest_recovery(bid = bid) # Returns newest for bid.
if valid(sn):
if os == 'default':
return default_recovery(sn = sn, ppp = ppp) # Returns oldest for sn.
else:
return latest_recovery(sn = sn, ppp = ppp) # Returns newest for sn.
return default_recovery(ppp = ppp) # Returns oldest.
"""
session = get_session(args)
valid_default = get_image_info(session, bid=RECENT_MAC, mlb=MLB_VALID, diag=False, os_type='default')
valid_latest = get_image_info(session, bid=RECENT_MAC, mlb=MLB_VALID, diag=False, os_type='latest')
product_default = get_image_info(session, bid=RECENT_MAC, mlb=MLB_PRODUCT, diag=False, os_type='default')
product_latest = get_image_info(session, bid=RECENT_MAC, mlb=MLB_PRODUCT, diag=False, os_type='latest')
generic_default = get_image_info(session, bid=RECENT_MAC, mlb=MLB_ZERO, diag=False, os_type='default')
generic_latest = get_image_info(session, bid=RECENT_MAC, mlb=MLB_ZERO, diag=False, os_type='latest')
if args.verbose:
print(valid_default)
print(valid_latest)
print(product_default)
print(product_latest)
print(generic_default)
print(generic_latest)
if valid_default[INFO_PRODUCT] == valid_latest[INFO_PRODUCT]:
# Valid MLB must give different default and latest if this is not a too new product.
print(f'ERROR: Cannot determine any previous product, got {valid_default[INFO_PRODUCT]}')
return 1
if product_default[INFO_PRODUCT] != product_latest[INFO_PRODUCT]:
# Product-only MLB must give the same value for default and latest.
print(f'ERROR: Latest and default do not match for product MLB, got {product_default[INFO_PRODUCT]} and {product_latest[INFO_PRODUCT]}')
return 1
if generic_default[INFO_PRODUCT] != generic_latest[INFO_PRODUCT]:
# Zero MLB always give the same value for default and latest.
print(f'ERROR: Generic MLB gives different product, got {generic_default[INFO_PRODUCT]} and {generic_latest[INFO_PRODUCT]}')
return 1
if valid_latest[INFO_PRODUCT] != generic_latest[INFO_PRODUCT]:
# Valid MLB must always equal generic MLB.
print(f'ERROR: Cannot determine unified latest product, got {valid_latest[INFO_PRODUCT]} and {generic_latest[INFO_PRODUCT]}')
return 1
if product_default[INFO_PRODUCT] != valid_default[INFO_PRODUCT]:
# Product-only MLB can give the same value with valid default MLB.
# This is not an error for all models, but for our chosen code it is.
print(f'ERROR: Valid and product MLB give mismatch, got {product_default[INFO_PRODUCT]} and {valid_default[INFO_PRODUCT]}')
return 1
print('SUCCESS: Found no discrepancies with MLB validation algorithm!')
return 0
def action_verify(args):
"""
Try to verify MLB serial number.
"""
session = get_session(args)
generic_latest = get_image_info(session, bid=RECENT_MAC, mlb=MLB_ZERO, diag=False, os_type='latest')
uvalid_default = get_image_info(session, bid=args.board_id, mlb=args.mlb, diag=False, os_type='default')
uvalid_latest = get_image_info(session, bid=args.board_id, mlb=args.mlb, diag=False, os_type='latest')
uproduct_default = get_image_info(session, bid=args.board_id, mlb=product_mlb(args.mlb), diag=False, os_type='default')
if args.verbose:
print(generic_latest)
print(uvalid_default)
print(uvalid_latest)
print(uproduct_default)
# Verify our MLB number.
if uvalid_default[INFO_PRODUCT] != uvalid_latest[INFO_PRODUCT]:
print(f'SUCCESS: {args.mlb} MLB looks valid and supported!' if uvalid_latest[INFO_PRODUCT] == generic_latest[INFO_PRODUCT] else f'SUCCESS: {args.mlb} MLB looks valid, but probably unsupported!')
return 0
print('UNKNOWN: Run selfcheck, check your board-id, or try again later!')
# Here we have matching default and latest products. This can only be true for very
# new models. These models get either latest or special builds.
if uvalid_default[INFO_PRODUCT] == generic_latest[INFO_PRODUCT]:
print(f'UNKNOWN: {args.mlb} MLB can be valid if very new!')
return 0
if uproduct_default[INFO_PRODUCT] != uvalid_default[INFO_PRODUCT]:
print(f'UNKNOWN: {args.mlb} MLB looks invalid, other models use product {uproduct_default[INFO_PRODUCT]} instead of {uvalid_default[INFO_PRODUCT]}!')
return 0
print(f'UNKNOWN: {args.mlb} MLB can be valid if very new and using special builds!')
return 0
def action_guess(args):
"""
Attempt to guess which model does this MLB belong.
"""
mlb = args.mlb
anon = mlb.startswith('000')
with open(args.board_db, 'r', encoding='utf-8') as fh:
db = json.load(fh)
supported = {}
session = get_session(args)
generic_latest = get_image_info(session, bid=RECENT_MAC, mlb=MLB_ZERO, diag=False, os_type='latest')
for model in db:
try:
if anon:
# For anonymous lookup check when given model does not match latest.
model_latest = get_image_info(session, bid=model, mlb=MLB_ZERO, diag=False, os_type='latest')
if model_latest[INFO_PRODUCT] != generic_latest[INFO_PRODUCT]:
if db[model] == 'current':
print(f'WARN: Skipped {model} due to using latest product {model_latest[INFO_PRODUCT]} instead of {generic_latest[INFO_PRODUCT]}')
continue
user_default = get_image_info(session, bid=model, mlb=mlb, diag=False, os_type='default')
if user_default[INFO_PRODUCT] != generic_latest[INFO_PRODUCT]:
supported[model] = [db[model], user_default[INFO_PRODUCT], generic_latest[INFO_PRODUCT]]
else:
# For normal lookup check when given model has mismatching normal and latest.
user_latest = get_image_info(session, bid=model, mlb=mlb, diag=False, os_type='latest')
user_default = get_image_info(session, bid=model, mlb=mlb, diag=False, os_type='default')
if user_latest[INFO_PRODUCT] != user_default[INFO_PRODUCT]:
supported[model] = [db[model], user_default[INFO_PRODUCT], user_latest[INFO_PRODUCT]]
except Exception as e:
print(f'WARN: Failed to check {model}, exception: {e}')
if len(supported) > 0:
print(f'SUCCESS: MLB {mlb} looks supported for:')
for model in supported.items():
print(f'- {model}, up to {supported[model][0]}, default: {supported[model][1]}, latest: {supported[model][2]}')
return 0
print(f'UNKNOWN: Failed to determine supported models for MLB {mlb}!')
return None
def main():
parser = argparse.ArgumentParser(description='Gather recovery information for Macs')
parser.add_argument('action', choices=['download', 'selfcheck', 'verify', 'guess'],
help='Action to perform: "download" - performs recovery downloading,'
' "selfcheck" checks whether MLB serial validation is possible, "verify" performs'
' MLB serial verification, "guess" tries to find suitable mac model for MLB.')
parser.add_argument('-o', '--outdir', type=str, default='com.apple.recovery.boot',
help='customise output directory for downloading, defaults to com.apple.recovery.boot')
parser.add_argument('-n', '--basename', type=str, default='',
help='customise base name for downloading, defaults to remote name')
parser.add_argument('-b', '--board-id', type=str, default=RECENT_MAC,
help=f'use specified board identifier for downloading, defaults to {RECENT_MAC}')
parser.add_argument('-m', '--mlb', type=str, default=MLB_ZERO,
help=f'use specified logic board serial for downloading, defaults to {MLB_ZERO}')
parser.add_argument('-e', '--code', type=str, default='',
help='generate product logic board serial with specified product EEEE code')
parser.add_argument('-os', '--os-type', type=str, default='default', choices=['default', 'latest'],
help=f'use specified os type, defaults to default {MLB_ZERO}')
parser.add_argument('-diag', '--diagnostics', action='store_true', help='download diagnostics image')
parser.add_argument('-v', '--verbose', action='store_true', help='print debug information')
parser.add_argument('-db', '--board-db', type=str, default=os.path.join(SELF_DIR, 'boards.json'),
help='use custom board list for checking, defaults to boards.json')
args = parser.parse_args()
if args.code != '':
args.mlb = mlb_from_eeee(args.code)
if len(args.mlb) != 17:
print('ERROR: Cannot use MLBs in non 17 character format!')
sys.exit(1)
if args.action == 'download':
return action_download(args)
if args.action == 'selfcheck':
return action_selfcheck(args)
if args.action == 'verify':
return action_verify(args)
if args.action == 'guess':
return action_guess(args)
assert False
if __name__ == '__main__':
sys.exit(main())

View File

@@ -0,0 +1,60 @@
Lion
./macrecovery.py -b Mac-2E6FAB96566FE58C -m 00000000000F25Y00 download
./macrecovery.py -b Mac-C3EC7CD22292981F -m 00000000000F0HM00 download
Mountain Lion:
./macrecovery.py -b Mac-7DF2A3B5E5D671ED -m 00000000000F65100 download
Mavericks
./macrecovery.py -b Mac-F60DEB81FF30ACF6 -m 00000000000FNN100 download
Yosemite:
./macrecovery.py -b Mac-E43C1C25D4880AD6 -m 00000000000GDVW00 download
El Capitan
./macrecovery.py -b Mac-FFE5EF870D7BA81A -m 00000000000GQRX00 download
Sierra
./macrecovery.py -b Mac-77F17D7DA9285301 -m 00000000000J0DX00 download
High Sierra
./macrecovery.py -b Mac-7BA5B2D9E42DDD94 -m 00000000000J80300 download
./macrecovery.py -b Mac-BE088AF8C5EB4FA2 -m 00000000000J80300 download
Mojave
./macrecovery.py -b Mac-7BA5B2DFE22DDD8C -m 00000000000KXPG00 download
Catalina
./macrecovery.py -b Mac-CFF7D910A743CAAF -m 00000000000PHCD00 download
./macrecovery.py -b Mac-00BE6ED71E35EB86 -m 00000000000000000 download
Big Sur
./macrecovery.py -b Mac-2BD1B31983FE1663 -m 00000000000000000 download
Monterey
./macrecovery.py -b Mac-E43C1C25D4880AD6 -m 00000000000000000 download
Ventura
./macrecovery.py -b Mac-B4831CEBD52A0C4C -m 00000000000000000 download
Sonoma
./macrecovery.py -b Mac-827FAC58A8FDFA22 -m 00000000000000000 download
Sequoia
./macrecovery.py -b Mac-7BA5B2D9E42DDD94 -m 00000000000000000 download
Tahoe
./macrecovery.py -b Mac-CFF7D910A743CAAF -m 00000000000000000 -os latest download
Diagnostics
./macrecovery.py -b Mac-7BA5B2D9E42DDD94 -m 00000000000000000 -diag download
./macrecovery.py -b Mac-7BA5B2D9E42DDD94 -m 00000000000JG3600 -diag download
./macrecovery.py -b Mac-7BA5B2D9E42DDD94 <real MLB> -diag download
Default version
./macrecovery.py -b Mac-7BA5B2D9E42DDD94 -m 00000000000JG3600 download (oldest)
./macrecovery.py -b Mac-7BA5B2D9E42DDD94 -m <real MLB> -os default download (newer)
Latest version
./macrecovery.py -b Mac-CFF7D910A743CAAF -m 00000000000000000 -os latest download
./macrecovery.py -b Mac-CFF7D910A743CAAF -m <real MLB> -os latest

View File

@@ -0,0 +1,224 @@
Apple Mac Serial Format
=======================
It is reasonably important to get more information about the goods you buy, especially if they are not new, and you do not have absolute confidence in the seller. Serial numbers are the first thing to look at. For Apple products [Apple Check Coverage](https://checkcoverage.apple.com) is your best friend.
However, it does not show all the details encoded in the serial, and in some case it may be important. For example, certain shady dealers may change one valid serial by the other, and it will not be obvious at a glance that the serial does not belong to the actual model. This FAQ attempts to explain the reverse-engineered structure of the serials used in Apple hardware.
You could always receive information about the current serial number of your Mac by running `./macserial`. For the other serial use `./macserial -i SERIALNUMBER`, where `SERIALNUMBER` is the serial you check.
## Apple base 34
Select fields in the numbers are encoded values in base 34. So, certain alpha-numeric characters represent a slightly uncommon base 34 code excluding `O` and `I`.
| Char | Value | Char | Value |
| ---- | ----- | ---- | ----- |
| `0` | `0` | `H` | `17` |
| `1` | `1` | `J` | `18` |
| `2` | `2` | `K` | `19` |
| `3` | `3` | `L` | `20` |
| `4` | `4` | `M` | `21` |
| `5` | `5` | `N` | `22` |
| `6` | `6` | `P` | `23` |
| `7` | `7` | `Q` | `24` |
| `8` | `8` | `R` | `25` |
| `9` | `9` | `S` | `26` |
| `A` | `10` | `T` | `27` |
| `B` | `11` | `U` | `28` |
| `C` | `12` | `V` | `29` |
| `D` | `13` | `W` | `30` |
| `E` | `14` | `X` | `31` |
| `F` | `15` | `Y` | `32` |
| `G` | `16` | `Z` | `33` |
## Serial number (SN)
There generally are 2 similar formats of serial encoding: the old 11 character format, and the new 12 character format.
| Type | Location | Year | Week | Line | Product |
| --------- | --------- | ---- | ---- | ----- | -------- |
| Old (11) | `LL` | `Y` | `WW` | `SSS` | `PPP` |
| New (12) | `LLL` | `Y` | `W` | `SSS` | `PPPP` |
Note: Models late 2021+ contain SN with 10 character format.
### Location
This value encodes the manufacturing location, which is often more descriptive than `Made in China`, since it may reveal the responsible company and the city. For example, `F5K` means `USA (Flextronics)` and `QT` means `Taiwan (Quanta Computer)`. The list is not standardised or published anywhere, but you can see several known locations by running `./macserial -l`.
One of the important locations for old-style serials (11 characters) is `RM`. It means that the model was refurbished. For new-style serials you have to call [Apple support](https://support.apple.com) to know this.
### Year
Year encodes the actual manufacturing year of each model. For refurbished models it is unknown whether it is replaced by the remanufacturing year.
For old-style serials it always is a digit that encodes the last digit of the year. For example, `8` means 2008 and `1` means 2011. Only `0` to `9` digitis are used for year encoding. Old-style serials are out of use starting with 2013, so `3` means 2003 not 2013.
| Char | Year |
| ---- | ---- |
| `3` | 2003 |
| `4` | 2004 |
| `5` | 2005 |
| `6` | 2006 |
| `7` | 2007 |
| `8` | 2008 |
| `9` | 2009 |
| `0` | 2010 |
| `1` | 2011 |
| `2` | 2012 |
For new-style serials it is an alphanumeric value, which not only encodes the year, but its half as well. Not all the values are allowed. The table below outlines the pairs of characters which are assumed to encode each supported year. First character in the pair is believed to encode the first half of the year, and the second character — the second half.
| Pair | Year |
| -------- | ---- |
| `C`, `D` | 2010 |
| `F`, `G` | 2011 |
| `H`, `J` | 2012 |
| `K`, `L` | 2013 |
| `M`, `N` | 2014 |
| `P`, `Q` | 2015 |
| `R`, `S` | 2016 |
| `T`, `V` | 2017 |
| `W`, `X` | 2018 |
| `Y`, `Z` | 2019 |
| `C`, `D` | 2020 |
| `F`, `G` | 2021 |
### Week
Week encodes the actual manufacturing week of each model. This week has nothing in common with [ISO 8601](https://en.wikipedia.org/wiki/ISO_week_date), and appears to be encoded literally as 7-day sequences starting from January, 1st. Since each year has either 365 or 366 days, 53rd week is extremely rare, and you are lucky to have such a serial.
For old-style serials week is encoded in plain numeric digits with leading zeroes. `01`, `02`, ... `53`. For new-style serials an alpha-numeric code is used. Encoded year half also counts and means adds 26 weeks for the second one.
| Char | 1st half | 2nd half |
| ---- | -------- | -------- |
| `1` | `1` | `27` |
| `2` | `2` | `28` |
| `3` | `3` | `29` |
| `4` | `4` | `30` |
| `5` | `5` | `31` |
| `6` | `6` | `32` |
| `7` | `7` | `33` |
| `8` | `8` | `34` |
| `9` | `9` | `35` |
| `C` | `10` | `36` |
| `D` | `11` | `37` |
| `F` | `12` | `38` |
| `G` | `13` | `39` |
| `H` | `14` | `40` |
| `J` | `15` | `41` |
| `K` | `16` | `42` |
| `L` | `17` | `43` |
| `M` | `18` | `44` |
| `N` | `19` | `45` |
| `P` | `20` | `46` |
| `Q` | `21` | `47` |
| `R` | `22` | `48` |
| `T` | `23` | `49` |
| `V` | `24` | `50` |
| `W` | `25` | `51` |
| `X` | `26` | `52` |
| `Y` | `-` | `53` |
For old-style serials it is a pair of two digits, which encode the manufacturing week.
### Production line and copy
Production line is believed to be related to some identifier at assembly stage. It is encoded in base 34, but the actual derivation process is unknown and can only be guessed with relative success.
Current model, which apparently works well, represents it as a sum of three alpha-numeric characters with `1`, `34`, and `68` multipliers. The actual formula looks as follows:
```
base34[S1] * 68 + base34[S2] * 34 + base34[S3] = production line
```
This formula effectively defines a compression function, which allows to encode a total of `3400` production lines from `0` to `3399`. The compression produced by shortening `39304` space to `3400` allows multiple encodings of the same line. For example, for `939` line there can be `14` derivatives or "copies": `0TM`, `1RM`, `2PM`, `3MM`, `4KM`, ..., `D1M`.
While the formula does look strange, it was experimentally discovered that up to `N` first encoded derivatives are valid, and starting with the first invalid derivative there will be no valid ones. Thus for a complete serial list made up with all the derivatives from the above the following is assumed to be true: if `0TM` and `2PM` are valid and `3MM` is invalid, then `1RM` will also be valid, and `4KM` to `D1M` will be invalid. From this data it could be theorised that the encoded value is incremented for each model produced from the same line. So `0TM` is the first copy produced, and `D1M` is the last copy.
**Update**: At a later stage very few examples of valid derivatives after invalid were found. These exceptions disprove at least some parts of the model, but currently there exists no better theory.
### Product model
Last 3 (for legacy serials) or 4 (for new serials) symbols encode the actual product identifier of this exact piece of the hardware. This is probably the most useful part of the serial, since it allows you to get the detailed description of your hardware directly from the dedicated Apple Specs portal. To do so you need to modify the following URI to contain your real product code instead of `PPPP` and follow it in your browser:
```
http://support-sp.apple.com/sp/index?page=cpuspec&cc=PPPP
```
For example, for iMacPro1,1 it could be [HX87](http://support-sp.apple.com/sp/index?page=cpuspec&cc=HX87) and for MacBookPro14,3 it could be [HTD5](http://support-sp.apple.com/sp/index?page=cpuspec&cc=HTD5).
The list is not standardised or published anywhere, but you can see most products by running `./macserial -lp` and `./macserial -l` to match against mac models. The value seems to be a classic base 34 sequence: `P1 * 39304 + P2 * 1156 + P3 * 34 + P4`. The ranges seem to be allocated in chunks in non-decreasing manner. Normally each chunk is distanced from another chunk by up to 64 (90% matches).
## Logic board serial number (MLB)
There generally are 2 formats of logic board serial encoding: the old 13 character format, and the new 17 character format. Unlike serial number, these formats are quite different and in addition very little is known about MLB in general.
| Type | Location | Year | Week | Item | Infix | Product | Suffix |
| --------- | --------- | ---- | ---- | ------ | ------ | -------- | ------ |
| Old (13) | `LL` | `Y` | `WW` | `IIII` | | `EEE` | `C` |
| New (17) | `LLL` | `Y` | `WW` | `III` | `AA` | `EEEE` | `CC` |
While it is unclear if this is intentional, for 17 character MLB it is possible to perform basic validation online through `osrecovery.apple.com`. The recovery server will return valid latest recovery image only when MLB is valid. Use `./macrecovery.py verify -m MLB -b BOARD-ID` to try verifying your MLB number.
It is not clear how strongly MLB is attached to serial number (SN). The following is known:
- Minimal supported macOS version is identified by `EEEE`
- Maximum supported macOS version is identified by `EEEE` and `board-id`
- Recovery server accepts a range of models with the same MLB (with only latest os different)
The following is suspected:
- `EEEE` is unique number for all MLBs
- `EEEE` are shared across different models and thus cannot identify the model
### Location
MLB location is equivalent to serial number location but does not necessarily match it, as logic boards can be manufactured at a different place.
### Year and week
MLB year and week in both 13-character and 17-character MLB are equivalent to legacy serial number year and week. The values are slightly lower as logic board is manufactured prior to the complete product.
### Item
MLB item is encoded differently for 13-character and 17-character MLB. It might serve as a production item per week and could be similar to 'Production line and copy' in the serial number.
- For old MLB, this is a variant of base 34 value. First item character is always `0`.
- For new MLB, this value always is a number.
### Infix
Base 34 value present in new MLBs only. No information is known about it. Could actually be part of Item.
### Product board
Similarly to 'Product model' this field encodes logic board model number. This code is often referred to as `EEE code` in part catalogues and is useful for purchasing a compatible logic board for replacement.
For new 17 character MLBs this field is also used for identification at `osrecovery.apple.com` to provide a compatible internet recovery image and diagnostic tools upon request.
### Suffix
Base 34 value with unclear designation. Might be used for checksum validation. Checksum validation algorithm is reverse engineered from diagnostics tools and is valid for all 17 character MLBs. It is not clear whether 13 character MLBs have any checksum. 17 character MLB checksum follows.
```C
static bool verify_mlb_checksum(const char *mlb, size_t len) {
const char alphabet[] = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ";
size_t checksum = 0;
for (size_t i = 0; i < len; ++i) {
for (size_t j = 0; j <= sizeof (alphabet); ++j) {
if (j == sizeof (alphabet))
return false;
if (mlb[i] == alphabet[j]) {
checksum += (((i & 1) == (len & 1)) * 2 + 1) * j;
break;
}
}
}
return checksum % (sizeof(alphabet) - 1) == 0;
}
```
## Appendix
This information was obtained experimentally and may not be accurate in certain details. Be warned that it is published at no warranty for educational and introductory purposes only.

View File

@@ -0,0 +1,7 @@
## macserial
macserial is a tool that obtains and decodes Mac serial number and board identifier to provide more information about the production of your hardware. Works as a decent companion to [Apple Check Coverage](https://checkcoverage.apple.com) and [Apple Specs](http://support-sp.apple.com/sp/index?page=cpuspec&cc=HTD5) portal. Check the [format description](https://github.com/acidanthera/OpenCorePkg/blob/master/Utilities/macserial/FORMAT.md) for more details.
Should be built with a compiler supporting C99. Prebuilt binaries are available for macOS 10.4 and higher.
Run with `-h` argument to see all available arguments.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,140 @@
ocvalidate
====================
Utility to validate whether a `config.plist` matches requirements and conventions imposed by OpenCore.
## Usage
- Pass one single path to `config.plist` to verify it.
- Pass `--version` for current supported OpenCore version.
## Technical background
### At a glance
- ocvalidate firstly calls `OcSerializeLib` which performs fundamental checks in terms of syntax and semantics. After that, the following will be checked.
- The error message `<OCS: No schema for xxx>` complained by `OcSerializeLib` indicates unknown keys that can be deprecated in new versions of OpenCore. Such keys should be ***removed*** in order to avoid undefined behaviours.
- Under active development, newer versions of OpenCore hardly have backward compatibility at this moment. As a result, please first run `ocvalidate --version` to check which version of OpenCore is supported, and thus please only use the specific version.
### Global Rules
- All entries must be set once only. Duplication is strictly prohibited.
- All strings (fields with plist `String` format) throughout the whole config only accept ASCII printable characters at most. Stricter rules may apply. For instance, some fields only accept specified values, as indicated in [Configuration.pdf](https://github.com/acidanthera/OpenCorePkg/blob/master/Docs/Configuration.pdf).
- All the paths relative to OpenCore root must be less than or equal to 192 bytes (`OC_STORAGE_SAFE_PATH_MAX`) in total including '\0' terminator.
- Most binary patches must have `Find`, `Replace`, `Mask` (if used), and `ReplaceMask` (if used) identical size set. Also, `Find` requires `Mask` (or `Replace` requires `ReplaceMask`) to be active (set to non-zero) for corresponding bits.
- `MinKernel` and `MaxKernel` entries should follow conventions specified in [Configuration.pdf](https://github.com/acidanthera/OpenCorePkg/blob/master/Docs/Configuration.pdf). (TODO: Bring decent checks for this)
- `MinKernel` cannot be a value that is below macOS 10.4 (Darwin version 8).
- Entries taking file system path only accept `0-9, A-Z, a-z, '_', '-', '.', '/', and '\'`.
- Device Paths (e.g. `PciRoot(0x0)/Pci(0x1b,0x0)`) only accept strings in canonic string format.
- Paths of UEFI Drivers only accept `0-9, A-Z, a-z, '_', '-', '.', and '/'`.
- Entries requiring bitwise operations (e.g. `ConsoleAttributes`, `PickerAttributes`, or `ScanPolicy`) only allow known bits stated in [Configuration.pdf](https://github.com/acidanthera/OpenCorePkg/blob/master/Docs/Configuration.pdf) to be set.
- Entries involving GUID (mainly in Section `NVRAM`) must have correct format set.
### ACPI
#### Add
- Entry[N]->Path: Only `.aml` and `.bin` filename suffix are accepted.
### Booter
#### MmioWhitelist
- Entry[N]->Enabled: When at least one entry is enabled, `DevirtualiseMmio` in `Booter->Quirks` should be enabled.
#### Patch
- Entry[N]->Arch: Only `Any`, `i386`, or `x86_64` are accepted.
- Entry[N]->Identifier: Only `Any`, `Apple`, or a specified bootloader with `.efi` sufffix, are accepted.
#### Quirks
- When `AllowRelocationBlock` is enabled, `ProvideCustomSlide` should be enabled altogether.
- When `EnableSafeModeSlide` is enabled, `ProvideCustomSlide` should be enabled altogether.
- If `ProvideMaxSlide` is set to a number greater than zero, `ProvideCustomSlide` should be enabled altogether.
- `ResizeAppleGpuBars` must be set to `0` or `-1`.
- When `DisableVariableWrite`, `EnableWriteUnprotector`, or `ProvideCustomSlide` is enabled, `OpenRuntime.efi` should be loaded in `UEFI->Drivers`.
### DeviceProperties
- Requirements here all follow Global Rules.
### Kernel
#### Add
- Entry[N]->Arch: Only `Any`, `i386`, or `x86_64` are accepted.
- Entry[N]->BundlePath: Filename should have `.kext` suffix.
- Entry[N]->PlistPath: Filename should have `.plist` suffix.
- Entry[N]: If `Lilu.kext` is used, `DisableLinkeditJettison` should be enabled in `Kernel->Quirks`.
- `BrcmFirmwareRepo.kext` must not be injected by OpenCore.
- For some known kexts, their `BundlePath`, `ExecutablePath`, and `PlistPath` must match against each other. Current list of rules can be found [here](https://github.com/acidanthera/OpenCorePkg/blob/master/Utilities/ocvalidate/KextInfo.c).
- Plugin kext must be placed after parent kext. For example, [plugins of Lilu](https://github.com/acidanthera/Lilu/blob/master/KnownPlugins.md) must be placed after `Lilu.kext`.
#### Delete
- Entry[N]->Arch: Only `Any`, `i386`, or `x86_64` are accepted.
- Entry[N]->Identifier: At least one dot (`.`) should exist, because any identifier looks like a domain sequence (`vendor.product`).
#### Quirks
- `CustomSMBIOSGuid` requires `PlatformInfo->UpdateSMBIOSMode` set to `Custom`.
- `SetApfsTrimTimeout` cannot be a value that is greater than `MAX_UINT32`, or less than `-1`.
#### Scheme
- KernelArch: Only `Auto`, `i386`, `i386-user32`, or `x86_64` are accepted.
- KernelCache: Only `Auto`, `Cacheless`, `Mkext`, or `Prelinked` are accepted.
### Misc
#### BlessOverride
- Entries cannot be `\EFI\Microsoft\Boot\bootmgfw.efi` or `\System\Library\CoreServices\boot.efi` since OpenCore knows these paths.
#### Boot
- HibernateMode: Only `None`, `Auto`, `RTC`, or `NVRAM` are accepted.
- PickerMode: Only `Builtin`, `External`, or `Apple` are accepted.
- `PickerAudioAssist` requires `AudioSupport` in `UEFI->Audio` to be enabled.
- LauncherOption: Only `Disabled`, `Full`, `Short`, or `System` are accepted.
- `LauncherPath` cannot be empty string.
#### Security
- AuthRestart: If enabled, `VirtualSMC.kext` should be present in `Kernel->Add`.
- DmgLoading: Only `Disabled`, `Signed`, or `Any` are accepted.
- Vault: Only `Optional`, `Basic`, or `Secure` are accepted.
- SecureBootModel: Only `Default`, `Disabled`, `j137`, `j680`, `j132`, `j174`, `j140k`, `j780`, `j213`, `j140a`, `j152f`, `j160`, `j230k`, `j214k`, `j223`, `j215`, `j185`, `j185f`, or `x86legacy` are accepted.
#### Serial
- RegisterAccessWidth: Only `8` or `32` are accepted.
- BaudRate: Only `921600`, `460800`, `230400`, `115200`, `57600`, `38400`, `19200`, `9600`, `7200`, `4800`, `3600`, `2400`, `2000`, `1800`, `1200`, `600`, `300`, `150`, `134`, `110`, `75`, or `50` are accepted.
- PciDeviceInfo: The last byte must be `0xFF`.
- PciDeviceInfo: Excluding the last byte `0xFF`, the rest must be divisible by 4.
- PciDeviceInfo: Maximum allowed size is 41.
### NVRAM
- Requirements here all follow Global Rules. In addition, the following keys and values are checked:
#### gAppleBootVariableGuid (`7C436110-AB2A-4BBB-A880-FE41995C9F82`)
- `nvda_drv` must have type `Plist Data` with the value of `0x30` or `0x31`.
- `boot-args` must be an ASCII string (thus `Plist String`) without trailing `\0`.
- `bootercfg` must be an ASCII string (thus `Plist String`) without trailing `\0`.
- `csr-active-config` must have type `Plist Data` and have length of 4 bytes.
- `StartupMute` must have type `Plist Data` and have length of 1 byte.
- `SystemAudioVolume` must have type `Plist Data` and have length of 1 byte.
#### gAppleVendorVariableGuid (`4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14`)
- `UIScale` must have type `Plist Data` with the value of `0x01` or `0x02`.
- `FirmwareFeatures` must have type `Plist Data` and have length of 4 bytes.
- `ExtendedFirmwareFeatures` must have type `Plist Data` and have length of 8 bytes.
- `FirmwareFeaturesMask` must have type `Plist Data` and have length of 4 bytes.
- `ExtendedFirmwareFeatures` must have type `Plist Data` and have length of 8 bytes.
- `DefaultBackgroundColor` must have type `Plist Data` and have length of 4 bytes. Also, its last byte must be `0x00`.
### PlatformInfo
- UpdateSMBIOSMode: Only `TryOverwrite`, `Create`, `Overwrite`, or `Custom` are accepted.
#### Generic
- SystemProductName: Only real Mac models are accepted.
- SystemMemoryStatus: Only `Auto`, `Upgradable`, or `Soldered` are accepted.
- SystemUUID: Only empty string, `OEM` or valid UUID are accepted.
- ProcessorType: Only known first byte can be set.
### UEFI
#### APFS
- When `EnableJumpstart` is enabled, `ScanPolicy` in `Misc->Security` should have `OC_SCAN_ALLOW_FS_APFS` (bit 8) set, together with `OC_SCAN_FILE_SYSTEM_LOCK` (bit 0) set. Or `ScanPolicy` should be `0` (failsafe value).
#### Audio
- When `AudioSupport` is enabled, `AudioDevice` must be either empty or a valid path.
- When `AudioSupport` is enabled, `AudioOutMask` must be non-zero.
#### Quirks
- When `RequestBootVarRouting` is enabled, `OpenRuntime.efi` should be loaded in `UEFI->Drivers`.
- `ResizeGpuBars` must be set to an integer value between `-1` and `19`.
#### Drivers
- When `OpenUsbKbDxe.efi` is in use, `KeySupport` in `UEFI->Input` should never be enabled altogether.
- When `Ps2KeyboardDxe.efi` is in use, `KeySupport` in `UEFI->Input` should always be enabled altogether.
- `OpenUsbKbDxe.efi` and `Ps2KeyboardDxe.efi` should never co-exist.
- When HFS+ filesystem driver or `AudioDxe.efi` is in use, `ConnectDrivers` should be enabled altogether.
- When `OpenCanopy.efi` is in use, `PickerMode` in `Misc->Boot` should be set to `External`.
- When `OpenVariableRuntimeDxe.efi` is in use, its `LoadEarly` option must be set to `TRUE`.
- `OpenRuntime.efi` must be placed after `OpenVariableRuntimeDxe.efi` when both are in use.
- `LoadEarly` for any other driver but `OpenVariableRuntimeDxe.efi` and `OpenRuntime.efi` must be set to `FALSE`.
#### Input
- KeySupportMode: Only `Auto`, `V1`, `V2`, or `AMI` are accepted.
- When `PointerSupport` is enabled, the value of `PointerSupportMode` should only be `ASUS`.
#### Output
- `ClearScreenOnModeSwitch`, `IgnoreTextInGraphics`, `ReplaceTabWithSpace`, and `SanitiseClearScreen` only apply to `System` TextRenderer
- `Resolution` should match `NUMBERxNUMBER` or `NUMBERxNUMBER@NUMBER` sequences (unless it is an `Empty string` or is set to `Max`).
- `UIScale` must be set to an integer value between `-1` and `2`.
#### ReservedMemory
- Type: Only `Reserved`, `LoaderCode`, `LoaderData`, `BootServiceCode`, `BootServiceData`, `RuntimeCode`, `RuntimeData`, `Available`, `Persistent`, `UnusableMemory`, `ACPIReclaimMemory`, `ACPIMemoryNVS`, `MemoryMappedIO`, `MemoryMappedIOPortSpace`, or `PalCode` are accepted.

Binary file not shown.

Binary file not shown.

Binary file not shown.