diff options
| -rw-r--r-- | configure.ac | 2 | ||||
| -rw-r--r-- | docs/idevicerestore.1 | 40 | ||||
| -rw-r--r-- | src/Makefile.am | 3 | ||||
| -rw-r--r-- | src/deviceinterfaced.c | 295 | ||||
| -rw-r--r-- | src/deviceinterfaced.h | 28 | ||||
| -rw-r--r-- | src/idevicerestore.c | 39 |
6 files changed, 407 insertions, 0 deletions
diff --git a/configure.ac b/configure.ac index 9855bde..d0c29e9 100644 --- a/configure.ac +++ b/configure.ac @@ -89,12 +89,14 @@ case ${host_os} in darwin*) AC_MSG_RESULT([${host_os}]) AC_DEFINE([_DARWIN_BETTER_REALPATH], [1], [Use better method for realpath]) + darwin=true ;; *) AC_MSG_RESULT([${host_os}]) ;; esac AM_CONDITIONAL(WIN32, test x$win32 = xtrue) +AM_CONDITIONAL(DARWIN, test x$darwin = xtrue) if test x$win32 != xtrue; then if test "x$ac_cv_func_mkstemp" != xyes; then diff --git a/docs/idevicerestore.1 b/docs/idevicerestore.1 index 633469a..df46dcb 100644 --- a/docs/idevicerestore.1 +++ b/docs/idevicerestore.1 @@ -110,6 +110,46 @@ Use given VARIANT to match the build identity to use, e.g. 'Customer Erase Insta Try to continue the restore process after certain errors (like a failed baseband update). \f[B]WARNING\f[]: This might render the device unable to boot or only partially functioning. \f[B]Use with caution\f[]. +.TP +.B \-\-exclusive\-usb +\f[B](macOS only)\f[] Keep the \f[B]deviceinterfaced\f[] system daemon from claiming the +USB device during Port DFU-to-DFU re-enumeration, which can otherwise cause +idevicerestore to time out waiting for the device to reconnect. + +idevicerestore itself does not need to run as root for this option. The few +\f[B]launchctl\f[](1) subcommands that manage the system-domain +\f[B]com.apple.deviceinterfaced\f[] daemon (\f[B]bootout\f[], \f[B]bootstrap\f[], +\f[B]kickstart\f[], \f[B]kill\f[]) are elevated individually via \f[B]sudo\f[](8), which +may prompt for your password (or Touch ID, if configured) on the controlling +terminal. See the \f[B]NOTES\f[] section below for unattended use. + +\f[B]EXPERIMENTAL\f[]: only use if you are hitting DFU reconnection timeouts. + +.SH NOTES +.SS Passwordless \-\-exclusive\-usb +By default, using \f[B]\-\-exclusive\-usb\f[] will prompt for your +password via \f[B]sudo\f[](8) whenever idevicerestore needs to boot out, +kickstart, or forcibly terminate the \f[B]com.apple.deviceinterfaced\f[] +daemon. For unattended/scripted use, you can allow just those specific +commands to run without a password by adding a narrowly-scoped rule to +\f[B]sudoers\f[](5), e.g. via \f[B]visudo \-f /etc/sudoers.d/idevicerestore\f[]: + +.nf +.RS +Cmnd_Alias DEVICEINTERFACED_CTL = \\ + /bin/launchctl bootout system/com.apple.deviceinterfaced, \\ + /bin/launchctl bootstrap system /Library/Apple/System/Library/PrivateFrameworks/DeviceInterface.framework/Support/com.apple.deviceinterfaced.plist, \\ + /bin/launchctl kickstart system/com.apple.deviceinterfaced, \\ + /bin/launchctl kill SIGKILL system/com.apple.deviceinterfaced + +%admin ALL=(root) NOPASSWD: DEVICEINTERFACED_CTL +.RE +.fi + +Replace \f[B]%admin\f[] with the specific user or group that should be +allowed to run these commands without a password. This grants no broader +privileges: only these exact \f[B]launchctl\f[] invocations, against this +one system daemon, may be run as root without authentication. .SH AUTHORS Martin Szulecki diff --git a/src/Makefile.am b/src/Makefile.am index 2b7084e..c61dfd7 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -47,6 +47,9 @@ idevicerestore_SOURCES = \ ace3.c ace3.h \ download.c download.h \ locking.c locking.h +if DARWIN +idevicerestore_SOURCES += deviceinterfaced.c deviceinterfaced.h +endif if HAVE_LIMERA1N idevicerestore_SOURCES += limera1n_payload.h limera1n.c limera1n.h endif diff --git a/src/deviceinterfaced.c b/src/deviceinterfaced.c new file mode 100644 index 0000000..345a59a --- /dev/null +++ b/src/deviceinterfaced.c @@ -0,0 +1,295 @@ +/* + * deviceinterfaced.c + * Control deviceinterfaced while accessing restore devices on macOS + * + * Copyright (c) 2026 libimobiledevice contributors. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <errno.h> +#include <fcntl.h> +#include <spawn.h> +#include <stdatomic.h> +#include <stdlib.h> +#include <string.h> +#include <sys/wait.h> +#include <unistd.h> + +#include <libimobiledevice-glue/thread.h> + +#include "deviceinterfaced.h" +#include "log.h" + +#define DEVICEINTERFACED_LABEL "com.apple.deviceinterfaced" +#define DEVICEINTERFACED_TARGET "system/" DEVICEINTERFACED_LABEL +#define DEVICEINTERFACED_PLIST "/Library/Apple/System/Library/PrivateFrameworks/DeviceInterface.framework/Support/com.apple.deviceinterfaced.plist" +#define SUDO_PATH "/usr/bin/sudo" +#define LAUNCHCTL_PATH "/bin/launchctl" +#define SUDO_PROMPT "[sudo] password to manage " DEVICEINTERFACED_LABEL ": " +/* "sudo" "-p" prompt "--" "/bin/launchctl" + subcommand args (max observed: 3) + NULL */ +#define LAUNCHCTL_ARGV_MAX 10 + +extern char **environ; + +enum deviceinterfaced_control_state { + DEVICEINTERFACED_INACTIVE, + DEVICEINTERFACED_BOOTED_OUT, + DEVICEINTERFACED_KILLING, + DEVICEINTERFACED_NEEDS_KICKSTART +}; + +static int cleanup_registered = 0; +static enum deviceinterfaced_control_state control_state = DEVICEINTERFACED_INACTIVE; +static atomic_bool stop_killer; +static THREAD_T killer_thread = THREAD_T_NULL; + +/* + * Runs "launchctl <args...>". Read-only queries (e.g. "print") never need + * elevated privileges and are run directly. State-changing subcommands + * ("bootout", "bootstrap", "kickstart", "kill") act on a system-domain + * LaunchDaemon and require root, so they are wrapped with "sudo" instead of + * requiring the whole idevicerestore process to run as root. sudo prompts + * for the password on the controlling terminal (/dev/tty) directly, so + * redirecting stdout/stderr below does not hide the prompt; it is only + * suppressed here to keep launchctl's own (uninteresting) output quiet. + * Any sudo diagnostic (e.g. "a password is required") is left visible on + * stderr for privileged calls so the user can see why it failed. + */ +static int launchctl_run(char *const args[], int privileged) +{ + posix_spawn_file_actions_t actions; + char *argv[LAUNCHCTL_ARGV_MAX]; + const char *program; + pid_t pid; + pid_t waited; + int error; + int status; + int argc = 0; + int i; + + if (privileged) { + argv[argc++] = SUDO_PATH; + argv[argc++] = "-p"; + argv[argc++] = SUDO_PROMPT; + argv[argc++] = "--"; + argv[argc++] = LAUNCHCTL_PATH; + program = SUDO_PATH; + } else { + argv[argc++] = LAUNCHCTL_PATH; + program = LAUNCHCTL_PATH; + } + for (i = 0; args[i] != NULL; i++) { + if (argc >= LAUNCHCTL_ARGV_MAX - 1) { + return -ENAMETOOLONG; + } + argv[argc++] = args[i]; + } + argv[argc] = NULL; + + error = posix_spawn_file_actions_init(&actions); + if (error != 0) { + return -error; + } + error = posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, "/dev/null", O_WRONLY, 0); + if (error == 0 && !privileged) { + error = posix_spawn_file_actions_addopen(&actions, STDERR_FILENO, "/dev/null", O_WRONLY, 0); + } + if (error != 0) { + posix_spawn_file_actions_destroy(&actions); + return -error; + } + + error = posix_spawn(&pid, program, &actions, NULL, argv, environ); + posix_spawn_file_actions_destroy(&actions); + if (error != 0) { + return -error; + } + + do { + waited = waitpid(pid, &status, 0); + } while (waited < 0 && errno == EINTR); + if (waited < 0) { + return -errno; + } + + if (!WIFEXITED(status)) { + return -EIO; + } + return WEXITSTATUS(status); +} + +static void log_launchctl_failure(const char *operation, int status) +{ + if (status < 0) { + logger(LL_ERROR, "Could not %s " DEVICEINTERFACED_LABEL ": %s.\n", operation, strerror(-status)); + } else { + logger(LL_ERROR, "Could not %s " DEVICEINTERFACED_LABEL ": launchctl exited with status %d.\n", operation, status); + } +} + +static int service_is_loaded(int *loaded) +{ + char *const argv[] = { + "print", DEVICEINTERFACED_TARGET, NULL + }; + int status = launchctl_run(argv, 0); + + if (status < 0) { + return status; + } + *loaded = (status == 0); + return 0; +} + +static int wait_for_service_to_unload(void) +{ + int i; + int loaded; + + for (i = 0; i < 100; i++) { + int status = service_is_loaded(&loaded); + if (status < 0) { + return status; + } + if (!loaded) { + return 0; + } + usleep(50000); + } + return -ETIMEDOUT; +} + +static void *kill_service(void *data) +{ + char *const argv[] = { + "kill", "SIGKILL", DEVICEINTERFACED_TARGET, NULL + }; + + (void)data; + while (!atomic_load_explicit(&stop_killer, memory_order_relaxed)) { + launchctl_run(argv, 1); + usleep(100000); + } + return NULL; +} + +void deviceinterfaced_control_stop(void) +{ + int status; + + if (control_state == DEVICEINTERFACED_KILLING) { + atomic_store_explicit(&stop_killer, 1, memory_order_relaxed); + status = thread_join(killer_thread); + if (status != 0) { + logger(LL_ERROR, "Could not stop " DEVICEINTERFACED_LABEL " termination loop: %s.\n", strerror(status)); + return; + } + killer_thread = THREAD_T_NULL; + control_state = DEVICEINTERFACED_NEEDS_KICKSTART; + } + + if (control_state == DEVICEINTERFACED_BOOTED_OUT) { + char *const bootstrap_argv[] = { + "bootstrap", "system", DEVICEINTERFACED_PLIST, NULL + }; + status = launchctl_run(bootstrap_argv, 1); + if (status != 0) { + log_launchctl_failure("bootstrap", status); + logger(LL_ERROR, "Run 'sudo launchctl bootstrap system " DEVICEINTERFACED_PLIST "' to load it.\n"); + return; + } + control_state = DEVICEINTERFACED_NEEDS_KICKSTART; + } + + if (control_state == DEVICEINTERFACED_NEEDS_KICKSTART) { + char *const kickstart_argv[] = { + "kickstart", DEVICEINTERFACED_TARGET, NULL + }; + status = launchctl_run(kickstart_argv, 1); + if (status != 0) { + log_launchctl_failure("kickstart", status); + logger(LL_ERROR, "Run 'sudo launchctl kickstart " DEVICEINTERFACED_TARGET "' to start it.\n"); + return; + } + control_state = DEVICEINTERFACED_INACTIVE; + logger(LL_INFO, "Restored " DEVICEINTERFACED_LABEL ".\n"); + } +} + +int deviceinterfaced_control_start(void) +{ + char *const bootout_argv[] = { + "bootout", DEVICEINTERFACED_TARGET, NULL + }; + int loaded; + int status; + + if (control_state != DEVICEINTERFACED_INACTIVE) { + return 0; + } + + if (!cleanup_registered) { + if (atexit(deviceinterfaced_control_stop) != 0) { + logger(LL_ERROR, "Could not register " DEVICEINTERFACED_LABEL " cleanup.\n"); + return -1; + } + cleanup_registered = 1; + } + + status = service_is_loaded(&loaded); + if (status < 0) { + log_launchctl_failure("query", status); + return -1; + } + if (!loaded) { + logger(LL_INFO, DEVICEINTERFACED_LABEL " is not loaded.\n"); + return 0; + } + + status = launchctl_run(bootout_argv, 1); + if (status == 0) { + control_state = DEVICEINTERFACED_BOOTED_OUT; + status = wait_for_service_to_unload(); + if (status < 0) { + if (status == -ETIMEDOUT) { + logger(LL_ERROR, "Timed out waiting for " DEVICEINTERFACED_LABEL " to stop.\n"); + } else { + log_launchctl_failure("query", status); + } + return -1; + } + logger(LL_INFO, "Booted out " DEVICEINTERFACED_LABEL " for the duration of the restore.\n"); + return 0; + } + if (status < 0) { + log_launchctl_failure("boot out", status); + return -1; + } + + logger(LL_WARNING, "Could not boot out " DEVICEINTERFACED_LABEL " (launchctl exit status %d); terminating it while the restore runs.\n", status); + atomic_store_explicit(&stop_killer, 0, memory_order_relaxed); + status = thread_new(&killer_thread, kill_service, NULL); + if (status != 0) { + logger(LL_ERROR, "Could not start " DEVICEINTERFACED_LABEL " termination loop: %s.\n", strerror(status)); + return -1; + } + control_state = DEVICEINTERFACED_KILLING; + return 0; +} diff --git a/src/deviceinterfaced.h b/src/deviceinterfaced.h new file mode 100644 index 0000000..c8623ad --- /dev/null +++ b/src/deviceinterfaced.h @@ -0,0 +1,28 @@ +/* + * deviceinterfaced.h + * Control deviceinterfaced while accessing restore devices on macOS + * + * Copyright (c) 2026 libimobiledevice contributors. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef DEVICEINTERFACED_H +#define DEVICEINTERFACED_H + +int deviceinterfaced_control_start(void); +void deviceinterfaced_control_stop(void); + +#endif diff --git a/src/idevicerestore.c b/src/idevicerestore.c index 04070a1..11d7ec4 100644 --- a/src/idevicerestore.c +++ b/src/idevicerestore.c @@ -61,6 +61,10 @@ #include "locking.h" +#ifdef __APPLE__ +#include "deviceinterfaced.h" +#endif + #define VERSION_XML "version.xml" #ifndef IDEVICERESTORE_NOMAIN @@ -91,6 +95,9 @@ static struct option longopts[] = { { "ignore-errors", no_argument, NULL, 1 }, { "variant", required_argument, NULL, 2 }, { "logfile", required_argument, NULL, 3 }, +#ifdef __APPLE__ + { "exclusive-usb", no_argument, NULL, 4 }, +#endif { NULL, 0, NULL, 0 } }; @@ -101,6 +108,14 @@ static void usage(int argc, char* argv[], int err) #else #define PWN_FLAG_LINE "" #endif +#ifdef __APPLE__ +#define EXCLUSIVE_USB_ACCESS_FLAG_LINE \ + " --exclusive-usb Keep deviceinterfaced from claiming the USB device\n" \ + " during the restore (may prompt for your password\n" \ + " via sudo)\n" +#else +#define EXCLUSIVE_USB_ACCESS_FLAG_LINE "" +#endif char* name = strrchr(argv[0], '/'); fprintf((err) ? stderr : stdout, "Usage: %s [OPTIONS] PATH\n" \ @@ -141,6 +156,7 @@ static void usage(int argc, char* argv[], int err) " -v, --version Print version information\n" \ "\n" \ "Advanced/experimental options:\n" + EXCLUSIVE_USB_ACCESS_FLAG_LINE \ " -c, --custom Restore with a custom firmware (requires bootrom exploit)\n" \ " -s, --server URL Override default signing server request URL\n" \ " -x, --exclude Exclude nor/baseband upgrade (legacy devices)\n" \ @@ -1777,6 +1793,9 @@ int main(int argc, char* argv[]) int ipsw_info = 0; int result = 0; const char* logfile = NULL; +#ifdef __APPLE__ + int exclusive_usb_access = 0; +#endif logger_set_print_func(tty_print); @@ -1978,6 +1997,12 @@ int main(int argc, char* argv[]) logfile = optarg; break; +#ifdef __APPLE__ + case 4: + exclusive_usb_access = 1; + break; +#endif + default: usage(argc, argv, 1); return EXIT_FAILURE; @@ -2036,12 +2061,26 @@ int main(int argc, char* argv[]) curl_global_init(CURL_GLOBAL_ALL); +#ifdef __APPLE__ + if (exclusive_usb_access && deviceinterfaced_control_start() < 0) { + idevicerestore_client_free(client); + curl_global_cleanup(); + return EXIT_FAILURE; + } +#endif + client->flags |= FLAG_IN_PROGRESS; result = idevicerestore_start(client); client->flags &= ~FLAG_IN_PROGRESS; idevicerestore_client_free(client); +#ifdef __APPLE__ + if (exclusive_usb_access) { + deviceinterfaced_control_stop(); + } +#endif + curl_global_cleanup(); return (result == 0) ? EXIT_SUCCESS : EXIT_FAILURE; |
