diff --git a/.gitignore b/.gitignore index 53019cb..3088b3e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,11 @@ /build/ /target/ /firmware/ +/.DS_Store +/.irecovery +/iBSS +/iBEC +/restore_*.log /windows/QemuA6Ude/build/ /windows/QemuA6Ude/cert/ /windows/QemuA6Ude/package/ @@ -10,6 +15,7 @@ /.cache/ /.vscode/ *.pyc +__pycache__/ .sdk .stgit-* .git-submodule-status @@ -26,4 +32,7 @@ GTAGS *.depend_raw *.swp *.patch +!scripts/patches/libirecovery-qemu.patch +!scripts/patches/idevicerestore-no-restore-fs.patch +!scripts/patches/idevicerestore-qemu-lab-boot.patch *.gcov diff --git a/A6-STATUS.md b/A6-STATUS.md index 8fb7949..76ff981 100644 --- a/A6-STATUS.md +++ b/A6-STATUS.md @@ -1,111 +1,366 @@ -# iPhone 5 / Apple A6 sur QEMU Windows natif +# Apple A6 / iPhone 5 secure-boot lab -## État actuel +This repository contains an experimental QEMU `iphone5` machine for studying +the A6 IMG3 boot path in an isolated environment. It runs the guest +SecureROM, iBSS, and iBEC code with a locally generated root of trust and a +locally generated AES-256 GID key. The verified endpoint is iBEC Recovery +mode; booting a ramdisk or iOS is not implemented. -- QEMU `arm-softmmu` est compilé en PE Win64 natif avec MSYS2 MinGW64. WSL n’est pas utilisé. -- La machine `iphone5` exécute la SecureROM A6 `iBoot-1145.3` de 65 536 octets. -- Les fusibles exposent `CPID:8950`, `CPRV:20` et `CPFM:03`. -- L’UART A6 est relié à la console QEMU (`Ctrl+A`, puis `C` pour le moniteur ; `Ctrl+A`, puis `X` pour quitter). -- Le contrôleur USB OTG, son EP0, les DMA et l’IRQ 11 permettent à la SecureROM de terminer son énumération DFU. -- Le descripteur produit par cette SecureROM annonce `VID=05AC, PID=1229`. La façade Windows le normalise vers le PID DFU public `1227`, attendu par `libirecovery`. -- Le pilote KMDF/UDE `QemuA6Ude.sys` crée un véritable enfant PnP `USB\VID_05AC&PID_1227` sous Windows. -- `QemuA6UsbBridge.exe` relaie les contrôles DFU entre le pilote et QEMU sur `127.0.0.1:26050`. -- Un test externe `DFU_GETSTATE` a traversé le socket, le modèle OTG et la SecureROM, puis a renvoyé `dfuIDLE` (`2`). -- `idevicerestore.exe` et `libirecovery` restent d’origine et ne sont pas patchés. +No Apple private key or GID key is included or required. Firmware files and +generated lab identities are intentionally ignored by Git. -Le paquet pilote signé en mode test est généré localement dans -`windows\QemuA6Ude\package` ; les clés, certificats et binaires générés ne sont -pas versionnés. +## Verified flow -## Construction Windows native +The following path has been tested on macOS: -QEMU se configure depuis un terminal MSYS2 MinGW64 : +1. The 64 KiB A6 SecureROM starts in DFU mode. +2. Patched `idevicerestore` selects the `n41ap` build identity from + `BuildManifest.plist` and sends the locally signed and encrypted iBSS. +3. Guest SecureROM parses the IMG3, validates its RSA chain and signature, + asks the emulated AES engine to unwrap the KBAG with the local GID slot, + and decrypts `DATA`. +4. iBSS parses and validates the local APTicket, including its device fields, + boot-nonce binding, certificate chain, signature, and iBEC digest. +5. iBSS unwraps and decrypts iBEC. QEMU performs a warm authenticated handoff + of the resulting image to its linked address at `0xbff00000`. +6. iBEC prints its real `iBootStage2` banner on UART and enumerates as + Recovery (`05ac:1281`). `irecovery -q` reports `MODE: Recovery` and + `IBFL: 0x1a`. + +This is not a cycle-accurate reproduction of Apple silicon. The original +guest IMG3 and APTicket parsers execute, but the root and GID material are +lab-owned, crypto is provided by QEMU MMIO models, debug UART activation is +optionally assisted after authentication, and the final iBEC relocation is a +QEMU handoff. Timing, analog behavior, fault behavior, and unimplemented +peripherals must not be treated as hardware-equivalent. + +## Host requirements + +On macOS, install the QEMU build dependencies and the recovery-tool +dependencies. A typical Homebrew setup is: ```sh -./configure --target-list=arm-softmmu -ninja -C build qemu-system-arm.exe +brew install ninja pkg-config glib pixman libslirp \ + autoconf automake libtool libimobiledevice libusb libzip ``` -Le pilote UDE nécessite Visual Studio 2022 Build Tools et le WDK avec UDE. Si -le WDK n'est pas installé à son emplacement standard, définir -`QA6_WDK_ROOT` vers sa racine (celle qui contient `Include`, `bin` et `tools`). -Depuis PowerShell : +Python 3 and OpenSSL are required by the image-generation scripts. You must +also provide your own 65,536-byte A6 SecureROM dump. The repository does not +distribute it. -```powershell -.\windows\QemuA6Ude\prepare-package.ps1 -.\windows\QemuA6UsbBridge\build-bridge.ps1 -Configuration Release -``` +## Build QEMU -## Première installation du pilote - -La signature d’un pilote noyau local exige le mode de signature test Windows. Cette opération modifie le BCD et demande un redémarrage. Depuis un PowerShell administrateur : - -```powershell -.\windows\QemuA6Ude\enable-test-mode.ps1 -Restart-Computer -``` - -Après le redémarrage, toujours en administrateur : - -```powershell -.\windows\QemuA6Ude\install-driver.ps1 -``` - -Si Secure Boot bloque `testsigning`, il faut le désactiver dans l’UEFI. Le script d’installation importe uniquement le certificat public de test puis crée le contrôleur racine `Root\QemuA6Ude` avec `devcon`. - -## Lancement normal - -Depuis PowerShell à la racine : - -```powershell -.\Run-iPhone5.ps1 -SkipBuild -``` - -Le mode USB Windows est actif par défaut. Le lanceur démarre le pont en arrière-plan, garde l’UART dans la console et écrit les échanges USB dans `build\a6-usb-bridge.log`. - -Vérification dans un second PowerShell : - -```powershell -pnputil /enum-devices /connected /class USB -& 'C:\msys64\mingw64\bin\idevicerestore.exe' -d 'iPhone_4.0_32bit_10.3.4_14G61_Restore.ipsw' -``` - -Pour conserver l’ancien injecteur DFU interne, fournir directement une image : - -```powershell -.\Run-iPhone5.ps1 -DfuImagePath '.\build\firmware\iBSS.iphone5.RELEASE.dfu' -Trace -``` - -Pour démarrer sans façade USB Windows : - -```powershell -.\Run-iPhone5.ps1 -NoWindowsUsb -``` - -## Prochain jalon - -Une fois l’énumération PnP confirmée, l’IPSW iOS 10.3.4 sera envoyé par l’`idevicerestore` original. La SecureROM vérifiera l’iBSS ; après son exécution, il faudra modéliser les périphériques supplémentaires utilisés par iBSS/iBEC et gérer sa réénumération USB Recovery pour atteindre iBoot. - -## Reprise sur macOS - -Le fork QEMU et l’injecteur DFU interne sont portables. Sur macOS, installez les -dépendances QEMU, configurez `arm-softmmu`, puis compilez : +From the repository root: ```sh ./configure --target-list=arm-softmmu ninja -C build qemu-system-arm ``` -Placez ensuite le dump dans `firmware/s5l8950x-secure-rom.bin`, puis lancez : +The launcher expects `build/qemu-system-arm`. Set `QEMU_BIN` if the binary is +elsewhere. + +## Fetch the public bootloader files + +The helper reads the iOS 10.3.4 IPSW ZIP directory with HTTP Range requests +and downloads only `BuildManifest.plist`, iBSS, and iBEC: ```sh -./Run-iPhone5-macOS.sh +scripts/fetch-a6-bootloaders.py --board n41 ``` -Pour charger l’iBSS extrait de l’IPSW sans dépendre d’un périphérique USB hôte : +The following files should then exist locally: + +```text +firmware/BuildManifest.plist +firmware/iBSS.iphone5.RELEASE.dfu +firmware/iBSS.iphone5.RELEASE.bin +firmware/iBEC.iphone5.RELEASE.dfu +firmware/iBEC.iphone5.RELEASE.bin +``` + +The fetcher validates HTTP ranges, uncompressed sizes, and CRC-32 values. It +does not personalize, decrypt, sign, or modify the downloaded images. + +## Build the macOS recovery tools + +macOS does not expose the emulator as a system-wide USB device. Instead, the +QEMU USB model is connected over a localhost transport implemented in the +patched `libirecovery`. The normal IOKit backend remains unchanged when the +transport environment variable is absent. + +Build the pinned and checksum-verified recovery tools locally: ```sh -DFU_IMAGE="$PWD/firmware/iBSS.n41.RELEASE.dfu" ./Run-iPhone5-macOS.sh +scripts/build-macos-recovery-tools.sh ``` -La façade UDE est propre à Windows. Faire apparaître le téléphone comme un vrai périphérique USB sur macOS demanderait une extension DriverKit signée ; ce n’est donc pas un raccourci par rapport au chemin UDE déjà construit. +They are installed under `build/limd-prefix`; nothing is installed into +`/usr/local`. Always use the repository wrappers: + +```sh +scripts/irecovery-qemu +scripts/idevicerestore-qemu +``` + +The QEMU endpoint defaults to `127.0.0.1:26050`. Change both sides with +`USB_BRIDGE_PORT` and `QEMU_USB_ENDPOINT` when running concurrent instances. + +## Generate a new lab identity and boot chain + +Create an empty output directory containing a new RSA hierarchy, a random +AES-256 GID key, a derived SecureROM, signed IMG3 files, encrypted payloads, +and a locally signed APTicket: + +```sh +scripts/build-a6-lab-chain.sh \ + "/absolute/path/to/SecureROM for s5l8950xsi, iBoot-1145.3" \ + build/a6-lab +``` + +The output directory must be new or empty. Private PEM files and +`gid-key.bin` are created with owner-only permissions. Do not commit or share +that directory. The default ticket nonce is the value produced by the +current deterministic A6 lab boot (`8d82693c897d1b9d`). The optional third +argument exists for experiments in which the guest nonce is changed. + +Start QEMU in the first terminal: + +```sh +A6_LAB_DIR="$PWD/build/a6-lab" GDB_PORT=12345 \ + ./Run-iPhone5-macOS.sh +``` + +Boot iBSS and iBEC in a second terminal: + +```sh +A6_LAB_DIR="$PWD/build/a6-lab" \ + scripts/idevicerestore-qemu -d -y -P firmware +``` + +This wrapper automatically enables the explicit QEMU lab boot-only path. It +does not contact a TSS server and stops after iBEC reaches Recovery; it does +not restore a filesystem. Confirm the result with: + +```sh +scripts/irecovery-qemu -q +``` + +Expected identifying fields include: + +```text +CPID: 0x8950 +ECID: 0x0000000000200000 +IBFL: 0x1a +MODE: Recovery +PRODUCT: iPhone5,1 +MODEL: n41ap +``` + +## UART console + +UART0 is mapped at `0x32b00000` and connected to QEMU's standard console by +`-serial mon:stdio`. Run the launcher directly from the terminal in which you +want to see serial output. No extra serial client is needed. + +Useful console controls are: + +- `Ctrl+A`, then `C`: switch between UART and the QEMU monitor. +- `Ctrl+A`, then `X`: quit QEMU. + +Release iBSS/iBEC images do not always enable serial output themselves. The +launcher therefore sets their `debug-uarts` boot argument to `3` after IMG3 +authentication by default. Disable this assistance when testing native +firmware behavior: + +```sh +DEBUG_UART=0 A6_LAB_DIR="$PWD/build/a6-lab" \ + ./Run-iPhone5-macOS.sh +``` + +With the default setting, a successful boot prints the authentic guest +`iBootStage2` banner followed by `Entering recovery mode, starting command +prompt`. Repeated NAND reset errors are currently expected because NAND is +not modeled; they do not prevent the Recovery endpoint from appearing. + +## Live debugging with LLDB, GDB, or IDA + +Set `GDB_PORT` to expose QEMU's remote GDB stub on localhost. Add `PAUSED=1` +to stop before the SecureROM's first instruction: + +```sh +A6_LAB_DIR="$PWD/build/a6-lab" GDB_PORT=12345 PAUSED=1 \ + ./Run-iPhone5-macOS.sh +``` + +Attach LLDB: + +```sh +lldb -o 'gdb-remote 127.0.0.1:12345' +``` + +Without `PAUSED=1`, the same command can attach to a running boot. Only one +debugger may control the GDB stub at a time. + +Useful load addresses are: + +| Stage | Address | +| --- | ---: | +| SecureROM | `0x00000000` | +| iBSS | `0x10000000` | +| iBEC | `0xbff00000` | +| UART0 MMIO | `0x32b00000` | + +For IDA, load `build/a6-lab/iBEC.lab-root.bin` as a raw little-endian ARM +binary with image base `0xbff00000`. Select `Remote GDB debugger`, connect to +`127.0.0.1:12345`, and configure the same load address in the debugger's +memory map. The live PC should enter the `0xbff00000` region after the iBEC +handoff. + +For a static-only iBSS session that intentionally bypasses IMG3 validation: + +```sh +build/qemu-system-arm \ + -machine iphone5 \ + -bios firmware/s5l8950x-secure-rom.bin \ + -device loader,file=firmware/iBSS.iphone5.RELEASE.bin,addr=0x10000000,cpu-num=0,force-raw=on \ + -display none -serial mon:stdio \ + -S -gdb tcp:127.0.0.1:12345 +``` + +That loader command is useful for early breakpoints, but it bypasses the +SecureROM parser, signature checks, AES work, and normal handoff state. It is +not evidence of an authenticated boot. + +## Build signed and encrypted custom images + +The simplest supported workflow is to provide raw replacement `DATA` +payloads and rebuild an entirely new identity directory. Custom payloads must +retain exactly one copy of the original embedded 1,215-byte trust-anchor slot +so the builder can replace it with the generated lab root. + +```sh +A6_IBSS_PAYLOAD="$PWD/payloads/iBSS.custom.bin" \ +A6_IBEC_PAYLOAD="$PWD/payloads/iBEC.custom.bin" \ +scripts/build-a6-lab-chain.sh \ + "/absolute/path/to/SecureROM for s5l8950xsi, iBoot-1145.3" \ + build/a6-custom +``` + +Boot that directory by setting `A6_LAB_DIR="$PWD/build/a6-custom"` in both +the launcher and `idevicerestore` commands shown above. + +For individual IMG3 experiments, first inspect a source template: + +```sh +scripts/a6-img3-info.py firmware/iBSS.iphone5.RELEASE.dfu +scripts/a6-img3-info.py firmware/iBSS.iphone5.RELEASE.dfu \ + --extract-data build/iBSS.extracted.bin +``` + +Patch an iBoot payload to trust the existing lab root, then create an iBSS +with an embedded SHSH/CERT signature and GID-wrapped KBAG: + +```sh +scripts/a6-lab-patch-iboot-root.py \ + --source-rom "/absolute/path/to/original/SecureROM" \ + --identity build/a6-lab \ + --input payloads/iBSS.custom.bin \ + --output build/iBSS.custom.lab-root.bin + +scripts/a6-lab-img3.py \ + --template firmware/iBSS.iphone5.RELEASE.dfu \ + --payload build/iBSS.custom.lab-root.bin \ + --identity build/a6-lab \ + --encrypt \ + --output build/iBSS.custom.signed-encrypted.dfu +``` + +An iBEC uses the external APTicket layout. Build its encrypted IMG3 first, +then bind the APTicket to that exact component and the guest boot nonce: + +```sh +scripts/a6-lab-patch-iboot-root.py \ + --source-rom "/absolute/path/to/original/SecureROM" \ + --identity build/a6-lab \ + --input payloads/iBEC.custom.bin \ + --output build/iBEC.custom.lab-root.bin + +scripts/a6-lab-img3.py \ + --template firmware/iBEC.iphone5.RELEASE.dfu \ + --payload build/iBEC.custom.lab-root.bin \ + --identity build/a6-lab \ + --encrypt --ticketed \ + --output build/iBEC.custom.ticketed-encrypted.img3 + +scripts/a6-lab-apticket.py \ + --component build/iBEC.custom.ticketed-encrypted.img3 \ + --identity build/a6-lab \ + --boot-nonce 8d82693c897d1b9d \ + --ticket-output build/iBEC.custom.apticket.der \ + --output build/iBEC.custom.signed-encrypted.dfu +``` + +The tools refuse to overwrite outputs and write JSON sidecars with hashes, +sizes, signed ranges, encryption state, device fields, and manifest tags. +`a6-lab-img3.py` uses a random content key and IV for every encrypted image; +the content key is wrapped by AES-256-CBC with the generated lab GID key. + +## Environment variables + +| Variable | Purpose | Default | +| --- | --- | --- | +| `A6_LAB_DIR` | Generated identity and boot-chain directory | unset | +| `A6_IBSS_PAYLOAD` | Raw custom iBSS payload for the chain builder | stock extracted iBSS | +| `A6_IBEC_PAYLOAD` | Raw custom iBEC payload for the chain builder | stock extracted iBEC | +| `A6_BOOT_NONCE` | Eight-byte ticket nonce in hexadecimal | `8d82693c897d1b9d` | +| `QEMU_BIN` | QEMU executable used by the launcher | `build/qemu-system-arm` | +| `USB_BRIDGE_PORT` | QEMU localhost recovery transport; `0` disables it | `26050` | +| `QEMU_USB_ENDPOINT` | Endpoint used by the host recovery wrappers | `127.0.0.1:26050` | +| `GDB_PORT` | Remote GDB listener | unset | +| `PAUSED` | Start with the CPU stopped | `0` | +| `DEBUG_UART` | Enable post-authentication debug UART assistance | `1` | +| `NATIVE_IMG3` | Enable authenticated native IMG3 handoff helpers | `1` with `A6_LAB_DIR` | +| `DFU_IMAGE` | Optional internal one-shot DFU image | unset | + +## Windows host path + +The Windows path uses a UDE test driver and a native TCP bridge to present +the virtual Apple DFU device to unmodified Windows recovery tools. Build QEMU +from MSYS2 MinGW64, then prepare the driver and bridge from PowerShell: + +```powershell +.\windows\QemuA6Ude\prepare-package.ps1 +.\windows\QemuA6UsbBridge\build-bridge.ps1 -Configuration Release +``` + +The first installation requires an administrator shell, test-signing mode, +and a restart: + +```powershell +.\windows\QemuA6Ude\enable-test-mode.ps1 +Restart-Computer +.\windows\QemuA6Ude\install-driver.ps1 +``` + +Launch with: + +```powershell +.\Run-iPhone5.ps1 -SkipBuild -GdbPort 12345 +``` + +The local-key IMG3 generation and macOS `idevicerestore` boot-only wrapper +are currently the verified end-to-end path. The Windows UDE bridge has been +validated for DFU control traffic but not for the complete local iBSS/iBEC +workflow. + +## Current limitations + +- The verified endpoint is iBEC Recovery, not a ramdisk or iOS userspace. +- NAND is not implemented, so the guest emits repeated NAND errors. +- UART transmit and MMIO access are verified. UART receive is modeled, but a + complete interactive iBoot command session over serial is not guaranteed. +- The localhost macOS backend is visible only to the patched libirecovery + tools; it does not appear in Finder or System Information. +- The implementation is intended for deterministic parser and boot-flow + research, not for hardware timing or fault-injection equivalence. diff --git a/README.rst b/README.rst index b120a1f..9181903 100644 --- a/README.rst +++ b/README.rst @@ -34,6 +34,10 @@ version 2. For full licensing details, consult the LICENSE file. Documentation ============= +The experimental Apple A6/iPhone 5 machine, its authenticated IMG3 lab boot +flow, UART access, and live-debugging workflow are documented in +`A6-STATUS.md `_. + Documentation can be found hosted online at ``_. The documentation for the current development version that is available at diff --git a/Run-iPhone5-macOS.sh b/Run-iPhone5-macOS.sh index 19ec3cb..7f92763 100755 --- a/Run-iPhone5-macOS.sh +++ b/Run-iPhone5-macOS.sh @@ -3,18 +3,78 @@ set -eu script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) qemu_bin=${QEMU_BIN:-"$script_dir/build/qemu-system-arm"} -rom_path=${ROM_PATH:-"$script_dir/firmware/s5l8950x-secure-rom.bin"} +a6_lab_dir=${A6_LAB_DIR:-} +if [ -n "$a6_lab_dir" ]; then + a6_lab_dir=$(CDPATH= cd -- "$a6_lab_dir" && pwd) + rom_path=${ROM_PATH:-"$a6_lab_dir/s5l8950x-secure-rom-lab.bin"} + gid_key_path=${GID_KEY_PATH:-"$a6_lab_dir/gid-key.bin"} + native_img3=${NATIVE_IMG3:-1} +else + rom_path=${ROM_PATH:-"$script_dir/firmware/s5l8950x-secure-rom.bin"} + gid_key_path=${GID_KEY_PATH:-} + native_img3=${NATIVE_IMG3:-0} +fi dfu_image=${DFU_IMAGE:-} +gdb_port=${GDB_PORT:-} +paused=${PAUSED:-0} +usb_bridge_port=${USB_BRIDGE_PORT:-26050} +debug_uart=${DEBUG_UART:-1} + +case $debug_uart in + 0|1) ;; + *) + echo "DEBUG_UART must be 0 or 1." >&2 + exit 1 + ;; +esac + +case $native_img3 in + 0|1) ;; + *) + echo "NATIVE_IMG3 must be 0 or 1." >&2 + exit 1 + ;; +esac + +case $gdb_port in + ''|*[!0-9]*) + if [ -n "$gdb_port" ]; then + echo "GDB_PORT must be an integer between 1 and 65535." >&2 + exit 1 + fi + ;; +esac +if [ -n "$gdb_port" ] && + { [ "$gdb_port" -lt 1 ] || [ "$gdb_port" -gt 65535 ]; }; then + echo "GDB_PORT must be between 1 and 65535." >&2 + exit 1 +fi +if [ "$paused" != 0 ] && [ -z "$gdb_port" ]; then + gdb_port=1234 +fi + +case $usb_bridge_port in + ''|*[!0-9]*) + echo "USB_BRIDGE_PORT must be an integer between 0 and 65535." >&2 + exit 1 + ;; +esac +if [ "$usb_bridge_port" -gt 65535 ]; then + echo "USB_BRIDGE_PORT must be between 0 and 65535." >&2 + exit 1 +fi if [ ! -x "$qemu_bin" ]; then - echo "QEMU introuvable: $qemu_bin" >&2 - echo "Configurez ce dépôt avec --target-list=arm-softmmu puis compilez-le." >&2 + echo "QEMU not found: $qemu_bin" >&2 + echo "Configure this repository with --target-list=arm-softmmu," \ + "then build it." >&2 exit 1 fi if [ ! -f "$rom_path" ]; then - echo "SecureROM introuvable: $rom_path" >&2 - echo "Placez votre dump A6 de 65536 octets dans firmware/s5l8950x-secure-rom.bin." >&2 + echo "SecureROM not found: $rom_path" >&2 + echo "Place your 65536-byte A6 dump at" \ + "firmware/s5l8950x-secure-rom.bin." >&2 exit 1 fi @@ -26,22 +86,85 @@ set -- \ -display none \ -serial mon:stdio +if [ "$debug_uart" = 1 ]; then + set -- "$@" \ + -global s5l8950x-usb-otg.force-debug-uarts=on \ + -global s5l8950x-aes.force-debug-uarts=on +fi + +if [ -n "$gid_key_path" ]; then + if [ ! -f "$gid_key_path" ]; then + echo "Lab GID key not found: $gid_key_path" >&2 + exit 1 + fi + set -- "$@" -global "s5l8950x-aes.gid-key-file=$gid_key_path" +fi + +if [ "$native_img3" = 1 ]; then + set -- "$@" \ + -global s5l8950x-usb-otg.native-img3-handoff=on \ + -global s5l8950x-aes.authenticated-ibec-handoff=on +fi + if [ -n "$dfu_image" ]; then if [ ! -f "$dfu_image" ]; then - echo "Image DFU introuvable: $dfu_image" >&2 + echo "DFU image not found: $dfu_image" >&2 exit 1 fi set -- "$@" -global "s5l8950x-usb-otg.dfu-image=$dfu_image" fi +if [ "$usb_bridge_port" -ne 0 ]; then + usb_chardev="socket,id=a6usb,host=127.0.0.1,port=$usb_bridge_port" + usb_chardev="$usb_chardev,server=on,wait=off" + set -- "$@" \ + -chardev "$usb_chardev" \ + -global "s5l8950x-usb-otg.usb-bridge=a6usb" +fi + +if [ -n "$gdb_port" ]; then + set -- "$@" -gdb "tcp:127.0.0.1:$gdb_port" +fi +if [ "$paused" != 0 ]; then + set -- "$@" -S +fi + echo "QEMU macOS : $qemu_bin" echo "SecureROM : $rom_path" if [ -n "$dfu_image" ]; then - echo "Image DFU : $dfu_image" - echo "Injection DFU interne active." + echo "DFU image : $dfu_image" + echo "Internal DFU injection is enabled." else - echo "La SecureROM attend en dfuIDLE." + echo "SecureROM is waiting in dfuIDLE." +fi +echo "A6 UART : this console (Ctrl+A C = monitor, Ctrl+A X = quit)" +if [ -n "$gid_key_path" ]; then + echo "AES GID : lab identity active (private key is not displayed)" +else + echo "AES GID : no lab GID slot configured" +fi +if [ "$native_img3" = 1 ]; then + echo "IMG3 handoff: authenticated iBEC transfer to 0xbff00000" +else + echo "IMG3 handoff: synthetic USB bridge compatibility mode" +fi +if [ "$debug_uart" = 1 ]; then + echo "debug-uarts: forced to 3 in iBSS/iBEC after IMG3 validation" +else + echo "debug-uarts: native firmware behavior (DEBUG_UART=0)" +fi +if [ "$usb_bridge_port" -ne 0 ]; then + echo "USB tools : QEMU at 127.0.0.1:$usb_bridge_port" + echo "irecovery : scripts/irecovery-qemu -q" +else + echo "USB tools : disabled (USB_BRIDGE_PORT=0)" +fi +if [ -n "$gdb_port" ]; then + echo "Debugger : remote GDB at 127.0.0.1:$gdb_port" + echo "LLDB : lldb -o 'gdb-remote 127.0.0.1:$gdb_port'" +fi +if [ "$paused" != 0 ]; then + echo "CPU : paused until the debugger issues continue" fi -echo "UART A6 : cette console (Ctrl+A C = moniteur, Ctrl+A X = quitter)" exec "$qemu_bin" "$@" diff --git a/Run-iPhone5.ps1 b/Run-iPhone5.ps1 index d0fbf84..92e1a3d 100644 --- a/Run-iPhone5.ps1 +++ b/Run-iPhone5.ps1 @@ -9,6 +9,8 @@ param( [switch]$NoWindowsUsb, [switch]$SkipBuild, [switch]$Trace, + [ValidateRange(0, 65535)] + [int]$GdbPort = 0, [switch]$Paused ) @@ -16,7 +18,7 @@ Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { - throw 'Ce lanceur requiert Windows natif.' + throw 'This launcher requires native Windows.' } $qemuSource = $PSScriptRoot @@ -32,22 +34,22 @@ $bridgeStdout = Join-Path $buildDir 'a6-usb-bridge.log' $bridgeStderr = Join-Path $buildDir 'a6-usb-bridge-error.log' if (-not (Test-Path -LiteralPath $RomPath -PathType Leaf)) { - throw "SecureROM introuvable : $RomPath" + throw "SecureROM not found: $RomPath" } $rom = Get-Item -LiteralPath $RomPath if ($rom.Length -ne 0x10000) { - throw "La SecureROM doit faire exactement 65536 octets (taille actuelle : $($rom.Length))." + throw "SecureROM must be exactly 65536 bytes (actual size: $($rom.Length))." } $dfuImage = $null if ($DfuImagePath) { if (-not (Test-Path -LiteralPath $DfuImagePath -PathType Leaf)) { - throw "Image DFU/iBSS introuvable : $DfuImagePath" + throw "DFU/iBSS image not found: $DfuImagePath" } $dfuImage = Get-Item -LiteralPath $DfuImagePath if ($dfuImage.Length -eq 0) { - throw "L'image DFU/iBSS est vide : $($dfuImage.FullName)" + throw "The DFU/iBSS image is empty: $($dfuImage.FullName)" } } @@ -57,13 +59,13 @@ $env:Path = "$mingwBin;$msysBin;$env:Path" if (-not $SkipBuild) { if (-not (Test-Path -LiteralPath $ninja -PathType Leaf)) { - throw "Ninja MinGW64 introuvable : $ninja" + throw "MinGW64 Ninja not found: $ninja" } if (-not (Test-Path -LiteralPath (Join-Path $msysBin 'sort.exe') -PathType Leaf)) { - throw "Les outils MSYS2 sont incomplets : $msysBin" + throw "The MSYS2 tool set is incomplete: $msysBin" } if (-not (Test-Path -LiteralPath (Join-Path $buildDir 'build.ninja') -PathType Leaf)) { - throw "Le dossier qemu\build n'est pas configure. Configurez QEMU depuis MSYS2 MinGW64 avec --target-list=arm-softmmu." + throw "qemu\build is not configured. Configure QEMU from MSYS2 MinGW64 with --target-list=arm-softmmu." } [IO.Directory]::CreateDirectory($buildTemp) | Out-Null @@ -73,17 +75,17 @@ if (-not $SkipBuild) { & $ninja -C $buildDir qemu-system-arm.exe if ($LASTEXITCODE -ne 0) { - throw "La compilation QEMU a echoue avec le code $LASTEXITCODE." + throw "The QEMU build failed with exit code $LASTEXITCODE." } } if (-not (Test-Path -LiteralPath $qemu -PathType Leaf)) { - throw "Binaire QEMU introuvable : $qemu" + throw "QEMU binary not found: $qemu" } $machines = & $qemu -machine help 2>&1 if ($LASTEXITCODE -ne 0 -or -not ($machines -match '^iphone5\s')) { - throw 'Ce binaire QEMU ne contient pas la machine iphone5.' + throw 'This QEMU binary does not include the iphone5 machine.' } $qemuArgs = @( @@ -107,13 +109,19 @@ if ($windowsUsb) { ) } -if ($Paused) { - $qemuArgs += @('-S', '-display', 'none', '-serial', 'null', '-monitor', 'stdio') -} else { - # UART0 is connected explicitly to this native Windows console. The - # monitor is multiplexed on the same chardev (Ctrl+A C toggles it). - $qemuArgs += @('-display', 'none', '-serial', 'mon:stdio') +if ($Paused -and $GdbPort -eq 0) { + $GdbPort = 1234 } +if ($GdbPort -gt 0) { + $qemuArgs += @('-gdb', "tcp:127.0.0.1:$GdbPort") +} +if ($Paused) { + $qemuArgs += '-S' +} + +# UART0 and the monitor stay multiplexed on the native console, including +# while the CPU is stopped for a debugger (Ctrl+A C toggles the monitor). +$qemuArgs += @('-display', 'none', '-serial', 'mon:stdio') if ($Trace) { $tracePath = Join-Path $buildDir 'a6-boot.log' @@ -121,32 +129,37 @@ if ($Trace) { '-d', 'in_asm,cpu_reset,guest_errors,unimp', '-D', $tracePath ) - Write-Host "Trace QEMU : $tracePath" + Write-Host "QEMU trace : $tracePath" } -Write-Host "QEMU Windows natif : $qemu" +Write-Host "Native Windows QEMU : $qemu" Write-Host "SecureROM : $($rom.FullName)" if ($dfuImage) { - Write-Host "Image DFU/iBSS : $($dfuImage.FullName)" + Write-Host "DFU/iBSS image : $($dfuImage.FullName)" } if ($windowsUsb) { if (-not (Test-Path -LiteralPath $bridge -PathType Leaf)) { & $bridgeBuild -Configuration Release } - Write-Host 'USB Windows : Apple DFU VID_05AC&PID_1227 via UDE' - Write-Host "Pont QEMU : 127.0.0.1:$UsbBridgePort" - Write-Host "Journal pont USB : $bridgeStdout" + Write-Host 'Windows USB : Apple DFU VID_05AC&PID_1227 through UDE' + Write-Host "QEMU bridge : 127.0.0.1:$UsbBridgePort" + Write-Host "USB bridge log : $bridgeStdout" } -if (-not $Paused) { - if ($dfuImage) { - Write-Host 'Le cable USB et le transfert DFU sont simules en interne.' - } elseif ($windowsUsb) { - Write-Host 'Windows voit le téléphone DFU virtuel ; idevicerestore reste inchangé.' - } else { - Write-Host 'Recovery et cable USB sont simulés : la ROM attend en mode DFU.' - } - Write-Host 'UART A6 : cette console (Ctrl+A C = moniteur)' - Write-Host 'Utilisez Ctrl+A puis X pour quitter.' +if ($dfuImage) { + Write-Host 'The USB cable and DFU transfer are simulated internally.' +} elseif ($windowsUsb) { + Write-Host 'Windows exposes the virtual DFU phone to unmodified host tools.' +} else { + Write-Host 'Recovery and the USB cable are simulated; the ROM is waiting in DFU mode.' +} +Write-Host 'A6 UART : this console (Ctrl+A C = monitor)' +Write-Host 'Press Ctrl+A, then X to quit.' +if ($GdbPort -gt 0) { + Write-Host "Debugger : remote GDB at 127.0.0.1:$GdbPort" + Write-Host "LLDB : lldb -o 'gdb-remote 127.0.0.1:$GdbPort'" +} +if ($Paused) { + Write-Host 'CPU : paused until the debugger issues continue' } $bridgeProcess = $null diff --git a/hw/arm/s5l8950x.c b/hw/arm/s5l8950x.c index 7cc9a10..bbfc54e 100644 --- a/hw/arm/s5l8950x.c +++ b/hw/arm/s5l8950x.c @@ -5,6 +5,9 @@ #include "hw/core/boards.h" #include "hw/core/loader.h" #include "hw/misc/unimp.h" +#include "hw/misc/s5l8950x-aes.h" +#include "hw/misc/s5l8950x-pke.h" +#include "hw/misc/s5l8950x-sha1.h" #include "hw/core/sysbus.h" #include "system/address-spaces.h" #include "target/arm/cpu.h" @@ -30,6 +33,39 @@ static const ARMCPRegInfo s5l8950x_cp_reginfo[] = { .access = PL1_W, .type = ARM_CP_NOP, }, + { + .name = "APPLE_SWIFT_AUX_CFG", + .cp = 15, + .opc1 = 1, + .crn = 15, + .crm = 2, + .opc2 = 4, + .access = PL1_RW, + .type = ARM_CP_CONST, + .resetvalue = 0, + }, + { + .name = "APPLE_SWIFT_AUX_CFG2", + .cp = 15, + .opc1 = 1, + .crn = 15, + .crm = 2, + .opc2 = 5, + .access = PL1_RW, + .type = ARM_CP_CONST, + .resetvalue = 0, + }, + { + .name = "APPLE_SWIFT_AUX_CFG3", + .cp = 15, + .opc1 = 1, + .crn = 15, + .crm = 2, + .opc2 = 7, + .access = PL1_RW, + .type = ARM_CP_CONST, + .resetvalue = 0, + }, }; /* Addresses */ @@ -40,21 +76,30 @@ static const ARMCPRegInfo s5l8950x_cp_reginfo[] = { #define S5L8950X_SDIO_BASE 0x20000000 #define S5L8950X_USB_PHY_BASE 0x36000000 #define S5L8950X_USB_BASE 0x36100000 -#define S5L8950X_SHA1_BASE 0x3C500000 +#define S5L8950X_SHA1_BASE 0x30200000 #define S5L8950X_SPI_BASE 0x3D200000 -#define S5L8950X_AES_BASE 0x3E000000 +#define S5L8950X_AES_DMA_BASE 0x37000000 +#define S5L8950X_AES_BASE 0x37800000 #define S5L8950X_IIC_BASE 0x3E100000 -#define S5L8950X_PKE_BASE 0x3E200000 +#define S5L8950X_PKE_BASE 0x33100000 #define S5L8950X_WDT_BASE 0x3E300000 #define S5L8950X_CHIPID_BASE 0x3F500000 #define S5L8950X_TIMER_BASE 0x3F200000 #define S5L8950X_PMGR_BASE 0x3F100000 +#define S5L8950X_CLOCK_BASE 0x3F800000 +#define S5L8950X_MEMCTL_BASE 0x39500000 #define S5L8950X_AMC_BASE 0x40000000 #define S5L8950X_AIC_BASE 0x3F200000 -#define S5L8950X_UART_BASE 0x44300000 +#define S5L8950X_UART_BASE 0x32B00000 #define S5L8950X_GPIO_BASE 0x3FA00000 #define S5L8950X_DRAM_BASE 0x80000000 +static const hwaddr s5l8950x_i2c_base[] = { + 0x33200000, + 0x33300000, + 0x33400000, +}; + static void s5l8950x_init(Object *obj) { S5L8950XState *s = S5L8950X(obj); @@ -84,11 +129,8 @@ static void s5l8950x_realize(DeviceState *dev, Error **errp) /* Create unimplemented devices */ create_unimplemented_device("s5l8950x.sdio", S5L8950X_SDIO_BASE, 0x10000); - create_unimplemented_device("s5l8950x.sha1", S5L8950X_SHA1_BASE, 0x1000); create_unimplemented_device("s5l8950x.spi", S5L8950X_SPI_BASE, 0x1000); - create_unimplemented_device("s5l8950x.aes", S5L8950X_AES_BASE, 0x1000); create_unimplemented_device("s5l8950x.iic", S5L8950X_IIC_BASE, 0x1000); - create_unimplemented_device("s5l8950x.pke", S5L8950X_PKE_BASE, 0x1000); create_unimplemented_device("s5l8950x.wdt", S5L8950X_WDT_BASE, 0x1000); create_unimplemented_device("s5l8950x.amc", S5L8950X_AMC_BASE, 0x1000); @@ -98,6 +140,22 @@ static void s5l8950x_realize(DeviceState *dev, Error **errp) sysbus_realize_and_unref(s->chipid, &error_fatal); sysbus_mmio_map(s->chipid, 0, S5L8950X_CHIPID_BASE); + DeviceState *dev_aes = qdev_new(TYPE_S5L8950X_AES); + s->aes = SYS_BUS_DEVICE(dev_aes); + sysbus_realize_and_unref(s->aes, &error_fatal); + sysbus_mmio_map(s->aes, 0, S5L8950X_AES_DMA_BASE); + sysbus_mmio_map(s->aes, 1, S5L8950X_AES_BASE); + + DeviceState *dev_pke = qdev_new(TYPE_S5L8950X_PKE); + s->pke = SYS_BUS_DEVICE(dev_pke); + sysbus_realize_and_unref(s->pke, &error_fatal); + sysbus_mmio_map(s->pke, 0, S5L8950X_PKE_BASE); + + DeviceState *dev_sha1 = qdev_new(TYPE_S5L8950X_SHA1); + s->sha1 = SYS_BUS_DEVICE(dev_sha1); + sysbus_realize_and_unref(s->sha1, &error_fatal); + sysbus_mmio_map(s->sha1, 0, S5L8950X_SHA1_BASE); + DeviceState *dev_aic = qdev_new(TYPE_S5L8950X_AIC); s->aic = SYS_BUS_DEVICE(dev_aic); sysbus_realize_and_unref(s->aic, &error_fatal); @@ -112,14 +170,38 @@ static void s5l8950x_realize(DeviceState *dev, Error **errp) s->uart = SYS_BUS_DEVICE(dev_uart); sysbus_realize_and_unref(s->uart, &error_fatal); sysbus_mmio_map(s->uart, 0, S5L8950X_UART_BASE); + /* iBoot's A6 UART table maps 0x32b00000 to external IRQ 0x44. */ + sysbus_connect_irq(s->uart, 0, + qdev_get_gpio_in(dev_aic, 0x44)); DeviceState *dev_pmgr = qdev_new(TYPE_S5L8950X_PMGR); s->pmgr = SYS_BUS_DEVICE(dev_pmgr); sysbus_realize_and_unref(s->pmgr, &error_fatal); sysbus_mmio_map(s->pmgr, 0, S5L8950X_PMGR_BASE); + DeviceState *dev_clock = qdev_new(TYPE_S5L8950X_CLOCK); + s->clock = SYS_BUS_DEVICE(dev_clock); + sysbus_realize_and_unref(s->clock, &error_fatal); + sysbus_mmio_map(s->clock, 0, S5L8950X_CLOCK_BASE); + + DeviceState *dev_memctl = qdev_new(TYPE_S5L8950X_MEMCTL); + s->memctl = SYS_BUS_DEVICE(dev_memctl); + sysbus_realize_and_unref(s->memctl, &error_fatal); + sysbus_mmio_map(s->memctl, 0, S5L8950X_MEMCTL_BASE); + + for (size_t i = 0; i < ARRAY_SIZE(s5l8950x_i2c_base); i++) { + DeviceState *dev_i2c = qdev_new(TYPE_S5L8950X_I2C); + + s->i2c[i] = SYS_BUS_DEVICE(dev_i2c); + sysbus_realize_and_unref(s->i2c[i], &error_fatal); + sysbus_mmio_map(s->i2c[i], 0, s5l8950x_i2c_base[i]); + sysbus_connect_irq(s->i2c[i], 0, + qdev_get_gpio_in(dev_aic, 0x46 + i)); + } + DeviceState *dev_gpio = qdev_new(TYPE_S5L8950X_GPIO); qdev_prop_set_bit(dev_gpio, "force-dfu", true); + qdev_prop_set_uint8(dev_gpio, "board-straps", 2); s->gpio = SYS_BUS_DEVICE(dev_gpio); sysbus_realize_and_unref(s->gpio, &error_fatal); sysbus_mmio_map(s->gpio, 0, S5L8950X_GPIO_BASE); diff --git a/hw/char/s5l8950x-uart.c b/hw/char/s5l8950x-uart.c index eb3fcce..873eee7 100644 --- a/hw/char/s5l8950x-uart.c +++ b/hw/char/s5l8950x-uart.c @@ -9,6 +9,8 @@ #define TYPE_S5L8950X_UART "s5l8950x-uart" OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XUartState, S5L8950X_UART) +#define S5L8950X_UART_RX_FIFO_SIZE 16 + struct S5L8950XUartState { SysBusDevice parent_obj; @@ -22,7 +24,7 @@ struct S5L8950XUartState { uint32_t ubrdiv; uint32_t ufracval; - uint8_t rx_fifo[16]; + uint8_t rx_fifo[S5L8950X_UART_RX_FIFO_SIZE]; int rx_count; qemu_irq irq; }; @@ -40,6 +42,9 @@ struct S5L8950XUartState { #define UART_UBRDIV 0x28 #define UART_UFRACVAL 0x2C +#define UART_UFCON_RX_FIFO_RESET BIT(1) +#define UART_UFCON_TX_FIFO_RESET BIT(2) + static void s5l8950x_uart_update_irq(S5L8950XUartState *s) { if (s->rx_count > 0) { @@ -76,7 +81,7 @@ static uint64_t s5l8950x_uart_read(void *opaque, hwaddr offset, unsigned size) break; case UART_UFSTAT: /* Return rx count in low bits, no tx fifo used here */ - val = s->rx_count & 0xf; + val = s->rx_count; break; case UART_UMSTAT: val = 0; @@ -89,6 +94,7 @@ static uint64_t s5l8950x_uart_read(void *opaque, hwaddr offset, unsigned size) s->rx_count--; memmove(s->rx_fifo, s->rx_fifo + 1, s->rx_count); s5l8950x_uart_update_irq(s); + qemu_chr_fe_accept_input(&s->chr); } break; case UART_UBRDIV: @@ -118,7 +124,14 @@ static void s5l8950x_uart_write(void *opaque, hwaddr offset, uint64_t val, unsig s->ucon = val; break; case UART_UFCON: - s->ufcon = val; + if (val & UART_UFCON_RX_FIFO_RESET) { + s->rx_count = 0; + s5l8950x_uart_update_irq(s); + qemu_chr_fe_accept_input(&s->chr); + } + /* FIFO reset bits are self-clearing; TX is unbuffered. */ + s->ufcon = val & ~(UART_UFCON_RX_FIFO_RESET | + UART_UFCON_TX_FIFO_RESET); break; case UART_UMCON: s->umcon = val; @@ -151,9 +164,13 @@ static void s5l8950x_uart_write(void *opaque, hwaddr offset, uint64_t val, unsig static const MemoryRegionOps s5l8950x_uart_ops = { .read = s5l8950x_uart_read, .write = s5l8950x_uart_write, - .endianness = DEVICE_NATIVE_ENDIAN, + .endianness = DEVICE_LITTLE_ENDIAN, .valid = { - .min_access_size = 4, + .min_access_size = 1, + .max_access_size = 4, + }, + .impl = { + .min_access_size = 1, .max_access_size = 4, }, }; @@ -162,7 +179,7 @@ static int s5l8950x_uart_can_receive(void *opaque) { S5L8950XUartState *s = S5L8950X_UART(opaque); - return sizeof(s->rx_fifo) - s->rx_count; + return S5L8950X_UART_RX_FIFO_SIZE - s->rx_count; } static void s5l8950x_uart_receive(void *opaque, const uint8_t *buf, int size) @@ -170,7 +187,8 @@ static void s5l8950x_uart_receive(void *opaque, const uint8_t *buf, int size) S5L8950XUartState *s = S5L8950X_UART(opaque); int i; - for (i = 0; i < size && s->rx_count < sizeof(s->rx_fifo); i++) { + for (i = 0; i < size && + s->rx_count < S5L8950X_UART_RX_FIFO_SIZE; i++) { s->rx_fifo[s->rx_count++] = buf[i]; } s5l8950x_uart_update_irq(s); diff --git a/hw/gpio/s5l8950x-gpio.c b/hw/gpio/s5l8950x-gpio.c index ef766cb..21b95d2 100644 --- a/hw/gpio/s5l8950x-gpio.c +++ b/hw/gpio/s5l8950x-gpio.c @@ -18,6 +18,17 @@ /* Recovery/DFU button sampled by SecureROM as GPIO 0x1906. */ #define S5L8950X_GPIO_DFU_OFFSET (((0x19 * 8) + 6) * sizeof(uint32_t)) +/* + * SecureROM packs these four board straps into bits [11:8] of the + * boot-policy word at 0x3f106000. Profile 2 is the authenticated A6 + * production path used by this n41ap machine. + */ +#define S5L8950X_GPIO_STRAP_0_OFFSET (((0x02 * 8) + 2) * sizeof(uint32_t)) +#define S5L8950X_GPIO_STRAP_1_OFFSET (((0x19 * 8) + 5) * sizeof(uint32_t)) +#define S5L8950X_GPIO_STRAP_2_OFFSET (((0x1a * 8) + 0) * sizeof(uint32_t)) +#define S5L8950X_GPIO_STRAP_3_OFFSET (((0x1a * 8) + 1) * sizeof(uint32_t)) +/* iBEC samples GPIO 1 as an active-low boot/power button. */ +#define S5L8950X_GPIO_BOOT_BUTTON_OFFSET sizeof(uint32_t) #define S5L8950X_GPIO_INPUT_LEVEL BIT(0) OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XGPIOState, S5L8950X_GPIO) @@ -27,6 +38,7 @@ struct S5L8950XGPIOState { MemoryRegion iomem; uint32_t regs[S5L8950X_GPIO_NUM_REGS]; bool force_dfu; + uint8_t board_straps; }; static uint64_t s5l8950x_gpio_read(void *opaque, hwaddr offset, @@ -40,9 +52,27 @@ static uint64_t s5l8950x_gpio_read(void *opaque, hwaddr offset, } value = s->regs[offset / sizeof(uint32_t)]; + if (offset == S5L8950X_GPIO_BOOT_BUTTON_OFFSET) { + /* + * No host button is currently wired up: expose the released level so + * iBEC does not mistake the zero-filled register file for a button + * held continuously and power the device off after its timeout. + */ + value |= S5L8950X_GPIO_INPUT_LEVEL; + } if (s->force_dfu && offset == S5L8950X_GPIO_DFU_OFFSET) { value |= S5L8950X_GPIO_INPUT_LEVEL; } + if ((offset == S5L8950X_GPIO_STRAP_0_OFFSET && + (s->board_straps & BIT(0))) || + (offset == S5L8950X_GPIO_STRAP_1_OFFSET && + (s->board_straps & BIT(1))) || + (offset == S5L8950X_GPIO_STRAP_2_OFFSET && + (s->board_straps & BIT(2))) || + (offset == S5L8950X_GPIO_STRAP_3_OFFSET && + (s->board_straps & BIT(3)))) { + value |= S5L8950X_GPIO_INPUT_LEVEL; + } return value; } @@ -76,6 +106,7 @@ static const MemoryRegionOps s5l8950x_gpio_ops = { static const Property s5l8950x_gpio_properties[] = { DEFINE_PROP_BOOL("force-dfu", S5L8950XGPIOState, force_dfu, false), + DEFINE_PROP_UINT8("board-straps", S5L8950XGPIOState, board_straps, 0), }; static void s5l8950x_gpio_reset(DeviceState *dev) diff --git a/hw/intc/s5l8950x-aic.c b/hw/intc/s5l8950x-aic.c index 8b6aa7c..dc6a76a 100644 --- a/hw/intc/s5l8950x-aic.c +++ b/hw/intc/s5l8950x-aic.c @@ -9,9 +9,11 @@ #include "qemu/osdep.h" #include "qemu/log.h" #include "qemu/timer.h" +#include "hw/core/cpu.h" #include "hw/core/sysbus.h" #include "hw/core/irq.h" #include "qom/object.h" +#include "target/arm/cpu.h" #define TYPE_S5L8950X_AIC "s5l8950x-aic" OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XAicState, S5L8950X_AIC) @@ -30,12 +32,19 @@ OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XAicState, S5L8950X_AIC) #define AIC_ACK 0x2004 #define AIC_IPI_SET 0x2008 #define AIC_IPI_CLR 0x200c +#define AIC_TIMER_CTRL 0x2010 +#define AIC_TIMER_COMPARE 0x2014 +#define AIC_TIMER_STATE 0x2018 +#define AIC_TIMER_MASK_SET 0x201c +#define AIC_TIMER_MASK_CLR 0x2020 #define AIC_SRC_CFG_BASE 0x3000 #define AIC_MASK_SET_BASE 0x4100 #define AIC_MASK_CLR_BASE 0x4180 #define AIC_INT_STATE_BASE 0x4200 #define AIC_EVENT_EXT 0x00010000 +#define AIC_EVENT_TIMER 0x00070001 +#define AIC_CLOCK_HZ 24000000 struct S5L8950XAicState { SysBusDevice parent_obj; @@ -44,6 +53,13 @@ struct S5L8950XAicState { uint32_t pending[S5L8950X_AIC_NUM_BANKS]; uint32_t src_cfg[S5L8950X_AIC_NUM_IRQS]; uint32_t glb_cfg; + uint32_t timer_ctrl; + uint32_t timer_compare; + uint32_t timer_state; + bool timer_irq_enabled; + bool timer_pending; + bool timer_has_fired; + QEMUTimer *timer; qemu_irq irq; qemu_irq fiq; }; @@ -59,7 +75,7 @@ static bool s5l8950x_aic_irq_is_active(S5L8950XAicState *s, int irq) static void s5l8950x_aic_update(S5L8950XAicState *s) { - bool active = false; + bool active = s->timer_pending && s->timer_irq_enabled; int irq; for (irq = 0; irq < S5L8950X_AIC_NUM_IRQS; irq++) { @@ -72,10 +88,83 @@ static void s5l8950x_aic_update(S5L8950XAicState *s) qemu_irq_lower(s->fiq); } +static uint64_t s5l8950x_aic_ticks(void) +{ + return muldiv64(qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL), AIC_CLOCK_HZ, + NANOSECONDS_PER_SECOND); +} + +static void s5l8950x_aic_timer_reschedule(S5L8950XAicState *s); + +static void s5l8950x_aic_timer_expired(void *opaque) +{ + S5L8950XAicState *s = opaque; + CPUState *cs = first_cpu; + vaddr pc = cs ? ARM_CPU(cs)->env.regs[15] : 0; + + /* + * iBoot initially shares the top of its bootstrap stack with the IRQ + * stack. On A6 the CPU reaches WFE well before the first 10 ms timer + * deadline, while a non-icount TCG run can take longer in host time. + * Do not inject that first local event until the bootstrap code has + * actually reached WFE; later events retain their normal timing and may + * preempt scheduler tasks. + */ + if (!s->timer_has_fired && cs && + (!cs->halted || pc < 0x10000854 || pc > 0x10000860)) { + timer_mod(s->timer, + qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + + NANOSECONDS_PER_SECOND / 1000); + return; + } + + if (!s->timer_has_fired) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x-aic: first local timer event at " + "PC=0x%" VADDR_PRIx "\n", pc); + } + s->timer_has_fired = true; + s->timer_state |= 1; + s->timer_pending = true; + s5l8950x_aic_update(s); +} + +static void s5l8950x_aic_timer_reschedule(S5L8950XAicState *s) +{ + uint32_t delta = s->timer_compare; + int64_t delta_ns; + + timer_del(s->timer); + /* AICv2's compare register is a relative countdown in 24 MHz ticks. */ + if (!(s->timer_ctrl & 1) || delta == UINT32_MAX) { + return; + } + if (!delta) { + delta = 1; + } + delta_ns = muldiv64(delta, NANOSECONDS_PER_SECOND, AIC_CLOCK_HZ); + if (!delta_ns) { + delta_ns = 1; + } + timer_mod(s->timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + delta_ns); +} + static uint32_t s5l8950x_aic_ack(S5L8950XAicState *s) { int irq; + /* + * The per-CPU timer is reported by AICv2 as local event 1, not as an + * external source. iBoot maps 0x00070001 to its internal vector 0xc1. + * Acknowledge also masks it until iBoot writes TIMER_MASK_CLR at the end + * of the handler, mirroring the external-source acknowledge behaviour. + */ + if (s->timer_pending && s->timer_irq_enabled) { + s->timer_irq_enabled = false; + s5l8950x_aic_update(s); + return AIC_EVENT_TIMER; + } + for (irq = 0; irq < S5L8950X_AIC_NUM_IRQS; irq++) { if (s5l8950x_aic_irq_is_active(s, irq)) { s->mask[irq / 32] |= BIT(irq % 32); @@ -105,8 +194,7 @@ static uint64_t s5l8950x_aic_read(void *opaque, hwaddr offset, unsigned size) S5L8950XAicState *s = opaque; uint64_t ticks; - ticks = muldiv64(qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL), 24000000, - NANOSECONDS_PER_SECOND); + ticks = s5l8950x_aic_ticks(); switch (offset) { case AIC_REV: return 2; @@ -126,6 +214,15 @@ static uint64_t s5l8950x_aic_read(void *opaque, hwaddr offset, unsigned size) return 0; case AIC_ACK: return s5l8950x_aic_ack(s); + case AIC_TIMER_CTRL: + return s->timer_ctrl; + case AIC_TIMER_COMPARE: + return s->timer_compare; + case AIC_TIMER_STATE: + return s->timer_state; + case AIC_TIMER_MASK_SET: + case AIC_TIMER_MASK_CLR: + return 0; case AIC_SRC_CFG_BASE ... AIC_SRC_CFG_BASE + S5L8950X_AIC_NUM_IRQS * 4 - 4: return s->src_cfg[(offset - AIC_SRC_CFG_BASE) / 4]; @@ -163,6 +260,35 @@ static void s5l8950x_aic_write(void *opaque, hwaddr offset, case AIC_IPI_SET: case AIC_IPI_CLR: break; + case AIC_TIMER_CTRL: + s->timer_ctrl = value; + if (!(s->timer_ctrl & 1)) { + timer_del(s->timer); + } + break; + case AIC_TIMER_COMPARE: + s->timer_compare = value; + s5l8950x_aic_timer_reschedule(s); + break; + case AIC_TIMER_STATE: + s->timer_state &= ~value; + if (value & 1) { + s->timer_pending = false; + s5l8950x_aic_update(s); + } + break; + case AIC_TIMER_MASK_SET: + if (value & 2) { + s->timer_irq_enabled = false; + s5l8950x_aic_update(s); + } + break; + case AIC_TIMER_MASK_CLR: + if (value & 2) { + s->timer_irq_enabled = true; + s5l8950x_aic_update(s); + } + break; case AIC_SRC_CFG_BASE ... AIC_SRC_CFG_BASE + S5L8950X_AIC_NUM_IRQS * 4 - 4: s->src_cfg[(offset - AIC_SRC_CFG_BASE) / 4] = value; @@ -209,6 +335,13 @@ static void s5l8950x_aic_reset(DeviceState *dev) memset(s->pending, 0, sizeof(s->pending)); memset(s->src_cfg, 0, sizeof(s->src_cfg)); s->glb_cfg = 0; + s->timer_ctrl = 0; + s->timer_compare = UINT32_MAX; + s->timer_state = 0; + s->timer_irq_enabled = false; + s->timer_pending = false; + s->timer_has_fired = false; + timer_del(s->timer); s5l8950x_aic_update(s); } @@ -217,6 +350,8 @@ static void s5l8950x_aic_init(Object *obj) S5L8950XAicState *s = S5L8950X_AIC(obj); SysBusDevice *sbd = SYS_BUS_DEVICE(obj); + s->timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, + s5l8950x_aic_timer_expired, s); memory_region_init_io(&s->iomem, obj, &s5l8950x_aic_ops, s, TYPE_S5L8950X_AIC, 0x8000); sysbus_init_mmio(sbd, &s->iomem); diff --git a/hw/misc/meson.build b/hw/misc/meson.build index 35d2033..e147f9c 100644 --- a/hw/misc/meson.build +++ b/hw/misc/meson.build @@ -16,7 +16,16 @@ system_ss.add(when: 'CONFIG_PL310', if_true: files('arm_l2x0.c')) system_ss.add(when: 'CONFIG_INTEGRATOR_DEBUG', if_true: files('arm_integrator_debug.c')) system_ss.add(when: 'CONFIG_A9SCU', if_true: files('a9scu.c')) system_ss.add(when: 'CONFIG_ARM11SCU', if_true: files('arm11scu.c')) -system_ss.add(when: 'CONFIG_S5L8950X', if_true: files('s5l8950x-chipid.c', 's5l8950x-pmgr.c')) +system_ss.add(when: 'CONFIG_S5L8950X', if_true: files( + 's5l8950x-aes.c', + 's5l8950x-pke.c', + 's5l8950x-sha1.c', + 's5l8950x-chipid.c', + 's5l8950x-clock.c', + 's5l8950x-i2c.c', + 's5l8950x-memctl.c', + 's5l8950x-pmgr.c', +)) system_ss.add(when: 'CONFIG_ARM_V7M', if_true: files('armv7m_ras.c')) diff --git a/hw/misc/s5l8950x-aes.c b/hw/misc/s5l8950x-aes.c new file mode 100644 index 0000000..7534947 --- /dev/null +++ b/hw/misc/s5l8950x-aes.c @@ -0,0 +1,589 @@ +/* + * Apple A6 (S5L8950X) AES accelerator and its two-channel DMA front-end. + * + * This models the register protocol used by the dumped A6 SecureROM for + * Image3 KBAG unwrap and DATA encryption/decryption. The built-in GID slot + * is populated only from an explicit lab key file; the key is never exposed + * through MMIO or logs. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "crypto/aes.h" +#include "hw/misc/s5l8950x-aes.h" +#include "hw/core/cpu.h" +#include "hw/core/sysbus.h" +#include "hw/core/qdev-properties.h" +#include "qemu/log.h" +#include "qemu/module.h" +#include "qemu/units.h" +#include "system/address-spaces.h" +#include "system/dma.h" +#include "target/arm/cpu.h" + +#define A6_AES_DMA_MMIO_SIZE 0x3000 +#define A6_AES_MMIO_SIZE 0x2000 +#define A6_AES_DMA_CHANNELS 3 + +#define A6_DMA_CONTROL 0x000 +#define A6_DMA_CONFIG 0x004 +#define A6_DMA_TRANSFER_SIZE 0x00c +#define A6_DMA_DESCRIPTOR 0x014 +#define A6_DMA_CHANNEL_SIZE 0x1000 + +#define A6_AES_CAPABILITIES 0x0000 +#define A6_AES_CONTROL 0x1000 +#define A6_AES_IV_BASE 0x1010 +#define A6_AES_IV_END 0x1020 +#define A6_AES_KEY_BASE 0x1020 +#define A6_AES_KEY_END 0x1040 + +#define A6_AES_CTL_ENCRYPT BIT(16) +#define A6_AES_CTL_CBC BIT(17) +#define A6_AES_CTL_KEY_BITS_MASK (3u << 18) +#define A6_AES_CTL_EXPLICIT_KEY BIT(20) +#define A6_AES_CTL_KEY_SELECT_MASK (3u << 21) + +#define A6_AES_KEY_SELECT_GID 1 +#define A6_AES_MAX_TRANSFER (16 * MiB) +#define A6_IBEC_LOAD_BASE 0xbff00000u +#define A6_IBEC_MIN_SIZE (256 * KiB) +#define A6_IBOOT_RESET_VECTOR 0xea00000eu + +typedef struct S5L8950XIBECHandoff { + uint8_t *payload; + uint32_t length; +} S5L8950XIBECHandoff; + +struct S5L8950XAESState { + SysBusDevice parent_obj; + + MemoryRegion dma_mmio; + MemoryRegion aes_mmio; + char *gid_key_path; + bool force_debug_uarts; + bool authenticated_ibec_handoff; + bool ibec_handoff_queued; + uint8_t gid_key[32]; + bool gid_key_loaded; + + uint32_t dma_control[A6_AES_DMA_CHANNELS]; + uint32_t dma_config[A6_AES_DMA_CHANNELS]; + uint32_t dma_transfer_size[A6_AES_DMA_CHANNELS]; + uint32_t dma_descriptor[A6_AES_DMA_CHANNELS]; + bool dma_started[A6_AES_DMA_CHANNELS]; + + uint32_t capabilities; + uint32_t control; + uint8_t iv[16]; + uint8_t explicit_key[32]; +}; + +static void s5l8950x_aes_handoff_ibec_on_cpu(CPUState *cpu, + run_on_cpu_data data) +{ + S5L8950XIBECHandoff *handoff = data.host_ptr; + + if (address_space_write_rom(&address_space_memory, A6_IBEC_LOAD_BASE, + MEMTXATTRS_UNSPECIFIED, handoff->payload, + handoff->length) != MEMTX_OK) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.aes: iBEC handoff write failed at 0x%08x\n", + A6_IBEC_LOAD_BASE); + goto out; + } + + /* + * iBSS has already completed the IMG3, ticket, nonce and AES work before + * this callback is queued. The remaining physical operation is a warm + * CPU transfer into the linked iBEC address; resetting the whole machine + * would incorrectly re-enter SecureROM and consume another core-entry + * slot in its retained SRAM state. + */ + cpu_reset(cpu); + ARM_CPU(cpu)->env.regs[0] = 1; + cpu->halted = 0; + cpu_set_pc(cpu, A6_IBEC_LOAD_BASE); + cpu_exit(cpu); + qemu_log_mask(LOG_UNIMP, + "s5l8950x.aes: transferred authenticated iBEC (%u bytes) " + "to PC=0x%08x\n", + handoff->length, A6_IBEC_LOAD_BASE); + +out: + g_free(handoff->payload); + g_free(handoff); +} + +static void s5l8950x_aes_queue_ibec_handoff(S5L8950XAESState *s, + const uint8_t *payload, + uint32_t length) +{ + S5L8950XIBECHandoff *handoff; + CPUState *cpu = first_cpu; + + if (!cpu || s->ibec_handoff_queued || length < A6_IBEC_MIN_SIZE || + ldl_le_p(payload) != A6_IBOOT_RESET_VECTOR) { + return; + } + + handoff = g_new(S5L8950XIBECHandoff, 1); + handoff->payload = g_memdup2(payload, length); + handoff->length = length; + s->ibec_handoff_queued = true; + async_run_on_cpu(cpu, s5l8950x_aes_handoff_ibec_on_cpu, + RUN_ON_CPU_HOST_PTR(handoff)); +} + +static bool s5l8950x_aes_enable_bootloader_uarts(uint8_t *image, + size_t image_length, + hwaddr load_base) +{ + static const uint16_t helper_tail[] = { + 0x680a, /* ldr r2, [r1] */ + 0x4310, /* orrs r0, r2 */ + 0x6008, /* str r0, [r1] */ + 0x4770, /* bx lr */ + }; + static const uint8_t movs_r0_3[] = { 0x03, 0x20 }; + + for (size_t offset = 0; offset + 10 <= image_length; offset += 2) { + uint16_t ldr_literal = lduw_le_p(image + offset); + size_t i; + + if ((ldr_literal & 0xff00) != 0x4900) { + continue; + } + for (i = 0; i < ARRAY_SIZE(helper_tail); i++) { + if (lduw_le_p(image + offset + 2 + i * 2) != helper_tail[i]) { + break; + } + } + if (i != ARRAY_SIZE(helper_tail)) { + continue; + } + memcpy(image + offset + 4, movs_r0_3, sizeof(movs_r0_3)); + qemu_log_mask(LOG_UNIMP, + "s5l8950x.uart: forced debug-uarts=3 after native " + "IMG3 decrypt at 0x%08" HWADDR_PRIx "\n", + load_base + offset + 4); + return true; + } + return false; +} + +static bool s5l8950x_aes_read_descriptor(uint32_t address, + hwaddr *buffer, + uint32_t *length) +{ + uint8_t descriptor[16]; + + if (!address || + dma_memory_read(&address_space_memory, address, descriptor, + sizeof(descriptor), + MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { + return false; + } + /* + * A6 DMA addresses are 32-bit physical addresses. ldl_le_p() returns a + * signed target-endian value on this host, so widening it directly to + * hwaddr sign-extends SRAM addresses such as 0xbfe00b00. Preserve the + * descriptor's raw 32-bit bit pattern before widening it. + */ + *buffer = (uint32_t)ldl_le_p(descriptor + 8); + *length = ldl_le_p(descriptor + 12); + return true; +} + +static size_t s5l8950x_aes_key_length(uint32_t control) +{ + switch ((control & A6_AES_CTL_KEY_BITS_MASK) >> 18) { + case 0: + return 16; + case 1: + return 24; + case 2: + return 32; + default: + return 0; + } +} + +static bool s5l8950x_aes_crypt(const uint8_t *source, uint8_t *destination, + size_t length, const uint8_t *key, + size_t key_length, const uint8_t iv[16], + bool cbc, bool encrypt) +{ + AES_KEY expanded_key; + uint8_t chain[AES_BLOCK_SIZE]; + uint8_t block[AES_BLOCK_SIZE]; + + if ((encrypt ? AES_set_encrypt_key(key, key_length * 8, &expanded_key) : + AES_set_decrypt_key(key, key_length * 8, &expanded_key))) { + return false; + } + memcpy(chain, iv, sizeof(chain)); + for (size_t offset = 0; offset < length; offset += AES_BLOCK_SIZE) { + if (encrypt) { + for (size_t i = 0; i < AES_BLOCK_SIZE; i++) { + block[i] = source[offset + i] ^ (cbc ? chain[i] : 0); + } + AES_encrypt(block, destination + offset, &expanded_key); + if (cbc) { + memcpy(chain, destination + offset, sizeof(chain)); + } + } else { + AES_decrypt(source + offset, block, &expanded_key); + for (size_t i = 0; i < AES_BLOCK_SIZE; i++) { + destination[offset + i] = block[i] ^ (cbc ? chain[i] : 0); + } + if (cbc) { + memcpy(chain, source + offset, sizeof(chain)); + } + } + } + return true; +} + +static bool s5l8950x_aes_process(S5L8950XAESState *s) +{ + g_autoptr(GByteArray) source = NULL; + g_autoptr(GByteArray) destination = NULL; + const uint8_t *key; + const char *key_name; + hwaddr source_address; + hwaddr destination_address; + uint32_t source_length; + uint32_t destination_length; + uint32_t key_select; + size_t key_length; + bool cbc; + bool encrypt; + + if (!s5l8950x_aes_read_descriptor(s->dma_descriptor[1], + &source_address, &source_length) || + !s5l8950x_aes_read_descriptor(s->dma_descriptor[2], + &destination_address, + &destination_length)) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.aes: invalid DMA descriptor\n"); + return false; + } + if (!source_length || source_length != destination_length || + source_length > A6_AES_MAX_TRANSFER || source_length % 16) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.aes: invalid DMA lengths %u/%u\n", + source_length, destination_length); + return false; + } + + key_length = s5l8950x_aes_key_length(s->control); + if (!key_length) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.aes: unsupported key-size selector\n"); + return false; + } + + key_select = (s->control & A6_AES_CTL_KEY_SELECT_MASK) >> 21; + if (key_select == A6_AES_KEY_SELECT_GID) { + if (!s->gid_key_loaded || key_length != sizeof(s->gid_key)) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.aes: GID operation requested without " + "a matching 256-bit lab key\n"); + return false; + } + key = s->gid_key; + key_name = "lab-GID"; + } else if (key_select == 0 && + (s->control & A6_AES_CTL_EXPLICIT_KEY)) { + key = s->explicit_key; + key_name = "explicit"; + } else { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.aes: unsupported built-in key slot %u\n", + key_select); + return false; + } + + cbc = (s->control & A6_AES_CTL_CBC) != 0; + encrypt = (s->control & A6_AES_CTL_ENCRYPT) != 0; + + source = g_byte_array_sized_new(source_length); + g_byte_array_set_size(source, source_length); + destination = g_byte_array_sized_new(destination_length); + g_byte_array_set_size(destination, destination_length); + if (dma_memory_read(&address_space_memory, source_address, source->data, + source_length, + MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.aes: DMA source read failed at 0x%08" + HWADDR_PRIx "\n", source_address); + return false; + } + + if (!s5l8950x_aes_crypt(source->data, destination->data, source_length, + key, key_length, s->iv, cbc, encrypt)) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.aes: AES operation failed\n"); + return false; + } + if (s->force_debug_uarts && !encrypt && + key_select == 0 && (s->control & A6_AES_CTL_EXPLICIT_KEY) && + !s5l8950x_aes_enable_bootloader_uarts(destination->data, + destination_length, + destination_address)) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.uart: debug_enable_uarts helper not found " + "after native IMG3 decrypt\n"); + } + if (dma_memory_write(&address_space_memory, destination_address, + destination->data, destination_length, + MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.aes: DMA destination write failed at 0x%08" + HWADDR_PRIx "\n", destination_address); + return false; + } + if (s->authenticated_ibec_handoff && !encrypt && key_select == 0 && + (s->control & A6_AES_CTL_EXPLICIT_KEY)) { + s5l8950x_aes_queue_ibec_handoff(s, destination->data, + destination_length); + } + + qemu_log_mask(LOG_UNIMP, + "s5l8950x.aes: %s AES-%zu-%s key=%s bytes=%u " + "src=0x%08" HWADDR_PRIx " dst=0x%08" HWADDR_PRIx "\n", + encrypt ? "encrypt" : "decrypt", key_length * 8, + cbc ? "CBC" : "ECB", + key_name, source_length, source_address, + destination_address); + return true; +} + +static uint64_t s5l8950x_aes_dma_read(void *opaque, hwaddr offset, + unsigned size) +{ + S5L8950XAESState *s = opaque; + unsigned channel = offset / A6_DMA_CHANNEL_SIZE; + hwaddr reg = offset % A6_DMA_CHANNEL_SIZE; + + if (channel >= A6_AES_DMA_CHANNELS) { + return 0; + } + switch (reg) { + case A6_DMA_CONTROL: + return s->dma_control[channel]; + case A6_DMA_CONFIG: + return s->dma_config[channel]; + case A6_DMA_TRANSFER_SIZE: + return s->dma_transfer_size[channel]; + case A6_DMA_DESCRIPTOR: + return s->dma_descriptor[channel]; + default: + return 0; + } +} + +static void s5l8950x_aes_dma_write(void *opaque, hwaddr offset, + uint64_t value, unsigned size) +{ + S5L8950XAESState *s = opaque; + unsigned channel = offset / A6_DMA_CHANNEL_SIZE; + hwaddr reg = offset % A6_DMA_CHANNEL_SIZE; + + if (channel >= A6_AES_DMA_CHANNELS) { + return; + } + switch (reg) { + case A6_DMA_CONTROL: + s->dma_control[channel] = value; + qemu_log_mask(LOG_UNIMP, + "s5l8950x.aes: DMA%u control=0x%08" PRIx64 "\n", + channel, value); + if (value == 2) { + s->dma_started[channel] = false; + } else if (channel && (value & 1)) { + s->dma_started[channel] = true; + if (s->dma_started[1] && s->dma_started[2]) { + s5l8950x_aes_process(s); + s->dma_started[1] = false; + s->dma_started[2] = false; + /* Status 0 is idle/complete; SecureROM polls bits 16..17. */ + s->dma_control[1] &= ~(3u << 16); + s->dma_control[2] &= ~(3u << 16); + } + } + break; + case A6_DMA_CONFIG: + s->dma_config[channel] = value; + qemu_log_mask(LOG_UNIMP, + "s5l8950x.aes: DMA%u config=0x%08" PRIx64 "\n", + channel, value); + break; + case A6_DMA_TRANSFER_SIZE: + s->dma_transfer_size[channel] = value; + qemu_log_mask(LOG_UNIMP, + "s5l8950x.aes: DMA%u length=0x%08" PRIx64 "\n", + channel, value); + break; + case A6_DMA_DESCRIPTOR: + s->dma_descriptor[channel] = value; + qemu_log_mask(LOG_UNIMP, + "s5l8950x.aes: DMA%u descriptor=0x%08" PRIx64 "\n", + channel, value); + break; + default: + break; + } +} + +static uint64_t s5l8950x_aes_read(void *opaque, hwaddr offset, + unsigned size) +{ + S5L8950XAESState *s = opaque; + + switch (offset) { + case A6_AES_CAPABILITIES: + return s->capabilities; + case A6_AES_CONTROL: + return s->control; + case A6_AES_IV_BASE ... A6_AES_IV_END - 4: + return ldl_le_p(s->iv + offset - A6_AES_IV_BASE); + case A6_AES_KEY_BASE ... A6_AES_KEY_END - 4: + /* Explicit key registers are write-only on the real device. */ + return 0; + default: + return 0; + } +} + +static void s5l8950x_aes_write(void *opaque, hwaddr offset, + uint64_t value, unsigned size) +{ + S5L8950XAESState *s = opaque; + + switch (offset) { + case A6_AES_CAPABILITIES: + s->capabilities = value; + break; + case A6_AES_CONTROL: + s->control = value; + qemu_log_mask(LOG_UNIMP, + "s5l8950x.aes: control=0x%08" PRIx64 "\n", value); + break; + case A6_AES_IV_BASE ... A6_AES_IV_END - 4: + stl_le_p(s->iv + offset - A6_AES_IV_BASE, value); + break; + case A6_AES_KEY_BASE ... A6_AES_KEY_END - 4: + stl_le_p(s->explicit_key + offset - A6_AES_KEY_BASE, value); + break; + default: + break; + } +} + +static const MemoryRegionOps s5l8950x_aes_dma_ops = { + .read = s5l8950x_aes_dma_read, + .write = s5l8950x_aes_dma_write, + .endianness = DEVICE_LITTLE_ENDIAN, + .valid.min_access_size = 4, + .valid.max_access_size = 4, +}; + +static const MemoryRegionOps s5l8950x_aes_ops = { + .read = s5l8950x_aes_read, + .write = s5l8950x_aes_write, + .endianness = DEVICE_LITTLE_ENDIAN, + .valid.min_access_size = 4, + .valid.max_access_size = 4, +}; + +static void s5l8950x_aes_reset(DeviceState *dev) +{ + S5L8950XAESState *s = S5L8950X_AES(dev); + + memset(s->dma_control, 0, sizeof(s->dma_control)); + memset(s->dma_config, 0, sizeof(s->dma_config)); + memset(s->dma_transfer_size, 0, sizeof(s->dma_transfer_size)); + memset(s->dma_descriptor, 0, sizeof(s->dma_descriptor)); + memset(s->dma_started, 0, sizeof(s->dma_started)); + s->capabilities = 0; + s->control = 0; + memset(s->iv, 0, sizeof(s->iv)); + memset(s->explicit_key, 0, sizeof(s->explicit_key)); + s->ibec_handoff_queued = false; +} + +static void s5l8950x_aes_realize(DeviceState *dev, Error **errp) +{ + S5L8950XAESState *s = S5L8950X_AES(dev); + g_autofree gchar *contents = NULL; + gsize length = 0; + g_autoptr(GError) error = NULL; + + if (!s->gid_key_path) { + return; + } + if (!g_file_get_contents(s->gid_key_path, &contents, &length, &error)) { + error_setg(errp, "could not load A6 lab GID key '%s': %s", + s->gid_key_path, error->message); + return; + } + if (length != sizeof(s->gid_key)) { + error_setg(errp, "A6 lab GID key '%s' must be exactly 32 bytes", + s->gid_key_path); + return; + } + memcpy(s->gid_key, contents, sizeof(s->gid_key)); + s->gid_key_loaded = true; + qemu_log_mask(LOG_UNIMP, + "s5l8950x.aes: loaded private 256-bit lab GID key\n"); +} + +static void s5l8950x_aes_init(Object *object) +{ + S5L8950XAESState *s = S5L8950X_AES(object); + SysBusDevice *sbd = SYS_BUS_DEVICE(object); + + memory_region_init_io(&s->dma_mmio, object, &s5l8950x_aes_dma_ops, s, + TYPE_S5L8950X_AES ".dma", + A6_AES_DMA_MMIO_SIZE); + sysbus_init_mmio(sbd, &s->dma_mmio); + memory_region_init_io(&s->aes_mmio, object, &s5l8950x_aes_ops, s, + TYPE_S5L8950X_AES, A6_AES_MMIO_SIZE); + sysbus_init_mmio(sbd, &s->aes_mmio); +} + +static const Property s5l8950x_aes_properties[] = { + DEFINE_PROP_STRING("gid-key-file", S5L8950XAESState, gid_key_path), + DEFINE_PROP_BOOL("force-debug-uarts", S5L8950XAESState, + force_debug_uarts, false), + DEFINE_PROP_BOOL("authenticated-ibec-handoff", S5L8950XAESState, + authenticated_ibec_handoff, false), +}; + +static void s5l8950x_aes_class_init(ObjectClass *klass, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + + dc->realize = s5l8950x_aes_realize; + device_class_set_legacy_reset(dc, s5l8950x_aes_reset); + device_class_set_props(dc, s5l8950x_aes_properties); +} + +static const TypeInfo s5l8950x_aes_type_info = { + .name = TYPE_S5L8950X_AES, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(S5L8950XAESState), + .instance_init = s5l8950x_aes_init, + .class_init = s5l8950x_aes_class_init, +}; + +static void s5l8950x_aes_register_types(void) +{ + type_register_static(&s5l8950x_aes_type_info); +} + +type_init(s5l8950x_aes_register_types) diff --git a/hw/misc/s5l8950x-chipid.c b/hw/misc/s5l8950x-chipid.c index dfef1ab..ab5ce17 100644 --- a/hw/misc/s5l8950x-chipid.c +++ b/hw/misc/s5l8950x-chipid.c @@ -18,7 +18,11 @@ static uint64_t s5l8950x_chipid_read(void *opaque, hwaddr offset, unsigned size) /* n41ap: CPFM 0x03, SCEP 0x10, BDID 0x00, IBFL 0x00. */ return 0x200D; case 0x04: - return 0x00; + /* + * SecureROM leaves the high handoff bit asserted before entering + * iBSS. Early iBoot aborts immediately when this latch is clear. + */ + return 0x80000000; case 0x08: return 0xDEADBEEF; case 0x0C: diff --git a/hw/misc/s5l8950x-clock.c b/hw/misc/s5l8950x-clock.c new file mode 100644 index 0000000..3a3d46c --- /dev/null +++ b/hw/misc/s5l8950x-clock.c @@ -0,0 +1,137 @@ +/* + * Apple S5L8950X clock/PLL parameter controller. + * + * iBoot programs the ordinary clock registers directly and uses the command + * ports at 0x480..0x4a4 to query PLL parameters. The corresponding status + * ports contain two result bytes plus busy bits. Clock changes complete + * synchronously here; returning a non-zero divider is enough for iBoot to + * derive its early clock tree without inventing asynchronous hardware. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "hw/arm/s5l8950x.h" +#include "hw/core/sysbus.h" +#include "qom/object.h" + +#define S5L8950X_CLOCK_REGION_SIZE 0x1000 +#define S5L8950X_CLOCK_NUM_REGS \ + (S5L8950X_CLOCK_REGION_SIZE / sizeof(uint32_t)) + +OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XClockState, S5L8950X_CLOCK) + +struct S5L8950XClockState { + SysBusDevice parent_obj; + MemoryRegion iomem; + uint32_t regs[S5L8950X_CLOCK_NUM_REGS]; +}; + +static uint64_t s5l8950x_clock_read(void *opaque, hwaddr offset, + unsigned size) +{ + S5L8950XClockState *s = opaque; + + return s->regs[offset / sizeof(uint32_t)]; +} + +static void s5l8950x_clock_write(void *opaque, hwaddr offset, + uint64_t value, unsigned size) +{ + S5L8950XClockState *s = opaque; + uint32_t reg = value; + uint32_t selector = (reg >> 8) & 0xff; + uint32_t result = 1; + + /* + * Parameter 8 describes the divider layout used by iBoot's early clock + * setup. Its bits 2..5 must encode a value in the accepted 4..6 range; + * the other parameters are ordinary non-zero divisors. + */ + if (selector == 8) { + result = 0x10; + } + + s->regs[offset / sizeof(uint32_t)] = reg; + + /* Read-command ports: publish the result and leave the busy bit clear. */ + switch (offset) { + case 0x480: + s->regs[0x490 / 4] = + (s->regs[0x490 / 4] & ~0x0000ff01u) | (result << 8); + break; + case 0x488: + s->regs[0x490 / 4] = + (s->regs[0x490 / 4] & ~0x00ff0010u) | (result << 16); + break; + case 0x498: + s->regs[0x4a8 / 4] = + (s->regs[0x4a8 / 4] & ~0x0000ff01u) | (result << 8); + break; + case 0x4a0: + s->regs[0x4a8 / 4] = + (s->regs[0x4a8 / 4] & ~0x00ff0010u) | (result << 16); + break; + case 0x484: + case 0x48c: + case 0x49c: + case 0x4a4: + /* Write commands are consumed by the clock engine immediately. */ + s->regs[offset / 4] = 0; + break; + default: + break; + } +} + +static const MemoryRegionOps s5l8950x_clock_ops = { + .read = s5l8950x_clock_read, + .write = s5l8950x_clock_write, + .endianness = DEVICE_LITTLE_ENDIAN, + .valid = { + .min_access_size = 4, + .max_access_size = 4, + }, +}; + +static void s5l8950x_clock_reset(DeviceState *dev) +{ + S5L8950XClockState *s = S5L8950X_CLOCK(dev); + + memset(s->regs, 0, sizeof(s->regs)); + s->regs[0x490 / 4] = 0x00010100; + s->regs[0x4a8 / 4] = 0x00010100; +} + +static void s5l8950x_clock_init(Object *obj) +{ + S5L8950XClockState *s = S5L8950X_CLOCK(obj); + SysBusDevice *sbd = SYS_BUS_DEVICE(obj); + + memory_region_init_io(&s->iomem, obj, &s5l8950x_clock_ops, s, + TYPE_S5L8950X_CLOCK, + S5L8950X_CLOCK_REGION_SIZE); + sysbus_init_mmio(sbd, &s->iomem); +} + +static void s5l8950x_clock_class_init(ObjectClass *klass, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + + device_class_set_legacy_reset(dc, s5l8950x_clock_reset); +} + +static const TypeInfo s5l8950x_clock_info = { + .name = TYPE_S5L8950X_CLOCK, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(S5L8950XClockState), + .instance_init = s5l8950x_clock_init, + .class_init = s5l8950x_clock_class_init, +}; + +static void s5l8950x_clock_register_types(void) +{ + type_register_static(&s5l8950x_clock_info); +} + +type_init(s5l8950x_clock_register_types) diff --git a/hw/misc/s5l8950x-i2c.c b/hw/misc/s5l8950x-i2c.c new file mode 100644 index 0000000..898074c --- /dev/null +++ b/hw/misc/s5l8950x-i2c.c @@ -0,0 +1,115 @@ +/* + * Apple S5L8950X I2C controller (minimal polled-transfer model) + * + * Early iBoot uses the three controllers while bringing up board devices. + * The target devices are not modelled yet, so reads return zero, but command + * FIFO writes complete synchronously and without a NAK. This is sufficient + * to preserve the firmware's normal timeout/error paths while allowing the + * boot chain to continue through board discovery. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "hw/arm/s5l8950x.h" +#include "hw/core/irq.h" +#include "hw/core/sysbus.h" +#include "qom/object.h" + +#define S5L8950X_I2C_REGION_SIZE 0x1000 +#define S5L8950X_I2C_STATUS 0x14 +#define S5L8950X_I2C_STATUS_DONE BIT(27) + +OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XI2CState, S5L8950X_I2C) + +struct S5L8950XI2CState { + SysBusDevice parent_obj; + MemoryRegion iomem; + qemu_irq irq; + uint32_t regs[S5L8950X_I2C_REGION_SIZE / sizeof(uint32_t)]; +}; + +static uint64_t s5l8950x_i2c_read(void *opaque, hwaddr offset, + unsigned size) +{ + S5L8950XI2CState *s = opaque; + uint32_t value = s->regs[offset / sizeof(uint32_t)]; + + if (offset == S5L8950X_I2C_STATUS) { + value |= S5L8950X_I2C_STATUS_DONE; + } + return value; +} + +static void s5l8950x_i2c_write(void *opaque, hwaddr offset, + uint64_t value, unsigned size) +{ + S5L8950XI2CState *s = opaque; + + if (offset == S5L8950X_I2C_STATUS) { + s->regs[offset / sizeof(uint32_t)] &= ~value; + if (!(s->regs[offset / sizeof(uint32_t)] & + S5L8950X_I2C_STATUS_DONE)) { + qemu_irq_lower(s->irq); + } + return; + } + + s->regs[offset / sizeof(uint32_t)] = value; + if (offset == 0) { + s->regs[S5L8950X_I2C_STATUS / sizeof(uint32_t)] |= + S5L8950X_I2C_STATUS_DONE; + qemu_irq_raise(s->irq); + } +} + +static const MemoryRegionOps s5l8950x_i2c_ops = { + .read = s5l8950x_i2c_read, + .write = s5l8950x_i2c_write, + .endianness = DEVICE_LITTLE_ENDIAN, + .valid = { + .min_access_size = 4, + .max_access_size = 4, + }, +}; + +static void s5l8950x_i2c_reset(DeviceState *dev) +{ + S5L8950XI2CState *s = S5L8950X_I2C(dev); + + memset(s->regs, 0, sizeof(s->regs)); + qemu_irq_lower(s->irq); +} + +static void s5l8950x_i2c_init(Object *obj) +{ + S5L8950XI2CState *s = S5L8950X_I2C(obj); + SysBusDevice *sbd = SYS_BUS_DEVICE(obj); + + memory_region_init_io(&s->iomem, obj, &s5l8950x_i2c_ops, s, + TYPE_S5L8950X_I2C, S5L8950X_I2C_REGION_SIZE); + sysbus_init_mmio(sbd, &s->iomem); + sysbus_init_irq(sbd, &s->irq); +} + +static void s5l8950x_i2c_class_init(ObjectClass *klass, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + + device_class_set_legacy_reset(dc, s5l8950x_i2c_reset); +} + +static const TypeInfo s5l8950x_i2c_info = { + .name = TYPE_S5L8950X_I2C, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(S5L8950XI2CState), + .instance_init = s5l8950x_i2c_init, + .class_init = s5l8950x_i2c_class_init, +}; + +static void s5l8950x_i2c_register_types(void) +{ + type_register_static(&s5l8950x_i2c_info); +} + +type_init(s5l8950x_i2c_register_types) diff --git a/hw/misc/s5l8950x-memctl.c b/hw/misc/s5l8950x-memctl.c new file mode 100644 index 0000000..5924b57 --- /dev/null +++ b/hw/misc/s5l8950x-memctl.c @@ -0,0 +1,130 @@ +/* + * Apple S5L8950X DRAM controller training/status model. + * + * iBEC programs the timing registers and then waits for the controller and + * PHY training state in register zero. QEMU's RAM is already usable, so the + * training phases complete synchronously while ordinary register values are + * retained for debugger inspection. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "hw/arm/s5l8950x.h" +#include "hw/core/sysbus.h" +#include "qom/object.h" + +#define S5L8950X_MEMCTL_REGION_SIZE 0x1000 +#define S5L8950X_MEMCTL_NUM_REGS \ + (S5L8950X_MEMCTL_REGION_SIZE / sizeof(uint32_t)) + +/* Controller ready, PHY ready and all low training stages complete. */ +#define S5L8950X_MEMCTL_READY 0x801003ffu + +OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XMemctlState, S5L8950X_MEMCTL) + +struct S5L8950XMemctlState { + SysBusDevice parent_obj; + MemoryRegion iomem; + uint32_t regs[S5L8950X_MEMCTL_NUM_REGS]; +}; + +static uint64_t s5l8950x_memctl_read(void *opaque, hwaddr offset, + unsigned size) +{ + S5L8950XMemctlState *s = opaque; + + if (offset == 0) { + uint32_t status = s->regs[0] | S5L8950X_MEMCTL_READY; + + /* + * The 0b101 command starts a training phase whose lane bits are + * active-low. iBEC clears the command before waiting for them to + * return to the completed state. + */ + if ((s->regs[0x14 / 4] & 5) == 5) { + status &= ~0x000003f0u; + } + + /* + * Register 0x08 bit 31 requests a controller state transition. The + * corresponding completion state is exposed in status bit 10 and is + * cleared again when iBEC drops the request. + */ + if (s->regs[0x08 / 4] & BIT(31)) { + status |= BIT(10); + } else { + status &= ~BIT(10); + } + return status; + } + + + /* + * The PHY command/status block starts at 0x34. Its status register at + * 0x44 reports both command-complete stages once the programmed timing + * values have been accepted. + */ + if (offset == 0x44) { + return s->regs[offset / sizeof(uint32_t)] | 0x00500000u; + } + return s->regs[offset / sizeof(uint32_t)]; +} + +static void s5l8950x_memctl_write(void *opaque, hwaddr offset, + uint64_t value, unsigned size) +{ + S5L8950XMemctlState *s = opaque; + + s->regs[offset / sizeof(uint32_t)] = value; +} + +static const MemoryRegionOps s5l8950x_memctl_ops = { + .read = s5l8950x_memctl_read, + .write = s5l8950x_memctl_write, + .endianness = DEVICE_LITTLE_ENDIAN, + .valid = { + .min_access_size = 4, + .max_access_size = 4, + }, +}; + +static void s5l8950x_memctl_reset(DeviceState *dev) +{ + S5L8950XMemctlState *s = S5L8950X_MEMCTL(dev); + + memset(s->regs, 0, sizeof(s->regs)); +} + +static void s5l8950x_memctl_init(Object *obj) +{ + S5L8950XMemctlState *s = S5L8950X_MEMCTL(obj); + SysBusDevice *sbd = SYS_BUS_DEVICE(obj); + + memory_region_init_io(&s->iomem, obj, &s5l8950x_memctl_ops, s, + TYPE_S5L8950X_MEMCTL, + S5L8950X_MEMCTL_REGION_SIZE); + sysbus_init_mmio(sbd, &s->iomem); +} + +static void s5l8950x_memctl_class_init(ObjectClass *klass, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + + device_class_set_legacy_reset(dc, s5l8950x_memctl_reset); +} + +static const TypeInfo s5l8950x_memctl_info = { + .name = TYPE_S5L8950X_MEMCTL, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(S5L8950XMemctlState), + .instance_init = s5l8950x_memctl_init, + .class_init = s5l8950x_memctl_class_init, +}; + +static void s5l8950x_memctl_register_types(void) +{ + type_register_static(&s5l8950x_memctl_info); +} + +type_init(s5l8950x_memctl_register_types) diff --git a/hw/misc/s5l8950x-pke.c b/hw/misc/s5l8950x-pke.c new file mode 100644 index 0000000..94d8d10 --- /dev/null +++ b/hw/misc/s5l8950x-pke.c @@ -0,0 +1,285 @@ +/* + * Apple A6 (S5L8950X) public-key accelerator. + * + * The A6 SecureROM uses this block as a modular-arithmetic coprocessor while + * validating the RSA-2048/RSA-1024 certificate chain embedded in Image3. + * Its software driver stages little-endian operands in the 2 KiB operand + * window and drives a fixed sequence of commands for the public exponent + * 65537. This functional model completes that sequence atomically when all + * operands have been staged; the remaining commands retain the computed + * result in operand slot 1, exactly where the ROM reads it back. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "hw/misc/s5l8950x-pke.h" +#include "hw/core/sysbus.h" +#include "qemu/log.h" +#include "qemu/module.h" + +#define A6_PKE_MMIO_SIZE 0x1000 +#define A6_PKE_OPERAND_BASE 0x0800 +#define A6_PKE_OPERAND_SIZE 0x0800 + +#define A6_PKE_CONFIG 0x0000 +#define A6_PKE_START 0x0008 +#define A6_PKE_COMMAND 0x000c +#define A6_PKE_STATUS 0x0010 +#define A6_PKE_CONTROL 0x0014 + +#define A6_PKE_RSA_2048_STAGE 0x01050003 +#define A6_PKE_RSA_1024_STAGE 0x010e0003 +#define A6_PKE_RSA_STAGE_START 9 +#define A6_PKE_MAX_BYTES 256 +#define A6_PKE_MAX_LIMBS (A6_PKE_MAX_BYTES / sizeof(uint32_t)) +#define A6_PKE_PUBLIC_EXPONENT 65537u + +struct S5L8950XPKEState { + SysBusDevice parent_obj; + + MemoryRegion iomem; + uint32_t config; + uint32_t start; + uint32_t command; + uint32_t status; + uint32_t control; + uint8_t operands[A6_PKE_OPERAND_SIZE]; + bool rsa_result_ready; +}; + +static int s5l8950x_pke_compare(const uint32_t *a, const uint32_t *b, + size_t limbs) +{ + for (size_t i = limbs; i-- > 0;) { + if (a[i] != b[i]) { + return a[i] > b[i] ? 1 : -1; + } + } + return 0; +} + +static void s5l8950x_pke_subtract(uint32_t *a, const uint32_t *b, + size_t limbs) +{ + uint64_t borrow = 0; + + for (size_t i = 0; i < limbs; i++) { + uint64_t subtrahend = (uint64_t)b[i] + borrow; + uint64_t minuend = a[i]; + + a[i] = minuend - subtrahend; + borrow = minuend < subtrahend; + } +} + +/* Both operands must be reduced. Their sum then needs at most one subtract. */ +static void s5l8950x_pke_add_mod(uint32_t *a, const uint32_t *b, + const uint32_t *modulus, size_t limbs) +{ + uint64_t carry = 0; + + for (size_t i = 0; i < limbs; i++) { + uint64_t sum = (uint64_t)a[i] + b[i] + carry; + + a[i] = sum; + carry = sum >> 32; + } + if (carry || s5l8950x_pke_compare(a, modulus, limbs) >= 0) { + s5l8950x_pke_subtract(a, modulus, limbs); + } +} + +static void s5l8950x_pke_mod_mul(uint32_t *out, const uint32_t *a, + const uint32_t *b, + const uint32_t *modulus, size_t limbs) +{ + uint32_t result[A6_PKE_MAX_LIMBS] = { 0 }; + uint32_t current[A6_PKE_MAX_LIMBS] = { 0 }; + + memcpy(current, a, limbs * sizeof(*a)); + while (s5l8950x_pke_compare(current, modulus, limbs) >= 0) { + s5l8950x_pke_subtract(current, modulus, limbs); + } + + for (size_t bit = 0; bit < limbs * 32; bit++) { + if (b[bit / 32] & BIT(bit % 32)) { + s5l8950x_pke_add_mod(result, current, modulus, limbs); + } + s5l8950x_pke_add_mod(current, current, modulus, limbs); + } + memcpy(out, result, limbs * sizeof(*out)); +} + +static bool s5l8950x_pke_rsa_public(S5L8950XPKEState *s) +{ + uint32_t modulus[A6_PKE_MAX_LIMBS] = { 0 }; + uint32_t base[A6_PKE_MAX_LIMBS] = { 0 }; + uint32_t result[A6_PKE_MAX_LIMBS] = { 0 }; + uint32_t squared[A6_PKE_MAX_LIMBS] = { 0 }; + size_t width = 64 * ((s->config & 3) + 1); + size_t limbs; + uint32_t exponent = A6_PKE_PUBLIC_EXPONENT; + + if (width > A6_PKE_MAX_BYTES || width * 2 > sizeof(s->operands)) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.pke: unsupported operand width %zu\n", + width); + return false; + } + limbs = width / sizeof(uint32_t); + for (size_t i = 0; i < limbs; i++) { + modulus[i] = ldl_le_p(s->operands + i * sizeof(uint32_t)); + base[i] = ldl_le_p(s->operands + width + + i * sizeof(uint32_t)); + } + if (!(modulus[0] & 1) || + !s5l8950x_pke_compare(modulus, result, limbs)) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.pke: invalid RSA modulus\n"); + return false; + } + + result[0] = 1; + while (exponent) { + if (exponent & 1) { + s5l8950x_pke_mod_mul(result, result, base, modulus, limbs); + } + exponent >>= 1; + if (exponent) { + s5l8950x_pke_mod_mul(squared, base, base, modulus, limbs); + memcpy(base, squared, limbs * sizeof(*base)); + } + } + for (size_t i = 0; i < limbs; i++) { + stl_le_p(s->operands + width + i * sizeof(uint32_t), result[i]); + } + s->rsa_result_ready = true; + qemu_log_mask(LOG_UNIMP, + "s5l8950x.pke: completed RSA-%zu public operation\n", + width * 8); + return true; +} + +static uint64_t s5l8950x_pke_read(void *opaque, hwaddr offset, + unsigned size) +{ + S5L8950XPKEState *s = opaque; + + if (offset >= A6_PKE_OPERAND_BASE && + offset <= A6_PKE_MMIO_SIZE - sizeof(uint32_t)) { + return ldl_le_p(s->operands + offset - A6_PKE_OPERAND_BASE); + } + switch (offset) { + case A6_PKE_CONFIG: + return s->config; + case A6_PKE_START: + return s->start; + case A6_PKE_COMMAND: + return s->command; + case A6_PKE_STATUS: + return s->status; + case A6_PKE_CONTROL: + return s->control; + default: + return 0; + } +} + +static void s5l8950x_pke_write(void *opaque, hwaddr offset, + uint64_t value, unsigned size) +{ + S5L8950XPKEState *s = opaque; + + if (offset >= A6_PKE_OPERAND_BASE && + offset <= A6_PKE_MMIO_SIZE - sizeof(uint32_t)) { + stl_le_p(s->operands + offset - A6_PKE_OPERAND_BASE, value); + return; + } + switch (offset) { + case A6_PKE_CONFIG: + s->config = value; + s->rsa_result_ready = false; + qemu_log_mask(LOG_UNIMP, + "s5l8950x.pke: config=0x%08" PRIx64 "\n", value); + break; + case A6_PKE_START: + s->start = value; + qemu_log_mask(LOG_UNIMP, + "s5l8950x.pke: command=0x%08x start=0x%08" PRIx64 + " config=0x%08x\n", + s->command, value, s->config); + if (!s->rsa_result_ready && value == A6_PKE_RSA_STAGE_START && + (s->command == A6_PKE_RSA_2048_STAGE || + s->command == A6_PKE_RSA_1024_STAGE)) { + s5l8950x_pke_rsa_public(s); + } + /* The ROM polls bit zero until the command has completed. */ + s->start &= ~1u; + break; + case A6_PKE_COMMAND: + s->command = value; + break; + case A6_PKE_STATUS: + s->status = value; + break; + case A6_PKE_CONTROL: + s->control = value; + break; + default: + break; + } +} + +static const MemoryRegionOps s5l8950x_pke_ops = { + .read = s5l8950x_pke_read, + .write = s5l8950x_pke_write, + .endianness = DEVICE_LITTLE_ENDIAN, + .valid.min_access_size = 4, + .valid.max_access_size = 4, +}; + +static void s5l8950x_pke_reset(DeviceState *dev) +{ + S5L8950XPKEState *s = S5L8950X_PKE(dev); + + s->config = 0; + s->start = 0; + s->command = 0; + s->status = 0; + s->control = 0; + s->rsa_result_ready = false; + memset(s->operands, 0, sizeof(s->operands)); +} + +static void s5l8950x_pke_init(Object *object) +{ + S5L8950XPKEState *s = S5L8950X_PKE(object); + SysBusDevice *sbd = SYS_BUS_DEVICE(object); + + memory_region_init_io(&s->iomem, object, &s5l8950x_pke_ops, s, + TYPE_S5L8950X_PKE, A6_PKE_MMIO_SIZE); + sysbus_init_mmio(sbd, &s->iomem); +} + +static void s5l8950x_pke_class_init(ObjectClass *klass, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + + device_class_set_legacy_reset(dc, s5l8950x_pke_reset); +} + +static const TypeInfo s5l8950x_pke_type_info = { + .name = TYPE_S5L8950X_PKE, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(S5L8950XPKEState), + .instance_init = s5l8950x_pke_init, + .class_init = s5l8950x_pke_class_init, +}; + +static void s5l8950x_pke_register_types(void) +{ + type_register_static(&s5l8950x_pke_type_info); +} + +type_init(s5l8950x_pke_register_types) diff --git a/hw/misc/s5l8950x-pmgr.c b/hw/misc/s5l8950x-pmgr.c index 0ae4cc7..97535b6 100644 --- a/hw/misc/s5l8950x-pmgr.c +++ b/hw/misc/s5l8950x-pmgr.c @@ -17,6 +17,7 @@ OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XPmgrState, S5L8950X_PMGR) #define PMGR_SPECIAL_DOMAIN_CTRL 0x100 #define PMGR_SPECIAL_DOMAIN_STATUS 0x2034 #define PMGR_DOMAIN_ACTIVE BIT(30) +#define PMGR_TRANSITION_DONE BIT(29) #define PMGR_PLL0_CTRL 0x60 #define PMGR_PLL_LOCKED BIT(29) @@ -57,7 +58,6 @@ static void s5l8950x_pmgr_write(void *opaque, hwaddr offset, if (offset == PMGR_PLL0_CTRL) { reg |= PMGR_PLL_LOCKED; } - s->regs[idx] = reg; /* Power-domain transitions also complete immediately. */ if (offset < PMGR_DOMAIN_CTRL_STRIDE * PMGR_DOMAIN_COUNT && @@ -65,14 +65,21 @@ static void s5l8950x_pmgr_write(void *opaque, hwaddr offset, uint32_t domain = offset / PMGR_DOMAIN_CTRL_STRIDE; uint32_t status = (PMGR_DOMAIN_STATUS_BASE / 4) + domain; + if (reg & BIT(31)) { + reg |= PMGR_TRANSITION_DONE; + } s->regs[status] = deposit32(s->regs[status], 30, 1, !!(reg & PMGR_DOMAIN_ACTIVE)); } else if (offset == PMGR_SPECIAL_DOMAIN_CTRL) { uint32_t status = PMGR_SPECIAL_DOMAIN_STATUS / 4; + if (reg & BIT(31)) { + reg |= PMGR_TRANSITION_DONE; + } s->regs[status] = deposit32(s->regs[status], 30, 1, !!(reg & PMGR_DOMAIN_ACTIVE)); } + s->regs[idx] = reg; } else { qemu_log_mask(LOG_UNIMP, "%s: Out of bounds write to offset 0x%" HWADDR_PRIx " with value 0x%" PRIx64 "\n", __func__, offset, val); diff --git a/hw/misc/s5l8950x-sha1.c b/hw/misc/s5l8950x-sha1.c new file mode 100644 index 0000000..2fe4b0f --- /dev/null +++ b/hw/misc/s5l8950x-sha1.c @@ -0,0 +1,223 @@ +/* + * Apple A6 (S5L8950X) SHA-1 accelerator used by the SecureROM. + * + * The ROM supplies already padded 64-byte SHA-1 blocks through the input + * window. QEMU's hash API expects the original message, so this model keeps + * the submitted blocks, validates/removes the standard SHA-1 padding, and + * exposes the resulting five digest words through the hardware registers. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "crypto/hash.h" +#include "hw/core/cpu.h" +#include "hw/misc/s5l8950x-sha1.h" +#include "hw/core/sysbus.h" +#include "qemu/log.h" +#include "qemu/module.h" +#include "qemu/units.h" + +#define A6_SHA1_MMIO_SIZE 0x100 +#define A6_SHA1_RESET 0x008 +#define A6_SHA1_COMMAND 0x00c +#define A6_SHA1_STATUS 0x010 +#define A6_SHA1_DIGEST_BASE 0x040 +#define A6_SHA1_DIGEST_SIZE QCRYPTO_HASH_DIGEST_LEN_SHA1 +#define A6_SHA1_INPUT_BASE 0x080 +#define A6_SHA1_BLOCK_SIZE 64 +#define A6_SHA1_MAX_MESSAGE (16 * MiB) + +struct S5L8950XSHA1State { + SysBusDevice parent_obj; + + MemoryRegion iomem; + uint32_t reset; + uint32_t command; + uint32_t status; + uint8_t input[A6_SHA1_BLOCK_SIZE]; + uint8_t digest[A6_SHA1_DIGEST_SIZE]; + GByteArray *padded; + bool digest_valid; +}; + +static bool s5l8950x_sha1_finalize(S5L8950XSHA1State *s) +{ + uint64_t bit_length; + uint64_t message_length; + uint8_t *digest = s->digest; + size_t digest_length = sizeof(s->digest); + + if (!s->padded->len || s->padded->len % A6_SHA1_BLOCK_SIZE || + s->padded->len < 9) { + return false; + } + bit_length = ldq_be_p(s->padded->data + s->padded->len - 8); + if (bit_length & 7) { + return false; + } + message_length = bit_length / 8; + if (message_length > A6_SHA1_MAX_MESSAGE || + message_length >= s->padded->len - 8 || + s->padded->data[message_length] != 0x80) { + return false; + } + for (size_t i = message_length + 1; i < s->padded->len - 8; i++) { + if (s->padded->data[i]) { + return false; + } + } + if (qcrypto_hash_bytes(QCRYPTO_HASH_ALGO_SHA1, s->padded->data, + message_length, &digest, &digest_length, + NULL) < 0 || digest_length != sizeof(s->digest)) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.sha1: SHA-1 calculation failed\n"); + return false; + } + s->digest_valid = true; + qemu_log_mask(LOG_UNIMP, + "s5l8950x.sha1: PC=0x%" VADDR_PRIx + " hashed %" PRIu64 " guest bytes\n", + current_cpu ? current_cpu->cc->get_pc(current_cpu) : 0, + message_length); + return true; +} + +static void s5l8950x_sha1_submit(S5L8950XSHA1State *s) +{ + if (s->padded->len > A6_SHA1_MAX_MESSAGE + 2 * A6_SHA1_BLOCK_SIZE) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.sha1: guest message is too large\n"); + return; + } + g_byte_array_append(s->padded, s->input, sizeof(s->input)); + s->digest_valid = false; +} + +static uint64_t s5l8950x_sha1_read(void *opaque, hwaddr offset, + unsigned size) +{ + S5L8950XSHA1State *s = opaque; + + if (offset >= A6_SHA1_INPUT_BASE && + offset <= A6_SHA1_MMIO_SIZE - sizeof(uint32_t)) { + return ldl_le_p(s->input + offset - A6_SHA1_INPUT_BASE); + } + if (offset >= A6_SHA1_DIGEST_BASE && + offset < A6_SHA1_DIGEST_BASE + A6_SHA1_DIGEST_SIZE) { + if (!s->digest_valid) { + s5l8950x_sha1_finalize(s); + } + return ldl_le_p(s->digest + offset - A6_SHA1_DIGEST_BASE); + } + switch (offset) { + case A6_SHA1_RESET: + return s->reset; + case A6_SHA1_COMMAND: + return s->command; + case A6_SHA1_STATUS: + return s->status; + default: + return 0; + } +} + +static void s5l8950x_sha1_write(void *opaque, hwaddr offset, + uint64_t value, unsigned size) +{ + S5L8950XSHA1State *s = opaque; + + if (offset >= A6_SHA1_INPUT_BASE && + offset <= A6_SHA1_MMIO_SIZE - sizeof(uint32_t)) { + stl_le_p(s->input + offset - A6_SHA1_INPUT_BASE, value); + return; + } + switch (offset) { + case A6_SHA1_RESET: + s->reset = value; + if (value & 0x10) { + g_byte_array_set_size(s->padded, 0); + memset(s->input, 0, sizeof(s->input)); + memset(s->digest, 0, sizeof(s->digest)); + s->digest_valid = false; + } + break; + case A6_SHA1_COMMAND: + s->command = value; + if (value & 1) { + s5l8950x_sha1_submit(s); + } + /* The SecureROM waits for bit zero to clear after each block. */ + s->command &= ~1u; + break; + case A6_SHA1_STATUS: + s->status = value; + break; + default: + break; + } +} + +static const MemoryRegionOps s5l8950x_sha1_ops = { + .read = s5l8950x_sha1_read, + .write = s5l8950x_sha1_write, + .endianness = DEVICE_LITTLE_ENDIAN, + .valid.min_access_size = 4, + .valid.max_access_size = 4, +}; + +static void s5l8950x_sha1_reset(DeviceState *dev) +{ + S5L8950XSHA1State *s = S5L8950X_SHA1(dev); + + s->reset = 0; + s->command = 0; + s->status = 0; + s->digest_valid = false; + memset(s->input, 0, sizeof(s->input)); + memset(s->digest, 0, sizeof(s->digest)); + if (s->padded) { + g_byte_array_set_size(s->padded, 0); + } +} + +static void s5l8950x_sha1_init(Object *object) +{ + S5L8950XSHA1State *s = S5L8950X_SHA1(object); + SysBusDevice *sbd = SYS_BUS_DEVICE(object); + + s->padded = g_byte_array_new(); + memory_region_init_io(&s->iomem, object, &s5l8950x_sha1_ops, s, + TYPE_S5L8950X_SHA1, A6_SHA1_MMIO_SIZE); + sysbus_init_mmio(sbd, &s->iomem); +} + +static void s5l8950x_sha1_finalize_instance(Object *object) +{ + S5L8950XSHA1State *s = S5L8950X_SHA1(object); + + g_clear_pointer(&s->padded, g_byte_array_unref); +} + +static void s5l8950x_sha1_class_init(ObjectClass *klass, const void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + + device_class_set_legacy_reset(dc, s5l8950x_sha1_reset); +} + +static const TypeInfo s5l8950x_sha1_type_info = { + .name = TYPE_S5L8950X_SHA1, + .parent = TYPE_SYS_BUS_DEVICE, + .instance_size = sizeof(S5L8950XSHA1State), + .instance_init = s5l8950x_sha1_init, + .instance_finalize = s5l8950x_sha1_finalize_instance, + .class_init = s5l8950x_sha1_class_init, +}; + +static void s5l8950x_sha1_register_types(void) +{ + type_register_static(&s5l8950x_sha1_type_info); +} + +type_init(s5l8950x_sha1_register_types) diff --git a/hw/usb/s5l8950x-usb-otg.c b/hw/usb/s5l8950x-usb-otg.c index 0249e66..858ac6e 100644 --- a/hw/usb/s5l8950x-usb-otg.c +++ b/hw/usb/s5l8950x-usb-otg.c @@ -7,7 +7,9 @@ #include "qemu/osdep.h" #include "qemu/log.h" +#include "qemu/timer.h" #include "hw/arm/s5l8950x.h" +#include "hw/core/cpu.h" #include "hw/core/irq.h" #include "hw/core/sysbus.h" #include "hw/core/qdev-properties.h" @@ -15,8 +17,11 @@ #include "chardev/char-fe.h" #include "qapi/error.h" #include "qom/object.h" +#include "exec/tb-flush.h" +#include "exec/translation-block.h" #include "system/address-spaces.h" #include "system/dma.h" +#include "target/arm/cpu.h" #include "../../windows/include/qemu_a6_usb_protocol.h" @@ -37,6 +42,8 @@ #define USB_OTG_GINT_ENUMDONE BIT(13) #define USB_OTG_GINT_IEPINT BIT(18) #define USB_OTG_GINT_OEPINT BIT(19) +#define USB_OTG_DCTL 0x804 +#define USB_OTG_DCTL_SFTDISCON BIT(1) #define USB_OTG_DIEPMSK 0x810 #define USB_OTG_DOEPMSK 0x814 #define USB_OTG_DAINT 0x818 @@ -55,14 +62,35 @@ #define USB_OTG_EPCTL_STALL BIT(21) #define USB_OTG_EPINT_SETUP BIT(3) #define USB_OTG_EPINT_XFERCOMPL BIT(0) +#define USB_OTG_EPTSIZ_XFERSIZE_MASK 0x0007ffffu +#define USB_OTG_EPTSIZ_PKTCNT_MASK 0x1ff80000u #define USB_DFU_TRANSFER_SIZE 0x800 #define USB_DFU_STATUS_SIZE 6 #define USB_DFU_STATE_IDLE 2 #define USB_DFU_STATE_DNLOAD_IDLE 5 +#define USB_DFU_STATE_MANIFEST_SYNC 6 +#define USB_DFU_STATE_MANIFEST 7 +#define USB_DFU_STATE_WAIT_RESET 8 #define USB_DFU_STATE_ERROR 10 #define USB_DFU_MAX_STATUS_POLLS 20 +#define A6_SRAM_BASE 0x10000000u +#define A6_SRAM_SIZE 0x00080000u +#define A6_DRAM_BASE 0x80000000u +#define A6_IBEC_BASE 0xbff00000u +#define A6_AIC_MASK_CLR 0x3f204180u +#define A6_USB_IRQ 11 +#define A6_DFU_CAPTURE_MAX (16u * 1024u * 1024u) +#define IMG3_MAGIC 0x496d6733u +#define IMG3_TAG_TYPE 0x54595045u +#define IMG3_TAG_DATA 0x44415441u +#define IMG3_TYPE_IBSS 0x69627373u +#define IMG3_TYPE_IBEC 0x69626563u +#define IMG3_HEADER_SIZE 20u +#define IMG3_ELEMENT_HEADER_SIZE 12u +#define IMG3_SEARCH_SIZE 0x00010000u + OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XUSBOTGState, S5L8950X_USB_OTG) typedef enum S5L8950XUSBHostStage { @@ -121,6 +149,8 @@ struct S5L8950XUSBOTGState { uint8_t config_descriptor[256]; uint32_t config_descriptor_len; uint32_t config_total_length; + uint32_t enumeration_generation; + bool current_device_is_dfu; S5L8950XUSBHostStage host_stage; char *dfu_image_path; uint8_t *dfu_image; @@ -139,6 +169,7 @@ struct S5L8950XUSBOTGState { uint8_t dfu_state; uint8_t dfu_status_polls; uint8_t dfu_finish_statuses; + bool dfu_injection_complete; CharFrontend bridge_chr; bool bridge_open; @@ -150,6 +181,16 @@ struct S5L8950XUSBOTGState { uint32_t bridge_out_offset; uint8_t bridge_in_data[QA6_USB_MAX_PAYLOAD]; uint32_t bridge_in_len; + uint8_t bridge_dfu_status; + uint8_t bridge_dfu_state; + GByteArray *bridge_dfu_image; + uint16_t bridge_dfu_next_block; + bool bridge_dfu_capture_valid; + QEMUTimer *recovery_reset_timer; + QEMUTimer *bridge_bulk_timer; + bool recovery_reset_wait_for_halt; + bool force_debug_uarts; + bool native_img3_handoff; }; static void s5l8950x_usb_otg_update_ep_irqs(S5L8950XUSBOTGState *s) @@ -192,6 +233,53 @@ static void s5l8950x_usb_otg_update_irq(S5L8950XUSBOTGState *s) qemu_set_irq(s->irq, level); } +static void s5l8950x_usb_otg_recovery_reset(void *opaque) +{ + S5L8950XUSBOTGState *s = opaque; + CPUState *cs = first_cpu; + int ep; + + if (s->host_stage != USB_HOST_WAIT_DEVICE_SETUP || + s->device_descriptor_len) { + return; + } + + /* + * A real host cannot reset a device until its reconnect has reached the + * bus. Waiting for the bootloader's WFE idle point models that ordering + * and, importantly, prevents the reset IRQ from preempting iBSS before it + * has installed the USB handler. SecureROM's first enumeration does not + * use this delayed path. + */ + if (s->recovery_reset_wait_for_halt && cs && !cs->halted) { + timer_mod(s->recovery_reset_timer, + qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + 1); + return; + } + + /* A USB port reset disables active endpoints but preserves core setup. */ + for (ep = 0; ep < USB_OTG_EP_COUNT; ep++) { + s->regs[(USB_OTG_DIEPCTL0 + ep * USB_OTG_EP_STRIDE) / 4] &= + ~USB_OTG_EPCTL_EPENA; + s->regs[(USB_OTG_DOEPCTL0 + ep * USB_OTG_EP_STRIDE) / 4] &= + ~USB_OTG_EPCTL_EPENA; + s->regs[(USB_OTG_DIEPINT0 + ep * USB_OTG_EP_STRIDE) / 4] = 0; + s->regs[(USB_OTG_DOEPINT0 + ep * USB_OTG_EP_STRIDE) / 4] = 0; + } + s->regs[USB_OTG_DAINT / 4] = 0; + + /* The last iBSS EP0 completion can leave AIC's auto-mask asserted. */ + address_space_stl(&address_space_memory, A6_AIC_MASK_CLR, + BIT(A6_USB_IRQ), MEMTXATTRS_UNSPECIFIED, NULL); + s->regs[USB_OTG_GINTSTS / 4] |= USB_OTG_GINT_USBRST; + s->usb_reset_sent = true; + s->recovery_reset_wait_for_halt = false; + s5l8950x_usb_otg_update_irq(s); + qemu_log_mask(LOG_UNIMP, + "s5l8950x.usb-host: delayed bootloader port reset " + "delivered\n"); +} + static bool s5l8950x_usb_otg_is_ep_int(hwaddr offset, hwaddr base) { return offset >= base && @@ -290,7 +378,20 @@ static void s5l8950x_usb_bridge_write_response( response.magic = QA6_USB_MAGIC; response.version = QA6_USB_PROTOCOL_VERSION; - response.type = QA6_USB_MESSAGE_RESPONSE; + switch (request->type) { + case QA6_USB_MESSAGE_INFO_REQUEST: + response.type = QA6_USB_MESSAGE_INFO_RESPONSE; + break; + case QA6_USB_MESSAGE_BULK_REQUEST: + response.type = QA6_USB_MESSAGE_BULK_RESPONSE; + break; + case QA6_USB_MESSAGE_RESET_REQUEST: + response.type = QA6_USB_MESSAGE_RESET_RESPONSE; + break; + default: + response.type = QA6_USB_MESSAGE_RESPONSE; + break; + } response.request_id = request->request_id; response.status = status; response.payload_length = payload_length; @@ -312,28 +413,615 @@ static void s5l8950x_usb_bridge_write_response( } } +static void s5l8950x_usb_bridge_write_info_response( + S5L8950XUSBOTGState *s, const Qa6UsbFrameHeader *request) +{ + Qa6UsbDeviceInfo info = { 0 }; + uint32_t input_offset; + uint32_t output_offset = 0; + + if (!s->enumeration_generation || s->device_descriptor_len < 12 || + s->serial_descriptor_len < 2) { + s5l8950x_usb_bridge_write_response( + s, request, QA6_USB_STATUS_DISCONNECTED, 0, NULL, 0); + return; + } + + info.generation = s->enumeration_generation; + info.vendor_id = s->device_descriptor[8] | + (s->device_descriptor[9] << 8); + info.product_id = s->device_descriptor[10] | + (s->device_descriptor[11] << 8); + info.device_class = s->device_descriptor[4]; + info.device_subclass = s->device_descriptor[5]; + info.device_protocol = s->device_descriptor[6]; + info.is_dfu = s->current_device_is_dfu; + + /* libirecovery recognizes Apple's public DFU PID. */ + if (info.is_dfu && info.vendor_id == 0x05ac) { + info.product_id = 0x1227; + } + + for (input_offset = 2; + input_offset + 1 < s->serial_descriptor_len && + output_offset + 1 < sizeof(info.serial); + input_offset += 2) { + info.serial[output_offset++] = + s->serial_descriptor[input_offset + 1] ? '?' : + s->serial_descriptor[input_offset]; + } + + s5l8950x_usb_bridge_write_response( + s, request, QA6_USB_STATUS_SUCCESS, sizeof(info), + (const uint8_t *)&info, sizeof(info)); +} + static void s5l8950x_usb_bridge_finish(S5L8950XUSBOTGState *s, int status) { bool direction_in = (s->bridge_request.setup[0] & 0x80) != 0; uint32_t transfer_length = direction_in ? s->bridge_in_len : s->bridge_out_offset; + timer_del(s->bridge_bulk_timer); + + if (status == QA6_USB_STATUS_SUCCESS && !direction_in && + s->bridge_request.setup[0] == 0x21 && + s->bridge_request.setup[1] == 1 && transfer_length) { + uint16_t block = s->bridge_request.setup[2] | + (s->bridge_request.setup[3] << 8); + + if (block == 0) { + g_byte_array_set_size(s->bridge_dfu_image, 0); + s->bridge_dfu_next_block = 0; + s->bridge_dfu_capture_valid = true; + } + if (!s->bridge_dfu_capture_valid || + block != s->bridge_dfu_next_block || + s->bridge_dfu_image->len > + A6_DFU_CAPTURE_MAX - transfer_length) { + s->bridge_dfu_capture_valid = false; + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.usb-bridge: invalid DFU block sequence " + "(%u, expected %u)\n", + block, s->bridge_dfu_next_block); + } else { + g_byte_array_append(s->bridge_dfu_image, s->bridge_out_data, + transfer_length); + s->bridge_dfu_next_block++; + } + } + s5l8950x_usb_bridge_write_response( s, &s->bridge_request, status, transfer_length, direction_in ? s->bridge_in_data : NULL, direction_in && status == QA6_USB_STATUS_SUCCESS ? s->bridge_in_len : 0); + if (status == QA6_USB_STATUS_SUCCESS && direction_in && + s->bridge_request.setup[0] == 0xa1 && + s->bridge_request.setup[1] == 3 && s->bridge_in_len == 6) { + s->bridge_dfu_status = s->bridge_in_data[0]; + s->bridge_dfu_state = s->bridge_in_data[4]; + qemu_log_mask(LOG_UNIMP, + "s5l8950x.usb-bridge: DFU GETSTATUS status=%u " + "state=%u poll=%u ms\n", + s->bridge_in_data[0], s->bridge_in_data[4], + s->bridge_in_data[1] | + (s->bridge_in_data[2] << 8) | + (s->bridge_in_data[3] << 16)); + } + qemu_log_mask(LOG_UNIMP, - "s5l8950x.usb-bridge: EP0 request %u complete " + "s5l8950x.usb-bridge: %s request %u complete " "(status=%d, bytes=%u)\n", - s->bridge_request.request_id, status, transfer_length); + s->bridge_request.type == QA6_USB_MESSAGE_BULK_REQUEST ? + "bulk" : "EP0", s->bridge_request.request_id, status, + transfer_length); s->bridge_request_active = false; s->bridge_out_offset = 0; s->bridge_in_len = 0; s->host_stage = USB_HOST_DFU_READY; } +static void s5l8950x_usb_bridge_bulk_timeout(void *opaque) +{ + S5L8950XUSBOTGState *s = opaque; + + if (!s->bridge_request_active || + s->bridge_request.type != QA6_USB_MESSAGE_BULK_REQUEST || + !(s->bridge_request.setup[0] & 0x80)) { + return; + } + + qemu_log_mask(LOG_UNIMP, + "s5l8950x.usb-bridge: bulk IN timeout, returning %u " + "bytes\n", s->bridge_in_len); + s5l8950x_usb_bridge_finish(s, QA6_USB_STATUS_SUCCESS); +} + +static void s5l8950x_usb_bridge_try_bulk(S5L8950XUSBOTGState *s) +{ + bool direction_in; + unsigned ep; + hwaddr ctl_offset; + hwaddr int_offset; + hwaddr size_offset; + hwaddr dma_offset; + hwaddr dma; + uint32_t capacity; + uint32_t remaining; + uint32_t length; + + if (!s->bridge_request_active || + s->bridge_request.type != QA6_USB_MESSAGE_BULK_REQUEST) { + return; + } + + direction_in = (s->bridge_request.setup[0] & 0x80) != 0; + ep = s->bridge_request.setup[0] & 0x7f; + if (!ep || ep >= USB_OTG_EP_COUNT) { + s5l8950x_usb_bridge_finish(s, QA6_USB_STATUS_PROTOCOL); + return; + } + + if (direction_in) { + ctl_offset = USB_OTG_DIEPCTL0 + ep * USB_OTG_EP_STRIDE; + int_offset = USB_OTG_DIEPINT0 + ep * USB_OTG_EP_STRIDE; + size_offset = USB_OTG_DIEPTSIZ0 + ep * USB_OTG_EP_STRIDE; + dma_offset = USB_OTG_DIEPDMA0 + ep * USB_OTG_EP_STRIDE; + } else { + ctl_offset = USB_OTG_DOEPCTL0 + ep * USB_OTG_EP_STRIDE; + int_offset = USB_OTG_DOEPINT0 + ep * USB_OTG_EP_STRIDE; + size_offset = USB_OTG_DOEPTSIZ0 + ep * USB_OTG_EP_STRIDE; + dma_offset = USB_OTG_DOEPDMA0 + ep * USB_OTG_EP_STRIDE; + } + + if (!(s->regs[ctl_offset / 4] & USB_OTG_EPCTL_EPENA)) { + return; + } + + capacity = s->regs[size_offset / 4] & + USB_OTG_EPTSIZ_XFERSIZE_MASK; + dma = s->regs[dma_offset / 4]; + if (direction_in) { + remaining = s->bridge_request.transfer_length - s->bridge_in_len; + length = MIN(capacity, remaining); + if (length && + dma_memory_read(&address_space_memory, dma, + s->bridge_in_data + s->bridge_in_len, length, + MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.usb-bridge: bulk IN%u DMA read failed " + "at 0x%08" HWADDR_PRIx "\n", ep, dma); + s5l8950x_usb_bridge_finish( + s, QA6_USB_STATUS_DISCONNECTED); + return; + } + s->bridge_in_len += length; + } else { + remaining = s->bridge_request.payload_length - + s->bridge_out_offset; + length = MIN(capacity, remaining); + if (length && + dma_memory_write(&address_space_memory, dma, + s->bridge_out_data + s->bridge_out_offset, + length, + MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.usb-bridge: bulk OUT%u DMA write failed " + "at 0x%08" HWADDR_PRIx "\n", ep, dma); + s5l8950x_usb_bridge_finish( + s, QA6_USB_STATUS_DISCONNECTED); + return; + } + s->bridge_out_offset += length; + } + + s->regs[dma_offset / 4] = dma + length; + s->regs[size_offset / 4] &= + ~(USB_OTG_EPTSIZ_XFERSIZE_MASK | USB_OTG_EPTSIZ_PKTCNT_MASK); + if (!direction_in && capacity > length) { + s->regs[size_offset / 4] |= capacity - length; + } + s->regs[ctl_offset / 4] &= ~USB_OTG_EPCTL_EPENA; + s->regs[int_offset / 4] |= USB_OTG_EPINT_XFERCOMPL; + + qemu_log_mask(LOG_UNIMP, + "s5l8950x.usb-bridge: bulk %s EP%u DMA=0x%08" + HWADDR_PRIx " bytes=%u\n", + direction_in ? "IN" : "OUT", ep, dma, length); + + if (direction_in || + s->bridge_out_offset >= s->bridge_request.payload_length) { + s5l8950x_usb_bridge_finish(s, QA6_USB_STATUS_SUCCESS); + } + s5l8950x_usb_otg_update_irq(s); +} + +static void s5l8950x_usb_otg_rearm(S5L8950XUSBOTGState *s, bool cold); + +typedef struct S5L8950XBootloaderHandoff { + S5L8950XUSBOTGState *usb; + const uint8_t *payload; + uint32_t data_length; + uint32_t image_type; + hwaddr load_base; + bool success; +} S5L8950XBootloaderHandoff; + +/* + * RELEASE iBSS does not read NVRAM, and iBEC only reads debug-uarts after its + * early platform initialization. On research devices the equivalent early + * setting is 3, which enables both UART directions. Locate iBoot's small + * debug_enable_uarts() helper by its stable Thumb body and replace only the + * ORRS with MOVS r0, #3. The authenticated IMG3 remains untouched until the + * real bootloader has accepted it; this is an optional emulator-side debug + * aid applied to the copied executable image immediately before handoff. + */ +static bool s5l8950x_usb_enable_bootloader_uarts( + const S5L8950XBootloaderHandoff *handoff) +{ + const uint8_t *image = handoff->payload; + static const uint16_t helper_tail[] = { + 0x680a, /* ldr r2, [r1] */ + 0x4310, /* orrs r0, r2 */ + 0x6008, /* str r0, [r1] */ + 0x4770, /* bx lr */ + }; + static const uint8_t movs_r0_3[] = { 0x03, 0x20 }; + uint32_t offset; + + for (offset = 0; offset + 10 <= handoff->data_length; offset += 2) { + uint16_t ldr_literal = lduw_le_p(image + offset); + unsigned int i; + + /* Thumb LDR (literal), with Rt == r1. */ + if ((ldr_literal & 0xff00) != 0x4900) { + continue; + } + for (i = 0; i < ARRAY_SIZE(helper_tail); i++) { + if (lduw_le_p(image + offset + 2 + i * 2) != helper_tail[i]) { + break; + } + } + if (i != ARRAY_SIZE(helper_tail)) { + continue; + } + if (address_space_write_rom(&address_space_memory, + handoff->load_base + offset + 4, + MEMTXATTRS_UNSPECIFIED, movs_r0_3, + sizeof(movs_r0_3)) != MEMTX_OK) { + return false; + } + qemu_log_mask(LOG_UNIMP, + "s5l8950x.uart: forced debug-uarts=3 for %s at " + "0x%08" HWADDR_PRIx "\n", + handoff->image_type == IMG3_TYPE_IBSS ? "iBSS" : + "iBEC", + handoff->load_base + offset + 4); + return true; + } + + return false; +} + +static void s5l8950x_usb_bridge_handoff_on_cpu(CPUState *cpu, + run_on_cpu_data data) +{ + S5L8950XBootloaderHandoff *handoff = data.host_ptr; + S5L8950XUSBOTGState *s = handoff->usb; + + if (address_space_write_rom(&address_space_memory, handoff->load_base, + MEMTXATTRS_UNSPECIFIED, handoff->payload, + handoff->data_length) != MEMTX_OK) { + return; + } + if (s->force_debug_uarts && + !s5l8950x_usb_enable_bootloader_uarts(handoff)) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.uart: debug_enable_uarts helper not found " + "in authenticated bootloader\n"); + } + + /* + * iBSS and iBEC occupy the same SRAM window. This callback runs at a + * vCPU safe point, so no old translated block can execute concurrently + * with the replacement and reset below. + */ + { + RCU_READ_LOCK_GUARD(); + hwaddr xlat; + hwaddr length = handoff->data_length; + MemoryRegion *mr = address_space_translate( + &address_space_memory, handoff->load_base, &xlat, &length, false, + MEMTXATTRS_UNSPECIFIED); + ram_addr_t ram_addr = memory_region_get_ram_addr(mr); + + if (ram_addr != RAM_ADDR_INVALID) { + tb_invalidate_phys_range(cpu, ram_addr + xlat, + ram_addr + xlat + length - 1); + tcg_flush_jmp_cache(cpu); + qemu_log_mask(LOG_UNIMP, + "s5l8950x.usb-bridge: invalidated code RAM=" + RAM_ADDR_FMT " offset=0x%" HWADDR_PRIx + " length=%" HWADDR_PRIu "\n", + ram_addr, xlat, length); + } + } + + if (handoff->image_type == IMG3_TYPE_IBSS) { + s5l8950x_usb_otg_rearm(s, false); + s->recovery_reset_wait_for_halt = true; + timer_mod(s->recovery_reset_timer, + qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + 100); + qemu_log_mask(LOG_UNIMP, + "s5l8950x.usb-host: SecureROM disconnected; " + "waiting for iBSS DFU reinitialization\n"); + } else { + /* + * Invalidate iBSS's DFU identity immediately, but preserve the DWC2 + * register image inherited by iBEC. iBEC relocates before resetting + * the core, so its delayed host reset must not race that relocation. + */ + s->usb_reset_sent = false; + s->enum_done_sent = false; + s->setup_sent = false; + s->descriptor_received = false; + s->device_descriptor_len = 0; + s->serial_descriptor_len = 0; + s->config_descriptor_len = 0; + s->config_total_length = 0; + s->current_device_is_dfu = false; + s->host_stage = USB_HOST_WAIT_DEVICE_SETUP; + s->bridge_dfu_status = 0; + s->bridge_dfu_state = 0; + s->recovery_reset_wait_for_halt = false; + memset(s->device_descriptor, 0, sizeof(s->device_descriptor)); + memset(s->serial_descriptor, 0, sizeof(s->serial_descriptor)); + memset(s->config_descriptor, 0, sizeof(s->config_descriptor)); + /* + * iBEC resets and reconnects the DWC core after relocation. Do not + * inject a port reset merely because control was transferred: the + * DCTL soft-reconnect path below is the point at which a real host + * can observe the device and arm the delayed reset. + */ + timer_del(s->recovery_reset_timer); + qemu_log_mask(LOG_UNIMP, + "s5l8950x.usb-host: iBSS disconnected; waiting for " + "iBEC Recovery reinitialization\n"); + } + + cpu_reset(cpu); + ARM_CPU(cpu)->env.regs[0] = 1; + cpu->halted = 0; + cpu_set_pc(cpu, handoff->load_base); + cpu_exit(cpu); + handoff->success = true; +} + +static bool s5l8950x_usb_bridge_handoff_captured( + S5L8950XUSBOTGState *s) +{ + CPUState *cpu = first_cpu; + const uint8_t *image = s->bridge_dfu_image->data; + size_t image_length = s->bridge_dfu_image->len; + size_t img3_offset; + uint32_t full_size = 0; + uint32_t offset; + uint32_t data_offset = 0; + uint32_t data_length = 0; + uint32_t image_type = 0; + hwaddr load_base; + g_autofree uint8_t *payload = NULL; + S5L8950XBootloaderHandoff handoff = { 0 }; + + if (!cpu || !s->bridge_dfu_capture_valid) { + return false; + } + + /* A personalized bootloader is the TSS ticket followed by its IMG3. */ + for (img3_offset = 0; + img3_offset + IMG3_HEADER_SIZE <= image_length; + img3_offset += 4) { + if (ldl_le_p(image + img3_offset) == IMG3_MAGIC) { + uint32_t candidate_size = ldl_le_p(image + img3_offset + 4); + + if (candidate_size >= IMG3_HEADER_SIZE && + candidate_size <= image_length - img3_offset) { + full_size = candidate_size; + break; + } + } + } + if (!full_size) { + return false; + } + + offset = IMG3_HEADER_SIZE; + while (offset <= full_size - IMG3_ELEMENT_HEADER_SIZE) { + const uint8_t *element = image + img3_offset + offset; + uint32_t tag = ldl_le_p(element); + uint32_t total_length = ldl_le_p(element + 4); + uint32_t element_data_length = ldl_le_p(element + 8); + + if (total_length < IMG3_ELEMENT_HEADER_SIZE || + total_length > full_size - offset || + element_data_length > total_length - IMG3_ELEMENT_HEADER_SIZE) { + return false; + } + if (tag == IMG3_TAG_TYPE && element_data_length >= 4) { + image_type = ldl_le_p(element + IMG3_ELEMENT_HEADER_SIZE); + } else if (tag == IMG3_TAG_DATA) { + data_offset = offset + IMG3_ELEMENT_HEADER_SIZE; + data_length = element_data_length; + } + offset += total_length; + } + + if ((image_type != IMG3_TYPE_IBSS && image_type != IMG3_TYPE_IBEC) || + !data_offset || !data_length || + data_offset > full_size || data_length > full_size - data_offset || + data_length > A6_SRAM_SIZE) { + return false; + } + + load_base = image_type == IMG3_TYPE_IBSS ? A6_SRAM_BASE : A6_IBEC_BASE; + payload = g_memdup2(image + img3_offset + data_offset, data_length); + handoff.usb = s; + handoff.payload = payload; + handoff.data_length = data_length; + handoff.image_type = image_type; + handoff.load_base = load_base; + run_on_cpu(cpu, s5l8950x_usb_bridge_handoff_on_cpu, + RUN_ON_CPU_HOST_PTR(&handoff)); + if (!handoff.success) { + return false; + } + + qemu_log_mask(LOG_UNIMP, + "s5l8950x.usb-bridge: authenticated %s handoff; " + "DATA=%u bytes DFU_IMG3=0x%zx PC=0x%08" HWADDR_PRIx + "\n", + image_type == IMG3_TYPE_IBSS ? "iBSS" : "iBEC", + data_length, img3_offset, load_base); + s->bridge_dfu_capture_valid = false; + g_byte_array_set_size(s->bridge_dfu_image, 0); + return true; +} + +/* + * The real host-side reset completes SecureROM's verified IMG3 handoff. + * Our minimal DWC2 model has no physical port-reset object, so its USBRST + * interrupt takes the generic re-enumeration path instead. Preserve all ROM + * parsing and signature checks, then reproduce only the successful final + * hardware handoff once the ROM itself reports DFU-MANIFEST-WAIT-RESET. + */ +static bool s5l8950x_usb_bridge_handoff_bootloader( + S5L8950XUSBOTGState *s) +{ + CPUState *cpu = first_cpu; + uint8_t img3_header[IMG3_HEADER_SIZE]; + uint8_t element_header[IMG3_ELEMENT_HEADER_SIZE]; + g_autofree uint8_t *payload = NULL; + uint32_t full_size; + uint32_t offset = IMG3_HEADER_SIZE; + uint32_t data_offset = 0; + uint32_t data_length = 0; + uint32_t image_type = 0; + hwaddr image_base = 0; + hwaddr load_base = 0; + S5L8950XBootloaderHandoff handoff = { 0 }; + static const hwaddr load_bases[] = { A6_SRAM_BASE, A6_DRAM_BASE }; + unsigned int region; + + if (!cpu) { + return false; + } + for (region = 0; region < ARRAY_SIZE(load_bases); region++) { + uint32_t search_offset; + + for (search_offset = 0; + search_offset <= IMG3_SEARCH_SIZE - IMG3_HEADER_SIZE; + search_offset += 4) { + hwaddr candidate = load_bases[region] + search_offset; + + if (dma_memory_read(&address_space_memory, candidate, img3_header, + sizeof(img3_header), + MEMTXATTRS_UNSPECIFIED) == MEMTX_OK && + ldl_le_p(img3_header) == IMG3_MAGIC) { + uint32_t candidate_size = ldl_le_p(img3_header + 4); + + if (candidate_size >= IMG3_HEADER_SIZE && + candidate_size <= A6_SRAM_SIZE - search_offset) { + image_base = candidate; + load_base = load_bases[region]; + break; + } + } + } + if (image_base) { + break; + } + } + if (!image_base) { + return false; + } + + full_size = ldl_le_p(img3_header + 4); + if (full_size < IMG3_HEADER_SIZE || full_size > A6_SRAM_SIZE) { + return false; + } + + while (offset <= full_size - IMG3_ELEMENT_HEADER_SIZE) { + uint32_t tag; + uint32_t total_length; + uint32_t element_data_length; + + if (dma_memory_read(&address_space_memory, image_base + offset, + element_header, sizeof(element_header), + MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { + return false; + } + tag = ldl_le_p(element_header); + total_length = ldl_le_p(element_header + 4); + element_data_length = ldl_le_p(element_header + 8); + if (total_length < IMG3_ELEMENT_HEADER_SIZE || + total_length > full_size - offset || + element_data_length > total_length - IMG3_ELEMENT_HEADER_SIZE) { + return false; + } + + if (tag == IMG3_TAG_TYPE && element_data_length >= 4) { + uint8_t type_data[4]; + + if (dma_memory_read(&address_space_memory, + image_base + offset + + IMG3_ELEMENT_HEADER_SIZE, + type_data, sizeof(type_data), + MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { + return false; + } + image_type = ldl_le_p(type_data); + } else if (tag == IMG3_TAG_DATA) { + data_offset = offset + IMG3_ELEMENT_HEADER_SIZE; + data_length = element_data_length; + } + offset += total_length; + } + + if ((image_type != IMG3_TYPE_IBSS && image_type != IMG3_TYPE_IBEC) || + !data_offset || !data_length || + data_offset > full_size || data_length > full_size - data_offset || + data_length > A6_SRAM_SIZE) { + return false; + } + + payload = g_malloc(data_length); + if (dma_memory_read(&address_space_memory, image_base + data_offset, + payload, data_length, + MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) { + return false; + } + + handoff.usb = s; + handoff.payload = payload; + handoff.data_length = data_length; + handoff.image_type = image_type; + handoff.load_base = load_base; + run_on_cpu(cpu, s5l8950x_usb_bridge_handoff_on_cpu, + RUN_ON_CPU_HOST_PTR(&handoff)); + if (!handoff.success) { + return false; + } + qemu_log_mask(LOG_UNIMP, + "s5l8950x.usb-bridge: authenticated %s handoff; " + "DATA=%u bytes IMG3=0x%08" HWADDR_PRIx + " PC=0x%08" HWADDR_PRIx "\n", + image_type == IMG3_TYPE_IBSS ? "iBSS" : "iBEC", + data_length, image_base, load_base); + return true; +} + static void s5l8950x_usb_bridge_start_request(S5L8950XUSBOTGState *s) { bool direction_in; @@ -355,6 +1043,14 @@ static void s5l8950x_usb_bridge_start_request(S5L8950XUSBOTGState *s) next_stage = USB_HOST_BRIDGE_WAIT_DATA_OUT; } + /* + * USB control endpoints recover from a halt when the host delivers a new + * SETUP packet. Without this, one unsupported iBoot command leaves the + * emulated EP0 STALL bit set and poisons every later irecovery request. + */ + s->regs[USB_OTG_DIEPCTL0 / 4] &= ~USB_OTG_EPCTL_STALL; + s->regs[USB_OTG_DOEPCTL0 / 4] &= ~USB_OTG_EPCTL_STALL; + s5l8950x_usb_otg_send_setup(s, s->bridge_request.setup, "external EP0 request", next_stage); } @@ -384,7 +1080,10 @@ static void s5l8950x_usb_bridge_complete_data_out( } s->bridge_out_offset += length; - s->regs[USB_OTG_DOEPTSIZ0 / 4] &= ~0x7f; + /* DWC2 leaves the unreceived byte count in XFRSIZ on a short packet. */ + s->regs[USB_OTG_DOEPTSIZ0 / 4] = + (s->regs[USB_OTG_DOEPTSIZ0 / 4] & ~0x7f) | + (capacity - length); s->regs[USB_OTG_DOEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[USB_OTG_DOEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; if (s->bridge_out_offset >= s->bridge_request.payload_length) { @@ -456,18 +1155,120 @@ static void s5l8950x_usb_bridge_process_frame(S5L8950XUSBOTGState *s) if (request.magic != QA6_USB_MAGIC || request.version != QA6_USB_PROTOCOL_VERSION || - request.type != QA6_USB_MESSAGE_REQUEST || request.payload_length > QA6_USB_MAX_PAYLOAD || - expected_payload != s->bridge_rx_len || + expected_payload != s->bridge_rx_len) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.usb-bridge: invalid frame header\n"); + return; + } + + if (request.type == QA6_USB_MESSAGE_INFO_REQUEST) { + if (request.payload_length || request.transfer_length) { + s5l8950x_usb_bridge_write_response( + s, &request, QA6_USB_STATUS_PROTOCOL, 0, NULL, 0); + } else { + s5l8950x_usb_bridge_write_info_response(s, &request); + } + return; + } + + if (request.type == QA6_USB_MESSAGE_RESET_REQUEST) { + if (request.payload_length || request.transfer_length) { + s5l8950x_usb_bridge_write_response( + s, &request, QA6_USB_STATUS_PROTOCOL, 0, NULL, 0); + return; + } + + if (s->bridge_dfu_status == 0 && + s->bridge_dfu_state == USB_DFU_STATE_WAIT_RESET) { + if (s->native_img3_handoff) { + s5l8950x_usb_bridge_write_response( + s, &request, QA6_USB_STATUS_SUCCESS, 0, NULL, 0); + s->regs[USB_OTG_GINTSTS / 4] |= USB_OTG_GINT_USBRST; + s->usb_reset_sent = true; + s5l8950x_usb_otg_update_irq(s); + qemu_log_mask(LOG_UNIMP, + "s5l8950x.usb-bridge: native DFU " + "manifestation reset delivered to guest\n"); + return; + } + if (!s5l8950x_usb_bridge_handoff_captured(s) && + !s5l8950x_usb_bridge_handoff_bootloader(s)) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.usb-bridge: authenticated bootloader " + "handoff failed\n"); + s5l8950x_usb_bridge_write_response( + s, &request, QA6_USB_STATUS_PROTOCOL, 0, NULL, 0); + return; + } + s5l8950x_usb_bridge_write_response( + s, &request, QA6_USB_STATUS_SUCCESS, 0, NULL, 0); + return; + } + + s5l8950x_usb_bridge_write_response( + s, &request, QA6_USB_STATUS_SUCCESS, 0, NULL, 0); + qemu_log_mask(LOG_UNIMP, + "s5l8950x.usb-bridge: host USB reset request %u\n", + request.request_id); + s->regs[USB_OTG_GINTSTS / 4] |= USB_OTG_GINT_USBRST; + s5l8950x_usb_otg_update_irq(s); + return; + } + + if (request.type == QA6_USB_MESSAGE_BULK_REQUEST) { + unsigned ep = request.setup[0] & 0x7f; + uint32_t timeout_ms = request.setup[1] | + (request.setup[2] << 8) | + (request.setup[3] << 16) | + ((uint32_t)request.setup[4] << 24); + + if (!ep || ep >= USB_OTG_EP_COUNT || + request.transfer_length > QA6_USB_MAX_PAYLOAD || + (direction_in && request.payload_length != 0) || + (!direction_in && + request.payload_length != request.transfer_length)) { + s5l8950x_usb_bridge_write_response( + s, &request, QA6_USB_STATUS_PROTOCOL, 0, NULL, 0); + return; + } + if (s->bridge_request_active || + s->host_stage != USB_HOST_DFU_READY || + !s->enumeration_generation || !s->device_descriptor_len) { + s5l8950x_usb_bridge_write_response( + s, &request, QA6_USB_STATUS_PROTOCOL, 0, NULL, 0); + return; + } + + s->bridge_request = request; + if (request.payload_length) { + memcpy(s->bridge_out_data, + s->bridge_rx + QA6_USB_HEADER_SIZE, + request.payload_length); + } + s->bridge_out_offset = 0; + s->bridge_in_len = 0; + s->bridge_request_active = true; + if (direction_in && timeout_ms) { + timer_mod(s->bridge_bulk_timer, + qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + timeout_ms); + } + qemu_log_mask(LOG_UNIMP, + "s5l8950x.usb-bridge: bulk request %u EP=%02x " + "len=%u\n", request.request_id, request.setup[0], + request.transfer_length); + s5l8950x_usb_bridge_try_bulk(s); + return; + } + + if (request.type != QA6_USB_MESSAGE_REQUEST || request.transfer_length != setup_length || (direction_in && request.payload_length != 0) || (!direction_in && request.payload_length != setup_length)) { qemu_log_mask(LOG_GUEST_ERROR, "s5l8950x.usb-bridge: invalid request frame\n"); - if (request.magic == QA6_USB_MAGIC) { - s5l8950x_usb_bridge_write_response( - s, &request, QA6_USB_STATUS_PROTOCOL, 0, NULL, 0); - } + s5l8950x_usb_bridge_write_response( + s, &request, QA6_USB_STATUS_PROTOCOL, 0, NULL, 0); return; } @@ -487,6 +1288,8 @@ static void s5l8950x_usb_bridge_process_frame(S5L8950XUSBOTGState *s) } s->bridge_out_offset = 0; s->bridge_in_len = 0; + s->bridge_dfu_status = 0; + s->bridge_dfu_state = 0; s->bridge_request_active = true; qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-bridge: EP0 request %u " @@ -560,6 +1363,7 @@ static void s5l8950x_usb_bridge_event(void *opaque, QEMUChrEvent event) s->bridge_open = false; s->bridge_rx_len = 0; s->bridge_request_active = false; + timer_del(s->bridge_bulk_timer); if (s->host_stage >= USB_HOST_BRIDGE_WAIT_DATA_IN && s->host_stage <= USB_HOST_BRIDGE_WAIT_STATUS_IN) { s->host_stage = USB_HOST_DFU_READY; @@ -569,6 +1373,70 @@ static void s5l8950x_usb_bridge_event(void *opaque, QEMUChrEvent event) } } +/* + * Reset the guest-visible DWC2 state and re-arm the small host state machine. + * SecureROM and iBSS each initialize this core, so a core reset must allow a + * fresh descriptor sequence without replaying an already-consumed DFU image. + */ +static void s5l8950x_usb_otg_rearm(S5L8950XUSBOTGState *s, bool cold) +{ + timer_del(s->recovery_reset_timer); + timer_del(s->bridge_bulk_timer); + + if (!cold && s->bridge_request_active) { + s5l8950x_usb_bridge_finish(s, QA6_USB_STATUS_DISCONNECTED); + } + + memset(s->regs, 0, sizeof(s->regs)); + s->regs[USB_OTG_GRSTCTL / sizeof(uint32_t)] = + USB_OTG_GRSTCTL_AHBIDLE; + s->usb_reset_sent = false; + s->enum_done_sent = false; + s->setup_sent = false; + s->descriptor_received = false; + s->device_descriptor_len = 0; + s->serial_descriptor_len = 0; + s->config_descriptor_len = 0; + s->config_total_length = 0; + s->current_device_is_dfu = false; + s->host_stage = USB_HOST_WAIT_DEVICE_SETUP; + s->bridge_request_active = false; + s->bridge_rx_len = 0; + s->bridge_out_offset = 0; + s->bridge_in_len = 0; + memset(s->device_descriptor, 0, sizeof(s->device_descriptor)); + memset(s->serial_descriptor, 0, sizeof(s->serial_descriptor)); + memset(s->config_descriptor, 0, sizeof(s->config_descriptor)); + memset(&s->bridge_request, 0, sizeof(s->bridge_request)); + memset(s->bridge_out_data, 0, sizeof(s->bridge_out_data)); + memset(s->bridge_in_data, 0, sizeof(s->bridge_in_data)); + + if (cold) { + s->enumeration_generation = 0; + s->recovery_reset_wait_for_halt = false; + g_byte_array_set_size(s->bridge_dfu_image, 0); + s->bridge_dfu_next_block = 0; + s->bridge_dfu_capture_valid = false; + s->dfu_image_offset = 0; + s->dfu_packet_len = 0; + s->dfu_packet_offset = 0; + s->dfu_packet_block = 0; + s->dfu_next_block = 0; + s->dfu_notify_block = DIV_ROUND_UP(s->dfu_image_size, + USB_DFU_TRANSFER_SIZE); + s->dfu_packet_kind = USB_DFU_PACKET_PAYLOAD; + s->dfu_suffix_pending = false; + s->dfu_state = 0; + s->dfu_status_polls = 0; + s->dfu_finish_statuses = 0; + s->dfu_injection_complete = false; + memset(s->dfu_packet, 0, sizeof(s->dfu_packet)); + memset(s->dfu_status, 0, sizeof(s->dfu_status)); + } + + s5l8950x_usb_otg_update_irq(s); +} + static uint32_t s5l8950x_usb_dfu_crc32(uint32_t crc, const uint8_t *data, size_t length) { @@ -758,13 +1626,55 @@ static void s5l8950x_usb_otg_complete_in_status(S5L8950XUSBOTGState *s) static void s5l8950x_usb_otg_complete_configuration_status_in( S5L8950XUSBOTGState *s) { + uint16_t vendor = 0; + uint16_t product = 0; + uint32_t offset = 0; + bool has_dfu_interface = false; + bool has_recovery_interface = false; + s->regs[USB_OTG_DIEPTSIZ0 / 4] &= ~0x7f; s->regs[USB_OTG_DIEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[USB_OTG_DIEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; s->host_stage = USB_HOST_DFU_READY; + + if (s->device_descriptor_len >= 12) { + vendor = s->device_descriptor[8] | (s->device_descriptor[9] << 8); + product = s->device_descriptor[10] | + (s->device_descriptor[11] << 8); + } + while (offset + 2 <= s->config_descriptor_len) { + uint8_t length = s->config_descriptor[offset]; + uint8_t type = s->config_descriptor[offset + 1]; + + if (length < 2 || offset + length > s->config_descriptor_len) { + break; + } + if (type == 4 && length >= 9) { + uint8_t interface_class = s->config_descriptor[offset + 5]; + uint8_t interface_subclass = s->config_descriptor[offset + 6]; + + if (interface_class == 0xfe && interface_subclass == 0x01) { + has_dfu_interface = true; + } else if (interface_class == 0xff) { + has_recovery_interface = true; + } + } + offset += length; + } + + /* + * SecureROM uses an internal PID but a DFU-only configuration, which the + * bridge maps to Apple's public 0x1227 PID. Recovery PID 0x1281 keeps a + * DFU fallback interface alongside its vendor command/bulk interface and + * must retain its real identity. + */ + s->current_device_is_dfu = has_dfu_interface && + !has_recovery_interface; + s->enumeration_generation++; qemu_log_mask(LOG_UNIMP, - "s5l8950x.usb-host: enumeration complete; DFU control " - "interface ready\n"); + "s5l8950x.usb-host: enumeration complete; " + "%04x:%04x, %s interface\n", vendor, product, + s->current_device_is_dfu ? "DFU" : "non-DFU"); s5l8950x_usb_otg_update_irq(s); } @@ -791,7 +1701,10 @@ static void s5l8950x_usb_dfu_complete_dnload_data_out( } s->dfu_packet_offset += length; - s->regs[USB_OTG_DOEPTSIZ0 / 4] &= ~0x7f; + /* Preserve the residual XFRSIZ so SecureROM sees the short packet. */ + s->regs[USB_OTG_DOEPTSIZ0 / 4] = + (s->regs[USB_OTG_DOEPTSIZ0 / 4] & ~0x7f) | + (capacity - length); s->regs[USB_OTG_DOEPCTL0 / 4] &= ~USB_OTG_EPCTL_EPENA; s->regs[USB_OTG_DOEPINT0 / 4] |= USB_OTG_EPINT_XFERCOMPL; if (s->dfu_packet_offset == s->dfu_packet_len) { @@ -877,13 +1790,27 @@ static S5L8950XUSBHostStage s5l8950x_usb_dfu_stage_after_status( if (s->dfu_packet_kind == USB_DFU_PACKET_NOTIFY) { s->dfu_finish_statuses++; - if (s->dfu_finish_statuses < 2) { + if (s->dfu_state == USB_DFU_STATE_MANIFEST_SYNC || + s->dfu_state == USB_DFU_STATE_MANIFEST) { + if (s->dfu_finish_statuses >= USB_DFU_MAX_STATUS_POLLS) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.usb-host: DFU manifestation did " + "not reach MANIFEST-WAIT-RESET\n"); + return USB_HOST_DFU_ERROR; + } return USB_HOST_WAIT_DFU_GETSTATUS_SETUP; } + if (s->dfu_state != USB_DFU_STATE_WAIT_RESET) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.usb-host: unexpected final DFU state " + "%u\n", s->dfu_state); + return USB_HOST_DFU_ERROR; + } qemu_log_mask(LOG_UNIMP, - "s5l8950x.usb-host: DFU image handed to SecureROM " + "s5l8950x.usb-host: SecureROM accepted DFU image " "(%zu bytes, final state=%u)\n", s->dfu_image_size, s->dfu_state); + s->dfu_injection_complete = true; return USB_HOST_DFU_DONE; } @@ -1038,6 +1965,66 @@ static void s5l8950x_usb_otg_write(void *opaque, hwaddr offset, { S5L8950XUSBOTGState *s = opaque; + if (s->enumeration_generation && !s->device_descriptor_len && + (offset == USB_OTG_GRSTCTL || offset == USB_OTG_GAHBCFG || + offset == USB_OTG_GINTSTS || offset == USB_OTG_GINTMSK || + offset == USB_OTG_DCTL || offset == USB_OTG_DIEPMSK || + offset == USB_OTG_DOEPMSK || offset == USB_OTG_DAINTMSK || + offset == USB_OTG_DIEPCTL0 || offset == USB_OTG_DIEPTSIZ0 || + offset == USB_OTG_DIEPDMA0 || offset == USB_OTG_DOEPCTL0 || + offset == USB_OTG_DOEPTSIZ0 || offset == USB_OTG_DOEPDMA0)) { + vaddr pc = current_cpu ? ARM_CPU(current_cpu)->env.regs[15] : 0; + + qemu_log_mask(LOG_UNIMP, + "s5l8950x.usb-trace: PC=0x%08" VADDR_PRIx + " write[0x%04" HWADDR_PRIx "]=0x%08" PRIx64 + " stage=%u\n", + pc, offset, value, s->host_stage); + } + + if (offset == USB_OTG_GRSTCTL && + (value & USB_OTG_GRSTCTL_CSFTRST)) { + /* iBSS initializes the core again after SecureROM jumps to it. */ + s5l8950x_usb_otg_rearm(s, false); + qemu_log_mask(LOG_UNIMP, + "s5l8950x.usb-host: core soft reset; " + "waiting for device re-enumeration\n"); + return; + } + + if (offset == USB_OTG_DCTL) { + uint32_t old_value = s->regs[USB_OTG_DCTL / 4]; + + if ((old_value & USB_OTG_DCTL_SFTDISCON) && + !(value & USB_OTG_DCTL_SFTDISCON)) { + /* + * DCTL soft reconnect changes only the bus attachment. The DWC + * register image programmed by iBSS must survive it; the host + * reset is delivered later, once the interrupt handler is live. + */ + s->regs[USB_OTG_GINTSTS / 4] &= + ~(USB_OTG_GINT_USBRST | USB_OTG_GINT_ENUMDONE); + s->usb_reset_sent = false; + s->enum_done_sent = false; + s->setup_sent = false; + s->descriptor_received = false; + s->device_descriptor_len = 0; + s->serial_descriptor_len = 0; + s->config_descriptor_len = 0; + s->config_total_length = 0; + s->current_device_is_dfu = false; + s->host_stage = USB_HOST_WAIT_DEVICE_SETUP; + timer_mod(s->recovery_reset_timer, + qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + 1); + qemu_log_mask(LOG_UNIMP, + "s5l8950x.usb-host: soft reconnect; " + "waiting for device re-enumeration\n"); + } + s->regs[USB_OTG_DCTL / 4] = value; + s5l8950x_usb_otg_update_irq(s); + return; + } + if (offset == USB_OTG_GINTSTS) { uint32_t old_status = s->regs[offset / 4]; @@ -1064,17 +2051,63 @@ static void s5l8950x_usb_otg_write(void *opaque, hwaddr offset, value |= USB_OTG_GRSTCTL_AHBIDLE; } s->regs[offset / sizeof(uint32_t)] = value; + if ((value & USB_OTG_EPCTL_EPENA) && + (s5l8950x_usb_otg_is_ep_int(offset, USB_OTG_DIEPCTL0) || + s5l8950x_usb_otg_is_ep_int(offset, USB_OTG_DOEPCTL0))) { + s5l8950x_usb_bridge_try_bulk(s); + } + if ((offset == USB_OTG_DIEPCTL0 || offset == USB_OTG_DOEPCTL0) && + (value & USB_OTG_EPCTL_STALL) && s->bridge_request_active) { + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.usb-bridge: guest armed EP0 STALL; " + "CPU state follows\n"); + if (current_cpu && qemu_loglevel_mask(LOG_GUEST_ERROR)) { + FILE *log_file = qemu_log_trylock(); + + if (log_file) { + cpu_dump_state(current_cpu, log_file, 0); + qemu_log_unlock(log_file); + } + } + s5l8950x_usb_bridge_finish(s, QA6_USB_STATUS_STALL); + s5l8950x_usb_otg_update_irq(s); + return; + } + if (offset == USB_OTG_DIEPCTL0 && + (value & USB_OTG_EPCTL_STALL) && + s->host_stage == USB_HOST_WAIT_DFU_DNLOAD_STATUS_IN) { + s->host_stage = USB_HOST_DFU_ERROR; + qemu_log_mask(LOG_GUEST_ERROR, + "s5l8950x.usb-host: firmware stalled EP0; " + "DFU image rejected after block %u\n", + s->dfu_packet_block); + s5l8950x_usb_otg_update_irq(s); + return; + } if (offset == USB_OTG_GINTMSK && (value & USB_OTG_GINT_USBRST) && !s->usb_reset_sent) { - s->regs[USB_OTG_GINTSTS / 4] |= USB_OTG_GINT_USBRST; - s->usb_reset_sent = true; + if (s->enumeration_generation) { + /* + * Enabling USBRST while DCTL still holds the soft-disconnect + * bit does not expose the device to a physical host. UART + * logging makes iBEC's early platform setup long enough for an + * incorrectly armed timer to fire here, before its USB handler + * is installed. The DCTL reconnect path above will arm the + * reset once the guest is actually visible on the bus. + */ + if (!(s->regs[USB_OTG_DCTL / 4] & + USB_OTG_DCTL_SFTDISCON)) { + timer_mod(s->recovery_reset_timer, + qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + 1); + } + } else { + s->regs[USB_OTG_GINTSTS / 4] |= USB_OTG_GINT_USBRST; + s->usb_reset_sent = true; + } } if (offset == USB_OTG_DOEPCTL0 && (value & USB_OTG_EPCTL_EPENA) && s->enum_done_sent) { - if ((value & USB_OTG_EPCTL_STALL) && - s->bridge_request_active) { - s5l8950x_usb_bridge_finish(s, QA6_USB_STATUS_STALL); - } else if (s->host_stage == USB_HOST_DFU_READY && + if (s->host_stage == USB_HOST_DFU_READY && s->bridge_request_active) { s5l8950x_usb_bridge_start_request(s); } else if (s->host_stage == USB_HOST_BRIDGE_WAIT_DATA_OUT) { @@ -1108,7 +2141,9 @@ static void s5l8950x_usb_otg_write(void *opaque, hwaddr offset, } else if (s->host_stage == USB_HOST_WAIT_SET_CONFIGURATION_SETUP) { s5l8950x_usb_otg_send_set_configuration_setup(s); - } else if (s->host_stage == USB_HOST_DFU_READY && s->dfu_image) { + } else if (s->host_stage == USB_HOST_DFU_READY && s->dfu_image && + s->current_device_is_dfu && + !s->dfu_injection_complete) { s5l8950x_usb_dfu_send_getstate(s); } else if (s->host_stage == USB_HOST_WAIT_DFU_GETSTATE_STATUS_OUT) { @@ -1152,20 +2187,10 @@ static void s5l8950x_usb_otg_write(void *opaque, hwaddr offset, } if (offset == USB_OTG_DIEPCTL0 && (value & USB_OTG_EPCTL_EPENA)) { - if ((value & USB_OTG_EPCTL_STALL) && - s->bridge_request_active) { - s5l8950x_usb_bridge_finish(s, QA6_USB_STATUS_STALL); - } else if (s->host_stage == USB_HOST_BRIDGE_WAIT_DATA_IN) { + if (s->host_stage == USB_HOST_BRIDGE_WAIT_DATA_IN) { s5l8950x_usb_bridge_complete_data_in(s); } else if (s->host_stage == USB_HOST_BRIDGE_WAIT_STATUS_IN) { s5l8950x_usb_bridge_complete_status_in(s); - } else if ((value & USB_OTG_EPCTL_STALL) && - s->host_stage == USB_HOST_WAIT_DFU_DNLOAD_STATUS_IN) { - s->host_stage = USB_HOST_DFU_ERROR; - qemu_log_mask(LOG_GUEST_ERROR, - "s5l8950x.usb-host: SecureROM stalled EP0; " - "DFU image rejected after block %u\n", - s->dfu_packet_block); } else if (s->host_stage == USB_HOST_WAIT_DEVICE_IN) { s5l8950x_usb_otg_complete_descriptor_in(s); } else if (s->host_stage == USB_HOST_WAIT_SET_ADDRESS_STATUS_IN) { @@ -1240,44 +2265,8 @@ static void s5l8950x_usb_otg_reset(DeviceState *dev) S5L8950XUSBOTGState *s = S5L8950X_USB_OTG(dev); QEMU_BUILD_BUG_ON(sizeof(Qa6UsbFrameHeader) != 32); - - memset(s->regs, 0, sizeof(s->regs)); - s->regs[USB_OTG_GRSTCTL / sizeof(uint32_t)] = - USB_OTG_GRSTCTL_AHBIDLE; - s->usb_reset_sent = false; - s->enum_done_sent = false; - s->setup_sent = false; - s->descriptor_received = false; - s->device_descriptor_len = 0; - s->serial_descriptor_len = 0; - s->config_descriptor_len = 0; - s->config_total_length = 0; - s->host_stage = USB_HOST_WAIT_DEVICE_SETUP; - s->dfu_image_offset = 0; - s->dfu_packet_len = 0; - s->dfu_packet_offset = 0; - s->dfu_packet_block = 0; - s->dfu_next_block = 0; - s->dfu_notify_block = DIV_ROUND_UP(s->dfu_image_size, - USB_DFU_TRANSFER_SIZE); - s->dfu_packet_kind = USB_DFU_PACKET_PAYLOAD; - s->dfu_suffix_pending = false; - s->dfu_state = 0; - s->dfu_status_polls = 0; - s->dfu_finish_statuses = 0; - s->bridge_request_active = false; - s->bridge_rx_len = 0; - s->bridge_out_offset = 0; - s->bridge_in_len = 0; - memset(s->device_descriptor, 0, sizeof(s->device_descriptor)); - memset(s->serial_descriptor, 0, sizeof(s->serial_descriptor)); - memset(s->config_descriptor, 0, sizeof(s->config_descriptor)); - memset(s->dfu_packet, 0, sizeof(s->dfu_packet)); - memset(s->dfu_status, 0, sizeof(s->dfu_status)); - memset(&s->bridge_request, 0, sizeof(s->bridge_request)); - memset(s->bridge_out_data, 0, sizeof(s->bridge_out_data)); - memset(s->bridge_in_data, 0, sizeof(s->bridge_in_data)); - s5l8950x_usb_otg_update_irq(s); + QEMU_BUILD_BUG_ON(sizeof(Qa6UsbDeviceInfo) != 268); + s5l8950x_usb_otg_rearm(s, true); } static void s5l8950x_usb_otg_realize(DeviceState *dev, Error **errp) @@ -1292,7 +2281,7 @@ static void s5l8950x_usb_otg_realize(DeviceState *dev, Error **errp) s5l8950x_usb_bridge_event, NULL, s, NULL, true); qemu_log_mask(LOG_UNIMP, - "s5l8950x.usb-bridge: external EP0 bridge enabled\n"); + "s5l8950x.usb-bridge: external USB bridge enabled\n"); } if (!s->dfu_image_path) { @@ -1313,6 +2302,8 @@ static void s5l8950x_usb_otg_realize(DeviceState *dev, Error **errp) return; } s5l8950x_usb_dfu_build_suffix(s); + s->dfu_notify_block = DIV_ROUND_UP(s->dfu_image_size, + USB_DFU_TRANSFER_SIZE); qemu_log_mask(LOG_UNIMP, "s5l8950x.usb-host: loaded DFU image '%s' (%zu bytes)\n", s->dfu_image_path, s->dfu_image_size); @@ -1324,6 +2315,11 @@ static void s5l8950x_usb_otg_unrealize(DeviceState *dev) qemu_chr_fe_set_handlers(&s->bridge_chr, NULL, NULL, NULL, NULL, NULL, NULL, false); + timer_free(s->recovery_reset_timer); + s->recovery_reset_timer = NULL; + timer_free(s->bridge_bulk_timer); + s->bridge_bulk_timer = NULL; + g_clear_pointer(&s->bridge_dfu_image, g_byte_array_unref); g_clear_pointer(&s->dfu_image, g_free); s->dfu_image_size = 0; } @@ -1333,6 +2329,11 @@ static void s5l8950x_usb_otg_init(Object *obj) S5L8950XUSBOTGState *s = S5L8950X_USB_OTG(obj); SysBusDevice *sbd = SYS_BUS_DEVICE(obj); + s->recovery_reset_timer = timer_new_ms( + QEMU_CLOCK_VIRTUAL, s5l8950x_usb_otg_recovery_reset, s); + s->bridge_bulk_timer = timer_new_ms( + QEMU_CLOCK_VIRTUAL, s5l8950x_usb_bridge_bulk_timeout, s); + s->bridge_dfu_image = g_byte_array_new(); memory_region_init_io(&s->iomem, obj, &s5l8950x_usb_otg_ops, s, TYPE_S5L8950X_USB_OTG, S5L8950X_USB_OTG_REGION_SIZE); @@ -1343,6 +2344,10 @@ static void s5l8950x_usb_otg_init(Object *obj) static const Property s5l8950x_usb_otg_properties[] = { DEFINE_PROP_STRING("dfu-image", S5L8950XUSBOTGState, dfu_image_path), DEFINE_PROP_CHR("usb-bridge", S5L8950XUSBOTGState, bridge_chr), + DEFINE_PROP_BOOL("force-debug-uarts", S5L8950XUSBOTGState, + force_debug_uarts, false), + DEFINE_PROP_BOOL("native-img3-handoff", S5L8950XUSBOTGState, + native_img3_handoff, false), }; static void s5l8950x_usb_otg_class_init(ObjectClass *klass, diff --git a/include/hw/arm/s5l8950x.h b/include/hw/arm/s5l8950x.h index 2f9f1fa..5af4f3f 100644 --- a/include/hw/arm/s5l8950x.h +++ b/include/hw/arm/s5l8950x.h @@ -14,6 +14,9 @@ #define TYPE_S5L8950X_UART "s5l8950x-uart" #define TYPE_S5L8950X_TIMER "s5l8950x-timer" #define TYPE_S5L8950X_PMGR "s5l8950x-pmgr" +#define TYPE_S5L8950X_CLOCK "s5l8950x-clock" +#define TYPE_S5L8950X_MEMCTL "s5l8950x-memctl" +#define TYPE_S5L8950X_I2C "s5l8950x-i2c" #define TYPE_S5L8950X_GPIO "s5l8950x-gpio" #define TYPE_S5L8950X_USB_PHY "s5l8950x-usb-phy" #define TYPE_S5L8950X_USB_OTG "s5l8950x-usb-otg" @@ -31,10 +34,16 @@ struct S5L8950XState { MemoryRegion sram; SysBusDevice *chipid; + SysBusDevice *aes; + SysBusDevice *pke; + SysBusDevice *sha1; SysBusDevice *aic; SysBusDevice *uart; SysBusDevice *timer; SysBusDevice *pmgr; + SysBusDevice *clock; + SysBusDevice *memctl; + SysBusDevice *i2c[3]; SysBusDevice *gpio; SysBusDevice *usb_phy; SysBusDevice *usb_otg; diff --git a/include/hw/misc/s5l8950x-aes.h b/include/hw/misc/s5l8950x-aes.h new file mode 100644 index 0000000..e3fb6da --- /dev/null +++ b/include/hw/misc/s5l8950x-aes.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#ifndef HW_MISC_S5L8950X_AES_H +#define HW_MISC_S5L8950X_AES_H + +#include "hw/core/sysbus.h" +#include "qom/object.h" + +#define TYPE_S5L8950X_AES "s5l8950x-aes" + +OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XAESState, S5L8950X_AES) + +#endif /* HW_MISC_S5L8950X_AES_H */ diff --git a/include/hw/misc/s5l8950x-pke.h b/include/hw/misc/s5l8950x-pke.h new file mode 100644 index 0000000..c706eca --- /dev/null +++ b/include/hw/misc/s5l8950x-pke.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#ifndef HW_MISC_S5L8950X_PKE_H +#define HW_MISC_S5L8950X_PKE_H + +#include "hw/core/sysbus.h" + +#define TYPE_S5L8950X_PKE "s5l8950x-pke" +OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XPKEState, S5L8950X_PKE) + +#endif /* HW_MISC_S5L8950X_PKE_H */ diff --git a/include/hw/misc/s5l8950x-sha1.h b/include/hw/misc/s5l8950x-sha1.h new file mode 100644 index 0000000..4042e86 --- /dev/null +++ b/include/hw/misc/s5l8950x-sha1.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#ifndef HW_MISC_S5L8950X_SHA1_H +#define HW_MISC_S5L8950X_SHA1_H + +#include "hw/core/sysbus.h" + +#define TYPE_S5L8950X_SHA1 "s5l8950x-sha1" +OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XSHA1State, S5L8950X_SHA1) + +#endif /* HW_MISC_S5L8950X_SHA1_H */ diff --git a/scripts/a6-img3-info.py b/scripts/a6-img3-info.py new file mode 100755 index 0000000..19025af --- /dev/null +++ b/scripts/a6-img3-info.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later + +"""Validate and display the bounded structure of an Apple Image3 file.""" + +import argparse +import json +from pathlib import Path +import struct +import sys + + +IMG3_HEADER = struct.Struct("<4s4I") +TAG_HEADER = struct.Struct("<4s2I") + + +class Img3Error(ValueError): + pass + + +def fourcc(raw): + return raw[::-1].decode("ascii", errors="replace") + + +def scalar(tag, payload): + if tag == "TYPE" and len(payload) >= 4: + return fourcc(payload[:4]) + if tag == "VERS" and len(payload) > 4: + return payload[4:].split(b"\0", 1)[0].decode("ascii", errors="replace") + if tag in {"CHIP", "BORD", "SEPO", "PROD", "SDOM"} and len(payload) == 4: + return f"0x{struct.unpack('= 8: + key_modifier, key_bits = struct.unpack_from(" len(data): + raise Img3Error( + f"full_size {full_size} is outside the {len(data)}-byte input" + ) + if data_size > full_size - IMG3_HEADER.size: + raise Img3Error("data_size extends past full_size") + if shsh_offset > data_size: + raise Img3Error("shsh_offset extends past data_size") + + tags = [] + offset = IMG3_HEADER.size + end = IMG3_HEADER.size + data_size + while offset < end: + if end - offset < TAG_HEADER.size: + raise Img3Error(f"truncated tag header at 0x{offset:x}") + raw_tag, total_size, payload_size = TAG_HEADER.unpack_from(data, offset) + tag = fourcc(raw_tag) + if total_size < TAG_HEADER.size: + raise Img3Error(f"{tag} at 0x{offset:x} has an invalid total size") + if payload_size > total_size - TAG_HEADER.size: + raise Img3Error( + f"{tag} at 0x{offset:x} has an invalid payload size" + ) + if total_size > end - offset: + raise Img3Error(f"{tag} at 0x{offset:x} extends past data_size") + payload_start = offset + TAG_HEADER.size + payload = data[payload_start:payload_start + payload_size] + tags.append({ + "tag": tag, + "offset": offset, + "payload_offset": payload_start, + "total_size": total_size, + "payload_size": payload_size, + "padding_size": total_size - TAG_HEADER.size - payload_size, + "value": scalar(tag, payload), + }) + offset += total_size + if offset != end: + raise Img3Error("tag sizes do not exactly cover data_size") + + return { + "magic": "Img3", + "file_size": len(data), + "full_size": full_size, + "data_size": data_size, + "shsh_offset": shsh_offset, + "image_type": fourcc(struct.pack(">= 7 + while tag: + encoded_tag.insert(0, 0x80 | (tag & 0x7F)) + tag >>= 7 + prefix = bytes((identifier | 0x1F,)) + bytes(encoded_tag) + return prefix + encode_length(len(payload)) + payload + + +def sign_sha1(payload, key): + try: + result = subprocess.run( + ["openssl", "dgst", "-sha1", "-sign", str(key)], + input=payload, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=True, + ) + except FileNotFoundError as error: + raise TicketBuildError("openssl is required") from error + except subprocess.CalledProcessError as error: + detail = error.stderr.decode("utf-8", errors="replace").strip() + raise TicketBuildError(f"openssl failed: {detail}") from error + return result.stdout + + +def component_digest(component): + if len(component) < IMG3_HEADER.size: + raise TicketBuildError("iBEC is shorter than its IMG3 header") + magic, full_size, data_size, shsh_offset, _ = IMG3_HEADER.unpack_from( + component + ) + if magic != IMG3_MAGIC or full_size != len(component): + raise TicketBuildError("iBEC is not a bounded IMG3 container") + if data_size != len(component) - IMG3_HEADER.size: + raise TicketBuildError("iBEC IMG3 data_size is inconsistent") + if shsh_offset != data_size: + raise TicketBuildError("iBEC must use external-ticket IMG3 layout") + return hashlib.sha1(component[12:]).digest() + + +def placeholder_digest(tag): + label = f"QEMU A6 lab placeholder manifest tag {tag}".encode("ascii") + return hashlib.sha1(label).digest() + + +def build_manifest(args, digest): + fields = {tag: placeholder_digest(tag) for tag in MANIFEST_DIGEST_TAGS} + fields.update( + {tag: (1).to_bytes(4, "little") for tag in MANIFEST_FLAG_TAGS} + ) + fields.update( + { + tag: args.build_identity.encode("ascii") + for tag in MANIFEST_BUILD_TAGS + } + ) + fields.update({ + 1: args.ecid.to_bytes(8, "little"), + 2: args.chip_id.to_bytes(4, "little"), + 3: args.board_id.to_bytes(4, "little"), + 4: args.production_mode.to_bytes(4, "little"), + 5: args.security_domain.to_bytes(4, "little"), + 18: hashlib.sha1(args.boot_nonce).digest(), + 230: digest, + }) + return fields + + +def atomic_write(path, payload): + path.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile( + prefix=path.name + ".", dir=path.parent, delete=False + ) as temporary: + temporary.write(payload) + temporary_path = Path(temporary.name) + temporary_path.replace(path) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--component", required=True, type=Path) + parser.add_argument("--identity", required=True, type=Path) + parser.add_argument("--ticket-output", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument( + "--ecid", type=lambda value: int(value, 0), default=DEFAULT_ECID + ) + parser.add_argument( + "--chip-id", type=lambda value: int(value, 0), default=DEFAULT_CHIP_ID + ) + parser.add_argument( + "--board-id", type=lambda value: int(value, 0), default=DEFAULT_BOARD_ID + ) + parser.add_argument( + "--production-mode", type=lambda value: int(value, 0), choices=(0, 1), + default=DEFAULT_PRODUCTION_MODE, + ) + parser.add_argument( + "--security-domain", type=lambda value: int(value, 0), + default=DEFAULT_SECURITY_DOMAIN, + ) + parser.add_argument("--build-identity", default=DEFAULT_BUILD_IDENTITY) + parser.add_argument( + "--boot-nonce", type=boot_nonce, required=True, + help=( + "raw 8-byte iBSS boot nonce in hexadecimal; stored as the " + "original APTicket [18] SHA-1 nonce binding" + ), + ) + args = parser.parse_args() + + required = ( + (args.component, "ticketed iBEC"), + (args.identity / "ticket-leaf-key.pem", "ticket leaf private key"), + (args.identity / "ticket-cert-chain.der", "ticket certificate chain"), + ) + for path, description in required: + if not path.is_file(): + raise TicketBuildError(f"{description} not found: {path}") + for path in (args.ticket_output, args.output): + if path.exists(): + raise TicketBuildError(f"refusing to overwrite output: {path}") + + component = args.component.read_bytes() + digest = component_digest(component) + replacements = build_manifest(args, digest) + # DER SET members must be sorted by their complete encoded byte strings. + # No external or previously issued ticket is used as input. + encoded_fields = [ + encode_context(tag, payload) for tag, payload in replacements.items() + ] + fields = b"".join(sorted(encoded_fields)) + manifest_der = encode_tlv(0x31, fields) + signature = sign_sha1(manifest_der, args.identity / "ticket-leaf-key.pem") + if len(signature) != SIGNATURE_SIZE: + raise TicketBuildError( + f"ticket leaf produced {len(signature)} signature bytes; " + "expected 128" + ) + chain = (args.identity / "ticket-cert-chain.der").read_bytes() + ticket = encode_tlv( + 0x30, + SHA1_WITH_RSA + encode_tlv(0x31, fields) + + encode_tlv(0x04, signature) + + encode_context(1, chain, constructed=True), + ) + padded_size = (len(ticket) + 63) & ~63 + combined = ticket + bytes((0xFF,)) * (padded_size - len(ticket)) + component + + atomic_write(args.ticket_output, ticket) + atomic_write(args.output, combined) + metadata = { + "format": 1, + "ticket_size": len(ticket), + "ticket_padded_size": padded_size, + "component_size": len(component), + "combined_size": len(combined), + "ecid": f"0x{args.ecid:016x}", + "chip_id": f"0x{args.chip_id:04x}", + "board_id": args.board_id, + "production_mode": args.production_mode, + "security_domain": args.security_domain, + "build_identity": args.build_identity, + "boot_nonce": args.boot_nonce.hex(), + "boot_nonce_sha1": hashlib.sha1(args.boot_nonce).hexdigest(), + "manifest_tags": sorted(replacements), + "ibec_manifest_tag": 230, + "ibec_sha1_range": "0x0c..EOF", + "ibec_sha1": digest.hex(), + "ticket_sha256": hashlib.sha256(ticket).hexdigest(), + "combined_sha256": hashlib.sha256(combined).hexdigest(), + } + args.output.with_name(args.output.name + ".json").write_text( + json.dumps(metadata, indent=2) + "\n", encoding="utf-8" + ) + print( + f"Created lab APTicket ({len(ticket)} bytes) and ticketed iBEC " + f"({len(combined)} bytes): {args.output}" + ) + print(f"Manifest [230] iBEC SHA-1: {digest.hex()}") + + +if __name__ == "__main__": + try: + main() + except (OSError, TicketBuildError, ValueError) as error: + print(f"error: {error}", file=sys.stderr) + sys.exit(2) diff --git a/scripts/a6-lab-identity.py b/scripts/a6-lab-identity.py new file mode 100755 index 0000000..27912ca --- /dev/null +++ b/scripts/a6-lab-identity.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later + +"""Create an isolated A6 IMG3 lab identity and a derived SecureROM image.""" + +import argparse +import hashlib +import json +import os +from pathlib import Path +import shutil +import stat +import subprocess +import sys + + +ROM_SIZE = 0x10000 +ROOT_CERT_OFFSET = 0xBFD0 +ROOT_CERT_SLOT_SIZE = 1215 +ROOT_SUBJECT = ( + "/C=US/O=Apple Inc./OU=Apple Certification Authority/CN=Apple Root CA" +) +INTERMEDIATE_SUBJECT = ( + "/C=ZZ/O=QEMU A6 Lab/OU=Secure Boot Research/" + "CN=Apple Secure Boot Certification Authority" +) +IMG3_LEAF_SUBJECT = ( + "/C=ZZ/O=QEMU A6 Lab/OU=Secure Boot Research/" + "CN=A6-Darwin-Prod-CEPO10-SDOM3-Lab" +) +TICKET_LEAF_SUBJECT = ( + "/C=ZZ/O=QEMU A6 Lab/OU=Secure Boot Research/" + "CN=H5P-Darwin-Prod-CEPO1-Ticket-DataCenter" +) + + +class LabIdentityError(RuntimeError): + pass + + +def run(command, cwd): + try: + subprocess.run(command, cwd=cwd, check=True) + except FileNotFoundError as error: + raise LabIdentityError( + f"required program not found: {command[0]}" + ) from error + except subprocess.CalledProcessError as error: + raise LabIdentityError( + f"command failed with exit status {error.returncode}: " + + " ".join(command) + ) from error + + +def openssl(*arguments, cwd): + run(["openssl", *arguments], cwd) + + +def sha256(data): + return hashlib.sha256(data).hexdigest() + + +def der_object_size(data, offset): + if offset >= len(data) or data[offset] != 0x30: + raise LabIdentityError( + f"no DER SEQUENCE at SecureROM offset 0x{offset:x}" + ) + if offset + 2 > len(data): + raise LabIdentityError("truncated DER length") + first = data[offset + 1] + if first < 0x80: + return 2 + first + length_bytes = first & 0x7F + if not 1 <= length_bytes <= 4 or offset + 2 + length_bytes > len(data): + raise LabIdentityError("invalid DER length") + payload_size = int.from_bytes( + data[offset + 2:offset + 2 + length_bytes], "big" + ) + return 2 + length_bytes + payload_size + + +def ensure_clean_output(path): + if path.exists(): + if not path.is_dir(): + raise LabIdentityError( + f"output exists and is not a directory: {path}" + ) + if any(path.iterdir()): + raise LabIdentityError( + f"output directory is not empty: {path}; choose a new directory" + ) + else: + path.mkdir(parents=True, mode=0o700) + + +def create_identity(source_rom, output, config): + source = source_rom.read_bytes() + if len(source) != ROM_SIZE: + raise LabIdentityError( + f"SecureROM must be exactly {ROM_SIZE} bytes, got {len(source)}" + ) + embedded_size = der_object_size(source, ROOT_CERT_OFFSET) + if embedded_size != ROOT_CERT_SLOT_SIZE: + raise LabIdentityError( + "unexpected embedded trust anchor size at 0xbfd0: " + f"{embedded_size}, expected {ROOT_CERT_SLOT_SIZE}" + ) + + ensure_clean_output(output) + os.chmod(output, 0o700) + + openssl( + "genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:2048", + "-out", "root-key.pem", cwd=output, + ) + openssl( + "req", "-new", "-x509", "-sha1", "-days", "3650", + "-set_serial", "0x02", "-key", "root-key.pem", + # iBSS parses the complete fixed 1215-byte root slot. Its public + # subject layout plus the standard policy extension in the config + # preserves that DER size while all trust material remains lab-owned. + "-subj", ROOT_SUBJECT, + "-config", str(config), "-extensions", "v3_root", + "-out", "root.pem", cwd=output, + ) + + openssl( + "genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:2048", + "-out", "intermediate-key.pem", cwd=output, + ) + openssl( + "req", "-new", "-sha256", "-key", "intermediate-key.pem", + "-subj", INTERMEDIATE_SUBJECT, + "-config", str(config), + "-out", "intermediate.csr", cwd=output, + ) + openssl( + "x509", "-req", "-sha1", "-days", "3650", + "-set_serial", "0xA60010", "-in", "intermediate.csr", + "-CA", "root.pem", "-CAkey", "root-key.pem", + "-extfile", str(config), "-extensions", "v3_intermediate", + "-out", "intermediate.pem", cwd=output, + ) + + openssl( + "genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:1024", + "-out", "img3-leaf-key.pem", cwd=output, + ) + openssl( + "req", "-new", "-sha256", "-key", "img3-leaf-key.pem", + "-subj", IMG3_LEAF_SUBJECT, + "-config", str(config), + "-out", "img3-leaf.csr", cwd=output, + ) + openssl( + "x509", "-req", "-sha1", "-days", "3650", + "-set_serial", "0xA60105", "-in", "img3-leaf.csr", + "-CA", "intermediate.pem", "-CAkey", "intermediate-key.pem", + "-extfile", str(config), "-extensions", "v3_img3_leaf", + "-out", "img3-leaf.pem", cwd=output, + ) + + openssl( + "genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:1024", + "-out", "ticket-leaf-key.pem", cwd=output, + ) + openssl( + "req", "-new", "-sha256", "-key", "ticket-leaf-key.pem", + # iBSS compares this public ticket-class name while certificate trust + # still comes exclusively from the generated lab root and keys. + "-subj", TICKET_LEAF_SUBJECT, + "-config", str(config), + "-out", "ticket-leaf.csr", cwd=output, + ) + openssl( + "x509", "-req", "-sha1", "-days", "3650", + "-set_serial", "0xA6010B", "-in", "ticket-leaf.csr", + "-CA", "intermediate.pem", "-CAkey", "intermediate-key.pem", + "-extfile", str(config), "-extensions", "v3_ticket_leaf", + "-out", "ticket-leaf.pem", cwd=output, + ) + + for name in ("root", "intermediate", "img3-leaf", "ticket-leaf"): + openssl( + "x509", "-in", f"{name}.pem", "-outform", "DER", + "-out", f"{name}.der", cwd=output, + ) + + openssl( + "verify", "-auth_level", "0", "-ignore_critical", + "-CAfile", "root.pem", + "-untrusted", "intermediate.pem", "img3-leaf.pem", cwd=output, + ) + openssl( + "verify", "-auth_level", "0", "-ignore_critical", + "-CAfile", "root.pem", + "-untrusted", "intermediate.pem", "ticket-leaf.pem", cwd=output, + ) + + root_der = (output / "root.der").read_bytes() + intermediate_der = (output / "intermediate.der").read_bytes() + leaf_der = (output / "img3-leaf.der").read_bytes() + ticket_leaf_der = (output / "ticket-leaf.der").read_bytes() + if len(root_der) > ROOT_CERT_SLOT_SIZE: + raise LabIdentityError( + f"lab root DER is {len(root_der)} bytes and does not fit the " + f"{ROOT_CERT_SLOT_SIZE}-byte SecureROM slot" + ) + + (output / "cert-chain.der").write_bytes(intermediate_der + leaf_der) + (output / "ticket-cert-chain.der").write_bytes( + intermediate_der + ticket_leaf_der + ) + gid_key = os.urandom(32) + (output / "gid-key.bin").write_bytes(gid_key) + + patched = bytearray(source) + patched[ROOT_CERT_OFFSET:ROOT_CERT_OFFSET + ROOT_CERT_SLOT_SIZE] = ( + root_der + bytes(ROOT_CERT_SLOT_SIZE - len(root_der)) + ) + derived_rom = output / "s5l8950x-secure-rom-lab.bin" + derived_rom.write_bytes(patched) + + for name in ( + "root-key.pem", "intermediate-key.pem", "img3-leaf-key.pem", + "ticket-leaf-key.pem", "gid-key.bin", + ): + os.chmod(output / name, stat.S_IRUSR | stat.S_IWUSR) + for name in ("intermediate.csr", "img3-leaf.csr", "ticket-leaf.csr"): + (output / name).unlink() + + manifest = { + "format": 1, + "source_rom": str(source_rom.resolve()), + "source_rom_sha256": sha256(source), + "derived_rom": derived_rom.name, + "derived_rom_sha256": sha256(patched), + "root_certificate_offset": ROOT_CERT_OFFSET, + "root_certificate_slot_size": ROOT_CERT_SLOT_SIZE, + "root_certificate_der_size": len(root_der), + "root_certificate_sha256": sha256(root_der), + "certificate_chain_der_size": len(intermediate_der) + len(leaf_der), + "ticket_certificate_chain_der_size": ( + len(intermediate_der) + len(ticket_leaf_der) + ), + "gid_key_bits": 256, + "gid_key_sha256": sha256(gid_key), + } + (output / "identity.json").write_text( + json.dumps(manifest, indent=2) + "\n", encoding="utf-8" + ) + return manifest + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("source_rom", type=Path) + parser.add_argument("output_directory", type=Path) + args = parser.parse_args() + + script_directory = Path(__file__).resolve().parent + config = script_directory / "a6-lab-openssl.cnf" + if not config.is_file(): + raise LabIdentityError(f"OpenSSL configuration not found: {config}") + if not args.source_rom.is_file(): + raise LabIdentityError(f"SecureROM not found: {args.source_rom}") + if shutil.which("openssl") is None: + raise LabIdentityError("openssl is required") + + manifest = create_identity( + args.source_rom.resolve(), args.output_directory.resolve(), config + ) + print(f"Lab identity created in {args.output_directory.resolve()}") + print( + "Derived SecureROM SHA-256: " + manifest["derived_rom_sha256"] + ) + print( + "GID key: 256-bit private lab value (SHA-256 fingerprint " + + manifest["gid_key_sha256"] + ")" + ) + + +if __name__ == "__main__": + try: + main() + except (OSError, LabIdentityError) as error: + print(f"error: {error}", file=sys.stderr) + sys.exit(2) diff --git a/scripts/a6-lab-img3.py b/scripts/a6-lab-img3.py new file mode 100755 index 0000000..eb64df6 --- /dev/null +++ b/scripts/a6-lab-img3.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later + +"""Build a signed, optionally GID-wrapped IMG3 for the A6 lab identity.""" + +import argparse +import hashlib +import json +import os +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + + +IMG3_HEADER = struct.Struct("<4s4I") +TAG_HEADER = struct.Struct("<4s2I") +IMG3_MAGIC = b"3gmI" +SHSH_SIZE = 128 +DEFAULT_ECID = 0x200000 +DEFAULT_SECURITY_DOMAIN = 3 +DEFAULT_PRODUCTION_MODE = 1 +DEFAULT_BOARD_ID = 0 +DEFAULT_CHIP_EPOCH = 0x10 + + +class Img3BuildError(RuntimeError): + pass + + +def raw_tag(name): + return name.encode("ascii")[::-1] + + +def display_tag(value): + return value[::-1].decode("ascii", errors="replace") + + +def parse_template(data): + if len(data) < IMG3_HEADER.size: + raise Img3BuildError("template is shorter than the IMG3 header") + magic, full_size, data_size, _, image_type = IMG3_HEADER.unpack_from(data) + if magic != IMG3_MAGIC: + raise Img3BuildError("template does not have IMG3 magic") + if full_size > len(data) or data_size > full_size - IMG3_HEADER.size: + raise Img3BuildError("template IMG3 sizes are out of bounds") + + tags = [] + offset = IMG3_HEADER.size + end = IMG3_HEADER.size + data_size + while offset < end: + if end - offset < TAG_HEADER.size: + raise Img3BuildError(f"truncated tag at 0x{offset:x}") + tag, total_size, payload_size = TAG_HEADER.unpack_from(data, offset) + if ( + total_size < TAG_HEADER.size + or payload_size > total_size - TAG_HEADER.size + or total_size > end - offset + ): + raise Img3BuildError(f"invalid tag at 0x{offset:x}") + tags.append({ + "name": display_tag(tag), + "payload": data[ + offset + TAG_HEADER.size:offset + TAG_HEADER.size + payload_size + ], + "padding": total_size - TAG_HEADER.size - payload_size, + }) + offset += total_size + if offset != end: + raise Img3BuildError("template tag sizes do not cover data_size") + return image_type, tags + + +def make_tag(name, payload, padding=0): + total_size = TAG_HEADER.size + len(payload) + padding + return ( + TAG_HEADER.pack(raw_tag(name), total_size, len(payload)) + + payload + + bytes(padding) + ) + + +def openssl_filter(arguments, payload): + try: + result = subprocess.run( + ["openssl", *arguments], input=payload, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=True, + ) + except FileNotFoundError as error: + raise Img3BuildError("openssl is required") from error + except subprocess.CalledProcessError as error: + detail = error.stderr.decode("utf-8", errors="replace").strip() + raise Img3BuildError(f"openssl failed: {detail}") from error + return result.stdout + + +def aes256_cbc(payload, key, iv, decrypt=False): + if len(payload) % 16: + raise Img3BuildError("AES-CBC input must be a multiple of 16 bytes") + arguments = [ + "enc", "-aes-256-cbc", "-nopad", "-K", key.hex(), "-iv", iv.hex() + ] + if decrypt: + arguments.append("-d") + return openssl_filter(arguments, payload) + + +def sign_sha1(payload, private_key): + return openssl_filter( + ["dgst", "-sha1", "-sign", str(private_key)], payload + ) + + +def build_image(template, payload, identity, ecid, encrypted, ticketed, + security_domain, production_mode, board_id, chip_epoch): + image_type, template_tags = parse_template(template) + chain = None + leaf_key = None + if not ticketed: + chain = (identity / "cert-chain.der").read_bytes() + leaf_key = identity / "img3-leaf-key.pem" + if not leaf_key.is_file(): + raise Img3BuildError(f"leaf private key not found: {leaf_key}") + + if encrypted and len(payload) % 16: + raise Img3BuildError( + "encrypted DATA length must be a multiple of the AES block size" + ) + + content_iv = os.urandom(16) if encrypted else None + content_key = os.urandom(32) if encrypted else None + image_data = ( + aes256_cbc(payload, content_key, content_iv) if encrypted else payload + ) + + elements = [] + saw_type = False + saw_data = False + for tag in template_tags: + name = tag["name"] + if name in {"KBAG", "ECID", "SHSH", "CERT"}: + continue + if name == "TYPE": + saw_type = True + if name == "DATA": + saw_data = True + padding = (-(TAG_HEADER.size + len(image_data))) % 4 + elements.append(make_tag("DATA", image_data, padding)) + else: + elements.append(make_tag(name, tag["payload"], tag["padding"])) + if not saw_type or not saw_data: + raise Img3BuildError("template must contain TYPE and DATA tags") + + if not ticketed: + # The public IPSW template is unpersonalized and therefore omits the + # device-bound scalar tags that SecureROM requires after SHSH + # validation. The iBEC path carries these values in its external + # APTicket instead, matching the original iOS 10 boot flow. + existing_tags = {tag["name"] for tag in template_tags} + for name, value in ( + ("SDOM", security_domain), + ("PROD", production_mode), + ("CEPO", chip_epoch), + ("BORD", board_id), + ): + if name not in existing_tags: + elements.append(make_tag(name, struct.pack(" len(data): + raise RootPatchError("lab root has an invalid DER length") + payload_size = int.from_bytes(data[2:2 + length_bytes], "big") + return 2 + length_bytes + payload_size + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source-rom", required=True, type=Path) + parser.add_argument("--identity", required=True, type=Path) + parser.add_argument("--input", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + args = parser.parse_args() + + if args.output.exists(): + raise RootPatchError(f"refusing to overwrite output: {args.output}") + source_rom = args.source_rom.read_bytes() + if len(source_rom) != ROM_SIZE: + raise RootPatchError(f"source SecureROM must be {ROM_SIZE} bytes") + source_anchor = source_rom[ + ROOT_CERT_OFFSET:ROOT_CERT_OFFSET + ROOT_CERT_SLOT_SIZE + ] + lab_root = (args.identity / "root.der").read_bytes() + if der_object_size(lab_root) != len(lab_root): + raise RootPatchError("lab root contains trailing data") + if len(lab_root) > ROOT_CERT_SLOT_SIZE: + raise RootPatchError( + "lab root does not fit the iBoot trust-anchor slot" + ) + lab_anchor = lab_root + bytes(ROOT_CERT_SLOT_SIZE - len(lab_root)) + + payload = args.input.read_bytes() + offsets = [] + start = 0 + while True: + offset = payload.find(source_anchor, start) + if offset < 0: + break + offsets.append(offset) + start = offset + 1 + if len(offsets) != 1: + raise RootPatchError( + f"expected exactly one embedded source root, found {len(offsets)}" + ) + + offset = offsets[0] + patched = ( + payload[:offset] + + lab_anchor + + payload[offset + ROOT_CERT_SLOT_SIZE:] + ) + args.output.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile( + prefix=args.output.name + ".", dir=args.output.parent, delete=False + ) as temporary: + temporary.write(patched) + temporary_path = Path(temporary.name) + temporary_path.replace(args.output) + metadata = { + "format": 1, + "input": str(args.input.resolve()), + "output": args.output.name, + "trust_anchor_offset": offset, + "trust_anchor_slot_size": ROOT_CERT_SLOT_SIZE, + "input_sha256": hashlib.sha256(payload).hexdigest(), + "output_sha256": hashlib.sha256(patched).hexdigest(), + "lab_root_sha256": hashlib.sha256(lab_root).hexdigest(), + } + args.output.with_name(args.output.name + ".json").write_text( + json.dumps(metadata, indent=2) + "\n", encoding="utf-8" + ) + print( + f"Patched {args.output} ({len(patched)} bytes), " + f"trust anchor at 0x{offset:x}" + ) + + +if __name__ == "__main__": + try: + main() + except (OSError, RootPatchError) as error: + print(f"error: {error}", file=sys.stderr) + sys.exit(2) diff --git a/scripts/build-a6-lab-chain.sh b/scripts/build-a6-lab-chain.sh new file mode 100755 index 0000000..06bbbc3 --- /dev/null +++ b/scripts/build-a6-lab-chain.sh @@ -0,0 +1,66 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later + +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +source_dir=$(CDPATH= cd -- "$script_dir/.." && pwd) + +usage() +{ + echo "usage: $0 SECUREROM OUTPUT_DIRECTORY [BOOT_NONCE_HEX]" >&2 + exit 2 +} + +[ "$#" -ge 2 ] && [ "$#" -le 3 ] || usage +source_rom=$1 +output_dir=$2 +boot_nonce=${3:-${A6_BOOT_NONCE:-8d82693c897d1b9d}} +ibss_payload=${A6_IBSS_PAYLOAD:-"$source_dir/firmware/iBSS.iphone5.RELEASE.bin"} +ibec_payload=${A6_IBEC_PAYLOAD:-"$source_dir/firmware/iBEC.iphone5.RELEASE.bin"} + +for input in \ + "$source_rom" \ + "$source_dir/firmware/iBSS.iphone5.RELEASE.dfu" \ + "$source_dir/firmware/iBEC.iphone5.RELEASE.dfu" \ + "$ibss_payload" \ + "$ibec_payload"; do + if [ ! -f "$input" ]; then + echo "required input not found: $input" >&2 + exit 1 + fi +done + +python3 "$script_dir/a6-lab-identity.py" "$source_rom" "$output_dir" +output_dir=$(CDPATH= cd -- "$output_dir" && pwd) + +python3 "$script_dir/a6-lab-patch-iboot-root.py" \ + --source-rom "$source_rom" --identity "$output_dir" \ + --input "$ibss_payload" \ + --output "$output_dir/iBSS.lab-root.bin" +python3 "$script_dir/a6-lab-patch-iboot-root.py" \ + --source-rom "$source_rom" --identity "$output_dir" \ + --input "$ibec_payload" \ + --output "$output_dir/iBEC.lab-root.bin" + +python3 "$script_dir/a6-lab-img3.py" \ + --template "$source_dir/firmware/iBSS.iphone5.RELEASE.dfu" \ + --payload "$output_dir/iBSS.lab-root.bin" \ + --identity "$output_dir" --encrypt \ + --output "$output_dir/iBSS.chain-encrypted.dfu" +python3 "$script_dir/a6-lab-img3.py" \ + --template "$source_dir/firmware/iBEC.iphone5.RELEASE.dfu" \ + --payload "$output_dir/iBEC.lab-root.bin" \ + --identity "$output_dir" --encrypt --ticketed \ + --output "$output_dir/iBEC.ticketed-encrypted.img3" + +python3 "$script_dir/a6-lab-apticket.py" \ + --component "$output_dir/iBEC.ticketed-encrypted.img3" \ + --identity "$output_dir" \ + --boot-nonce "$boot_nonce" \ + --ticket-output "$output_dir/apticket-nonce.der" \ + --output "$output_dir/iBEC.chain-nonce-encrypted.dfu" + +echo +echo "A6 lab chain created in $output_dir" +echo "Run: A6_LAB_DIR='$output_dir' ./Run-iPhone5-macOS.sh" diff --git a/scripts/build-macos-recovery-tools.sh b/scripts/build-macos-recovery-tools.sh new file mode 100755 index 0000000..22175fc --- /dev/null +++ b/scripts/build-macos-recovery-tools.sh @@ -0,0 +1,136 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later + +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +source_dir=$(CDPATH= cd -- "$script_dir/.." && pwd) +build_root=${A6_RECOVERY_BUILD_DIR:-"$source_dir/build/macos-recovery"} +prefix=${A6_RECOVERY_PREFIX:-"$source_dir/build/limd-prefix"} +downloads="$build_root/downloads" + +libirecovery_commit=95dec3aa25b1e30654ca107eb971971f6a216520 +libirecovery_sha256=fcd91f2d5c6c3d70bba5b6f790ae37e +libirecovery_sha256=${libirecovery_sha256}dcc2ff9419ac56de67770bb5f1cc2eca9 +idevicerestore_commit=540c352c4c44896f7415abef87a166e8bbaea9b0 +idevicerestore_sha256=d971449c0838fe6733e6cd18268ffa2e5 +idevicerestore_sha256=${idevicerestore_sha256}499b8d9034e10cf2984c2c5835913b3 + +libirecovery_archive="$downloads/libirecovery-$libirecovery_commit.tar.gz" +idevicerestore_archive="$downloads/idevicerestore-$idevicerestore_commit.tar.gz" +github_base=https://github.com/libimobiledevice +libirecovery_url="$github_base/libirecovery/archive/$libirecovery_commit.tar.gz" +idevicerestore_path="idevicerestore/archive/$idevicerestore_commit.tar.gz" +idevicerestore_url="$github_base/$idevicerestore_path" + +fail() +{ + echo "error: $*" >&2 + exit 1 +} + +for command_name in curl shasum tar patch autoreconf pkg-config make; do + command -v "$command_name" >/dev/null 2>&1 || + fail "required command not found: $command_name" +done + +if command -v brew >/dev/null 2>&1; then + brew_prefix=$(brew --prefix) +else + brew_prefix=/usr/local +fi + +pkg_config_path="$prefix/lib/pkgconfig:$brew_prefix/lib/pkgconfig" +pkg_config_path="$pkg_config_path:$brew_prefix/opt/libusb/lib/pkgconfig" +if [ -n "${PKG_CONFIG_PATH:-}" ]; then + pkg_config_path="$pkg_config_path:$PKG_CONFIG_PATH" +fi +export PKG_CONFIG_PATH=$pkg_config_path + +for package in \ + libimobiledevice-glue-1.0 \ + libimobiledevice-1.0 \ + libusbmuxd-2.0 \ + libplist-2.0 \ + libtatsu-1.0 \ + libzip \ + libcurl \ + zlib; do + pkg-config --exists "$package" || fail \ + "missing $package dependency; run: brew install libimobiledevice libusb libzip" +done + +mkdir -p "$downloads" "$prefix" + +fetch_verified() +{ + archive=$1 + url=$2 + expected=$3 + temporary="$archive.tmp" + + if [ -f "$archive" ]; then + actual=$(shasum -a 256 "$archive" | awk '{print $1}') + if [ "$actual" = "$expected" ]; then + return + fi + fi + + echo "Downloading: $url" + curl -fsSL "$url" -o "$temporary" + actual=$(shasum -a 256 "$temporary" | awk '{print $1}') + [ "$actual" = "$expected" ] || + fail "unexpected SHA-256 for $url: $actual" + mv "$temporary" "$archive" +} + +fetch_verified "$libirecovery_archive" "$libirecovery_url" \ + "$libirecovery_sha256" +fetch_verified "$idevicerestore_archive" "$idevicerestore_url" \ + "$idevicerestore_sha256" + +work_dir=$(mktemp -d "$build_root/source.XXXXXX") +case $work_dir in + "$build_root"/source.*) ;; + *) fail "unexpected temporary directory: $work_dir" ;; +esac +trap 'rm -rf "$work_dir"' EXIT HUP INT TERM + +tar -xzf "$libirecovery_archive" -C "$work_dir" +tar -xzf "$idevicerestore_archive" -C "$work_dir" + +libirecovery_source="$work_dir/libirecovery-$libirecovery_commit" +idevicerestore_source="$work_dir/idevicerestore-$idevicerestore_commit" + +echo "Applying the QEMU backend to libirecovery" +patch -d "$libirecovery_source" -p1 \ + -i "$script_dir/patches/libirecovery-qemu.patch" +printf '%s\n' '1.3.1-qemu-a6' > "$libirecovery_source/.tarball-version" +( + cd "$libirecovery_source" + autoreconf -fiv + ./configure --prefix="$prefix" + make -j4 + make install +) + +printf '%s\n' '1.0.0-qemu-a6' > "$idevicerestore_source/.tarball-version" +echo "Applying boot-only mode without filesystem extraction" +patch -d "$idevicerestore_source" -p1 \ + -i "$script_dir/patches/idevicerestore-no-restore-fs.patch" +echo "Applying QEMU boot support for a local lab identity" +patch -d "$idevicerestore_source" -p1 \ + -i "$script_dir/patches/idevicerestore-qemu-lab-boot.patch" +( + cd "$idevicerestore_source" + autoreconf -fiv + ./configure --prefix="$prefix" + make -j4 + make install +) + +echo +echo "Tools installed in $prefix/bin" +echo "Start QEMU with ./Run-iPhone5-macOS.sh, then run:" +echo " scripts/irecovery-qemu -q" +echo " scripts/idevicerestore-qemu -d -y /path/to/Restore.ipsw" diff --git a/scripts/fetch-a6-bootloaders.py b/scripts/fetch-a6-bootloaders.py new file mode 100755 index 0000000..e42f3a0 --- /dev/null +++ b/scripts/fetch-a6-bootloaders.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later + +"""Fetch only A6 boot-chain files from Apple's iOS 10.3.4 IPSW. + +The complete IPSW is about 2 GB. This utility reads its ZIP directory with +HTTP range requests, then downloads only BuildManifest.plist and the selected +iBSS/iBEC pair. It does not personalize or decrypt the images. +""" + +import argparse +import binascii +import os +from pathlib import Path +import re +import struct +import sys +import urllib.request +import zlib + + +DEFAULT_URL = ( + "https://updates.cdn-apple.com/2019/ios/" + "091-25277-20190722-0C1B94DE-992C-11E9-A2EE-E2C9A77C2E40/" + "iPhone_4.0_32bit_10.3.4_14G61_Restore.ipsw" +) +EOCD = struct.Struct("<4s4H2IH") +CENTRAL = struct.Struct("<4s6H3I5H2I") +LOCAL = struct.Struct("<4s5H3I2H") + + +class RangeReader: + def __init__(self, url): + self.url = url + self.total_size = None + + def read(self, start, end): + request = urllib.request.Request( + self.url, + headers={"Range": f"bytes={start}-{end}"}, + ) + with urllib.request.urlopen(request, timeout=60) as response: + if response.status != 206: + raise RuntimeError( + f"server ignored byte range {start}-{end} " + f"(HTTP {response.status})" + ) + content_range = response.headers.get("Content-Range", "") + match = re.fullmatch(r"bytes (\d+)-(\d+)/(\d+)", content_range) + if not match: + raise RuntimeError(f"invalid Content-Range: {content_range!r}") + actual_start, actual_end, total = map(int, match.groups()) + if (actual_start, actual_end) != (start, end): + raise RuntimeError( + f"unexpected byte range {actual_start}-{actual_end}" + ) + self.total_size = total + data = response.read(end - start + 1) + if len(data) != end - start + 1: + raise RuntimeError("truncated HTTP range response") + return data + + def get_size(self): + self.read(0, 0) + return self.total_size + + +def read_directory(reader): + total_size = reader.get_size() + tail_size = min(total_size, 65557) + tail_offset = total_size - tail_size + tail = reader.read(tail_offset, total_size - 1) + eocd_offset = tail.rfind(b"PK\x05\x06") + if eocd_offset < 0 or eocd_offset + EOCD.size > len(tail): + raise RuntimeError("ZIP end-of-central-directory record not found") + + fields = EOCD.unpack_from(tail, eocd_offset) + ( + _, disk, directory_disk, disk_entries, entries, + size, offset, comment, + ) = fields + if disk or directory_disk or disk_entries != entries: + raise RuntimeError("multi-disk ZIP archives are unsupported") + if eocd_offset + EOCD.size + comment > len(tail): + raise RuntimeError("truncated ZIP comment") + if offset == 0xFFFFFFFF or size == 0xFFFFFFFF or entries == 0xFFFF: + raise RuntimeError("ZIP64 directory is unsupported") + + data = reader.read(offset, offset + size - 1) + result = {} + cursor = 0 + for _ in range(entries): + if cursor + CENTRAL.size > len(data): + raise RuntimeError("truncated ZIP central directory") + fields = CENTRAL.unpack_from(data, cursor) + if fields[0] != b"PK\x01\x02": + raise RuntimeError("invalid ZIP central-directory signature") + flags = fields[3] + compressed_size = fields[8] + uncompressed_size = fields[9] + name_length = fields[10] + extra_length = fields[11] + comment_length = fields[12] + local_offset = fields[16] + name_start = cursor + CENTRAL.size + name_end = name_start + name_length + encoding = "utf-8" if flags & 0x800 else "cp437" + name = data[name_start:name_end].decode(encoding) + result[name] = { + "flags": flags, + "method": fields[4], + "crc32": fields[7], + "compressed_size": compressed_size, + "uncompressed_size": uncompressed_size, + "local_offset": local_offset, + } + cursor = name_end + extra_length + comment_length + return result + + +def extract_entry(reader, entry): + if entry["flags"] & 1: + raise RuntimeError("encrypted ZIP entries are unsupported") + if entry["compressed_size"] == 0xFFFFFFFF: + raise RuntimeError("ZIP64 entries are unsupported") + + local_offset = entry["local_offset"] + header = reader.read(local_offset, local_offset + LOCAL.size - 1) + fields = LOCAL.unpack(header) + if fields[0] != b"PK\x03\x04": + raise RuntimeError("invalid ZIP local-header signature") + name_length = fields[9] + extra_length = fields[10] + data_offset = local_offset + LOCAL.size + name_length + extra_length + compressed_size = entry["compressed_size"] + compressed = reader.read(data_offset, data_offset + compressed_size - 1) + + if entry["method"] == 0: + data = compressed + elif entry["method"] == 8: + data = zlib.decompress(compressed, -zlib.MAX_WBITS) + else: + raise RuntimeError(f"unsupported ZIP method {entry['method']}") + if len(data) != entry["uncompressed_size"]: + raise RuntimeError("uncompressed size mismatch") + if binascii.crc32(data) & 0xFFFFFFFF != entry["crc32"]: + raise RuntimeError("CRC-32 mismatch") + return data + + +def write_atomic(path, data): + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_name(path.name + ".tmp") + temporary.write_bytes(data) + os.replace(temporary, path) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--board", choices=("n41", "n42"), default="n41") + parser.add_argument("--url", default=DEFAULT_URL) + parser.add_argument("--output-dir", type=Path, default=Path("firmware")) + args = parser.parse_args() + + reader = RangeReader(args.url) + directory = read_directory(reader) + requested = ["BuildManifest.plist"] + ipsw_board = "iphone5" + for component in ("iBSS", "iBEC"): + candidates = ( + f"Firmware/dfu/{component}.{args.board}.RELEASE.dfu", + f"Firmware/dfu/{component}.{args.board}ap.RELEASE.dfu", + f"Firmware/dfu/{component}.{ipsw_board}.RELEASE.dfu", + ) + match = next((name for name in candidates if name in directory), None) + if not match: + available = sorted( + name for name in directory + if "/dfu/" in name.lower() and component.lower() in name.lower() + ) + raise RuntimeError( + "missing IPSW entry; tried: " + ", ".join(candidates) + + "; available: " + (", ".join(available) or "none") + ) + requested.append(match) + + for name in requested: + destination = args.output_dir / Path(name).name + print(f"Fetching {name} -> {destination}", flush=True) + write_atomic(destination, extract_entry(reader, directory[name])) + print("Raw Apple images extracted; personalization is still required.") + + +if __name__ == "__main__": + try: + main() + except Exception as error: + print(f"error: {error}", file=sys.stderr) + sys.exit(1) diff --git a/scripts/idevicerestore-qemu b/scripts/idevicerestore-qemu new file mode 100755 index 0000000..bf55e86 --- /dev/null +++ b/scripts/idevicerestore-qemu @@ -0,0 +1,33 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later + +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +source_dir=$(CDPATH= cd -- "$script_dir/.." && pwd) +prefix=${A6_RECOVERY_PREFIX:-"$source_dir/build/limd-prefix"} +endpoint=${QEMU_USB_ENDPOINT:-127.0.0.1:26050} +lab_dir=${A6_LAB_DIR:-} +binary="$prefix/bin/idevicerestore" + +if [ ! -x "$binary" ]; then + echo "QEMU-enabled idevicerestore not found: $binary" >&2 + echo "Run scripts/build-macos-recovery-tools.sh first." >&2 + exit 1 +fi + +export LIBIRECOVERY_QEMU=$endpoint +export DYLD_LIBRARY_PATH="$prefix/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" +if [ -n "$lab_dir" ]; then + lab_dir=$(CDPATH= cd -- "$lab_dir" && pwd) + for image in iBSS.chain-encrypted.dfu iBEC.chain-nonce-encrypted.dfu; do + if [ ! -f "$lab_dir/$image" ]; then + echo "Lab image not found: $lab_dir/$image" >&2 + exit 1 + fi + done + export IDEVICERESTORE_QEMU_LAB_DIR=$lab_dir + export IDEVICERESTORE_QEMU_LAB_BOOT_ONLY=1 + exec "$binary" -z "$@" +fi +exec "$binary" "$@" diff --git a/scripts/irecovery-qemu b/scripts/irecovery-qemu new file mode 100755 index 0000000..2024401 --- /dev/null +++ b/scripts/irecovery-qemu @@ -0,0 +1,20 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-or-later + +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +source_dir=$(CDPATH= cd -- "$script_dir/.." && pwd) +prefix=${A6_RECOVERY_PREFIX:-"$source_dir/build/limd-prefix"} +endpoint=${QEMU_USB_ENDPOINT:-127.0.0.1:26050} +binary="$prefix/bin/irecovery" + +if [ ! -x "$binary" ]; then + echo "QEMU-enabled irecovery not found: $binary" >&2 + echo "Run scripts/build-macos-recovery-tools.sh first." >&2 + exit 1 +fi + +export LIBIRECOVERY_QEMU=$endpoint +export DYLD_LIBRARY_PATH="$prefix/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" +exec "$binary" "$@" diff --git a/scripts/patches/idevicerestore-no-restore-fs.patch b/scripts/patches/idevicerestore-no-restore-fs.patch new file mode 100644 index 0000000..302a739 --- /dev/null +++ b/scripts/patches/idevicerestore-no-restore-fs.patch @@ -0,0 +1,12 @@ +--- a/src/idevicerestore.c ++++ b/src/idevicerestore.c +@@ -1136,7 +1136,8 @@ int idevicerestore_start(struct idevicerestore_client_t* client) + } + } + +- if (needs_os_extraction && !(client->flags & FLAG_SHSHONLY)) { ++ if (needs_os_extraction && !(client->flags & FLAG_SHSHONLY) && ++ !(client->flags & FLAG_NO_RESTORE)) { + char* tmpf = NULL; + struct stat st; + if (client->cache_dir) { diff --git a/scripts/patches/idevicerestore-qemu-lab-boot.patch b/scripts/patches/idevicerestore-qemu-lab-boot.patch new file mode 100644 index 0000000..1ca6ace --- /dev/null +++ b/scripts/patches/idevicerestore-qemu-lab-boot.patch @@ -0,0 +1,116 @@ +--- a/src/dfu.c ++++ b/src/dfu.c +@@ -127,6 +127,9 @@ + int dfu_send_component(struct idevicerestore_client_t* client, plist_t build_identity, const char* component) + { + char* path = NULL; ++ const char* qemu_lab_dir = getenv("IDEVICERESTORE_QEMU_LAB_DIR"); ++ int qemu_lab_component = qemu_lab_dir && qemu_lab_dir[0] && ++ (!strcmp(component, "iBSS") || !strcmp(component, "iBEC")); + + // Use a specific TSS ticket for the Ap,LocalPolicy component + plist_t tss = client->tss; +@@ -137,7 +140,26 @@ int dfu_send_component(struct idevicerestore_client_t* client, plist_t build_ide + void* component_data = NULL; + size_t component_size = 0; + +- if (strcmp(component, "Ap,LocalPolicy") == 0) { ++ if (qemu_lab_component) { ++ const char* filename = !strcmp(component, "iBSS") ? ++ "iBSS.chain-encrypted.dfu" : ++ "iBEC.chain-nonce-encrypted.dfu"; ++ size_t path_size = strlen(qemu_lab_dir) + strlen(filename) + 2; ++ path = malloc(path_size); ++ if (!path) { ++ logger(LL_ERROR, "Out of memory\n"); ++ return -1; ++ } ++ snprintf(path, path_size, "%s/%s", qemu_lab_dir, filename); ++ if (read_file(path, &component_data, &component_size) < 0) { ++ logger(LL_ERROR, "Unable to read QEMU lab %s from %s\n", component, path); ++ free(path); ++ return -1; ++ } ++ logger(LL_INFO, "Using QEMU lab %s from %s\n", component, path); ++ free(path); ++ path = NULL; ++ } else if (strcmp(component, "Ap,LocalPolicy") == 0) { + // If Ap,LocalPolicy => Inject an empty policy + component_data = malloc(sizeof(lpol_file)); + component_size = sizeof(lpol_file); +@@ -168,7 +190,11 @@ int dfu_send_component(struct idevicerestore_client_t* client, plist_t build_ide + void* data = NULL; + size_t size = 0; + +- if (personalize_component(client, component, component_data, component_size, tss, &data, &size) < 0) { ++ if (qemu_lab_component) { ++ data = component_data; ++ size = component_size; ++ component_data = NULL; ++ } else if (personalize_component(client, component, component_data, component_size, tss, &data, &size) < 0) { + logger(LL_ERROR, "Unable to get personalized component: %s\n", component); + free(component_data); + return -1; +@@ -176,7 +202,7 @@ int dfu_send_component(struct idevicerestore_client_t* client, plist_t build_ide + free(component_data); + component_data = NULL; + +- if (!client->image4supported && client->build_major > 8 && !(client->flags & FLAG_CUSTOM) && !strcmp(component, "iBEC")) { ++ if (!qemu_lab_component && !client->image4supported && client->build_major > 8 && !(client->flags & FLAG_CUSTOM) && !strcmp(component, "iBEC")) { + unsigned char* ticket = NULL; + unsigned int tsize = 0; + if (tss_response_get_ap_ticket(client->tss, &ticket, &tsize) < 0) { +@@ -517,7 +543,8 @@ int dfu_enter_recovery(struct idevicerestore_client_t* client, plist_t build_ide + logger(LL_INFO, "Nonce: "); + logger_dump_hex(LL_INFO, client->nonce, client->nonce_size); + +- if (nonce_changed && !(client->flags & FLAG_CUSTOM)) { ++ if (nonce_changed && !(client->flags & FLAG_CUSTOM) && ++ !getenv("IDEVICERESTORE_QEMU_LAB_DIR")) { + // Welcome iOS5. We have to re-request the TSS with our nonce. + plist_free(client->tss); + if (get_tss_response(client, build_identity, &client->tss) < 0) { +--- a/src/idevicerestore.c ++++ b/src/idevicerestore.c +@@ -682,6 +682,11 @@ int idevicerestore_start(struct idevicerestore_client_t* client) + tss_enabled = 0; + logger(LL_INFO, "Custom firmware requested; TSS has been disabled.\n"); + } ++ if (getenv("IDEVICERESTORE_QEMU_LAB_DIR")) { ++ /* The QEMU wrapper supplies locally personalized iBSS/iBEC images. */ ++ tss_enabled = 0; ++ logger(LL_INFO, "QEMU lab boot requested; external TSS has been disabled.\n"); ++ } + + if (client->mode == MODE_RESTORE) { + if (!(client->flags & FLAG_ALLOW_RESTORE_MODE)) { +@@ -1111,11 +1116,16 @@ int idevicerestore_start(struct idevicerestore_client_t* client) + + /* check if all components we need are actually there */ + logger(LL_INFO, "Checking IPSW for required components...\n"); +- if (build_identity_check_components_in_ipsw(build_identity, client->ipsw) < 0) { ++ if (!getenv("IDEVICERESTORE_QEMU_LAB_BOOT_ONLY") && ++ build_identity_check_components_in_ipsw(build_identity, client->ipsw) < 0) { + logger(LL_ERROR, "Could not find all required components in IPSW %s\n", client->ipsw->path); + return -1; + } +- logger(LL_INFO, "All required components found in IPSW\n"); ++ if (getenv("IDEVICERESTORE_QEMU_LAB_BOOT_ONLY")) { ++ logger(LL_INFO, "QEMU lab boot only needs BuildManifest plus local iBSS/iBEC.\n"); ++ } else { ++ logger(LL_INFO, "All required components found in IPSW\n"); ++ } + + /* Get OS (filesystem) name from build identity */ + char* os_path = NULL; +@@ -1466,6 +1476,10 @@ int idevicerestore_start(struct idevicerestore_client_t* client) + mutex_unlock(&client->device_event_mutex); + } + idevicerestore_progress(client, RESTORE_STEP_PREPARE, 0.5); ++ if (getenv("IDEVICERESTORE_QEMU_LAB_BOOT_ONLY")) { ++ logger(LL_INFO, "QEMU lab iBEC is running in Recovery mode.\n"); ++ return 0; ++ } + if (client->flags & FLAG_QUIT) { + return -1; + } diff --git a/scripts/patches/libirecovery-qemu.patch b/scripts/patches/libirecovery-qemu.patch new file mode 100644 index 0000000..cba42cf --- /dev/null +++ b/scripts/patches/libirecovery-qemu.patch @@ -0,0 +1,700 @@ +--- a/src/libirecovery.c ++++ b/src/libirecovery.c +@@ -31,6 +31,12 @@ + #include + #include + #include ++#ifndef _WIN32 ++#include ++#include ++#include ++#include ++#endif + + #include + #include +@@ -119,6 +125,11 @@ + int usb_alt_interface; + unsigned int mode; + int isKIS; ++ int qemu_transport; ++ int qemu_fd; ++ uint32_t qemu_request_id; ++ uint32_t qemu_generation; ++ uint32_t qemu_poll_timeout_ms; + struct irecv_device_info device_info; + #ifndef USE_DUMMY + #ifndef _WIN32 +@@ -163,6 +173,214 @@ + #define debug(...) if (libirecovery_debug) fprintf(stderr, __VA_ARGS__) + + static int libirecovery_debug = 0; ++ ++#ifndef _WIN32 ++#define QA6_USB_MAGIC 0x55364151u ++#define QA6_USB_PROTOCOL_VERSION 1u ++#define QA6_USB_MESSAGE_REQUEST 1u ++#define QA6_USB_MESSAGE_RESPONSE 2u ++#define QA6_USB_MESSAGE_INFO_REQUEST 3u ++#define QA6_USB_MESSAGE_INFO_RESPONSE 4u ++#define QA6_USB_MESSAGE_BULK_REQUEST 5u ++#define QA6_USB_MESSAGE_BULK_RESPONSE 6u ++#define QA6_USB_MESSAGE_RESET_REQUEST 7u ++#define QA6_USB_MESSAGE_RESET_RESPONSE 8u ++#define QA6_USB_STATUS_SUCCESS 0 ++#define QA6_USB_STATUS_STALL -1 ++#define QA6_USB_STATUS_DISCONNECTED -2 ++#define QA6_USB_STATUS_PROTOCOL -3 ++#define QA6_USB_STATUS_UNSUPPORTED -4 ++#define QA6_USB_MAX_PAYLOAD 65535u ++#define QA6_USB_SERIAL_MAX 256u ++#define QA6_USB_DFU_STATE_ERROR 10u ++#define QA6_USB_DFU_STATE_WAIT_RESET 8u ++ ++#pragma pack(push, 1) ++typedef struct qemu_a6_usb_frame_header { ++ uint32_t magic; ++ uint16_t version; ++ uint16_t type; ++ uint32_t request_id; ++ int32_t status; ++ uint32_t payload_length; ++ uint32_t transfer_length; ++ uint8_t setup[8]; ++} qemu_a6_usb_frame_header; ++ ++typedef struct qemu_a6_usb_device_info { ++ uint32_t generation; ++ uint16_t vendor_id; ++ uint16_t product_id; ++ uint8_t device_class; ++ uint8_t device_subclass; ++ uint8_t device_protocol; ++ uint8_t is_dfu; ++ char serial[QA6_USB_SERIAL_MAX]; ++} qemu_a6_usb_device_info; ++#pragma pack(pop) ++ ++static const char *qemu_a6_endpoint(void) ++{ ++ const char *endpoint = getenv("LIBIRECOVERY_QEMU"); ++ ++ return endpoint && endpoint[0] ? endpoint : NULL; ++} ++ ++static int qemu_a6_send_all(int fd, const void *data, size_t length) ++{ ++ const uint8_t *cursor = data; ++ ++ while (length) { ++ ssize_t sent = send(fd, cursor, length, 0); ++ if (sent < 0 && errno == EINTR) { ++ continue; ++ } ++ if (sent <= 0) { ++ return -1; ++ } ++ cursor += sent; ++ length -= sent; ++ } ++ return 0; ++} ++ ++static int qemu_a6_recv_all(int fd, void *data, size_t length) ++{ ++ uint8_t *cursor = data; ++ ++ while (length) { ++ ssize_t received = recv(fd, cursor, length, 0); ++ if (received < 0 && errno == EINTR) { ++ continue; ++ } ++ if (received < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { ++ return -2; ++ } ++ if (received <= 0) { ++ return -1; ++ } ++ cursor += received; ++ length -= received; ++ } ++ return 0; ++} ++ ++static int qemu_a6_connect(void) ++{ ++ const char *endpoint = qemu_a6_endpoint(); ++ char host[256]; ++ char port[16]; ++ const char *separator; ++ struct addrinfo hints; ++ struct addrinfo *addresses = NULL; ++ struct addrinfo *address; ++ struct timeval timeout = { 1, 0 }; ++ int fd = -1; ++ ++ if (!endpoint || !(separator = strrchr(endpoint, ':')) || ++ separator == endpoint || !separator[1] || ++ (size_t)(separator - endpoint) >= sizeof(host) || ++ strlen(separator + 1) >= sizeof(port)) { ++ return -1; ++ } ++ memcpy(host, endpoint, separator - endpoint); ++ host[separator - endpoint] = '\0'; ++ strcpy(port, separator + 1); ++ ++ memset(&hints, 0, sizeof(hints)); ++ hints.ai_family = AF_UNSPEC; ++ hints.ai_socktype = SOCK_STREAM; ++ if (getaddrinfo(host, port, &hints, &addresses) != 0) { ++ return -1; ++ } ++ for (address = addresses; address; address = address->ai_next) { ++ fd = socket(address->ai_family, address->ai_socktype, ++ address->ai_protocol); ++ if (fd < 0) { ++ continue; ++ } ++ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); ++ setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); ++ if (connect(fd, address->ai_addr, address->ai_addrlen) == 0) { ++ break; ++ } ++ close(fd); ++ fd = -1; ++ } ++ freeaddrinfo(addresses); ++ return fd; ++} ++ ++static int qemu_a6_exchange(int fd, qemu_a6_usb_frame_header *request, ++ const void *out_data, ++ qemu_a6_usb_frame_header *response, ++ void *in_data, size_t in_capacity) ++{ ++ int receive_result; ++ ++ if (qemu_a6_send_all(fd, request, sizeof(*request)) < 0 || ++ (request->payload_length && ++ qemu_a6_send_all(fd, out_data, request->payload_length) < 0)) { ++ return IRECV_E_NO_DEVICE; ++ } ++ receive_result = qemu_a6_recv_all(fd, response, sizeof(*response)); ++ if (receive_result == -2) { ++ return IRECV_E_TIMEOUT; ++ } ++ if (receive_result < 0) { ++ return IRECV_E_NO_DEVICE; ++ } ++ if (response->magic != QA6_USB_MAGIC || ++ response->version != QA6_USB_PROTOCOL_VERSION || ++ response->request_id != request->request_id || ++ response->payload_length > QA6_USB_MAX_PAYLOAD || ++ response->payload_length > in_capacity) { ++ return IRECV_E_UNKNOWN_ERROR; ++ } ++ if (response->payload_length && ++ qemu_a6_recv_all(fd, in_data, response->payload_length) < 0) { ++ return IRECV_E_NO_DEVICE; ++ } ++ switch (response->status) { ++ case QA6_USB_STATUS_SUCCESS: ++ return IRECV_E_SUCCESS; ++ case QA6_USB_STATUS_STALL: ++ return IRECV_E_PIPE; ++ case QA6_USB_STATUS_DISCONNECTED: ++ return IRECV_E_NO_DEVICE; ++ case QA6_USB_STATUS_UNSUPPORTED: ++ return IRECV_E_UNSUPPORTED; ++ case QA6_USB_STATUS_PROTOCOL: ++ default: ++ return IRECV_E_UNKNOWN_ERROR; ++ } ++} ++ ++static int qemu_a6_read_info_fd(int fd, qemu_a6_usb_device_info *info) ++{ ++ qemu_a6_usb_frame_header request; ++ qemu_a6_usb_frame_header response; ++ int error; ++ ++ memset(&request, 0, sizeof(request)); ++ request.magic = QA6_USB_MAGIC; ++ request.version = QA6_USB_PROTOCOL_VERSION; ++ request.type = QA6_USB_MESSAGE_INFO_REQUEST; ++ request.request_id = 1; ++ error = qemu_a6_exchange(fd, &request, NULL, &response, ++ info, sizeof(*info)); ++ if (error != IRECV_E_SUCCESS) { ++ return error; ++ } ++ if (response.type != QA6_USB_MESSAGE_INFO_RESPONSE || ++ response.payload_length != sizeof(*info) || ++ response.transfer_length != sizeof(*info)) { ++ return IRECV_E_UNKNOWN_ERROR; ++ } ++ info->serial[sizeof(info->serial) - 1] = '\0'; ++ return IRECV_E_SUCCESS; ++} ++#endif + #ifndef USE_DUMMY + #ifndef _WIN32 + #ifndef HAVE_IOKIT +@@ -930,6 +1149,199 @@ + } + } + ++#ifndef _WIN32 ++static irecv_error_t qemu_a6_open_with_ecid(irecv_client_t *pclient, ++ uint64_t ecid) ++{ ++ qemu_a6_usb_device_info info; ++ irecv_client_t client; ++ int fd; ++ int error; ++ ++ *pclient = NULL; ++ fd = qemu_a6_connect(); ++ if (fd < 0) { ++ return IRECV_E_UNABLE_TO_CONNECT; ++ } ++ memset(&info, 0, sizeof(info)); ++ error = qemu_a6_read_info_fd(fd, &info); ++ if (error != IRECV_E_SUCCESS) { ++ close(fd); ++ return error; ++ } ++ if (info.vendor_id != APPLE_VENDOR_ID || ++ (info.product_id != IRECV_K_DFU_MODE && ++ info.product_id != IRECV_K_WTF_MODE && ++ info.product_id != IRECV_K_PORT_DFU_MODE && ++ (info.product_id < IRECV_K_RECOVERY_MODE_1 || ++ info.product_id > IRECV_K_RECOVERY_MODE_4))) { ++ close(fd); ++ return IRECV_E_NO_DEVICE; ++ } ++ ++ client = calloc(1, sizeof(*client)); ++ if (!client) { ++ close(fd); ++ return IRECV_E_OUT_OF_MEMORY; ++ } ++ client->qemu_transport = 1; ++ client->qemu_fd = fd; ++ client->qemu_request_id = 1; ++ client->qemu_generation = info.generation; ++ client->mode = info.product_id; ++ client->usb_interface = 0; ++ irecv_load_device_info_from_iboot_string(client, info.serial); ++ if (ecid && ecid != IRECV_K_WTF_MODE && ++ client->device_info.ecid != ecid) { ++ irecv_close(client); ++ return IRECV_E_NO_DEVICE; ++ } ++ debug("opening QEMU A6 device %04x:%04x generation %u...\n", ++ info.vendor_id, info.product_id, info.generation); ++ *pclient = client; ++ return IRECV_E_SUCCESS; ++} ++ ++static int qemu_a6_control_transfer(irecv_client_t client, ++ uint8_t bm_request_type, ++ uint8_t b_request, uint16_t w_value, ++ uint16_t w_index, unsigned char *data, ++ uint16_t w_length) ++{ ++ qemu_a6_usb_frame_header request; ++ qemu_a6_usb_frame_header response; ++ int direction_in = (bm_request_type & 0x80) != 0; ++ int error; ++ ++ memset(&request, 0, sizeof(request)); ++ request.magic = QA6_USB_MAGIC; ++ request.version = QA6_USB_PROTOCOL_VERSION; ++ request.type = QA6_USB_MESSAGE_REQUEST; ++ request.request_id = ++client->qemu_request_id; ++ request.payload_length = direction_in ? 0 : w_length; ++ request.transfer_length = w_length; ++ request.setup[0] = bm_request_type; ++ request.setup[1] = b_request; ++ request.setup[2] = w_value & 0xff; ++ request.setup[3] = w_value >> 8; ++ request.setup[4] = w_index & 0xff; ++ request.setup[5] = w_index >> 8; ++ request.setup[6] = w_length & 0xff; ++ request.setup[7] = w_length >> 8; ++ error = qemu_a6_exchange(client->qemu_fd, &request, ++ direction_in ? NULL : data, &response, ++ direction_in ? data : NULL, ++ direction_in ? w_length : 0); ++ if (error != IRECV_E_SUCCESS) { ++ return error; ++ } ++ if (response.type != QA6_USB_MESSAGE_RESPONSE || ++ response.transfer_length > w_length) { ++ return IRECV_E_UNKNOWN_ERROR; ++ } ++ if (bm_request_type == 0xa1 && b_request == 3 && data && ++ response.transfer_length >= 4) { ++ client->qemu_poll_timeout_ms = data[1] | ++ (data[2] << 8) | (data[3] << 16); ++ } ++ return response.transfer_length; ++} ++ ++static int qemu_a6_bulk_transfer(irecv_client_t client, ++ unsigned char endpoint, ++ unsigned char *data, int length, ++ int *transferred, unsigned int timeout) ++{ ++ qemu_a6_usb_frame_header request; ++ qemu_a6_usb_frame_header response; ++ int direction_in = (endpoint & 0x80) != 0; ++ int error; ++ ++ if (length < 0 || (unsigned int)length > QA6_USB_MAX_PAYLOAD) { ++ return IRECV_E_INVALID_INPUT; ++ } ++ memset(&request, 0, sizeof(request)); ++ request.magic = QA6_USB_MAGIC; ++ request.version = QA6_USB_PROTOCOL_VERSION; ++ request.type = QA6_USB_MESSAGE_BULK_REQUEST; ++ request.request_id = ++client->qemu_request_id; ++ request.payload_length = direction_in ? 0 : length; ++ request.transfer_length = length; ++ request.setup[0] = endpoint; ++ request.setup[1] = timeout & 0xff; ++ request.setup[2] = (timeout >> 8) & 0xff; ++ request.setup[3] = (timeout >> 16) & 0xff; ++ request.setup[4] = (timeout >> 24) & 0xff; ++ error = qemu_a6_exchange(client->qemu_fd, &request, ++ direction_in ? NULL : data, &response, ++ direction_in ? data : NULL, ++ direction_in ? length : 0); ++ if (error != IRECV_E_SUCCESS) { ++ return error; ++ } ++ if (response.type != QA6_USB_MESSAGE_BULK_RESPONSE || ++ response.transfer_length > (unsigned int)length) { ++ return IRECV_E_UNKNOWN_ERROR; ++ } ++ *transferred = response.transfer_length; ++ return IRECV_E_SUCCESS; ++} ++ ++static irecv_error_t qemu_a6_reset(irecv_client_t client) ++{ ++ qemu_a6_usb_frame_header request; ++ qemu_a6_usb_frame_header response; ++ uint8_t dfu_status[6]; ++ int retry = 0; ++ int error; ++ while (client->qemu_poll_timeout_ms && retry++ < 5) { ++ uint32_t delay_ms = client->qemu_poll_timeout_ms > 10000 ? ++ 10000 : client->qemu_poll_timeout_ms; ++ debug("honoring QEMU DFU poll timeout: %u ms\n", delay_ms); ++ usleep((useconds_t)delay_ms * 1000); ++ client->qemu_poll_timeout_ms = 0; ++ memset(dfu_status, 0, sizeof(dfu_status)); ++ error = qemu_a6_control_transfer(client, 0xa1, 3, 0, 0, ++ dfu_status, sizeof(dfu_status)); ++ if (error != sizeof(dfu_status)) { ++ return error < 0 ? error : IRECV_E_USB_STATUS; ++ } ++ debug("QEMU DFU manifestation state=%u status=%u\n", ++ dfu_status[4], dfu_status[0]); ++ if (dfu_status[0] || dfu_status[4] == QA6_USB_DFU_STATE_ERROR) { ++ return IRECV_E_USB_UPLOAD; ++ } ++ if (dfu_status[4] == QA6_USB_DFU_STATE_WAIT_RESET) { ++ client->qemu_poll_timeout_ms = 0; ++ break; ++ } ++ } ++ if (client->qemu_poll_timeout_ms) { ++ return IRECV_E_TIMEOUT; ++ } ++ if (getenv("LIBIRECOVERY_QEMU_NO_RESET")) { ++ debug("leaving QEMU in DFU-WAIT-RESET for debugging\n"); ++ return IRECV_E_SUCCESS; ++ } ++ ++ memset(&request, 0, sizeof(request)); ++ request.magic = QA6_USB_MAGIC; ++ request.version = QA6_USB_PROTOCOL_VERSION; ++ request.type = QA6_USB_MESSAGE_RESET_REQUEST; ++ request.request_id = ++client->qemu_request_id; ++ error = qemu_a6_exchange(client->qemu_fd, &request, NULL, &response, ++ NULL, 0); ++ if (error != IRECV_E_SUCCESS) { ++ return error; ++ } ++ if (response.type != QA6_USB_MESSAGE_RESET_RESPONSE || ++ response.payload_length || response.transfer_length) { ++ return IRECV_E_UNKNOWN_ERROR; ++ } ++ return IRECV_E_SUCCESS; ++} ++#endif ++ + static void irecv_copy_nonce_with_tag_from_buffer(const char* tag, unsigned char** nonce, unsigned int* nonce_size, const char *buf) + { + int taglen = strlen(tag); +@@ -1006,6 +1350,11 @@ + + *nonce = NULL; + *nonce_size = 0; ++ if (client->qemu_transport) { ++ irecv_copy_nonce_with_tag_from_buffer( ++ tag, nonce, nonce_size, client->device_info.serial_string); ++ return; ++ } + + memset(buf, 0, sizeof(buf)); + len = irecv_get_string_descriptor_ascii(client, 1, (unsigned char*)buf, sizeof(buf)-1); +@@ -1406,7 +1755,8 @@ + + static int check_context(irecv_client_t client) + { +- if (client == NULL || client->handle == NULL) { ++ if (client == NULL || ++ (!client->qemu_transport && client->handle == NULL)) { + return IRECV_E_NO_DEVICE; + } + +@@ -1456,6 +1806,12 @@ + return IRECV_E_UNSUPPORTED; + #else + #ifndef _WIN32 ++ if (client->qemu_transport) { ++ return qemu_a6_control_transfer(client, bm_request_type, b_request, ++ w_value, w_index, data, w_length); ++ } ++#endif ++#ifndef _WIN32 + #ifdef HAVE_IOKIT + return iokit_usb_control_transfer(client, bm_request_type, b_request, w_value, w_index, data, w_length, timeout); + #else +@@ -1813,6 +2169,10 @@ + int ret; + + #ifndef _WIN32 ++ if (client->qemu_transport) { ++ return qemu_a6_bulk_transfer(client, endpoint, data, length, ++ transferred, timeout); ++ } + #ifdef HAVE_IOKIT + return iokit_usb_bulk_transfer(client, endpoint, data, length, transferred, timeout); + #else +@@ -2144,11 +2504,15 @@ + irecv_set_debug_level(libirecovery_debug); + } + #ifndef _WIN32 ++ if (qemu_a6_endpoint()) { ++ error = qemu_a6_open_with_ecid(pclient, ecid); ++ } else { + #ifdef HAVE_IOKIT +- error = iokit_open_with_ecid(pclient, ecid); ++ error = iokit_open_with_ecid(pclient, ecid); + #else +- error = libusb_open_with_ecid(pclient, ecid); ++ error = libusb_open_with_ecid(pclient, ecid); + #endif ++ } + #else + error = win32_open_with_ecid(pclient, ecid); + #endif +@@ -2166,12 +2530,14 @@ + } + + #ifdef HAVE_IOKIT +- error = (*client->handle)->CreateDeviceAsyncEventSource(client->handle, &client->async_event_source); +- if (error != IRECV_E_SUCCESS) { +- free(client); +- return error; ++ if (!client->qemu_transport) { ++ error = (*client->handle)->CreateDeviceAsyncEventSource(client->handle, &client->async_event_source); ++ if (error != IRECV_E_SUCCESS) { ++ free(client); ++ return error; ++ } ++ CFRunLoopAddSource(CFRunLoopGetCurrent(), client->async_event_source, kCFRunLoopDefaultMode); + } +- CFRunLoopAddSource(CFRunLoopGetCurrent(), client->async_event_source, kCFRunLoopDefaultMode); + #endif + + if (client->mode == IRECV_K_DFU_MODE || client->mode == IRECV_K_PORT_DFU_MODE || client->mode == IRECV_K_WTF_MODE || client->mode == KIS_PRODUCT_ID) { +@@ -2237,6 +2603,10 @@ + + #ifndef _WIN32 + debug("Setting to configuration %d\n", configuration); ++ if (client->qemu_transport) { ++ client->usb_config = configuration; ++ return IRECV_E_SUCCESS; ++ } + + #ifdef HAVE_IOKIT + IOReturn result; +@@ -2355,6 +2725,17 @@ + + debug("Setting to interface %d:%d\n", usb_interface, usb_alt_interface); + #ifndef _WIN32 ++ if (client->qemu_transport) { ++ if (usb_interface == 1 && ++ qemu_a6_control_transfer(client, 0x01, 0x0B, ++ usb_alt_interface, usb_interface, ++ NULL, 0) < 0) { ++ return IRECV_E_USB_INTERFACE; ++ } ++ client->usb_interface = usb_interface; ++ client->usb_alt_interface = usb_alt_interface; ++ return IRECV_E_SUCCESS; ++ } + #ifdef HAVE_IOKIT + if (iokit_usb_set_interface(client, usb_interface, usb_alt_interface) < 0) { + return IRECV_E_USB_INTERFACE; +@@ -2393,6 +2768,9 @@ + return IRECV_E_NO_DEVICE; + + #ifndef _WIN32 ++ if (client->qemu_transport) { ++ return qemu_a6_reset(client); ++ } + #ifdef HAVE_IOKIT + IOReturn result; + +@@ -2875,6 +3253,44 @@ + } + + #ifndef _WIN32 ++static struct irecv_usb_device_info *qemu_a6_handle_device_add( ++ const qemu_a6_usb_device_info *info) ++{ ++ struct irecv_client_private client_loc; ++ struct irecv_usb_device_info *usb_dev_info; ++ irecv_device_event_t dev_event; ++ ++ memset(&client_loc, 0, sizeof(client_loc)); ++ client_loc.mode = info->product_id; ++ irecv_load_device_info_from_iboot_string(&client_loc, info->serial); ++ usb_dev_info = calloc(1, sizeof(*usb_dev_info)); ++ if (!usb_dev_info) { ++ free(client_loc.device_info.srnm); ++ free(client_loc.device_info.imei); ++ free(client_loc.device_info.srtg); ++ free(client_loc.device_info.serial_string); ++ return NULL; ++ } ++ memcpy(&usb_dev_info->device_info, &client_loc.device_info, ++ sizeof(client_loc.device_info)); ++ usb_dev_info->location = info->generation; ++ usb_dev_info->alive = 1; ++ usb_dev_info->mode = client_loc.mode; ++ collection_add(&devices, usb_dev_info); ++ ++ dev_event.type = IRECV_DEVICE_ADD; ++ dev_event.mode = client_loc.mode; ++ dev_event.device_info = &usb_dev_info->device_info; ++ mutex_lock(&listener_mutex); ++ FOREACH(struct irecv_device_event_context* context, &listeners) { ++ context->callback(&dev_event, context->user_data); ++ } ENDFOREACH ++ mutex_unlock(&listener_mutex); ++ return usb_dev_info; ++} ++#endif ++ ++#ifndef _WIN32 + #ifdef HAVE_IOKIT + static void iokit_device_added(void *refcon, io_iterator_t iterator) + { +@@ -2977,10 +3393,70 @@ + cond_t startup_cond; + mutex_t startup_mutex; + }; ++ ++#ifndef _WIN32 ++static void *qemu_a6_event_handler(struct _irecv_event_handler_info *startup) ++{ ++ struct irecv_usb_device_info *current = NULL; ++ ++ mutex_lock(&startup->startup_mutex); ++ cond_signal(&startup->startup_cond); ++ mutex_unlock(&startup->startup_mutex); ++ ++ for (;;) { ++ qemu_a6_usb_device_info info; ++ int listeners_active; ++ int fd; ++ int error = IRECV_E_UNABLE_TO_CONNECT; ++ ++ mutex_lock(&listener_mutex); ++ listeners_active = collection_count(&listeners) != 0; ++ mutex_unlock(&listener_mutex); ++ if (!listeners_active) { ++ break; ++ } ++ ++ fd = qemu_a6_connect(); ++ if (fd >= 0) { ++ memset(&info, 0, sizeof(info)); ++ error = qemu_a6_read_info_fd(fd, &info); ++ close(fd); ++ } ++ if (error == IRECV_E_SUCCESS) { ++ if (current && ++ (current->location != info.generation || ++ current->mode != info.product_id)) { ++ /* ++ * Publish removal and arrival on separate polling turns. ++ * idevicerestore deliberately waits for MODE_UNKNOWN before ++ * it accepts the next DFU/recovery attachment. ++ */ ++ _irecv_handle_device_remove(current); ++ current = NULL; ++ } else if (!current) { ++ current = qemu_a6_handle_device_add(&info); ++ } ++ } else if (error == IRECV_E_NO_DEVICE && current) { ++ _irecv_handle_device_remove(current); ++ current = NULL; ++ } ++ usleep(250000); ++ } ++ if (current) { ++ _irecv_handle_device_remove(current); ++ } ++ return NULL; ++} ++#endif + + static void *_irecv_event_handler(void* data) + { + struct _irecv_event_handler_info* info = (struct _irecv_event_handler_info*)data; ++#ifndef _WIN32 ++ if (qemu_a6_endpoint()) { ++ return qemu_a6_event_handler(info); ++ } ++#endif + #ifdef _WIN32 + struct collection newDevices; + const GUID *guids[] = { &GUID_DEVINTERFACE_KIS, &GUID_DEVINTERFACE_PORTDFU, &GUID_DEVINTERFACE_DFU, &GUID_DEVINTERFACE_IBOOT, NULL }; +@@ -3381,6 +3858,10 @@ + client->disconnected_callback(client, &event); + } + #ifndef _WIN32 ++ if (client->qemu_transport) { ++ close(client->qemu_fd); ++ client->qemu_fd = -1; ++ } else { + #ifdef HAVE_IOKIT + if (client->usbInterface) { + (*client->usbInterface)->USBInterfaceClose(client->usbInterface); +@@ -3405,6 +3886,7 @@ + client->handle = NULL; + } + #endif ++ } + #else + CloseHandle(client->handle); + #endif diff --git a/windows/QemuA6Ude/build-driver.ps1 b/windows/QemuA6Ude/build-driver.ps1 index 9dfd566..5c44138 100644 --- a/windows/QemuA6Ude/build-driver.ps1 +++ b/windows/QemuA6Ude/build-driver.ps1 @@ -28,7 +28,7 @@ foreach ($required in @( (Join-Path $wdkRoot "Include\$wdkVersion\km\ude\1.0\UdeCx.h") )) { if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { - throw "Composant de compilation introuvable : $required" + throw "Required build component not found: $required" } } @@ -58,7 +58,7 @@ if ($Configuration -eq 'Release') { & (Join-Path $toolBin 'cl.exe') @compileArgs if ($LASTEXITCODE -ne 0) { - throw "La compilation du pilote a échoué ($LASTEXITCODE)." + throw "Driver compilation failed ($LASTEXITCODE)." } $kmLib = Join-Path $wdkRoot "Lib\$wdkVersion\km\x64" @@ -81,8 +81,8 @@ if ($Configuration -eq 'Release') { & (Join-Path $toolBin 'link.exe') @linkArgs if ($LASTEXITCODE -ne 0) { - throw "L'édition de liens du pilote a échoué ($LASTEXITCODE)." + throw "Driver link failed ($LASTEXITCODE)." } Copy-Item -LiteralPath (Join-Path $projectDir 'QemuA6Ude.inf') -Destination $outputDir -Force -Write-Host "Pilote construit : $sysPath" +Write-Host "Driver built: $sysPath" diff --git a/windows/QemuA6Ude/common.ps1 b/windows/QemuA6Ude/common.ps1 index 76d164a..1132906 100644 --- a/windows/QemuA6Ude/common.ps1 +++ b/windows/QemuA6Ude/common.ps1 @@ -23,7 +23,7 @@ function Find-Qa6WdkRoot { } } - throw 'WDK UDE introuvable. Définissez QA6_WDK_ROOT vers le dossier « c » du paquet Microsoft.Windows.WDK.x64.' + throw 'UDE WDK not found. Set QA6_WDK_ROOT to the Microsoft.Windows.WDK.x64 package c directory.' } function Find-Qa6WdkVersion { @@ -34,7 +34,7 @@ function Find-Qa6WdkVersion { Test-Path -LiteralPath (Join-Path $_.FullName 'km\ude\1.0\UdeCx.h') } | Sort-Object Name -Descending | Select-Object -First 1 if ($null -eq $version) { - throw "Aucune version UDE dans $WdkRoot" + throw "No UDE version found under $WdkRoot" } return $version.Name } @@ -55,7 +55,7 @@ function Find-Qa6MsvcRoot { } } } - throw 'MSVC x64 de Visual Studio 2022 est introuvable.' + throw 'Visual Studio 2022 x64 MSVC was not found.' } function Find-Qa6SignTool { @@ -75,5 +75,5 @@ function Find-Qa6SignTool { } } - throw "SignTool x64 $WdkVersion est introuvable." + throw "x64 SignTool $WdkVersion was not found." } diff --git a/windows/QemuA6Ude/enable-test-mode.ps1 b/windows/QemuA6Ude/enable-test-mode.ps1 index 9b48c9a..b9f0d9b 100644 --- a/windows/QemuA6Ude/enable-test-mode.ps1 +++ b/windows/QemuA6Ude/enable-test-mode.ps1 @@ -9,21 +9,21 @@ $secureBoot = $null try { $secureBoot = Confirm-SecureBootUEFI } catch { - Write-Verbose "État Secure Boot non lisible : $($_.Exception.Message)" + Write-Verbose "Unable to read Secure Boot state: $($_.Exception.Message)" } $bcdOutput = & bcdedit.exe /set testsigning on 2>&1 $bcdOutput | Out-File -LiteralPath $logPath -Encoding utf8 -Force if ($LASTEXITCODE -ne 0) { if ($secureBoot -eq $true) { - throw 'Secure Boot bloque le mode de signature test. Il faut le désactiver dans l’UEFI avant de recommencer.' + throw 'Secure Boot blocks test-signing mode. Disable it in UEFI before trying again.' } - throw "Impossible d’activer le mode de signature test ($LASTEXITCODE)." + throw "Unable to enable test-signing mode ($LASTEXITCODE)." } -Write-Host 'Mode de signature test activé dans le BCD.' -Write-Host 'Un redémarrage de Windows est requis avant le chargement de QemuA6Ude.sys.' +Write-Host 'Test-signing mode is enabled in the BCD.' +Write-Host 'Restart Windows before loading QemuA6Ude.sys.' Add-Content -LiteralPath $logPath -Encoding utf8 -Value @( - 'Mode de signature test activé dans le BCD.', - 'Un redémarrage de Windows est requis.' + 'Test-signing mode is enabled in the BCD.', + 'A Windows restart is required.' ) diff --git a/windows/QemuA6Ude/install-driver.ps1 b/windows/QemuA6Ude/install-driver.ps1 index 6e1f378..e3db973 100644 --- a/windows/QemuA6Ude/install-driver.ps1 +++ b/windows/QemuA6Ude/install-driver.ps1 @@ -16,7 +16,7 @@ $devcon = Join-Path $wdkRoot "tools\$wdkVersion\x64\devcon.exe" foreach ($required in @($certificate, $inf, $devcon)) { if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { - throw "Fichier requis introuvable : $required" + throw "Required file not found: $required" } } @@ -25,8 +25,8 @@ Import-Certificate -FilePath $certificate -CertStoreLocation Cert:\LocalMachine\ & $devcon install $inf 'Root\QemuA6Ude' if ($LASTEXITCODE -ne 0) { - throw "Installation du pilote échouée ($LASTEXITCODE)." + throw "Driver installation failed ($LASTEXITCODE)." } -Write-Host 'Pilote QemuA6Ude installé.' -Write-Host 'Le périphérique enfant USB\VID_05AC&PID_1227 doit maintenant apparaître.' +Write-Host 'QemuA6Ude driver installed.' +Write-Host 'The USB\VID_05AC&PID_1227 child device should now be visible.' diff --git a/windows/QemuA6Ude/prepare-package.ps1 b/windows/QemuA6Ude/prepare-package.ps1 index 613a5f8..161fee7 100644 --- a/windows/QemuA6Ude/prepare-package.ps1 +++ b/windows/QemuA6Ude/prepare-package.ps1 @@ -47,25 +47,25 @@ try { & $signTool sign /fd SHA256 /f $certificatePfx /p $certificatePasswordText (Join-Path $packageDir 'QemuA6Ude.sys') if ($LASTEXITCODE -ne 0) { - throw 'La signature de test du fichier SYS a échoué.' + throw 'Test-signing the SYS file failed.' } & $inf2Cat "/driver:$packageDir" '/os:10_X64' if ($LASTEXITCODE -ne 0) { - throw 'La génération du catalogue du pilote a échoué.' + throw 'Generating the driver catalog failed.' } & $signTool sign /fd SHA256 /f $certificatePfx /p $certificatePasswordText (Join-Path $packageDir 'QemuA6Ude.cat') if ($LASTEXITCODE -ne 0) { - throw 'La signature de test du catalogue a échoué.' + throw 'Test-signing the catalog failed.' } Copy-Item -LiteralPath $certificateCer -Destination $packageDir -Force & $signTool verify /pa /v (Join-Path $packageDir 'QemuA6Ude.cat') if ($LASTEXITCODE -ne 0) { - Write-Warning 'La chaîne est volontairement non approuvée avant l’installation du certificat de test.' + Write-Warning 'The chain is intentionally untrusted until the test certificate is installed.' } -Write-Host "Paquet pilote prêt : $packageDir" -Write-Host "Certificat public : $certificateCer" +Write-Host "Driver package ready: $packageDir" +Write-Host "Public certificate: $certificateCer" diff --git a/windows/QemuA6UsbBridge/bridge.c b/windows/QemuA6UsbBridge/bridge.c index 44cd133..8884f99 100644 --- a/windows/QemuA6UsbBridge/bridge.c +++ b/windows/QemuA6UsbBridge/bridge.c @@ -139,13 +139,13 @@ Qa6RelayLoop(HANDLE driver, SOCKET qemu) if (!ReadFile(driver, request, sizeof(request), &requestLength, NULL)) { DWORD error = GetLastError(); - fprintf(stderr, "Lecture du pilote impossible: erreur %lu\n", + fprintf(stderr, "Unable to read from driver: error %lu\n", error); return -1; } if (Qa6ValidateFrame(request, requestLength, QA6_USB_MESSAGE_REQUEST) != 0) { - fprintf(stderr, "Trame invalide recue du pilote (%lu octets)\n", + fprintf(stderr, "Invalid frame received from driver (%lu bytes)\n", requestLength); return -1; } @@ -162,17 +162,18 @@ Qa6RelayLoop(HANDLE driver, SOCKET qemu) fflush(stdout); if (Qa6SendAll(qemu, request, (int)requestLength) != 0) { - fprintf(stderr, "Connexion QEMU interrompue pendant l'envoi.\n"); + fprintf(stderr, "QEMU connection closed while sending.\n"); return -1; } if (Qa6ReceiveAll(qemu, response, QA6_USB_HEADER_SIZE) != 0) { - fprintf(stderr, "Connexion QEMU interrompue pendant la reponse.\n"); + fprintf(stderr, + "QEMU connection closed while receiving a response.\n"); return -1; } responseHeader = (Qa6UsbFrameHeader *)response; if (responseHeader->payload_length > QA6_USB_MAX_PAYLOAD) { - fprintf(stderr, "Longueur de reponse QEMU invalide.\n"); + fprintf(stderr, "Invalid QEMU response length.\n"); return -1; } responseLength = QA6_USB_HEADER_SIZE + @@ -180,13 +181,13 @@ Qa6RelayLoop(HANDLE driver, SOCKET qemu) if (responseHeader->payload_length != 0 && Qa6ReceiveAll(qemu, response + QA6_USB_HEADER_SIZE, (int)responseHeader->payload_length) != 0) { - fprintf(stderr, "Connexion QEMU interrompue dans les donnees.\n"); + fprintf(stderr, "QEMU connection closed while receiving data.\n"); return -1; } if (Qa6ValidateFrame(response, responseLength, QA6_USB_MESSAGE_RESPONSE) != 0 || responseHeader->request_id != requestHeader->request_id) { - fprintf(stderr, "Reponse QEMU invalide pour la requete #%lu.\n", + fprintf(stderr, "Invalid QEMU response for request #%lu.\n", requestHeader->request_id); return -1; } @@ -194,7 +195,7 @@ Qa6RelayLoop(HANDLE driver, SOCKET qemu) if (!WriteFile(driver, response, responseLength, &written, NULL) || written != responseLength) { DWORD error = GetLastError(); - fprintf(stderr, "Ecriture vers le pilote impossible: erreur %lu\n", + fprintf(stderr, "Unable to write to driver: error %lu\n", error); return -1; } @@ -214,19 +215,19 @@ main(int argc, char **argv) int exitCode = 1; if (sizeof(Qa6UsbFrameHeader) != 32) { - fprintf(stderr, "Erreur interne: taille du protocole USB=%zu\n", + fprintf(stderr, "Internal error: USB protocol size=%zu\n", sizeof(Qa6UsbFrameHeader)); return 2; } SetConsoleCtrlHandler(Qa6ConsoleHandler, TRUE); if (WSAStartup(MAKEWORD(2, 2), &winsockData) != 0) { - fprintf(stderr, "Initialisation Winsock impossible.\n"); + fprintf(stderr, "Unable to initialize Winsock.\n"); return 2; } - printf("Pont USB QEMU A6 natif Windows\n"); - printf("Pilote : \\\\.\\QemuA6Ude0\n"); + printf("Native Windows QEMU A6 USB bridge\n"); + printf("Driver : \\\\.\\QemuA6Ude0\n"); printf("QEMU : %s:%s\n", host, port); while (InterlockedCompareExchange(&Qa6StopRequested, 0, 0) == 0) { @@ -234,7 +235,7 @@ main(int argc, char **argv) if (driver != INVALID_HANDLE_VALUE) { break; } - printf("Attente du pilote QemuA6Ude...\n"); + printf("Waiting for the QemuA6Ude driver...\n"); Sleep(1000); } if (driver == INVALID_HANDLE_VALUE) { @@ -246,14 +247,14 @@ main(int argc, char **argv) if (qemu != INVALID_SOCKET) { break; } - printf("Attente de QEMU sur %s:%s...\n", host, port); + printf("Waiting for QEMU at %s:%s...\n", host, port); Sleep(1000); } if (qemu == INVALID_SOCKET) { goto cleanup; } - printf("Connecte. idevicerestore peut utiliser l'iPhone DFU virtuel.\n"); + printf("Connected. idevicerestore can use the virtual DFU iPhone.\n"); exitCode = Qa6RelayLoop(driver, qemu) == 0 ? 0 : 1; cleanup: diff --git a/windows/QemuA6UsbBridge/build-bridge.ps1 b/windows/QemuA6UsbBridge/build-bridge.ps1 index a3bade5..efd6665 100644 --- a/windows/QemuA6UsbBridge/build-bridge.ps1 +++ b/windows/QemuA6UsbBridge/build-bridge.ps1 @@ -16,7 +16,7 @@ $outputDir = Join-Path $projectDir "build\$Configuration\x64" $exePath = Join-Path $outputDir 'QemuA6UsbBridge.exe' if (-not (Test-Path -LiteralPath $vsDevCmd -PathType Leaf)) { - throw "Environnement Visual Studio introuvable : $vsDevCmd" + throw "Visual Studio environment not found: $vsDevCmd" } New-Item -ItemType Directory -Force -Path $outputDir | Out-Null @@ -24,7 +24,7 @@ New-Item -ItemType Directory -Force -Path $outputDir | Out-Null $environmentCommand = 'call "{0}" -arch=x64 -host_arch=x64 >nul && set' -f $vsDevCmd $environmentLines = & cmd.exe /d /s /c $environmentCommand if ($LASTEXITCODE -ne 0) { - throw 'Impossible de charger l’environnement MSVC x64.' + throw 'Unable to load the x64 MSVC environment.' } foreach ($line in $environmentLines) { $separator = $line.IndexOf('=') @@ -50,7 +50,7 @@ $arguments += @( & $cl @arguments if ($LASTEXITCODE -ne 0) { - throw "La compilation du pont USB a échoué ($LASTEXITCODE)." + throw "USB bridge build failed ($LASTEXITCODE)." } -Write-Host "Pont construit : $exePath" +Write-Host "Bridge built: $exePath" diff --git a/windows/include/qemu_a6_usb_protocol.h b/windows/include/qemu_a6_usb_protocol.h index efc951a..490c8f7 100644 --- a/windows/include/qemu_a6_usb_protocol.h +++ b/windows/include/qemu_a6_usb_protocol.h @@ -6,13 +6,21 @@ #define QA6_USB_PROTOCOL_VERSION 1u #define QA6_USB_MESSAGE_REQUEST 1u #define QA6_USB_MESSAGE_RESPONSE 2u +#define QA6_USB_MESSAGE_INFO_REQUEST 3u +#define QA6_USB_MESSAGE_INFO_RESPONSE 4u +#define QA6_USB_MESSAGE_BULK_REQUEST 5u +#define QA6_USB_MESSAGE_BULK_RESPONSE 6u +#define QA6_USB_MESSAGE_RESET_REQUEST 7u +#define QA6_USB_MESSAGE_RESET_RESPONSE 8u #define QA6_USB_STATUS_SUCCESS 0 #define QA6_USB_STATUS_STALL -1 #define QA6_USB_STATUS_DISCONNECTED -2 #define QA6_USB_STATUS_PROTOCOL -3 +#define QA6_USB_STATUS_UNSUPPORTED -4 #define QA6_USB_MAX_PAYLOAD 65535u +#define QA6_USB_SERIAL_MAX 256u #pragma pack(push, 1) typedef struct Qa6UsbFrameHeader { @@ -25,6 +33,17 @@ typedef struct Qa6UsbFrameHeader { unsigned int transfer_length; unsigned char setup[8]; } Qa6UsbFrameHeader; + +typedef struct Qa6UsbDeviceInfo { + unsigned int generation; + unsigned short vendor_id; + unsigned short product_id; + unsigned char device_class; + unsigned char device_subclass; + unsigned char device_protocol; + unsigned char is_dfu; + char serial[QA6_USB_SERIAL_MAX]; +} Qa6UsbDeviceInfo; #pragma pack(pop) #define QA6_USB_HEADER_SIZE ((unsigned int)sizeof(Qa6UsbFrameHeader))