summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--configure.ac2
-rw-r--r--docs/idevicerestore.140
-rw-r--r--src/Makefile.am3
-rw-r--r--src/deviceinterfaced.c295
-rw-r--r--src/deviceinterfaced.h28
-rw-r--r--src/idevicerestore.c39
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;