Move su folder into core

This commit is contained in:
topjohnwu
2023-05-23 01:36:25 -07:00
parent 0f666de5e6
commit 5627053b74
8 changed files with 4 additions and 5 deletions

View File

@@ -0,0 +1,224 @@
#include <sys/types.h>
#include <sys/wait.h>
#include <base.hpp>
#include <selinux.hpp>
#include "su.hpp"
extern int SDK_INT;
using namespace std;
#define CALL_PROVIDER \
exe, "/system/bin", "com.android.commands.content.Content", \
"call", "--uri", target, "--user", user, "--method", action
#define START_ACTIVITY \
exe, "/system/bin", "com.android.commands.am.Am", \
"start", "-p", target, "--user", user, "-a", "android.intent.action.VIEW", \
"-f", "0x58800020", "--es", "action", action
// 0x58800020 = FLAG_ACTIVITY_NEW_TASK|FLAG_ACTIVITY_MULTIPLE_TASK|
// FLAG_ACTIVITY_NO_HISTORY|FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS|
// FLAG_INCLUDE_STOPPED_PACKAGES
#define get_cmd(to) \
((to).command.empty() ? \
((to).shell.empty() ? DEFAULT_SHELL : (to).shell.data()) : \
(to).command.data())
class Extra {
const char *key;
enum {
INT,
BOOL,
STRING
} type;
union {
int int_val;
bool bool_val;
const char *str_val;
};
string str;
public:
Extra(const char *k, int v): key(k), type(INT), int_val(v) {}
Extra(const char *k, bool v): key(k), type(BOOL), bool_val(v) {}
Extra(const char *k, const char *v): key(k), type(STRING), str_val(v) {}
void add_intent(vector<const char *> &vec) {
char buf[32];
const char *val;
switch (type) {
case INT:
vec.push_back("--ei");
ssprintf(buf, sizeof(buf), "%d", int_val);
str = buf;
val = str.data();
break;
case BOOL:
vec.push_back("--ez");
val = bool_val ? "true" : "false";
break;
case STRING:
vec.push_back("--es");
val = str_val;
break;
}
vec.push_back(key);
vec.push_back(val);
}
void add_bind(vector<const char *> &vec) {
char buf[32];
str = key;
switch (type) {
case INT:
str += ":i:";
ssprintf(buf, sizeof(buf), "%d", int_val);
str += buf;
break;
case BOOL:
str += ":b:";
str += bool_val ? "true" : "false";
break;
case STRING:
str += ":s:";
if (SDK_INT >= 30) {
string tmp = str_val;
replace_all(tmp, "\\", "\\\\");
replace_all(tmp, ":", "\\:");
str += tmp;
} else {
str += str_val;
}
break;
}
vec.push_back("--extra");
vec.push_back(str.data());
}
};
static bool check_no_error(int fd) {
char buf[1024];
auto out = xopen_file(fd, "r");
while (fgets(buf, sizeof(buf), out.get())) {
if (strncmp(buf, "Error", 5) == 0)
return false;
}
return true;
}
static void exec_cmd(const char *action, vector<Extra> &data,
const shared_ptr<su_info> &info, bool provider = true) {
char exe[128];
char target[128];
char user[4];
ssprintf(user, sizeof(user), "%d", to_user_id(info->eval_uid));
if (zygisk_enabled) {
#if defined(__LP64__)
ssprintf(exe, sizeof(exe), "/proc/self/fd/%d", app_process_64);
#else
ssprintf(exe, sizeof(exe), "/proc/self/fd/%d", app_process_32);
#endif
} else {
strscpy(exe, "/system/bin/app_process", sizeof(exe));
}
// First try content provider call method
if (provider) {
ssprintf(target, sizeof(target), "content://%s.provider", info->mgr_pkg.data());
vector<const char *> args{ CALL_PROVIDER };
for (auto &e : data) {
e.add_bind(args);
}
args.push_back(nullptr);
exec_t exec {
.err = true,
.fd = -1,
.pre_exec = [] { setenv("CLASSPATH", "/system/framework/content.jar", 1); },
.argv = args.data()
};
exec_command_sync(exec);
if (check_no_error(exec.fd))
return;
}
// Then try start activity with package name
strscpy(target, info->mgr_pkg.data(), sizeof(target));
vector<const char *> args{ START_ACTIVITY };
for (auto &e : data) {
e.add_intent(args);
}
args.push_back(nullptr);
exec_t exec {
.fd = -2,
.pre_exec = [] { setenv("CLASSPATH", "/system/framework/am.jar", 1); },
.fork = fork_dont_care,
.argv = args.data()
};
exec_command(exec);
}
void app_log(const su_context &ctx) {
if (fork_dont_care() == 0) {
vector<Extra> extras;
extras.reserve(6);
extras.emplace_back("from.uid", ctx.info->uid);
extras.emplace_back("to.uid", static_cast<int>(ctx.req.uid));
extras.emplace_back("pid", ctx.pid);
extras.emplace_back("policy", ctx.info->access.policy);
extras.emplace_back("command", get_cmd(ctx.req));
extras.emplace_back("notify", (bool) ctx.info->access.notify);
exec_cmd("log", extras, ctx.info);
exit(0);
}
}
void app_notify(const su_context &ctx) {
if (fork_dont_care() == 0) {
vector<Extra> extras;
extras.reserve(3);
extras.emplace_back("from.uid", ctx.info->uid);
extras.emplace_back("pid", ctx.pid);
extras.emplace_back("policy", ctx.info->access.policy);
exec_cmd("notify", extras, ctx.info);
exit(0);
}
}
int app_request(const su_context &ctx) {
// Create FIFO
char fifo[64];
strcpy(fifo, "/dev/socket/");
gen_rand_str(fifo + 12, 32);
mkfifo(fifo, 0600);
chown(fifo, ctx.info->mgr_uid, ctx.info->mgr_uid);
setfilecon(fifo, MAGISK_FILE_CON);
// Send request
vector<Extra> extras;
extras.reserve(3);
extras.emplace_back("fifo", fifo);
extras.emplace_back("uid", ctx.info->eval_uid);
extras.emplace_back("pid", ctx.pid);
exec_cmd("request", extras, ctx.info, false);
// Wait for data input for at most 70 seconds
// Open with O_RDWR to prevent FIFO open block
int fd = xopen(fifo, O_RDWR | O_CLOEXEC);
struct pollfd pfd = {
.fd = fd,
.events = POLLIN
};
if (xpoll(&pfd, 1, 70 * 1000) <= 0) {
close(fd);
fd = -1;
}
unlink(fifo);
return fd;
}

294
native/src/core/su/pts.cpp Normal file
View File

@@ -0,0 +1,294 @@
/*
* 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
*/
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <base.hpp>
#include "pts.hpp"
/**
* 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(int input, int output, bool close_output = true) {
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);
}
static void* pump_thread(void* data) {
int *fds = (int*) data;
pump(fds[0], fds[1]);
delete[] fds;
return nullptr;
}
static void pump_async(int input, int output) {
pthread_t writer;
int *fds = new int[2];
fds[0] = input;
fds[1] = output;
pthread_create(&writer, nullptr, pump_thread, fds);
}
/**
* 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;
// 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;
}
int get_pty_num(int fd) {
int pty_num = -1;
if (ioctl(fd, TIOCGPTN, &pty_num) != 0) {
LOGW("get_pty_num failed with %d: %s\n", errno, std::strerror(errno));
return -1;
}
return pty_num;
}
// 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() {
struct termios termios{};
if (tcgetattr(STDIN_FILENO, &termios) < 0) {
return -1;
}
old_stdin = termios;
cfmakeraw(&termios);
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &termios) < 0) {
// https://blog.zhanghai.me/fixing-line-editing-on-android-8-0/
if (tcsetattr(STDIN_FILENO, TCSADRAIN, &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() {
if (!stdin_is_raw) return 0;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_stdin) < 0) {
if (tcsetattr(STDIN_FILENO, TCSADRAIN, &old_stdin) < 0) {
return -1;
}
}
stdin_is_raw = 0;
return 0;
}
// Flag indicating whether the sigwinch watcher should terminate.
static volatile bool close_sigwinch_watcher = false;
/**
* Thread process. Wait for a SIGWINCH to be received, then update
* the terminal size.
*/
static void *watch_sigwinch(void *data) {
sigset_t winch;
int *fds = (int *)data;
int sig;
sigemptyset(&winch);
sigaddset(&winch, SIGWINCH);
pthread_sigmask(SIG_UNBLOCK, &winch, nullptr);
do {
if (close_sigwinch_watcher)
break;
// Get the new terminal size
struct winsize w;
if (ioctl(fds[0], TIOCGWINSZ, &w) == -1)
continue;
// Set the new terminal size
ioctl(fds[1], TIOCSWINSZ, &w);
} while (sigwait(&winch, &sig) == 0);
delete[] fds;
return nullptr;
}
/**
* 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 *fds = new int[2];
// Block SIGWINCH so sigwait can later receive it
sigset_t winch;
sigemptyset(&winch);
sigaddset(&winch, SIGWINCH);
if (pthread_sigmask(SIG_BLOCK, &winch, nullptr) == -1) {
delete[] fds;
return -1;
}
// Initialize some variables, then start the thread
close_sigwinch_watcher = 0;
fds[0] = master;
fds[1] = slave;
int ret = pthread_create(&watcher, nullptr, &watch_sigwinch, fds);
if (ret != 0) {
delete[] fds;
errno = ret;
return -1;
}
return 0;
}
/**
* pump_stdin_async
*
* Forward data from STDIN to the given FD
* in a separate 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(infd, STDOUT_FILENO, false /* Don't close output when done */);
// Cleanup
restore_stdin();
close_sigwinch_watcher = true;
raise(SIGWINCH);
}

102
native/src/core/su/pts.hpp Normal file
View File

@@ -0,0 +1,102 @@
/*
* Copyright 2013, Tan Chee Eng (@tan-ce)
*/
/*
* pts.hpp
*
* 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 <sys/types.h>
/**
* 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 get_pty_num(int fd);
/**
* 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);
/**
* pump_stdin_async
*
* Forward data from STDIN to the given FD
* in a separate 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

238
native/src/core/su/su.cpp Normal file
View File

@@ -0,0 +1,238 @@
/*
* Copyright 2017 - 2021, John Wu (@topjohnwu)
* Copyright 2015, Pierre-Hugues Husson <phh@phh.me>
* Copyright 2010, Adam Shanks (@ChainsDD)
* Copyright 2008, Zinx Verituse (@zinxv)
*/
#include <unistd.h>
#include <getopt.h>
#include <fcntl.h>
#include <pwd.h>
#include <sched.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <magisk.hpp>
#include <base.hpp>
#include <flags.h>
#include "su.hpp"
#include "pts.hpp"
int quit_signals[] = { SIGALRM, SIGABRT, SIGHUP, SIGPIPE, SIGQUIT, SIGTERM, SIGINT, 0 };
[[noreturn]] static void usage(int status) {
FILE *stream = (status == EXIT_SUCCESS) ? stdout : stderr;
fprintf(stream,
"MagiskSU\n\n"
"Usage: su [options] [-] [user [argument...]]\n\n"
"Options:\n"
" -c, --command COMMAND pass COMMAND to the invoked shell\n"
" -g, --group GROUP Specify the primary group\n"
" -G, --supp-group GROUP Specify a supplementary group. The first specified supplementary group is also used as a primary group if the option --group is not specified.\n"
" -z, --context CONTEXT change SELinux context\n"
" -t, --target PID PID to take mount namespace from\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"
" -mm, -M,\n"
" --mount-master force run in the global mount namespace\n\n");
exit(status);
}
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, nullptr);
}
}
static void setup_sighandlers(void (*handler)(int)) {
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = handler;
for (int i = 0; quit_signals[i]; ++i) {
sigaction(quit_signals[i], &act, nullptr);
}
}
int su_client_main(int argc, char *argv[]) {
int c;
struct option long_opts[] = {
{ "command", required_argument, nullptr, 'c' },
{ "help", no_argument, nullptr, 'h' },
{ "login", no_argument, nullptr, 'l' },
{ "preserve-environment", no_argument, nullptr, 'p' },
{ "shell", required_argument, nullptr, 's' },
{ "version", no_argument, nullptr, 'v' },
{ "context", required_argument, nullptr, 'z' },
{ "mount-master", no_argument, nullptr, 'M' },
{ "target", required_argument, nullptr, 't' },
{ "group", required_argument, nullptr, 'g' },
{ "supp-group", required_argument, nullptr, 'G' },
{ nullptr, 0, nullptr, 0 },
};
su_request su_req;
for (int i = 0; i < argc; 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");
}
while ((c = getopt_long(argc, argv, "c:hlmps:Vvuz:Mt:g:G:", long_opts, nullptr)) != -1) {
switch (c) {
case 'c':
for (int i = optind - 1; i < argc; ++i) {
if (!su_req.command.empty())
su_req.command += ' ';
su_req.command += argv[i];
}
optind = argc;
break;
case 'h':
usage(EXIT_SUCCESS);
case 'l':
su_req.login = true;
break;
case 'm':
case 'p':
su_req.keepenv = true;
break;
case 's':
su_req.shell = optarg;
break;
case 'V':
printf("%d\n", MAGISK_VER_CODE);
exit(EXIT_SUCCESS);
case 'v':
printf("%s\n", MAGISK_VERSION ":MAGISKSU");
exit(EXIT_SUCCESS);
case 'z':
su_req.context = optarg;
break;
case 'M':
case 't':
if (su_req.target != -1) {
fprintf(stderr, "Can't use -M and -t at the same time\n");
usage(EXIT_FAILURE);
}
if (optarg == nullptr) {
su_req.target = 0;
} else {
su_req.target = parse_int(optarg);
if (*optarg == '-' || su_req.target == -1) {
fprintf(stderr, "Invalid PID: %s\n", optarg);
usage(EXIT_FAILURE);
}
}
break;
case 'g':
case 'G':
if (int gid = parse_int(optarg); gid >= 0) {
su_req.gids.insert(c == 'g' ? su_req.gids.begin() : su_req.gids.end(), gid);
} else {
fprintf(stderr, "Invalid GID: %s\n", optarg);
usage(EXIT_FAILURE);
}
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_req.login = true;
optind++;
}
/* username or uid */
if (optind < argc) {
struct passwd *pw;
pw = getpwnam(argv[optind]);
if (pw)
su_req.uid = pw->pw_uid;
else
su_req.uid = parse_int(argv[optind]);
optind++;
}
int ptmx, fd;
// Connect to client
fd = connect_daemon(MainRequest::SUPERUSER);
// Send su_request
xwrite(fd, &su_req, sizeof(su_req_base));
write_string(fd, su_req.shell);
write_string(fd, su_req.command);
write_string(fd, su_req.context);
write_vector(fd, su_req.gids);
// Wait for ack from daemon
if (read_int(fd)) {
// Fast fail
fprintf(stderr, "%s\n", strerror(EACCES));
return EACCES;
}
// 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;
// Send stdin
send_fd(fd, (atty & ATTY_IN) ? -1 : STDIN_FILENO);
// Send stdout
send_fd(fd, (atty & ATTY_OUT) ? -1 : STDOUT_FILENO);
// Send stderr
send_fd(fd, (atty & ATTY_ERR) ? -1 : STDERR_FILENO);
if (atty) {
// We need a PTY. Get one.
write_int(fd, 1);
ptmx = recv_fd(fd);
} else {
write_int(fd, 0);
}
if (atty) {
setup_sighandlers(sighandler);
watch_sigwinch_async(STDOUT_FILENO, ptmx);
pump_stdin_async(ptmx);
pump_stdout_blocking(ptmx);
}
// Get the exit code
int code = read_int(fd);
close(fd);
return code;
}

66
native/src/core/su/su.hpp Normal file
View File

@@ -0,0 +1,66 @@
#pragma once
#include <sys/types.h>
#include <sys/stat.h>
#include <memory>
#include <db.hpp>
#include <daemon.hpp>
#define DEFAULT_SHELL "/system/bin/sh"
// Constants for atty
#define ATTY_IN (1 << 0)
#define ATTY_OUT (1 << 1)
#define ATTY_ERR (1 << 2)
class su_info {
public:
// Unique key
const int uid;
// These should be guarded with internal lock
int eval_uid; // The effective UID, taking multiuser settings into consideration
db_settings cfg;
su_access access;
std::string mgr_pkg;
int mgr_uid;
void check_db();
// These should be guarded with global cache lock
bool is_fresh();
void refresh();
su_info(int uid);
~su_info();
mutex_guard lock();
private:
long timestamp;
// Internal lock
pthread_mutex_t _lock;
};
struct su_req_base {
uid_t uid = AID_ROOT;
bool login = false;
bool keepenv = false;
pid_t target = -1;
} __attribute__((packed));
struct su_request : public su_req_base {
std::string shell = DEFAULT_SHELL;
std::string command;
std::string context;
std::vector<gid_t> gids;
};
struct su_context {
std::shared_ptr<su_info> info;
su_request req;
int pid;
};
void app_log(const su_context &ctx);
void app_notify(const su_context &ctx);
int app_request(const su_context &ctx);

View File

@@ -0,0 +1,469 @@
#include <unistd.h>
#include <fcntl.h>
#include <pwd.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <magisk.hpp>
#include <base.hpp>
#include <selinux.hpp>
#include "su.hpp"
#include "pts.hpp"
using namespace std;
static pthread_mutex_t cache_lock = PTHREAD_MUTEX_INITIALIZER;
static shared_ptr<su_info> cached;
su_info::su_info(int uid) :
uid(uid), eval_uid(-1), access(DEFAULT_SU_ACCESS), mgr_uid(-1),
timestamp(0), _lock(PTHREAD_MUTEX_INITIALIZER) {}
su_info::~su_info() {
pthread_mutex_destroy(&_lock);
}
mutex_guard su_info::lock() {
return mutex_guard(_lock);
}
bool su_info::is_fresh() {
timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
long current = ts.tv_sec * 1000L + ts.tv_nsec / 1000000L;
return current - timestamp < 3000; /* 3 seconds */
}
void su_info::refresh() {
timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
timestamp = ts.tv_sec * 1000L + ts.tv_nsec / 1000000L;
}
void su_info::check_db() {
eval_uid = uid;
get_db_settings(cfg);
// Check multiuser settings
switch (cfg[SU_MULTIUSER_MODE]) {
case MULTIUSER_MODE_OWNER_ONLY:
if (to_user_id(uid) != 0) {
eval_uid = -1;
access = NO_SU_ACCESS;
}
break;
case MULTIUSER_MODE_OWNER_MANAGED:
eval_uid = to_app_id(uid);
break;
case MULTIUSER_MODE_USER:
default:
break;
}
if (eval_uid > 0) {
char query[256], *err;
ssprintf(query, sizeof(query),
"SELECT policy, logging, notification FROM policies "
"WHERE uid=%d AND (until=0 OR until>%li)", eval_uid, time(nullptr));
err = db_exec(query, [&](db_row &row) -> bool {
access.policy = (policy_t) parse_int(row["policy"]);
access.log = parse_int(row["logging"]);
access.notify = parse_int(row["notification"]);
LOGD("magiskdb: query policy=[%d] log=[%d] notify=[%d]\n",
access.policy, access.log, access.notify);
return true;
});
db_err_cmd(err, return);
}
// We need to check our manager
if (access.log || access.notify) {
check_pkg_refresh();
mgr_uid = get_manager(to_user_id(eval_uid), &mgr_pkg, true);
}
}
bool uid_granted_root(int uid) {
if (uid == AID_ROOT)
return true;
db_settings cfg;
get_db_settings(cfg);
// Check user root access settings
switch (cfg[ROOT_ACCESS]) {
case ROOT_ACCESS_DISABLED:
return false;
case ROOT_ACCESS_APPS_ONLY:
if (uid == AID_SHELL)
return false;
break;
case ROOT_ACCESS_ADB_ONLY:
if (uid != AID_SHELL)
return false;
break;
case ROOT_ACCESS_APPS_AND_ADB:
break;
}
// Check multiuser settings
switch (cfg[SU_MULTIUSER_MODE]) {
case MULTIUSER_MODE_OWNER_ONLY:
if (to_user_id(uid) != 0)
return false;
break;
case MULTIUSER_MODE_OWNER_MANAGED:
uid = to_app_id(uid);
break;
case MULTIUSER_MODE_USER:
default:
break;
}
bool granted = false;
char query[256], *err;
ssprintf(query, sizeof(query),
"SELECT policy FROM policies WHERE uid=%d AND (until=0 OR until>%li)",
uid, time(nullptr));
err = db_exec(query, [&](db_row &row) -> bool {
granted = parse_int(row["policy"]) == ALLOW;
return true;
});
db_err_cmd(err, return false);
return granted;
}
void prune_su_access() {
cached.reset();
vector<bool> app_no_list = get_app_no_list();
vector<int> rm_uids;
char query[256], *err;
strscpy(query, "SELECT uid FROM policies", sizeof(query));
err = db_exec(query, [&](db_row &row) -> bool {
int uid = parse_int(row["uid"]);
int app_id = to_app_id(uid);
if (app_id >= AID_APP_START && app_id <= AID_APP_END) {
int app_no = app_id - AID_APP_START;
if (app_no >= app_no_list.size() || !app_no_list[app_no]) {
// The app_id is no longer installed
rm_uids.push_back(uid);
}
}
return true;
});
db_err_cmd(err, return);
for (int uid : rm_uids) {
ssprintf(query, sizeof(query), "DELETE FROM policies WHERE uid == %d", uid);
// Don't care about errors
db_exec(query);
}
}
static shared_ptr<su_info> get_su_info(unsigned uid) {
LOGD("su: request from uid=[%d]\n", uid);
if (uid == AID_ROOT) {
auto info = make_shared<su_info>(uid);
info->access = SILENT_SU_ACCESS;
return info;
}
shared_ptr<su_info> info;
{
mutex_guard lock(cache_lock);
if (!cached || cached->uid != uid || !cached->is_fresh())
cached = make_shared<su_info>(uid);
cached->refresh();
info = cached;
}
mutex_guard lock = info->lock();
if (info->access.policy == QUERY) {
// Not cached, get data from database
info->check_db();
// If it's the manager, allow it silently
if (to_app_id(info->uid) == to_app_id(info->mgr_uid)) {
info->access = SILENT_SU_ACCESS;
return info;
}
// Check su access settings
switch (info->cfg[ROOT_ACCESS]) {
case ROOT_ACCESS_DISABLED:
LOGW("Root access is disabled!\n");
info->access = NO_SU_ACCESS;
break;
case ROOT_ACCESS_ADB_ONLY:
if (info->uid != AID_SHELL) {
LOGW("Root access limited to ADB only!\n");
info->access = NO_SU_ACCESS;
}
break;
case ROOT_ACCESS_APPS_ONLY:
if (info->uid == AID_SHELL) {
LOGW("Root access is disabled for ADB!\n");
info->access = NO_SU_ACCESS;
}
break;
case ROOT_ACCESS_APPS_AND_ADB:
default:
break;
}
if (info->access.policy != QUERY)
return info;
// If still not determined, check if manager exists
if (info->mgr_uid < 0) {
info->access = NO_SU_ACCESS;
return info;
}
}
return info;
}
// Set effective uid back to root, otherwise setres[ug]id will fail if uid isn't root
static void set_identity(uid_t uid, const std::vector<uid_t> &groups) {
if (seteuid(0)) {
PLOGE("seteuid (root)");
}
gid_t gid;
if (groups.size() > 0) {
if (setgroups(groups.size(), groups.data())) {
PLOGE("setgroups");
}
gid = groups[0];
} else {
gid = uid;
}
if (setresgid(gid, gid, gid)) {
PLOGE("setresgid (%u)", uid);
}
if (setresuid(uid, uid, uid)) {
PLOGE("setresuid (%u)", uid);
}
}
void su_daemon_handler(int client, const sock_cred *cred) {
LOGD("su: request from pid=[%d], client=[%d]\n", cred->pid, client);
su_context ctx = {
.info = get_su_info(cred->uid),
.req = su_request(),
.pid = cred->pid
};
// Read su_request
if (xxread(client, &ctx.req, sizeof(su_req_base)) < 0
|| !read_string(client, ctx.req.shell)
|| !read_string(client, ctx.req.command)
|| !read_string(client, ctx.req.context)
|| !read_vector(client, ctx.req.gids)) {
LOGW("su: remote process probably died, abort\n");
ctx.info.reset();
write_int(client, DENY);
close(client);
return;
}
// If still not determined, ask manager
if (ctx.info->access.policy == QUERY) {
int fd = app_request(ctx);
if (fd < 0) {
ctx.info->access.policy = DENY;
} else {
int ret = read_int_be(fd);
ctx.info->access.policy = ret < 0 ? DENY : static_cast<policy_t>(ret);
close(fd);
}
}
if (ctx.info->access.log)
app_log(ctx);
else if (ctx.info->access.notify)
app_notify(ctx);
// Fail fast
if (ctx.info->access.policy == DENY) {
LOGW("su: request rejected (%u)\n", ctx.info->uid);
ctx.info.reset();
write_int(client, DENY);
close(client);
return;
}
// Fork a child root process
//
// The child process will need to setsid, open a pseudo-terminal
// if needed, and eventually exec shell.
// The parent process will wait for the result and
// send the return code back to our client.
if (int child = xfork(); child) {
ctx.info.reset();
// Wait result
LOGD("su: waiting child pid=[%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;
}
LOGD("su: fork handler\n");
// Abort upon any error occurred
exit_on_error(true);
// ack
write_int(client, 0);
// Become session leader
xsetsid();
// The FDs for each of the streams
int infd = recv_fd(client);
int outfd = recv_fd(client);
int errfd = recv_fd(client);
// App need a PTY
if (read_int(client)) {
string pts;
string ptmx;
auto magiskpts = MAGISKTMP + "/" SHELLPTS;
if (access(magiskpts.data(), F_OK)) {
pts = "/dev/pts";
ptmx = "/dev/ptmx";
} else {
pts = magiskpts;
ptmx = magiskpts + "/ptmx";
}
int ptmx_fd = xopen(ptmx.data(), O_RDWR);
grantpt(ptmx_fd);
unlockpt(ptmx_fd);
int pty_num = get_pty_num(ptmx_fd);
if (pty_num < 0) {
// Kernel issue? Fallback to /dev/pts
close(ptmx_fd);
pts = "/dev/pts";
ptmx_fd = xopen("/dev/ptmx", O_RDWR);
grantpt(ptmx_fd);
unlockpt(ptmx_fd);
pty_num = get_pty_num(ptmx_fd);
}
send_fd(client, ptmx_fd);
close(ptmx_fd);
string pts_slave = pts + "/" + to_string(pty_num);
LOGD("su: pts_slave=[%s]\n", pts_slave.data());
// Opening the TTY has to occur after the
// fork() and setsid() so that it becomes
// our controlling TTY and not the daemon's
int ptsfd = xopen(pts_slave.data(), O_RDWR);
if (infd < 0)
infd = ptsfd;
if (outfd < 0)
outfd = ptsfd;
if (errfd < 0)
errfd = ptsfd;
}
// Swap out stdin, stdout, stderr
xdup2(infd, STDIN_FILENO);
xdup2(outfd, STDOUT_FILENO);
xdup2(errfd, STDERR_FILENO);
close(infd);
close(outfd);
close(errfd);
close(client);
// Handle namespaces
if (ctx.req.target == -1)
ctx.req.target = ctx.pid;
else if (ctx.req.target == 0)
ctx.info->cfg[SU_MNT_NS] = NAMESPACE_MODE_GLOBAL;
else if (ctx.info->cfg[SU_MNT_NS] == NAMESPACE_MODE_GLOBAL)
ctx.info->cfg[SU_MNT_NS] = NAMESPACE_MODE_REQUESTER;
switch (ctx.info->cfg[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", ctx.req.target);
if (switch_mnt_ns(ctx.req.target))
LOGD("su: setns failed, fallback to global\n");
break;
case NAMESPACE_MODE_ISOLATE:
LOGD("su: use new isolated namespace\n");
switch_mnt_ns(ctx.req.target);
xunshare(CLONE_NEWNS);
xmount(nullptr, "/", nullptr, MS_PRIVATE | MS_REC, nullptr);
break;
}
const char *argv[4] = { nullptr };
argv[0] = ctx.req.login ? "-" : ctx.req.shell.data();
if (!ctx.req.command.empty()) {
argv[1] = "-c";
argv[2] = ctx.req.command.data();
}
// Setup environment
umask(022);
char path[32];
ssprintf(path, sizeof(path), "/proc/%d/cwd", ctx.pid);
char cwd[4096];
if (realpath(path, cwd, sizeof(cwd)) > 0)
chdir(cwd);
ssprintf(path, sizeof(path), "/proc/%d/environ", ctx.pid);
auto env = full_read(path);
clearenv();
for (size_t pos = 0; pos < env.size(); ++pos) {
putenv(env.data() + pos);
pos = env.find_first_of('\0', pos);
if (pos == std::string::npos)
break;
}
if (!ctx.req.keepenv) {
struct passwd *pw;
pw = getpwuid(ctx.req.uid);
if (pw) {
setenv("HOME", pw->pw_dir, 1);
setenv("USER", pw->pw_name, 1);
setenv("LOGNAME", pw->pw_name, 1);
setenv("SHELL", ctx.req.shell.data(), 1);
}
}
// Unblock all signals
sigset_t block_set;
sigemptyset(&block_set);
sigprocmask(SIG_SETMASK, &block_set, nullptr);
if (!ctx.req.context.empty() && selinux_enabled()) {
auto f = xopen_file("/proc/self/attr/exec", "we");
if (f) fprintf(f.get(), "%s", ctx.req.context.data());
}
set_identity(ctx.req.uid, ctx.req.gids);
execvp(ctx.req.shell.data(), (char **) argv);
fprintf(stderr, "Cannot execute %s: %s\n", ctx.req.shell.data(), strerror(errno));
PLOGE("exec");
}