diff --git a/native/jni/su/activity.c b/native/jni/su/activity.c new file mode 100644 index 000000000..9058fa1b1 --- /dev/null +++ b/native/jni/su/activity.c @@ -0,0 +1,141 @@ +/* +** Copyright 2017, John Wu (@topjohnwu) +** Copyright 2010, Adam Shanks (@ChainsDD) +** Copyright 2008, Zinx Verituse (@zinxv) +** +*/ + +#include +#include +#include +#include +#include +#include + +#include "magisk.h" +#include "su.h" + +/* intent actions */ +#define ACTION_REQUEST "%s/" REQUESTOR_PREFIX ".RequestActivity" +#define ACTION_RESULT "%s/" REQUESTOR_PREFIX ".SuReceiver" + +#define AM_PATH "/system/bin/app_process", "/system/bin", "com.android.commands.am.Am" + +static char *get_command(const struct su_request *to) { + if (to->command) + return to->command; + if (to->shell) + return to->shell; + return DEFAULT_SHELL; +} + +static void silent_run(char* const args[]) { + set_identity(0); + if (fork()) + return; + int zero = open("/dev/zero", O_RDONLY | O_CLOEXEC); + dup2(zero, 0); + int null = open("/dev/null", O_WRONLY | O_CLOEXEC); + dup2(null, 1); + dup2(null, 2); + setenv("CLASSPATH", "/system/framework/am.jar", 1); + execv(args[0], args); + PLOGE("exec am"); + _exit(EXIT_FAILURE); +} + +static int setup_user(struct su_context *ctx, char* user) { + switch (ctx->info->dbs.v[SU_MULTIUSER_MODE]) { + case MULTIUSER_MODE_OWNER_ONLY: /* Should already be denied if not owner */ + case MULTIUSER_MODE_OWNER_MANAGED: + sprintf(user, "%d", 0); + return ctx->info->uid / 100000; + case MULTIUSER_MODE_USER: + sprintf(user, "%d", ctx->info->uid / 100000); + break; + } + return 0; +} + +void app_send_result(struct su_context *ctx, policy_t policy) { + char fromUid[16]; + if (ctx->info->dbs.v[SU_MULTIUSER_MODE] == MULTIUSER_MODE_OWNER_MANAGED) + sprintf(fromUid, "%d", ctx->info->uid % 100000); + else + sprintf(fromUid, "%d", ctx->info->uid); + + char toUid[16]; + sprintf(toUid, "%d", ctx->to.uid); + + char pid[16]; + sprintf(pid, "%d", ctx->pid); + + char user[16]; + int notify = setup_user(ctx, user); + + char activity[128]; + sprintf(activity, ACTION_RESULT, ctx->info->str.s[SU_MANAGER]); + + // Send notice to manager, enable logging + char *result_command[] = { + AM_PATH, "broadcast", "-n", + activity, + "--user", user, + "--ei", "mode", "0", + "--ei", "from.uid", fromUid, + "--ei", "to.uid", toUid, + "--ei", "pid", pid, + "--es", "command", get_command(&ctx->to), + "--es", "action", policy == ALLOW ? "allow" : "deny", + NULL + }; + silent_run(result_command); + + // Send notice to user (if needed) to create toasts + if (notify) { + sprintf(fromUid, "%d", ctx->info->uid); + sprintf(user, "%d", notify); + char *notify_command[] = { + AM_PATH, "broadcast", "-n", + activity, + "--user", user, + "--ei", "mode", "1", + "--ei", "from.uid", fromUid, + "--es", "action", policy == ALLOW ? "allow" : "deny", + NULL + }; + silent_run(notify_command); + } +} + +void app_send_request(struct su_context *ctx) { + char user[16]; + int notify = setup_user(ctx, user); + + char activity[128]; + sprintf(activity, ACTION_REQUEST, ctx->info->str.s[SU_MANAGER]); + + char *request_command[] = { + AM_PATH, "start", "-n", + activity, + "--user", user, + "--es", "socket", ctx->sock_path, + "--ez", "timeout", notify ? "false" : "true", + NULL + }; + silent_run(request_command); + + // Send notice to user to tell them root is managed by owner + if (notify) { + sprintf(user, "%d", notify); + sprintf(activity, ACTION_RESULT, ctx->info->str.s[SU_MANAGER]); + char *notify_command[] = { + AM_PATH, "broadcast", "-n", + activity, + "--user", user, + "--ei", "mode", "2", + NULL + }; + silent_run(notify_command); + } +} diff --git a/native/jni/su/pts.c b/native/jni/su/pts.c new file mode 100644 index 000000000..e9d4d60fc --- /dev/null +++ b/native/jni/su/pts.c @@ -0,0 +1,318 @@ +/* + * Copyright 2013, Tan Chee Eng (@tan-ce) + */ + + /* + * pts.c + * + * Manages the pseudo-terminal driver on Linux/Android and provides some + * helper functions to handle raw input mode and terminal window resizing + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +#include "magisk.h" +#include "pts.h" + +/** + * Helper functions + */ +// Ensures all the data is written out +static int write_blocking(int fd, char *buf, ssize_t bufsz) { + ssize_t ret, written; + + written = 0; + do { + ret = write(fd, buf + written, bufsz - written); + if (ret == -1) return -1; + written += ret; + } while (written < bufsz); + + return 0; +} + +/** + * Pump data from input FD to output FD. If close_output is + * true, then close the output FD when we're done. + */ +static void pump_ex(int input, int output, int close_output) { + char buf[4096]; + int len; + while ((len = read(input, buf, 4096)) > 0) { + if (write_blocking(output, buf, len) == -1) break; + } + close(input); + if (close_output) close(output); +} + +/** + * Pump data from input FD to output FD. Will close the + * output FD when done. + */ +static void pump(int input, int output) { + pump_ex(input, output, 1); +} + +static void* pump_thread(void* data) { + int* files = (int*)data; + int input = files[0]; + int output = files[1]; + pump(input, output); + free(data); + return NULL; +} + +static void pump_async(int input, int output) { + pthread_t writer; + int* files = (int*)malloc(sizeof(int) * 2); + if (files == NULL) { + exit(-1); + } + files[0] = input; + files[1] = output; + pthread_create(&writer, NULL, pump_thread, files); +} + + +/** + * pts_open + * + * Opens a pts device and returns the name of the slave tty device. + * + * Arguments + * slave_name the name of the slave device + * slave_name_size the size of the buffer passed via slave_name + * + * Return Values + * on failure either -2 or -1 (errno set) is returned. + * on success, the file descriptor of the master device is returned. + */ +int pts_open(char *slave_name, size_t slave_name_size) { + int fdm; + char sn_tmp[256]; + + // Open master ptmx device + fdm = open("/dev/ptmx", O_RDWR); + if (fdm == -1) + goto error; + + // Get the slave name + if (ptsname_r(fdm, slave_name, slave_name_size-1)) + goto error; + + slave_name[slave_name_size - 1] = '\0'; + + // Grant, then unlock + if (grantpt(fdm) == -1) + goto error; + + if (unlockpt(fdm) == -1) + goto error; + + return fdm; +error: + close(fdm); + PLOGE("pts_open"); + return -1; +} + +// Stores the previous termios of stdin +static struct termios old_stdin; +static int stdin_is_raw = 0; + +/** + * set_stdin_raw + * + * Changes stdin to raw unbuffered mode, disables echo, + * auto carriage return, etc. + * + * Return Value + * on failure -1, and errno is set + * on success 0 + */ +int set_stdin_raw(void) { + struct termios new_termios; + + // Save the current stdin termios + if (tcgetattr(STDIN_FILENO, &old_stdin) < 0) { + return -1; + } + + // Start from the current settings + new_termios = old_stdin; + + // Make the terminal like an SSH or telnet client + new_termios.c_iflag |= IGNPAR; + new_termios.c_iflag &= ~(ISTRIP | INLCR | IGNCR | ICRNL | IXON | IXANY | IXOFF); + new_termios.c_lflag &= ~(ISIG | ICANON | ECHO | ECHOE | ECHOK | ECHONL); + new_termios.c_oflag &= ~OPOST; + new_termios.c_cc[VMIN] = 1; + new_termios.c_cc[VTIME] = 0; + + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &new_termios) < 0) { + return -1; + } + + stdin_is_raw = 1; + + return 0; +} + +/** + * restore_stdin + * + * Restore termios on stdin to the state it was before + * set_stdin_raw() was called. If set_stdin_raw() was + * never called, does nothing and doesn't return an error. + * + * This function is async-safe. + * + * Return Value + * on failure, -1 and errno is set + * on success, 0 + */ +int restore_stdin(void) { + if (!stdin_is_raw) return 0; + + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_stdin) < 0) { + return -1; + } + + stdin_is_raw = 0; + + return 0; +} + +// Flag indicating whether the sigwinch watcher should terminate. +static volatile int closing_time = 0; + +/** + * Thread process. Wait for a SIGWINCH to be received, then update + * the terminal size. + */ +static void *watch_sigwinch(void *data) { + sigset_t winch; + int sig; + int master = ((int *)data)[0]; + int slave = ((int *)data)[1]; + + sigemptyset(&winch); + sigaddset(&winch, SIGWINCH); + + do { + if (closing_time) break; + + // Get the new terminal size + struct winsize w; + if (ioctl(master, TIOCGWINSZ, &w) == -1) { + continue; + } + + // Set the new terminal size + ioctl(slave, TIOCSWINSZ, &w); + + } while (sigwait(&winch, &sig) == 0); + + free(data); + return NULL; +} + +/** + * watch_sigwinch_async + * + * After calling this function, if the application receives + * SIGWINCH, the terminal window size will be read from + * "input" and set on "output". + * + * NOTE: This function blocks SIGWINCH and spawns a thread. + * NOTE 2: This function must be called before any of the + * pump functions. + * + * Arguments + * master A file descriptor of the TTY window size to follow + * slave A file descriptor of the TTY window size which is + * to be set on SIGWINCH + * + * Return Value + * on failure, -1 and errno will be set. In this case, no + * thread has been spawned and SIGWINCH will not be + * blocked. + * on success, 0 + */ +int watch_sigwinch_async(int master, int slave) { + pthread_t watcher; + int *files = (int *) malloc(sizeof(int) * 2); + if (files == NULL) { + return -1; + } + + // Block SIGWINCH so sigwait can later receive it + sigset_t winch; + sigemptyset(&winch); + sigaddset(&winch, SIGWINCH); + if (sigprocmask(SIG_BLOCK, &winch, NULL) == -1) { + free(files); + return -1; + } + + // Initialize some variables, then start the thread + closing_time = 0; + files[0] = master; + files[1] = slave; + int ret = pthread_create(&watcher, NULL, &watch_sigwinch, files); + if (ret != 0) { + free(files); + errno = ret; + return -1; + } + + return 0; +} + +/** + * watch_sigwinch_cleanup + * + * Cause the SIGWINCH watcher thread to terminate + */ +void watch_sigwinch_cleanup(void) { + closing_time = 1; + raise(SIGWINCH); +} + +/** + * pump_stdin_async + * + * Forward data from STDIN to the given FD + * in a seperate thread + */ +void pump_stdin_async(int outfd) { + // Put stdin into raw mode + set_stdin_raw(); + + // Pump data from stdin to the PTY + pump_async(STDIN_FILENO, outfd); +} + +/** + * pump_stdout_blocking + * + * Forward data from the FD to STDOUT. + * Returns when the remote end of the FD closes. + * + * Before returning, restores stdin settings. + */ +void pump_stdout_blocking(int infd) { + // Pump data from stdout to PTY + pump_ex(infd, STDOUT_FILENO, 0 /* Don't close output when done */); + + // Cleanup + restore_stdin(); + watch_sigwinch_cleanup(); +} diff --git a/native/jni/su/pts.h b/native/jni/su/pts.h new file mode 100644 index 000000000..8f645562f --- /dev/null +++ b/native/jni/su/pts.h @@ -0,0 +1,106 @@ +/* + * Copyright 2013, Tan Chee Eng (@tan-ce) + */ + + /* + * pts.h + * + * Manages the pseudo-terminal driver on Linux/Android and provides some + * helper functions to handle raw input mode and terminal window resizing + */ + +#ifndef _PTS_H_ +#define _PTS_H_ + +#include + +/** + * pts_open + * + * Opens a pts device and returns the name of the slave tty device. + * + * Arguments + * slave_name the name of the slave device + * slave_name_size the size of the buffer passed via slave_name + * + * Return Values + * on failure either -2 or -1 (errno set) is returned. + * on success, the file descriptor of the master device is returned. + */ +int pts_open(char *slave_name, size_t slave_name_size); + +/** + * set_stdin_raw + * + * Changes stdin to raw unbuffered mode, disables echo, + * auto carriage return, etc. + * + * Return Value + * on failure -1, and errno is set + * on success 0 + */ +int set_stdin_raw(void); + +/** + * restore_stdin + * + * Restore termios on stdin to the state it was before + * set_stdin_raw() was called. If set_stdin_raw() was + * never called, does nothing and doesn't return an error. + * + * This function is async-safe. + * + * Return Value + * on failure, -1 and errno is set + * on success, 0 + */ +int restore_stdin(void); + +/** + * watch_sigwinch_async + * + * After calling this function, if the application receives + * SIGWINCH, the terminal window size will be read from + * "input" and set on "output". + * + * NOTE: This function blocks SIGWINCH and spawns a thread. + * + * Arguments + * master A file descriptor of the TTY window size to follow + * slave A file descriptor of the TTY window size which is + * to be set on SIGWINCH + * + * Return Value + * on failure, -1 and errno will be set. In this case, no + * thread has been spawned and SIGWINCH will not be + * blocked. + * on success, 0 + */ +int watch_sigwinch_async(int master, int slave); + +/** + * watch_sigwinch_cleanup + * + * Cause the SIGWINCH watcher thread to terminate + */ +void watch_sigwinch_cleanup(void); + +/** + * pump_stdin_async + * + * Forward data from STDIN to the given FD + * in a seperate thread + */ +void pump_stdin_async(int outfd); + +/** + * pump_stdout_blocking + * + * Forward data from the FD to STDOUT. + * Returns when the remote end of the FD closes. + * + * Before returning, restores stdin settings. + */ +void pump_stdout_blocking(int infd); + +#endif diff --git a/native/jni/su/su.c b/native/jni/su/su.c new file mode 100644 index 000000000..569694223 --- /dev/null +++ b/native/jni/su/su.c @@ -0,0 +1,284 @@ +/* + * Copyright 2017, John Wu (@topjohnwu) + * Copyright 2015, Pierre-Hugues Husson + * Copyright 2010, Adam Shanks (@ChainsDD) + * Copyright 2008, Zinx Verituse (@zinxv) + */ + +/* su.c - The main function running in the daemon + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "magisk.h" +#include "utils.h" +#include "su.h" + +struct su_context *su_ctx; + +static void usage(int status) { + FILE *stream = (status == EXIT_SUCCESS) ? stdout : stderr; + + fprintf(stream, + "MagiskSU v" xstr(MAGISK_VERSION) "(" xstr(MAGISK_VER_CODE) ")\n\n" + "Usage: su [options] [-] [user [argument...]]\n\n" + "Options:\n" + " -c, --command COMMAND pass COMMAND to the invoked shell\n" + " -h, --help display this help message and exit\n" + " -, -l, --login pretend the shell to be a login shell\n" + " -m, -p,\n" + " --preserve-environment preserve the entire environment\n" + " -s, --shell SHELL use SHELL instead of the default " DEFAULT_SHELL "\n" + " -v, --version display version number and exit\n" + " -V display version code and exit,\n" + " this is used almost exclusively by Superuser.apk\n" + " -mm, -M,\n" + " --mount-master run in the global mount namespace,\n" + " use if you need to publicly apply mounts\n"); + exit2(status); +} + +static char *concat_commands(int argc, char *argv[]) { + char command[ARG_MAX]; + command[0] = '\0'; + for (int i = optind - 1; i < argc; ++i) { + if (command[0]) + sprintf(command, "%s %s", command, argv[i]); + else + strcpy(command, argv[i]); + } + return strdup(command); +} + +static void populate_environment(const struct su_context *ctx) { + struct passwd *pw; + + if (ctx->to.keepenv) + return; + + pw = getpwuid(ctx->to.uid); + if (pw) { + setenv("HOME", pw->pw_dir, 1); + if (ctx->to.shell) + setenv("SHELL", ctx->to.shell, 1); + else + setenv("SHELL", DEFAULT_SHELL, 1); + if (ctx->to.login || ctx->to.uid) { + setenv("USER", pw->pw_name, 1); + setenv("LOGNAME", pw->pw_name, 1); + } + } +} + +void set_identity(unsigned uid) { + /* + * Set effective uid back to root, otherwise setres[ug]id will fail + * if uid isn't root. + */ + if (seteuid(0)) { + PLOGE("seteuid (root)"); + } + if (setresgid(uid, uid, uid)) { + PLOGE("setresgid (%u)", uid); + } + if (setresuid(uid, uid, uid)) { + PLOGE("setresuid (%u)", uid); + } +} + +static __attribute__ ((noreturn)) void allow() { + char* argv[] = { NULL, NULL, NULL, NULL }; + + if (su_ctx->info->access.notify || su_ctx->info->access.log) + app_send_result(su_ctx, ALLOW); + + if (su_ctx->to.login) + argv[0] = "-"; + else + argv[0] = basename(su_ctx->to.shell); + + if (su_ctx->to.command) { + argv[1] = "-c"; + argv[2] = su_ctx->to.command; + } + + // Setup shell + umask(022); + populate_environment(su_ctx); + set_identity(su_ctx->to.uid); + + execvp(su_ctx->to.shell, argv); + fprintf(stderr, "Cannot execute %s: %s\n", su_ctx->to.shell, strerror(errno)); + PLOGE("exec"); + exit(EXIT_FAILURE); +} + +static __attribute__ ((noreturn)) void deny() { + if (su_ctx->info->access.notify || su_ctx->info->access.log) + app_send_result(su_ctx, DENY); + + LOGW("su: request rejected (%u->%u)", su_ctx->info->uid, su_ctx->to.uid); + fprintf(stderr, "%s\n", strerror(EACCES)); + exit(EXIT_FAILURE); +} + +static void socket_cleanup() { + if (su_ctx && su_ctx->sock_path[0]) { + unlink(su_ctx->sock_path); + su_ctx->sock_path[0] = '\0'; + } +} + +static void cleanup_signal(int sig) { + socket_cleanup(); + exit2(EXIT_FAILURE); +} + +__attribute__ ((noreturn)) void exit2(int status) { + // Handle the pipe, or the daemon will get stuck + if (su_ctx->pipefd[0] >= 0) { + xwrite(su_ctx->pipefd[1], &su_ctx->info->access.policy, sizeof(policy_t)); + close(su_ctx->pipefd[0]); + close(su_ctx->pipefd[1]); + } + exit(status); +} + +int su_daemon_main(int argc, char **argv) { + int c, socket_serv_fd, fd; + char result[64]; + struct option long_opts[] = { + { "command", required_argument, NULL, 'c' }, + { "help", no_argument, NULL, 'h' }, + { "login", no_argument, NULL, 'l' }, + { "preserve-environment", no_argument, NULL, 'p' }, + { "shell", required_argument, NULL, 's' }, + { "version", no_argument, NULL, 'v' }, + { "context", required_argument, NULL, 'z' }, + { "mount-master", no_argument, NULL, 'M' }, + { NULL, 0, NULL, 0 }, + }; + + while ((c = getopt_long(argc, argv, "c:hlmps:Vvuz:M", long_opts, NULL)) != -1) { + switch (c) { + case 'c': + su_ctx->to.command = concat_commands(argc, argv); + optind = argc; + break; + case 'h': + usage(EXIT_SUCCESS); + break; + case 'l': + su_ctx->to.login = 1; + break; + case 'm': + case 'p': + su_ctx->to.keepenv = 1; + break; + case 's': + su_ctx->to.shell = optarg; + break; + case 'V': + printf("%d\n", MAGISK_VER_CODE); + exit2(EXIT_SUCCESS); + case 'v': + printf("%s\n", MAGISKSU_VER_STR); + exit2(EXIT_SUCCESS); + case 'z': + // Do nothing, placed here for legacy support :) + break; + case 'M': + su_ctx->info->dbs.v[SU_MNT_NS] = NAMESPACE_MODE_GLOBAL; + break; + default: + /* Bionic getopt_long doesn't terminate its error output by newline */ + fprintf(stderr, "\n"); + usage(2); + } + } + + if (optind < argc && strcmp(argv[optind], "-") == 0) { + su_ctx->to.login = 1; + optind++; + } + /* username or uid */ + if (optind < argc) { + struct passwd *pw; + pw = getpwnam(argv[optind]); + if (pw) + su_ctx->to.uid = pw->pw_uid; + else + su_ctx->to.uid = atoi(argv[optind]); + optind++; + } + + // Handle namespaces + switch (su_ctx->info->dbs.v[SU_MNT_NS]) { + case NAMESPACE_MODE_GLOBAL: + LOGD("su: use global namespace\n"); + break; + case NAMESPACE_MODE_REQUESTER: + LOGD("su: use namespace of pid=[%d]\n", su_ctx->pid); + if (switch_mnt_ns(su_ctx->pid)) { + LOGD("su: setns failed, fallback to isolated\n"); + xunshare(CLONE_NEWNS); + } + break; + case NAMESPACE_MODE_ISOLATE: + LOGD("su: use new isolated namespace\n"); + xunshare(CLONE_NEWNS); + break; + } + + // Change directory to cwd + chdir(su_ctx->cwd); + + // New request or no db exist, notify user for response + if (su_ctx->pipefd[0] >= 0) { + socket_serv_fd = socket_create_temp(su_ctx->sock_path, sizeof(su_ctx->sock_path)); + setup_sighandlers(cleanup_signal); + + // Start activity + app_send_request(su_ctx); + + atexit(socket_cleanup); + + fd = socket_accept(socket_serv_fd); + socket_send_request(fd, su_ctx); + socket_receive_result(fd, result, sizeof(result)); + + close(fd); + close(socket_serv_fd); + socket_cleanup(); + + if (strcmp(result, "socket:ALLOW") == 0) + su_ctx->info->access.policy = ALLOW; + else + su_ctx->info->access.policy = DENY; + + // Report the policy to main daemon + xwrite(su_ctx->pipefd[1], &su_ctx->info->access.policy, sizeof(policy_t)); + close(su_ctx->pipefd[0]); + close(su_ctx->pipefd[1]); + } + + if (su_ctx->info->access.policy == ALLOW) + allow(); + else + deny(); +} + diff --git a/native/jni/su/su.h b/native/jni/su/su.h new file mode 100644 index 000000000..43c936c19 --- /dev/null +++ b/native/jni/su/su.h @@ -0,0 +1,76 @@ +/* su.h - Store all general su info + */ + +#ifndef _SU_H_ +#define _SU_H_ + +#include +#include +#include + +#include "db.h" +#include "list.h" + +#define MAGISKSU_VER_STR xstr(MAGISK_VERSION) ":MAGISKSU (topjohnwu)" + +// This is used if wrapping the fragment classes and activities +// with classes in another package. +#define REQUESTOR_PREFIX JAVA_PACKAGE_NAME ".superuser" + +#define DEFAULT_SHELL "/system/bin/sh" + +struct su_info { + unsigned uid; /* Unique key to find su_info */ + pthread_mutex_t lock; /* Internal lock */ + int count; /* Just a count for debugging purpose */ + + /* These values should be guarded with internal lock */ + struct db_settings dbs; + struct db_strings str; + struct su_access access; + struct stat manager_stat; + + /* These should be guarded with global list lock */ + struct list_head pos; + int ref; + int clock; +}; + +struct su_request { + unsigned uid; + int login; + int keepenv; + char *shell; + char *command; +}; + +struct su_context { + struct su_info *info; + struct su_request to; + pid_t pid; + char cwd[PATH_MAX]; + char sock_path[PATH_MAX]; + int pipefd[2]; +}; + +extern struct su_context *su_ctx; + +// su.c + +int su_daemon_main(int argc, char **argv); +__attribute__ ((noreturn)) void exit2(int status); +void set_identity(unsigned uid); + +// su_client.c + +int socket_create_temp(char *path, size_t len); +int socket_accept(int serv_fd); +void socket_send_request(int fd, const struct su_context *ctx); +void socket_receive_result(int fd, char *result, ssize_t result_len); + +// activity.c + +void app_send_result(struct su_context *ctx, policy_t policy); +void app_send_request(struct su_context *ctx); + +#endif diff --git a/native/jni/su/su_daemon.c b/native/jni/su/su_daemon.c new file mode 100644 index 000000000..48f98ff65 --- /dev/null +++ b/native/jni/su/su_daemon.c @@ -0,0 +1,452 @@ +/* su_daemon.c - The entrypoint for su, connect to daemon and send correct info + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "magisk.h" +#include "daemon.h" +#include "utils.h" +#include "su.h" +#include "pts.h" +#include "list.h" + +// Constants for the atty bitfield +#define ATTY_IN 1 +#define ATTY_OUT 2 +#define ATTY_ERR 4 + +#define TIMEOUT 3 + +#define LOCK_LIST() pthread_mutex_lock(&list_lock) +#define LOCK_UID() pthread_mutex_lock(&info->lock) +#define UNLOCK_LIST() pthread_mutex_unlock(&list_lock) +#define UNLOCK_UID() pthread_mutex_unlock(&ctx.info->lock) + +static struct list_head info_cache = { .prev = &info_cache, .next = &info_cache }; +static pthread_mutex_t list_lock = PTHREAD_MUTEX_INITIALIZER; + +static void sighandler(int sig) { + restore_stdin(); + + // Assume we'll only be called before death + // See note before sigaction() in set_stdin_raw() + // + // Now, close all standard I/O to cause the pumps + // to exit so we can continue and retrieve the exit + // code + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + // Put back all the default handlers + struct sigaction act; + + memset(&act, 0, sizeof(act)); + act.sa_handler = SIG_DFL; + for (int i = 0; quit_signals[i]; ++i) { + sigaction(quit_signals[i], &act, NULL); + } +} + +static void *info_collector(void *node) { + struct su_info *info = node; + while (1) { + sleep(1); + if (info->clock && --info->clock == 0) { + LOCK_LIST(); + list_pop(&info->pos); + UNLOCK_LIST(); + } + if (!info->clock && !info->ref) { + pthread_mutex_destroy(&info->lock); + free(info); + return NULL; + } + } +} + +static void database_check(struct su_info *info) { + int uid = info->uid; + sqlite3 *db = get_magiskdb(); + if (db) { + get_db_settings(db, -1, &info->dbs); + get_db_strings(db, -1, &info->str); + + // Check multiuser settings + switch (info->dbs.v[SU_MULTIUSER_MODE]) { + case MULTIUSER_MODE_OWNER_ONLY: + if (info->uid / 100000) { + uid = -1; + info->access = NO_SU_ACCESS; + } + break; + case MULTIUSER_MODE_OWNER_MANAGED: + uid = info->uid % 100000; + break; + case MULTIUSER_MODE_USER: + default: + break; + } + + if (uid > 0) + get_uid_policy(db, uid, &info->access); + sqlite3_close(db); + } + + // We need to check our manager + if (info->access.log || info->access.notify) + validate_manager(info->str.s[SU_MANAGER], uid / 100000, &info->manager_stat); +} + +static struct su_info *get_su_info(unsigned uid) { + struct su_info *info = NULL, *node; + + LOCK_LIST(); + + // Search for existing info in cache + list_for_each(node, &info_cache, struct su_info, pos) { + if (node->uid == uid) { + info = node; + break; + } + } + + int cache_miss = info == NULL; + + if (cache_miss) { + // If cache miss, create a new one and push to cache + info = malloc(sizeof(*info)); + info->uid = uid; + info->dbs = DEFAULT_DB_SETTINGS; + info->access = DEFAULT_SU_ACCESS; + INIT_DB_STRINGS(&info->str); + info->ref = 0; + info->count = 0; + pthread_mutex_init(&info->lock, NULL); + list_insert_end(&info_cache, &info->pos); + } + + // Update the cache status + info->clock = TIMEOUT; + ++info->ref; + + // Start a thread to maintain the info cache + if (cache_miss) { + pthread_t thread; + xpthread_create(&thread, NULL, info_collector, info); + pthread_detach(thread); + } + + UNLOCK_LIST(); + + LOGD("su: request from uid=[%d] (#%d)\n", info->uid, ++info->count); + + // Lock before the policy is determined + LOCK_UID(); + + if (info->access.policy == QUERY) { + // Not cached, get data from database + database_check(info); + + // Check su access settings + switch (info->dbs.v[ROOT_ACCESS]) { + case ROOT_ACCESS_DISABLED: + LOGE("Root access is disabled!\n"); + info->access = NO_SU_ACCESS; + break; + case ROOT_ACCESS_ADB_ONLY: + if (info->uid != UID_SHELL) { + LOGE("Root access limited to ADB only!\n"); + info->access = NO_SU_ACCESS; + } + break; + case ROOT_ACCESS_APPS_ONLY: + if (info->uid == UID_SHELL) { + LOGE("Root access is disabled for ADB!\n"); + info->access = NO_SU_ACCESS; + } + break; + case ROOT_ACCESS_APPS_AND_ADB: + default: + break; + } + + // If it's the manager, allow it silently + if ((info->uid % 100000) == (info->manager_stat.st_uid % 100000)) + info->access = SILENT_SU_ACCESS; + + // Allow if it's root + if (info->uid == UID_ROOT) + info->access = SILENT_SU_ACCESS; + + // If still not determined, check if manager exists + if (info->access.policy == QUERY && info->str.s[SU_MANAGER][0] == '\0') + info->access = NO_SU_ACCESS; + } + return info; +} + +static void su_executor(int client) { + LOGD("su: executor started\n"); + + // ack + write_int(client, 0); + + // Become session leader + xsetsid(); + + // Migrate environment from client + char path[32], buf[4096]; + snprintf(path, sizeof(path), "/proc/%d/cwd", su_ctx->pid); + xreadlink(path, su_ctx->cwd, sizeof(su_ctx->cwd)); + snprintf(path, sizeof(path), "/proc/%d/environ", su_ctx->pid); + memset(buf, 0, sizeof(buf)); + int fd = open(path, O_RDONLY); + read(fd, buf, sizeof(buf)); + clearenv(); + for (size_t pos = 0; buf[pos];) { + putenv(buf + pos); + pos += strlen(buf + pos) + 1; + } + + // Let's read some info from the socket + int argc = read_int(client); + if (argc < 0 || argc > 512) { + LOGE("unable to allocate args: %d", argc); + exit2(1); + } + LOGD("su: argc=[%d]\n", argc); + + char **argv = (char**) xmalloc(sizeof(char*) * (argc + 1)); + argv[argc] = NULL; + for (int i = 0; i < argc; i++) { + argv[i] = read_string(client); + LOGD("su: argv[%d]=[%s]\n", i, argv[i]); + // Replace -cn with -z, -mm with -M for supporting getopt_long + if (strcmp(argv[i], "-cn") == 0) + strcpy(argv[i], "-z"); + else if (strcmp(argv[i], "-mm") == 0) + strcpy(argv[i], "-M"); + } + + // Get pts_slave + char *pts_slave = read_string(client); + + // The FDs for each of the streams + int infd = recv_fd(client); + int outfd = recv_fd(client); + int errfd = recv_fd(client); + int ptsfd = -1; + + // We no longer need the access to socket in the child, close it + close(client); + + if (pts_slave[0]) { + LOGD("su: pts_slave=[%s]\n", pts_slave); + // Check pts_slave file is owned by daemon_from_uid + struct stat st; + xstat(pts_slave, &st); + + // If caller is not root, ensure the owner of pts_slave is the caller + if(st.st_uid != su_ctx->info->uid && su_ctx->info->uid != 0) { + LOGE("su: Wrong permission of pts_slave"); + su_ctx->info->access.policy = DENY; + exit2(1); + } + + // Opening the TTY has to occur after the + // fork() and setsid() so that it becomes + // our controlling TTY and not the daemon's + ptsfd = xopen(pts_slave, O_RDWR); + + if (infd < 0) { + LOGD("su: stdin using PTY"); + infd = ptsfd; + } + if (outfd < 0) { + LOGD("su: stdout using PTY"); + outfd = ptsfd; + } + if (errfd < 0) { + LOGD("su: stderr using PTY"); + errfd = ptsfd; + } + } + + free(pts_slave); + + // Swap out stdin, stdout, stderr + xdup2(infd, STDIN_FILENO); + xdup2(outfd, STDOUT_FILENO); + xdup2(errfd, STDERR_FILENO); + + close(ptsfd); + + // Run the actual main + su_daemon_main(argc, argv); +} + +void su_daemon_receiver(int client, struct ucred *credential) { + LOGD("su: request from client: %d\n", client); + + // Default values + struct su_context ctx = { + .info = get_su_info(credential->uid), + .to = { + .uid = UID_ROOT, + .login = 0, + .keepenv = 0, + .shell = DEFAULT_SHELL, + .command = NULL, + }, + .pid = credential->pid, + .pipefd = { -1, -1 } + }; + + // Fail fast + if (ctx.info->access.policy == DENY && !ctx.info->access.log && !ctx.info->access.notify) { + UNLOCK_UID(); + write_int(client, DENY); + return; + } + + // If still not determined, open a pipe and wait for results + if (ctx.info->access.policy == QUERY) + xpipe2(ctx.pipefd, O_CLOEXEC); + + /* Fork a new process, the child process will need to setsid, + * open a pseudo-terminal if needed, and will eventually run exec + * The parent process will wait for the result and + * send the return code back to our client + */ + int child = xfork(); + if (child == 0) { + su_ctx = &ctx; + su_executor(client); + } + + // Wait for results + if (ctx.pipefd[0] >= 0) { + xxread(ctx.pipefd[0], &ctx.info->access.policy, sizeof(policy_t)); + close(ctx.pipefd[0]); + close(ctx.pipefd[1]); + } + + // The policy is determined, unlock + UNLOCK_UID(); + + // Info is now useless to us, decrement reference count + --ctx.info->ref; + + // Wait result + LOGD("su: waiting child: [%d]\n", child); + int status, code; + + if (waitpid(child, &status, 0) > 0) + code = WEXITSTATUS(status); + else + code = -1; + + LOGD("su: return code: [%d]\n", code); + write(client, &code, sizeof(code)); + close(client); + + return; +} + +/* + * Connect daemon, send argc, argv, cwd, pts slave + */ +int su_client_main(int argc, char *argv[]) { + char buffer[PATH_MAX]; + int ptmx, socketfd; + + // Connect to client + socketfd = connect_daemon(); + + // Tell the daemon we are su + write_int(socketfd, SUPERUSER); + + // Number of command line arguments + write_int(socketfd, argc); + + // Command line arguments + for (int i = 0; i < argc; i++) { + write_string(socketfd, argv[i]); + } + + // Determine which one of our streams are attached to a TTY + int atty = 0; + if (isatty(STDIN_FILENO)) atty |= ATTY_IN; + if (isatty(STDOUT_FILENO)) atty |= ATTY_OUT; + if (isatty(STDERR_FILENO)) atty |= ATTY_ERR; + + if (atty) { + // We need a PTY. Get one. + ptmx = pts_open(buffer, sizeof(buffer)); + } else { + buffer[0] = '\0'; + } + + // Send the pts_slave path to the daemon + write_string(socketfd, buffer); + + // Send stdin + if (atty & ATTY_IN) { + // Using PTY + send_fd(socketfd, -1); + } else { + send_fd(socketfd, STDIN_FILENO); + } + + // Send stdout + if (atty & ATTY_OUT) { + // Forward SIGWINCH + watch_sigwinch_async(STDOUT_FILENO, ptmx); + + // Using PTY + send_fd(socketfd, -1); + } else { + send_fd(socketfd, STDOUT_FILENO); + } + + // Send stderr + if (atty & ATTY_ERR) { + // Using PTY + send_fd(socketfd, -1); + } else { + send_fd(socketfd, STDERR_FILENO); + } + + // Wait for acknowledgement from daemon + if (read_int(socketfd)) { + // Fast fail + fprintf(stderr, "%s\n", strerror(EACCES)); + return DENY; + } + + if (atty & ATTY_IN) { + setup_sighandlers(sighandler); + pump_stdin_async(ptmx); + } + if (atty & ATTY_OUT) { + pump_stdout_blocking(ptmx); + } + + // Get the exit code + int code = read_int(socketfd); + close(socketfd); + + return code; +} diff --git a/native/jni/su/su_socket.c b/native/jni/su/su_socket.c new file mode 100644 index 000000000..28b2e18bd --- /dev/null +++ b/native/jni/su/su_socket.c @@ -0,0 +1,108 @@ +/* su_socket.c - Functions for communication to client + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "magisk.h" +#include "utils.h" +#include "su.h" +#include "magiskpolicy.h" + +int socket_create_temp(char *path, size_t len) { + int fd; + struct sockaddr_un sun; + + fd = xsocket(AF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (fcntl(fd, F_SETFD, FD_CLOEXEC)) { + PLOGE("fcntl FD_CLOEXEC"); + } + + memset(&sun, 0, sizeof(sun)); + sun.sun_family = AF_LOCAL; + snprintf(path, len, "/dev/.socket%d", getpid()); + strcpy(sun.sun_path, path); + + /* + * Delete the socket to protect from situations when + * something bad occured previously and the kernel reused pid from that process. + * Small probability, isn't it. + */ + unlink(path); + + xbind(fd, (struct sockaddr*) &sun, sizeof(sun)); + xlisten(fd, 1); + + // Set attributes so requester can access it + setfilecon(path, "u:object_r:"SEPOL_FILE_DOMAIN":s0"); + chown(path, su_ctx->info->manager_stat.st_uid, su_ctx->info->manager_stat.st_gid); + + return fd; +} + +int socket_accept(int serv_fd) { + struct timeval tv; + fd_set fds; + int rc; + + /* Wait 60 seconds for a connection, then give up. */ + tv.tv_sec = 60; + tv.tv_usec = 0; + FD_ZERO(&fds); + FD_SET(serv_fd, &fds); + do { + rc = select(serv_fd + 1, &fds, NULL, NULL, &tv); + } while (rc < 0 && errno == EINTR); + if (rc < 1) { + PLOGE("select"); + } + + return xaccept4(serv_fd, NULL, NULL, SOCK_CLOEXEC); +} + +#define write_data(fd, data, data_len) \ +do { \ + uint32_t __len = htonl(data_len); \ + __len = write((fd), &__len, sizeof(__len)); \ + if (__len != sizeof(__len)) { \ + PLOGE("write(" #data ")"); \ + } \ + __len = write((fd), data, data_len); \ + if (__len != data_len) { \ + PLOGE("write(" #data ")"); \ + } \ +} while (0) + +#define write_string_data(fd, name, data) \ +do { \ + write_data(fd, name, strlen(name)); \ + write_data(fd, data, strlen(data)); \ +} while (0) + +// stringify everything. +#define write_token(fd, name, data) \ +do { \ + char buf[16]; \ + snprintf(buf, sizeof(buf), "%d", data); \ + write_string_data(fd, name, buf); \ +} while (0) + +void socket_send_request(int fd, const struct su_context *ctx) { + write_token(fd, "uid", ctx->info->uid); + write_token(fd, "eof", 1); +} + +void socket_receive_result(int fd, char *result, ssize_t result_len) { + ssize_t len; + len = xread(fd, result, result_len - 1); + result[len] = '\0'; +}