diff options
Diffstat (limited to 'src/deviceinterfaced.c')
| -rw-r--r-- | src/deviceinterfaced.c | 295 |
1 files changed, 295 insertions, 0 deletions
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; +} |
