diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4c445e..28032ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,15 @@ jobs: needs: [check] strategy: matrix: - builder: [ghcr.io/nyamisty/altserver_builder_alpine_armv7, ghcr.io/nyamisty/altserver_builder_alpine_aarch64, ghcr.io/nyamisty/altserver_builder_alpine_amd64, ghcr.io/nyamisty/altserver_builder_alpine_i386] + include: + - builder: ghcr.io/nyamisty/altserver_builder_alpine_armv7 + rust_target: armv7-unknown-linux-musleabihf + - builder: ghcr.io/nyamisty/altserver_builder_alpine_aarch64 + rust_target: aarch64-unknown-linux-musl + - builder: ghcr.io/nyamisty/altserver_builder_alpine_amd64 + rust_target: x86_64-unknown-linux-musl + - builder: ghcr.io/nyamisty/altserver_builder_alpine_i386 + rust_target: i686-unknown-linux-musl runs-on: ubuntu-latest steps: - name: Checkout @@ -92,11 +100,15 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Prepare build run: mkdir /tmp/build_output + - name: Build idevice + run: | + lib=$(makefiles/idevice-build/build-idevice-cross.sh ${{ matrix.rust_target }}) + echo "IDEVICE_FFI_LIB=/workdir/${lib#$PWD/}" >> "$GITHUB_ENV" - name: Build run: | image=${{ matrix.builder }} docker pull $image - docker run -v ${PWD}:/workdir -w /workdir $image bash -c 'mkdir build; cd build; make -f ../Makefile -j3' + docker run -v ${PWD}:/workdir -w /workdir $image bash -c "mkdir build; cd build; make -f ../Makefile -j3 IDEVICE_FFI_LIB=$IDEVICE_FFI_LIB" cp build/AltServer-* /tmp/build_output; chmod +x /tmp/build_output/* sudo rm -rf build git clean -fdX diff --git a/.gitmodules b/.gitmodules index a62d5ac..ec350a0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "libraries/libimobiledevice-glue"] path = libraries/libimobiledevice-glue url = https://github.com/libimobiledevice/libimobiledevice-glue +[submodule "libraries/idevice"] + path = libraries/idevice + url = https://github.com/jkcoxson/idevice diff --git a/Makefile b/Makefile index 0d3ce07..0ca64d5 100644 --- a/Makefile +++ b/Makefile @@ -42,10 +42,18 @@ lib_dnssd_loader_clean : $(MAKE) -f $(ROOT_DIR)/makefiles/dnssd_loader-build/dnssd_loader.mak clean .PHONY: $(BUILD_DIR)/dnssd_loader.a lib_dnssd_loader lib_dnssd_loader_clean +$(BUILD_DIR)/idevice_ffi.o: + $(MAKE) -f $(ROOT_DIR)/makefiles/idevice-build/idevice.mak +lib_idevice : $(BUILD_DIR)/idevice_ffi.o +lib_idevice_clean : + $(MAKE) -f $(ROOT_DIR)/makefiles/idevice-build/idevice.mak clean +.PHONY: $(BUILD_DIR)/idevice_ffi.o lib_idevice lib_idevice_clean + include $(ROOT_DIR)/makefiles/libimobiledevice-build/libimobiledevice-files.mak include $(ROOT_DIR)/makefiles/AltSign-build/AltSign-files.mak include $(ROOT_DIR)/makefiles/dnssd_loader-build/dnssd_loader-files.mak +include $(ROOT_DIR)/makefiles/idevice-build/idevice-files.mak #libimobiledevice_include := -I$(LIB_DIR)/libimobiledevice/include -I$(LIB_DIR)/libimobiledevice -I$(LIB_DIR)/libusbmuxd/include #libplist_include := -I$(LIB_DIR)/libplist/include @@ -56,6 +64,7 @@ INC_CFLAGS += $(libimobiledevice_include) INC_CFLAGS += $(libplist_include) INC_CFLAGS += $(altsign_include) INC_CFLAGS += $(dnssd_loader_include) +INC_CFLAGS += $(idevice_include) include $(ROOT_DIR)/makefiles/AltWindowsShim.mak @@ -113,17 +122,17 @@ main_newsrc := $(main_orisrc:$(main_srcroot)/%=$(main_patched_root)/%) main_objs = $(main_newsrc:$(BUILD_DIR)/%=$(BUILD_DIR)/objs/%.o) $(main_override_src:$(ROOT_DIR)/%=$(BUILD_DIR)/objs/%.o) $(shim_src:$(MAIN_DIR)/%=$(BUILD_DIR)/objs/%.o) -$(main_objs) : lib_AltSign lib_libimobiledevice lib_dnssd_loader +$(main_objs) : lib_AltSign lib_libimobiledevice lib_dnssd_loader lib_idevice $(main_objs) : EXTRA_FLAGS := -I$(main_patched_root) -I$(ROOT_DIR)/src -fpermissive -include "common.h" $(INC_CFLAGS) LDFLAGS = -static -lssl -lcrypto -lpthread -lcorecrypto_static -lzip -lm -lz -lcpprest -lboost_system -lboost_filesystem -lstdc++ -lssl -lcrypto -luuid -$(BUILD_DIR)/$(PROGRAM):: $(main_objs) $(BUILD_DIR)/libimobiledevice.a $(BUILD_DIR)/AltSign.a $(BUILD_DIR)/libplist.a $(BUILD_DIR)/dnssd_loader.a +$(BUILD_DIR)/$(PROGRAM):: $(main_objs) $(BUILD_DIR)/libimobiledevice.a $(BUILD_DIR)/AltSign.a $(BUILD_DIR)/libplist.a $(BUILD_DIR)/dnssd_loader.a $(BUILD_DIR)/idevice_ffi.o $(CC) -o $@ $^ $(LDFLAGS) .PHONY: clean all -clean:: lib_libimobiledevice_clean lib_AltSign_clean lib_dnssd_loader_clean +clean:: lib_libimobiledevice_clean lib_AltSign_clean lib_dnssd_loader_clean lib_idevice_clean rm -f $(main_objs) $(BUILD_DIR)/$(PROGRAM) all:: preprocess $(BUILD_DIR)/$(PROGRAM) diff --git a/README-NG.md b/README-NG.md index 0c246f1..6067574 100644 --- a/README-NG.md +++ b/README-NG.md @@ -98,6 +98,42 @@ assumed the BSD layout (byte 0 = `sa_len`, byte 1 = family) and failed with `libraries/libimobiledevice/src/idevice.c` now detects both layouts, so `USBMUXD_SOCKET_ADDRESS` can point straight at netmuxd. A BSD-format proxy in front of netmuxd keeps working as well. +## Fix 4: Remote AltServer setup (`PairingFileRequest`) + +### Symptom + +In AltStore Classic 2.3, **Set up Remote AltServer…** fails with "unable to pair" and AltServer +logs `Failed to handle request:AltServer does not support this request.` + +### Cause + +AltStore Classic 2.3 asks AltServer for a RemotePairing (RPPairing) file, the iOS 17+ pairing +record with a `private_key`. AltStore keeps it in its keychain and uses it to install and refresh +apps on the device itself over LocalDevVPN. AltServer for macOS creates it in +`DevicePairingManager.swift` with idevice's `tunnel_pair_usb()`; AltServer-Linux had no handler for +the request. + +### Fix + +- `upstream_repo/AltServer/ClientConnection.cpp` handles `PairingFileRequest` and answers with a + `PairingFileResponse` carrying the file. `upstream_repo/AltServer/DevicePairingManager.h` declares + the pairing interface; the `DevicePairingManager.cpp` next to it is the Windows version, which + throws `UnknownRequest`, and the Linux build replaces it with `src/DevicePairingManager.cpp`. +- `src/DevicePairingManager.cpp` is a C++ port of `DevicePairingManager.swift`: it looks up the + device by UDID in usbmuxd, calls `tunnel_pair_usb()` (the device shows a Trust prompt for + "AltServer on ") and serializes the pairing file. It reads `USBMUXD_SOCKET_ADDRESS` by + libusbmuxd's rules (`UNIX:`, `:`, otherwise `/var/run/usbmuxd`), so the device + lookup and the pairing connection use the same usbmuxd as the rest of AltServer-Linux. It pairs + only over the device's USB connection, not a network entry for the same UDID. The pairing + runs on its own thread with an 8 MiB stack: `tunnel_pair_usb()` polls idevice's pairing future on + the calling thread, and that overflows the 128 KiB stack musl gives cpprestsdk's pool threads. +- `libraries/idevice` is [idevice](https://github.com/jkcoxson/idevice) v0.1.68, built with the + features `usbmuxd,tunnel_tcp_stack,ring` (`makefiles/idevice-build/idevice-features.txt`). + idevice's static library also exports libplist-compatible `plist_*` functions and the Rust standard + library, which would clash with the vendored libplist. `makefiles/idevice-build/idevice.mak` + therefore prelinks it into one object (`ld -r`) and keeps only the functions in + `idevice-api.txt` global (`objcopy --keep-global-symbols`). + ## Prebuilt binaries Static binaries for x86_64, aarch64, armv7 and i586 are attached to the @@ -109,24 +145,33 @@ produces them as GitHub Actions artifacts, which expire after 90 days. Builds run in the prebuilt upstream Alpine image. On an Apple Silicon Mac, Rosetta runs the amd64 container at native speed and a clean build takes about 30 seconds. +The builder images have no Rust toolchain, so idevice (Fix 4) is built first with +`makefiles/idevice-build/build-idevice-cross.sh`, which runs cargo-zigbuild in the +`ghcr.io/rust-cross/cargo-zigbuild` image and prints the path of the static library: + ```bash git clone --recursive -b ng https://github.com/jaakkopalvaila/AltServer-Linux cd AltServer-Linux +lib=$(makefiles/idevice-build/build-idevice-cross.sh x86_64-unknown-linux-musl) mkdir -p build docker run --rm --platform linux/amd64 -v "$PWD:/workdir" -w /workdir \ ghcr.io/nyamisty/altserver_builder_alpine_amd64:latest \ - bash -c 'cd build && make -f ../Makefile -j8' + bash -c "cd build && make -f ../Makefile -j8 IDEVICE_FFI_LIB=/workdir/${lib#$PWD/}" ``` The result is a statically linked `build/AltServer-x86_64`. Other architectures use the -`altserver_builder_alpine_{aarch64,armv7,i386}` images. +`altserver_builder_alpine_{aarch64,armv7,i386}` images with the Rust targets +`aarch64-unknown-linux-musl`, `armv7-unknown-linux-musleabihf` and `i686-unknown-linux-musl`. +Without `IDEVICE_FFI_LIB`, the Makefile runs `cargo build` itself, which needs cargo in the build environment +(tested with Rust 1.93 and 1.98). -Sanity-check that all three fixes are present: +Sanity-check that the fixes are present: ```bash strings build/AltServer-x86_64 | grep -c "missing path separator" # Fix 1, new ldid -> 1 strings build/AltServer-x86_64 | grep -c "Sanitized client info" # Fix 2, sign-in -> 1 grep -c idevice_sockaddr_len libraries/libimobiledevice/src/idevice.c # Fix 3, netmuxd -> >= 1 +strings build/AltServer-x86_64 | grep -c "Generated pairing file" # Fix 4, pairing file -> 1 ``` ## Running diff --git a/libraries/idevice b/libraries/idevice new file mode 160000 index 0000000..d32c818 --- /dev/null +++ b/libraries/idevice @@ -0,0 +1 @@ +Subproject commit d32c8189c51c2789496b0768039419c3705498c3 diff --git a/makefiles/idevice-build/build-idevice-cross.sh b/makefiles/idevice-build/build-idevice-cross.sh new file mode 100755 index 0000000..5d55418 --- /dev/null +++ b/makefiles/idevice-build/build-idevice-cross.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# Cross-compiles idevice's static library for a musl Rust target with cargo-zigbuild, for +# architectures whose builder image has no musl-hosted Rust toolchain (i686, armv7). +# zig provides the C compiler and musl headers for ring's C and assembly code. +# Pass the printed path to make as IDEVICE_FFI_LIB. The output goes under libraries/idevice/target, +# which idevice's .gitignore already covers, so the submodule stays clean. +# +# Usage: makefiles/idevice-build/build-idevice-cross.sh +set -eu +target="$1" +root="$(cd "$(dirname "$0")/../.." && pwd)" +features="$(cat "$root/makefiles/idevice-build/idevice-features.txt")" + +docker run --rm -v "$root/libraries/idevice:/src" -w /src/ffi ghcr.io/rust-cross/cargo-zigbuild:0.23.4 sh -c " + rustup target add $target >/dev/null && + cargo zigbuild --release --locked --target $target --target-dir /src/target/cross --no-default-features --features $features" >&2 + +echo "$root/libraries/idevice/target/cross/$target/release/libidevice_ffi.a" diff --git a/makefiles/idevice-build/idevice-api.txt b/makefiles/idevice-build/idevice-api.txt new file mode 100644 index 0000000..88b9a7c --- /dev/null +++ b/makefiles/idevice-build/idevice-api.txt @@ -0,0 +1,19 @@ +idevice_data_free +idevice_error_free +idevice_provider_free +idevice_string_free +idevice_usbmuxd_addr_free +idevice_usbmuxd_connection_free +idevice_usbmuxd_device_get_connection_type +idevice_usbmuxd_device_get_device_id +idevice_usbmuxd_device_get_udid +idevice_usbmuxd_device_list_free +idevice_usbmuxd_get_devices +idevice_usbmuxd_new_tcp_connection +idevice_usbmuxd_new_unix_socket_connection +idevice_usbmuxd_tcp_addr_new +idevice_usbmuxd_unix_addr_new +rp_pairing_file_free +rp_pairing_file_to_bytes +tunnel_pair_usb +usbmuxd_provider_new diff --git a/makefiles/idevice-build/idevice-features.txt b/makefiles/idevice-build/idevice-features.txt new file mode 100644 index 0000000..ae880cd --- /dev/null +++ b/makefiles/idevice-build/idevice-features.txt @@ -0,0 +1 @@ +usbmuxd,tunnel_tcp_stack,ring diff --git a/makefiles/idevice-build/idevice-files.mak b/makefiles/idevice-build/idevice-files.mak new file mode 100644 index 0000000..a547198 --- /dev/null +++ b/makefiles/idevice-build/idevice-files.mak @@ -0,0 +1 @@ +idevice_include := -I$(LIB_DIR)/idevice diff --git a/makefiles/idevice-build/idevice.mak b/makefiles/idevice-build/idevice.mak new file mode 100644 index 0000000..38d12a3 --- /dev/null +++ b/makefiles/idevice-build/idevice.mak @@ -0,0 +1,44 @@ +# Builds idevice's C API (https://github.com/jkcoxson/idevice) for DevicePairingManager. +# +# idevice's static library also exports libplist-compatible plist_* functions and the Rust standard +# library, which would clash with the vendored libplist. So the library is prelinked into one object +# (ld -r) whose only global symbols are the functions in idevice-api.txt. +# +# Set IDEVICE_FFI_LIB to a libidevice_ffi.a built elsewhere to skip the cargo build, for example +# one from build-idevice-cross.sh for architectures without a musl-hosted Rust toolchain. +# Either cargo build also generates the header, libraries/idevice/ffi/idevice.h. +ROOT_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) +include $(ROOT_DIR)/../main.mak + +# A failed ld or objcopy must not leave a partial idevice_ffi.o that the next make treats as built. +.DELETE_ON_ERROR: + +IDEVICE_FEATURES := $(shell cat $(ROOT_DIR)/idevice-features.txt) +IDEVICE_TARGET_DIR := $(BUILD_DIR)/idevice-target +IDEVICE_FFI_LIB ?= $(IDEVICE_TARGET_DIR)/release/libidevice_ffi.a + +idevice_api := $(ROOT_DIR)/idevice-api.txt + +# cargo runs on every make, because only cargo knows when the library is out of date: a submodule +# bump, a change to idevice-features.txt or a new toolchain. When nothing changed it leaves the +# library untouched, and make then does not prelink it again. +$(IDEVICE_TARGET_DIR)/release/libidevice_ffi.a: FORCE + cd $(LIB_DIR)/idevice/ffi && cargo build --release --locked --target-dir $(IDEVICE_TARGET_DIR) --no-default-features --features $(IDEVICE_FEATURES) + +$(BUILD_DIR)/idevice_ffi.o: $(IDEVICE_FFI_LIB) $(idevice_api) + ld -r $(addprefix -u ,$(shell cat $(idevice_api))) -o $@.tmp $(IDEVICE_FFI_LIB) + objcopy --keep-global-symbols=$(idevice_api) $@.tmp $@ + rm -f $@.tmp + +FORCE: +.PHONY : FORCE + +clean:: + rm -f $(BUILD_DIR)/idevice_ffi.o + rm -rf $(IDEVICE_TARGET_DIR) +.PHONY : clean + +all :: $(BUILD_DIR)/idevice_ffi.o +.PHONY : all + +.DEFAULT_GOAL := all diff --git a/src/DevicePairingManager.cpp b/src/DevicePairingManager.cpp new file mode 100644 index 0000000..7146638 --- /dev/null +++ b/src/DevicePairingManager.cpp @@ -0,0 +1,411 @@ +#include "DevicePairingManager.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ServerError.hpp" + +// idevice.h also declares plist_* functions from idevice's own plist implementation, which clash with +// libplist's declarations, so this is the only file that includes it. The build prelinks idevice into +// a single object whose only global symbols are the functions listed in makefiles/idevice-build/idevice-api.txt. +extern "C" { +#include "ffi/idevice.h" +} + +namespace +{ + std::string FFIErrorDescription(IdeviceFfiError* error) + { + if (error == NULL) + { + return ""; + } + + std::string description = "idevice error " + std::to_string(error->code); + if (error->message != NULL) + { + description += ": " + std::string(error->message); + } + + idevice_error_free(error); + return description; + } + + // Same messages as RemotePairingError in AltServer for macOS. + ServerError PairingError(ServerErrorCode code, std::string failureReason, std::string recoverySuggestion, IdeviceFfiError* ffiError) + { + auto ffiDescription = FFIErrorDescription(ffiError); + std::cout << "Failed to generate pairing file. " << failureReason; + if (!ffiDescription.empty()) + { + std::cout << " (" << ffiDescription << ")"; + } + std::cout << std::endl; + + return ServerError(code, { + { "NSLocalizedFailureReason", failureReason }, + { "NSLocalizedRecoverySuggestion", recoverySuggestion }, + }); + } + + ServerError UnknownPairingError(std::string reason, IdeviceFfiError* ffiError = NULL) + { + std::cout << "Remote AltServer setup failed: " << reason << std::endl; + return PairingError(ServerErrorCode::Unknown, + "Remote AltServer couldn't be configured for this device.", + "Try again. If the problem continues, reconnect your device or restart your computer.", + ffiError); + } + + // Where usbmuxd listens. The rest of AltServer reaches usbmuxd through libusbmuxd, so this reads + // USBMUXD_SOCKET_ADDRESS by libusbmuxd's rules and pairing uses the same usbmuxd: + // UNIX: the unix socket at + // : or []: TCP, where may be a name + // unset, or any other value the default socket, /var/run/usbmuxd + // idevice's own reader (idevice_usbmuxd_new_default_connection) takes every value that contains a + // ':' as a numeric TCP address, so it rejects UNIX:. The macOS version always uses the + // default socket. + struct UsbmuxdLocation + { + std::string socketPath; // Empty for TCP. + sockaddr_storage tcpAddress = {}; + socklen_t tcpAddressLength = 0; + std::string description; + }; + + UsbmuxdLocation ReadUsbmuxdLocation() + { + UsbmuxdLocation location; + location.socketPath = "/var/run/usbmuxd"; + location.description = location.socketPath; + + const char* value = getenv("USBMUXD_SOCKET_ADDRESS"); + if (value == NULL) + { + return location; + } + + std::string address = value; + if (address.compare(0, 5, "UNIX:") == 0) + { + if (address.size() > 5) + { + location.socketPath = address.substr(5); + location.description = location.socketPath; + } + return location; + } + + auto separator = address.rfind(':'); + if (separator == std::string::npos) + { + return location; + } + + // Like libusbmuxd: the whole text after the last ':' must be a port from 1 to 65535. + std::string port = address.substr(separator + 1); + char* portEnd = NULL; + long portNumber = strtol(port.c_str(), &portEnd, 10); + if (*portEnd != '\0' || portNumber < 1 || portNumber > 65535) + { + return location; + } + + std::string host = address.substr(0, separator); + if (!host.empty() && host.front() == '[') + { + host = host.substr(1); + auto bracket = host.rfind(']'); + if (bracket != std::string::npos) + { + host = host.substr(0, bracket); + } + } + + if (host.empty()) + { + return location; + } + + struct addrinfo hints = {}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + struct addrinfo* result = NULL; + int status = getaddrinfo(host.c_str(), std::to_string(portNumber).c_str(), &hints, &result); + if (status != 0 || result == NULL) + { + throw UnknownPairingError("Couldn't resolve USBMUXD_SOCKET_ADDRESS " + address + ": " + gai_strerror(status)); + } + + memcpy(&location.tcpAddress, result->ai_addr, result->ai_addrlen); + location.tcpAddressLength = result->ai_addrlen; + freeaddrinfo(result); + + location.socketPath.clear(); + location.description = address; + return location; + } + + UsbmuxdConnectionHandle* ConnectToUsbmuxd(const UsbmuxdLocation& location) + { + UsbmuxdConnectionHandle* connection = NULL; + IdeviceFfiError* error = NULL; + if (location.socketPath.empty()) + { + error = idevice_usbmuxd_new_tcp_connection((const idevice_sockaddr*)&location.tcpAddress, location.tcpAddressLength, 0, &connection); + } + else + { + error = idevice_usbmuxd_new_unix_socket_connection(location.socketPath.c_str(), 0, &connection); + } + + if (error != NULL) + { + throw UnknownPairingError("Couldn't connect to usbmuxd at " + location.description + ".", error); + } + + return connection; + } + + UsbmuxdAddrHandle* CreateUsbmuxdAddress(const UsbmuxdLocation& location) + { + UsbmuxdAddrHandle* address = NULL; + IdeviceFfiError* error = NULL; + if (location.socketPath.empty()) + { + error = idevice_usbmuxd_tcp_addr_new((const idevice_sockaddr*)&location.tcpAddress, location.tcpAddressLength, &address); + } + else + { + error = idevice_usbmuxd_unix_addr_new(location.socketPath.c_str(), &address); + } + + if (error != NULL) + { + throw UnknownPairingError("Couldn't create usbmuxd address for " + location.description + ".", error); + } + + return address; + } + + // The value idevice_usbmuxd_device_get_connection_type() returns for a USB connection. It is + // idevice's UsbmuxdConnectionType::Usb, which idevice.h does not declare. + const uint8_t UsbmuxdConnectionTypeUSB = 1; + + // Finds the usbmuxd device_id of this UDID's USB connection. usbmuxd also lists a device that it + // reaches over the network, under the same UDID, and pairing must not use that entry. + uint32_t ResolveDeviceID(std::string udid, const UsbmuxdLocation& location) + { + std::unique_ptr muxConnection(ConnectToUsbmuxd(location), idevice_usbmuxd_connection_free); + + UsbmuxdDeviceHandle** devices = NULL; + int deviceCount = 0; + if (auto error = idevice_usbmuxd_get_devices(muxConnection.get(), &devices, &deviceCount)) + { + throw UnknownPairingError("Couldn't query connected devices.", error); + } + + std::optional deviceID; + for (int i = 0; i < deviceCount; i++) + { + if (idevice_usbmuxd_device_get_connection_type(devices[i]) != UsbmuxdConnectionTypeUSB) + { + continue; + } + + char* deviceUDID = idevice_usbmuxd_device_get_udid(devices[i]); + if (deviceUDID == NULL) + { + continue; + } + + if (udid == deviceUDID) + { + deviceID = idevice_usbmuxd_device_get_device_id(devices[i]); + } + + idevice_string_free(deviceUDID); + + if (deviceID.has_value()) + { + break; + } + } + + idevice_usbmuxd_device_list_free(devices, deviceCount); + + if (!deviceID.has_value()) + { + throw PairingError(ServerErrorCode::DeviceNotFound, + "This device isn't connected to AltServer via USB.", + "Connect your device to this computer with a cable, then try again.", + NULL); + } + + return *deviceID; + } + + std::vector PairAndSerialize(std::string udid, std::string hostName) + { + // 1. Resolve the device_id (required by idevice) of this UDID's USB connection. + UsbmuxdLocation location = ReadUsbmuxdLocation(); + uint32_t deviceID = ResolveDeviceID(udid, location); + + // 2. Build a device provider for the same usbmuxd. usbmuxd_provider_new() takes ownership of the address only when it succeeds. + UsbmuxdAddrHandle* address = CreateUsbmuxdAddress(location); + + IdeviceProviderHandle* provider = NULL; + if (auto error = usbmuxd_provider_new(address, 0, udid.c_str(), deviceID, hostName.c_str(), &provider)) + { + idevice_usbmuxd_addr_free(address); + throw UnknownPairingError("Couldn't create device provider.", error); + } + std::unique_ptr providerOwner(provider, idevice_provider_free); + + // 3. Pair. A NULL pin_callback makes idevice use "000000". Triggers the on-device "Trust" prompt. + RpPairingFileHandle* pairingFile = NULL; + if (auto error = tunnel_pair_usb(provider, hostName.c_str(), NULL, NULL, &pairingFile)) + { + throw PairingError(ServerErrorCode::ConnectionFailed, + "AltServer couldn't pair with this device.", + "Make sure your device is unlocked, then tap Trust when prompted.", + error); + } + std::unique_ptr pairingFileOwner(pairingFile, rp_pairing_file_free); + + // 4. Serialize the plist. + uint8_t* bytes = NULL; + uintptr_t length = 0; + if (auto error = rp_pairing_file_to_bytes(pairingFile, &bytes, &length)) + { + throw UnknownPairingError("Couldn't serialize pairing file.", error); + } + + if (bytes == NULL || length == 0) + { + throw UnknownPairingError("Pairing file serialized to empty data."); + } + + std::vector data(bytes, bytes + length); + idevice_data_free(bytes, length); + + return data; + } + + // Marks a pairing with one device as running, for as long as the object lives. tunnel_pair_usb() + // returns only when the user answers the Trust prompt, the device disconnects or the connection + // fails, and idevice has no call that cancels it. So a retry while the prompt is still up would + // start a second pairing with the same device and block one more request thread and 8 MiB stack. + // A second request for the same UDID fails at once instead. + class PairingInProgress + { + public: + PairingInProgress(std::string udid) : udid(udid) + { + std::lock_guard lock(mutex()); + if (!udids().insert(udid).second) + { + throw PairingError(ServerErrorCode::Unknown, + "AltServer is already pairing with this device.", + "Tap Trust or Don't Trust on your device, or disconnect it, then try again.", + NULL); + } + } + + ~PairingInProgress() + { + std::lock_guard lock(mutex()); + udids().erase(udid); + } + + PairingInProgress(const PairingInProgress&) = delete; + PairingInProgress& operator=(const PairingInProgress&) = delete; + + private: + std::string udid; + + static std::mutex& mutex() + { + static std::mutex mutex; + return mutex; + } + + static std::set& udids() + { + static std::set udids; + return udids; + } + }; + + // Runs work on a new thread with an 8 MiB stack and returns its result or rethrows its exception. + // tunnel_pair_usb() polls idevice's pairing future on the calling thread, and that poll needs more + // than the 128 KiB that musl gives threads by default, including cpprestsdk's thread pool threads. + // The kernel only backs the stack pages the thread touches. + std::vector RunOnLargeStack(std::function()> work) + { + struct Context + { + std::function()> work; + std::vector result; + std::exception_ptr exception; + } context = { work, {}, nullptr }; + + auto threadMain = [](void* argument) -> void* { + auto context = static_cast(argument); + try + { + context->result = context->work(); + } + catch (...) + { + context->exception = std::current_exception(); + } + return NULL; + }; + + pthread_attr_t attributes; + pthread_attr_init(&attributes); + pthread_attr_setstacksize(&attributes, 8 * 1024 * 1024); + + pthread_t thread; + int result = pthread_create(&thread, &attributes, threadMain, &context); + pthread_attr_destroy(&attributes); + + if (result != 0) + { + throw UnknownPairingError("Couldn't start pairing thread: " + std::string(strerror(result))); + } + + pthread_join(thread, NULL); + + if (context.exception) + { + std::rethrow_exception(context.exception); + } + + return context.result; + } +} + +DevicePairingManager* DevicePairingManager::instance() +{ + static DevicePairingManager instance; + return &instance; +} + +std::vector DevicePairingManager::GeneratePairingFile(std::string udid, std::string hostName) +{ + PairingInProgress pairing(udid); + return RunOnLargeStack([udid, hostName]() { return PairAndSerialize(udid, hostName); }); +} diff --git a/upstream_repo b/upstream_repo index dae9501..b0d2846 160000 --- a/upstream_repo +++ b/upstream_repo @@ -1 +1 @@ -Subproject commit dae9501abad10a4f008cc8a73138310679decd81 +Subproject commit b0d2846ec47d91d20532f6e951b96a2a92e32f65