From 3f0f06aec29776d9b423f38f2dc578f930d5f5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Or=C5=A1uli=C4=87?= Date: Sat, 26 Sep 2026 20:23:51 +0200 Subject: [PATCH 1/4] Generate Remote AltServer pairing files with idevice AltStore Classic 2.3's "Set up Remote AltServer" sends PairingFileRequest, which AltServer-Linux rejected. upstream_repo now handles the request (PairingFileRequest handler in AltServer-Windows ng-fixes); this commit adds the Linux implementation and the idevice build. src/DevicePairingManager.cpp ports DevicePairingManager.swift from AltServer for macOS: it finds the device's usbmuxd device_id by UDID, builds a usbmuxd provider, pairs with tunnel_pair_usb() (the device shows a Trust prompt) and serializes the RPPairing file. Both usbmuxd connections honor USBMUXD_SOCKET_ADDRESS, like the rest of AltServer-Linux. The pairing runs on a thread with an 8 MiB stack because polling idevice's pairing future overflowed the 128 KiB default stack of musl threads (SIGSEGV in tunnel_pair_usb on a cpprestsdk pool thread). libraries/idevice is jkcoxson/idevice v0.1.68 with the features usbmuxd,tunnel_tcp_stack,ring. Its static library also defines libplist-compatible plist_* functions and the Rust standard library, which clash with the vendored libplist, so makefiles/idevice-build/idevice.mak prelinks it with ld -r and keeps only the functions in idevice-api.txt global. IDEVICE_FFI_LIB selects a prebuilt library; without it the Makefile runs cargo. The builder images have no Rust toolchain, so CI cross-compiles the library for each architecture with cargo-zigbuild (build-idevice-cross.sh) and passes it in. Tested with AltStore Classic 2.3 on an iPhone 16 Pro running iOS 26.6.2: Remote AltServer setup received the pairing file, and with Prefer Remote AltServer on and AltServer stopped, AltStore installed an app on-device. x86_64, i586, armv7 and aarch64 build and link statically; the armv7 and aarch64 binaries were started under QEMU user emulation and found the device through usbmuxd. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 16 +- .gitmodules | 3 + Makefile | 15 +- README-NG.md | 49 +++- libraries/idevice | 1 + .../idevice-build/build-idevice-cross.sh | 17 ++ makefiles/idevice-build/idevice-api.txt | 18 ++ makefiles/idevice-build/idevice-features.txt | 1 + makefiles/idevice-build/idevice-files.mak | 1 + makefiles/idevice-build/idevice.mak | 35 +++ src/DevicePairingManager.cpp | 274 ++++++++++++++++++ upstream_repo | 2 +- 12 files changed, 423 insertions(+), 9 deletions(-) create mode 160000 libraries/idevice create mode 100755 makefiles/idevice-build/build-idevice-cross.sh create mode 100644 makefiles/idevice-build/idevice-api.txt create mode 100644 makefiles/idevice-build/idevice-features.txt create mode 100644 makefiles/idevice-build/idevice-files.mak create mode 100644 makefiles/idevice-build/idevice.mak create mode 100644 src/DevicePairingManager.cpp 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..81c3245 100644 --- a/README-NG.md +++ b/README-NG.md @@ -98,6 +98,40 @@ 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 uses `USBMUXD_SOCKET_ADDRESS` for + both the device lookup and the pairing connection, like the rest of AltServer-Linux. 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 +143,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..d97e136 --- /dev/null +++ b/makefiles/idevice-build/build-idevice-cross.sh @@ -0,0 +1,17 @@ +#!/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. +# +# 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..be81a12 --- /dev/null +++ b/makefiles/idevice-build/idevice-api.txt @@ -0,0 +1,18 @@ +idevice_data_free +idevice_error_free +idevice_provider_free +idevice_string_free +idevice_usbmuxd_addr_free +idevice_usbmuxd_connection_free +idevice_usbmuxd_default_addr_new +idevice_usbmuxd_device_get_device_id +idevice_usbmuxd_device_get_udid +idevice_usbmuxd_device_list_free +idevice_usbmuxd_get_devices +idevice_usbmuxd_new_default_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..529f78c --- /dev/null +++ b/makefiles/idevice-build/idevice.mak @@ -0,0 +1,35 @@ +# 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 + +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 + +$(IDEVICE_TARGET_DIR)/release/libidevice_ffi.a: + 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 + +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..f5a7e9d --- /dev/null +++ b/src/DevicePairingManager.cpp @@ -0,0 +1,274 @@ +#include "DevicePairingManager.h" + +#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); + } + + // Resolves the usbmuxd address the same way idevice_usbmuxd_new_default_connection() does + // (USBMUXD_SOCKET_ADDRESS, then the default socket), so the device lookup and the pairing + // connection go through the same usbmuxd. The macOS version always uses the default socket. + UsbmuxdAddrHandle* CreateUsbmuxdAddress() + { + UsbmuxdAddrHandle* address = NULL; + IdeviceFfiError* error = NULL; + + const char* socketAddress = getenv("USBMUXD_SOCKET_ADDRESS"); + if (socketAddress == NULL) + { + error = idevice_usbmuxd_default_addr_new(&address); + } + else if (strchr(socketAddress, ':') == NULL) + { + error = idevice_usbmuxd_unix_addr_new(socketAddress, &address); + } + else + { + // host:port, or [host]:port for IPv6. + std::string value = socketAddress; + auto separator = value.rfind(':'); + std::string host = value.substr(0, separator); + std::string port = value.substr(separator + 1); + if (host.size() >= 2 && host.front() == '[' && host.back() == ']') + { + host = host.substr(1, host.size() - 2); + } + + struct addrinfo hints = {}; + hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV; + hints.ai_socktype = SOCK_STREAM; + + struct addrinfo* result = NULL; + if (getaddrinfo(host.c_str(), port.c_str(), &hints, &result) != 0 || result == NULL) + { + throw UnknownPairingError("Invalid USBMUXD_SOCKET_ADDRESS: " + value); + } + + error = idevice_usbmuxd_tcp_addr_new(result->ai_addr, result->ai_addrlen, &address); + freeaddrinfo(result); + } + + if (error != NULL) + { + throw UnknownPairingError("Couldn't create usbmuxd address.", error); + } + + return address; + } + + // Finds the usbmuxd device_id for this UDID. + uint32_t ResolveDeviceID(std::string udid, UsbmuxdConnectionHandle* muxConnection) + { + UsbmuxdDeviceHandle** devices = NULL; + int deviceCount = 0; + if (auto error = idevice_usbmuxd_get_devices(muxConnection, &devices, &deviceCount)) + { + throw UnknownPairingError("Couldn't query connected devices.", error); + } + + std::optional deviceID; + for (int i = 0; i < deviceCount; i++) + { + 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. Connect to usbmuxd and resolve the device_id (required by idevice) for this UDID. + UsbmuxdConnectionHandle* muxConnection = NULL; + if (auto error = idevice_usbmuxd_new_default_connection(0, &muxConnection)) + { + throw UnknownPairingError("Couldn't connect to usbmuxd.", error); + } + std::unique_ptr muxConnectionOwner(muxConnection, idevice_usbmuxd_connection_free); + + uint32_t deviceID = ResolveDeviceID(udid, muxConnection); + + // 2. Build a device provider. usbmuxd_provider_new() takes ownership of the address only when it succeeds. + UsbmuxdAddrHandle* address = CreateUsbmuxdAddress(); + + 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; + } + + // 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) +{ + 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 From f3ea6f4f7d1408d492e1b0bbda414c2457a4cc12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Or=C5=A1uli=C4=87?= Date: Sat, 26 Sep 2026 21:16:18 +0200 Subject: [PATCH 2/4] Pair over USB through the usbmuxd the rest of AltServer uses DevicePairingManager left USBMUXD_SOCKET_ADDRESS to idevice, which reads every value containing ':' as a numeric TCP address. So the libusbmuxd form UNIX:, the one deploy/altserver-stack.yml sets, failed with FfiInvalidArg and AltStore got a generic "couldn't be configured" error, although the rest of AltServer accepts that value. The address is now read once by libusbmuxd's rules (UNIX:, : or []:, otherwise /var/run/usbmuxd), and the device lookup and the pairing provider both connect to it. usbmuxd can list one UDID twice, over USB and over the network. The lookup took the first match, so a network entry could be paired over. It now accepts only an entry whose connection type is USB, which is what the DeviceNotFound message already says. The usbmuxd connection used for the device list is closed as soon as the device_id is known, instead of staying open until the user answers the Trust prompt. idevice-api.txt exports the three functions this needs and drops the two it no longer calls. Tested with a temporary instance and a PairingFileRequest for a UDID that is not connected: DeviceNotFound with UNIX: relayed to netmuxd and with 127.0.0.1:27015. The build before this change answered the UNIX: case with "idevice error 33: FfiInvalidArg". The phone's UDID, listed by netmuxd only as Network, also gets DeviceNotFound. Co-Authored-By: Claude Opus 5.5 --- README-NG.md | 6 +- makefiles/idevice-build/idevice-api.txt | 5 +- src/DevicePairingManager.cpp | 177 ++++++++++++++++++------ 3 files changed, 140 insertions(+), 48 deletions(-) diff --git a/README-NG.md b/README-NG.md index 81c3245..6067574 100644 --- a/README-NG.md +++ b/README-NG.md @@ -121,8 +121,10 @@ the request. 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 uses `USBMUXD_SOCKET_ADDRESS` for - both the device lookup and the pairing connection, like the rest of AltServer-Linux. The pairing + "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 diff --git a/makefiles/idevice-build/idevice-api.txt b/makefiles/idevice-build/idevice-api.txt index be81a12..88b9a7c 100644 --- a/makefiles/idevice-build/idevice-api.txt +++ b/makefiles/idevice-build/idevice-api.txt @@ -4,12 +4,13 @@ idevice_provider_free idevice_string_free idevice_usbmuxd_addr_free idevice_usbmuxd_connection_free -idevice_usbmuxd_default_addr_new +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_default_connection +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 diff --git a/src/DevicePairingManager.cpp b/src/DevicePairingManager.cpp index f5a7e9d..794b180 100644 --- a/src/DevicePairingManager.cpp +++ b/src/DevicePairingManager.cpp @@ -3,8 +3,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -63,63 +65,151 @@ namespace ffiError); } - // Resolves the usbmuxd address the same way idevice_usbmuxd_new_default_connection() does - // (USBMUXD_SOCKET_ADDRESS, then the default socket), so the device lookup and the pairing - // connection go through the same usbmuxd. The macOS version always uses the default socket. - UsbmuxdAddrHandle* CreateUsbmuxdAddress() + // 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 { - UsbmuxdAddrHandle* address = NULL; - IdeviceFfiError* error = NULL; + std::string socketPath; // Empty for TCP. + sockaddr_storage tcpAddress = {}; + socklen_t tcpAddressLength = 0; + std::string description; + }; - const char* socketAddress = getenv("USBMUXD_SOCKET_ADDRESS"); - if (socketAddress == NULL) - { - error = idevice_usbmuxd_default_addr_new(&address); - } - else if (strchr(socketAddress, ':') == NULL) + UsbmuxdLocation ReadUsbmuxdLocation() + { + UsbmuxdLocation location; + location.socketPath = "/var/run/usbmuxd"; + location.description = location.socketPath; + + const char* value = getenv("USBMUXD_SOCKET_ADDRESS"); + if (value == NULL) { - error = idevice_usbmuxd_unix_addr_new(socketAddress, &address); + return location; } - else + + std::string address = value; + if (address.compare(0, 5, "UNIX:") == 0) { - // host:port, or [host]:port for IPv6. - std::string value = socketAddress; - auto separator = value.rfind(':'); - std::string host = value.substr(0, separator); - std::string port = value.substr(separator + 1); - if (host.size() >= 2 && host.front() == '[' && host.back() == ']') + if (address.size() > 5) { - host = host.substr(1, host.size() - 2); + location.socketPath = address.substr(5); + location.description = location.socketPath; } + return location; + } - struct addrinfo hints = {}; - hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV; - hints.ai_socktype = SOCK_STREAM; + 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; + } - struct addrinfo* result = NULL; - if (getaddrinfo(host.c_str(), port.c_str(), &hints, &result) != 0 || result == NULL) + 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) { - throw UnknownPairingError("Invalid USBMUXD_SOCKET_ADDRESS: " + value); + 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); + } - error = idevice_usbmuxd_tcp_addr_new(result->ai_addr, result->ai_addrlen, &address); - freeaddrinfo(result); + 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.", error); + throw UnknownPairingError("Couldn't create usbmuxd address for " + location.description + ".", error); } return address; } - // Finds the usbmuxd device_id for this UDID. - uint32_t ResolveDeviceID(std::string udid, UsbmuxdConnectionHandle* muxConnection) + // 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, &devices, &deviceCount)) + if (auto error = idevice_usbmuxd_get_devices(muxConnection.get(), &devices, &deviceCount)) { throw UnknownPairingError("Couldn't query connected devices.", error); } @@ -127,6 +217,11 @@ namespace 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) { @@ -161,18 +256,12 @@ namespace std::vector PairAndSerialize(std::string udid, std::string hostName) { - // 1. Connect to usbmuxd and resolve the device_id (required by idevice) for this UDID. - UsbmuxdConnectionHandle* muxConnection = NULL; - if (auto error = idevice_usbmuxd_new_default_connection(0, &muxConnection)) - { - throw UnknownPairingError("Couldn't connect to usbmuxd.", error); - } - std::unique_ptr muxConnectionOwner(muxConnection, idevice_usbmuxd_connection_free); - - uint32_t deviceID = ResolveDeviceID(udid, muxConnection); + // 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. usbmuxd_provider_new() takes ownership of the address only when it succeeds. - UsbmuxdAddrHandle* address = CreateUsbmuxdAddress(); + // 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)) From 1d13d1bf41362eca0b9f8c9b35f99102e064f966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Or=C5=A1uli=C4=87?= Date: Sat, 26 Sep 2026 21:16:18 +0200 Subject: [PATCH 3/4] idevice build: rerun cargo, and keep partial and cross output out of the way The cargo rule had no prerequisites, so after the first build make never ran cargo again: a submodule bump or a change to idevice-features.txt was prelinked from the old library. cargo now runs on every make. When nothing changed it leaves libidevice_ffi.a untouched, so make does not prelink again. idevice.mak sets .DELETE_ON_ERROR like the other makefiles, so a failed ld or objcopy does not leave a partial idevice_ffi.o that the next make treats as built. build-idevice-cross.sh builds into libraries/idevice/target/cross instead of target-cross. idevice's .gitignore covers target/, so the submodule no longer shows untracked content after a cross build. Tested in rust:latest with plain make: an unchanged tree and a touched idevice-features.txt run cargo in about 0.1 s and leave idevice_ffi.o alone; adding a feature rebuilds the crate and prelinks again. Co-Authored-By: Claude Opus 5.5 --- makefiles/idevice-build/build-idevice-cross.sh | 7 ++++--- makefiles/idevice-build/idevice.mak | 11 ++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/makefiles/idevice-build/build-idevice-cross.sh b/makefiles/idevice-build/build-idevice-cross.sh index d97e136..5d55418 100755 --- a/makefiles/idevice-build/build-idevice-cross.sh +++ b/makefiles/idevice-build/build-idevice-cross.sh @@ -2,7 +2,8 @@ # 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. +# 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 @@ -12,6 +13,6 @@ 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 + 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" +echo "$root/libraries/idevice/target/cross/$target/release/libidevice_ffi.a" diff --git a/makefiles/idevice-build/idevice.mak b/makefiles/idevice-build/idevice.mak index 529f78c..38d12a3 100644 --- a/makefiles/idevice-build/idevice.mak +++ b/makefiles/idevice-build/idevice.mak @@ -10,13 +10,19 @@ 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 -$(IDEVICE_TARGET_DIR)/release/libidevice_ffi.a: +# 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) @@ -24,6 +30,9 @@ $(BUILD_DIR)/idevice_ffi.o: $(IDEVICE_FFI_LIB) $(idevice_api) 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) From 04fea72fe689775f86fde1174f9fc42729340e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Or=C5=A1uli=C4=87?= Date: Sat, 26 Sep 2026 21:59:11 +0200 Subject: [PATCH 4/4] Refuse a second pairing with a device that is already pairing 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 each retry while the prompt was still up started another pairing with the same device and blocked one more request thread and 8 MiB pairing stack. A request for a UDID that is already pairing now fails at once with "AltServer is already pairing with this device." The first pairing still waits for the user, as the macOS version does. Tested with a fake usbmuxd that lists the UDID as Network and then USB and never answers the pairing's Connect: the pairing connects with the USB entry's DeviceID, a second request fails in 0.03 s, and after the fake drops the connection a new request is accepted. Without this change the second request got no reply within 60 s. Co-Authored-By: Claude Opus 5.5 --- src/DevicePairingManager.cpp | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/DevicePairingManager.cpp b/src/DevicePairingManager.cpp index 794b180..7146638 100644 --- a/src/DevicePairingManager.cpp +++ b/src/DevicePairingManager.cpp @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include "ServerError.hpp" @@ -301,6 +303,51 @@ namespace 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. @@ -359,5 +406,6 @@ DevicePairingManager* DevicePairingManager::instance() std::vector DevicePairingManager::GeneratePairingFile(std::string udid, std::string hostName) { + PairingInProgress pairing(udid); return RunOnLargeStack([udid, hostName]() { return PairAndSerialize(udid, hostName); }); }