Files
vanilla/gui/pipemgmt.c
T

603 lines
17 KiB
C

#define _GNU_SOURCE
#include "pipemgmt.h"
#ifdef VANILLA_PIPE_AVAILABLE
#include <errno.h>
#include <libgen.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vanilla.h>
#ifdef ANDROID
#include <dlfcn.h>
#include <limits.h>
#endif
#include "config.h"
#include "platform.h"
static pid_t pipe_pid = -1;
static pid_t potential_pipe_pid = -1;
static int pipe_input;
static int pipe_err;
static pthread_t pipe_log_thread = 0;
static int err_pipes[2], in_pipes[2];
#ifdef VANILLA_POLKIT_AVAILABLE
#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE
#include <polkit/polkit.h>
#include <polkitagent/polkitagent.h>
static gpointer polkit_handle = 0;
static GMainContext *gmainctx = 0;
static PolkitAgentSession *pk_session = 0;
typedef struct {
PolkitAgentListener parent;
} VanillaPolkitListener;
typedef struct {
PolkitAgentListenerClass parent_class;
} VanillaPolkitListenerClass;
static VanillaPolkitListener *listener = 0;
static vpi_pw_callback pk_pw_callback = 0;
static void *pk_pw_callback_userdata = 0;
static PolkitIdentity *pk_identity = 0;
static GTask *pk_task = 0;
static int pk_attempts = 0;
static int pk_ignore_complete = 0;
static char pk_cookie[256]; // TODO: No actual idea how long a Polkit cookie usually is
G_DEFINE_TYPE(VanillaPolkitListener, vanilla_polkit_listener, POLKIT_AGENT_TYPE_LISTENER)
static void try_again();
static void on_request(PolkitAgentSession *s, const gchar *prompt, gboolean echo_on, gpointer user_data)
{
vpilog("Polkit authentication requested\n");
pk_session = s;
}
static void on_show_info(PolkitAgentSession *s, const gchar *text, gpointer user_data)
{
vpilog("Info\n");
if (text) vpilog("%s\n", text);
}
static void on_show_error(PolkitAgentSession *s, const gchar *text, gpointer user_data)
{
vpilog("Error\n");
if (text) vpilog("Error: %s\n", text);
}
static void on_complete(PolkitAgentSession *s, gboolean gained, gpointer data)
{
vpilog("Complete: %i\n", gained);
g_object_unref(pk_session);
pk_session = 0;
if (!pk_ignore_complete) {
pk_attempts++;
if (pk_pw_callback) {
int cb_ret;
if (gained) {
cb_ret = VPI_POLKIT_RESULT_SUCCESS;
g_task_return_boolean(pk_task, gained);
g_object_unref(pk_task);
pk_task = 0;
} else if (pk_attempts < 3) {
cb_ret = VPI_POLKIT_RESULT_RETRY;
try_again();
} else {
cb_ret = VPI_POLKIT_RESULT_FAIL;
g_task_return_error(pk_task, g_error_new_literal(G_IO_ERROR, G_IO_ERROR_FAILED, "Maximum retries exceeded"));
// g_task_return_boolean(pk_task, gained);
g_object_unref(pk_task);
pk_task = 0;
}
pk_pw_callback(cb_ret, pk_pw_callback_userdata);
}
}
}
static void try_again()
{
pk_ignore_complete = 0;
g_main_context_push_thread_default(gmainctx);
PolkitAgentSession *session = polkit_agent_session_new(pk_identity, pk_cookie);
g_signal_connect(session, "request", G_CALLBACK(on_request), NULL);
g_signal_connect(session, "show-info", G_CALLBACK(on_show_info), NULL);
g_signal_connect(session, "show-error", G_CALLBACK(on_show_error), NULL);
// When session completes, call the async callback with success/failure.
g_signal_connect(session, "completed", G_CALLBACK(on_complete), NULL);
// Start PAM conversation
polkit_agent_session_initiate(session);
vpilog("Polkit session initiated\n");
g_main_context_pop_thread_default(gmainctx);
}
static void vanilla_polkit_initiate_authentication(PolkitAgentListener *listener,
const gchar *action_id,
const gchar *message,
const gchar *icon_name,
PolkitDetails *details,
const gchar *cookie,
GList *identities,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_main_context_push_thread_default(gmainctx);
pk_identity = identities->data;
g_object_ref(pk_identity);
pk_task = g_task_new(listener, cancellable, callback, user_data);
strncpy(pk_cookie, cookie, sizeof(pk_cookie));
try_again();
g_main_context_pop_thread_default(gmainctx);
}
static gboolean vanilla_polkit_initiate_authentication_finish(PolkitAgentListener *listener,
GAsyncResult *res,
GError **error)
{
return g_task_propagate_boolean(G_TASK(res), error);
}
static void vanilla_polkit_listener_finalize(GObject *object)
{
g_return_if_fail(object != NULL);
g_return_if_fail(G_TYPE_CHECK_INSTANCE_TYPE((object), vanilla_polkit_listener_get_type()));
G_OBJECT_CLASS(vanilla_polkit_listener_parent_class)->finalize(object);
}
static void vanilla_polkit_listener_class_init(VanillaPolkitListenerClass *self)
{
GObjectClass *g_object_class = G_OBJECT_CLASS(self);
g_object_class->finalize = vanilla_polkit_listener_finalize;
PolkitAgentListenerClass *lklass = POLKIT_AGENT_LISTENER_CLASS(self);
lklass->initiate_authentication = vanilla_polkit_initiate_authentication;
lklass->initiate_authentication_finish = vanilla_polkit_initiate_authentication_finish;
}
static void vanilla_polkit_listener_init(VanillaPolkitListener *self)
{
}
#endif
void *vpi_pipe_log_thread(void *data)
{
char buf[512];
int pipe_err = (intptr_t) data;
size_t buf_read = 0;
while (1) {
ssize_t this_read = read(pipe_err, buf + buf_read, 1);
if (this_read == 1) {
if (buf[buf_read] == '\n') {
// Print line and reset
buf[buf_read + 1] = '\0';
vpilog("%s", buf);
buf_read = 0;
} else {
buf_read += this_read;
}
} else {
// Encountered error, pipe likely exited so break here
break;
}
}
return 0;
}
struct sigaction old_sigint_action;
struct sigaction old_sigterm_action;
void sigint_handler(int signal)
{
// On the Steam Deck, if the user selects "exit game", we get handed a
// SIGINT and SIGTERM, but then get SIGKILL'd shortly afterwards, before we
// can properly clean everything up and send `vanilla-pipe` a "quit" signal.
// This means `vanilla-pipe` often gets stuck in limbo, and even worse, the
// Deck's Wi-Fi interface remains under its control until reboot.
//
// To resolve this, we make sure the first thing we do upon receiving SIGINT
// or SIGTERM is to tell the pipe to quit, so hopefully even if we are killed,
// it can clean itself up without us.
vpi_stop_pipe();
// vpi_stop_pipe should have restored the old sigaction (SDL's), so let's
// follow that through
raise(signal);
}
#ifdef VANILLA_POLKIT_AVAILABLE
void vpi_close_polkit_session()
{
if (polkit_handle) {
pk_ignore_complete = 1;
if (pk_session) {
polkit_agent_session_cancel(pk_session);
pk_session = 0;
}
if (pk_task) {
g_task_return_boolean(pk_task, 0);
g_object_unref(pk_task);
pk_task = 0;
}
vpilog("Unregistering Polkit listener\n");
polkit_agent_listener_unregister(polkit_handle);
g_clear_object(&listener);
polkit_handle = 0;
g_object_unref(listener);
listener = 0;
g_main_context_unref(gmainctx);
gmainctx = 0;
if (pk_identity) {
g_object_unref(pk_identity);
pk_identity = 0;
}
}
}
void vpi_cancel_pw()
{
// Close existing pkexec process
if (potential_pipe_pid != -1) {
kill(potential_pipe_pid, SIGTERM);
potential_pipe_pid = -1;
close(in_pipes[1]);
close(err_pipes[0]);
}
vpi_close_polkit_session();
}
void vpi_submit_pw(const char *s, vpi_pw_callback callback, void *userdata)
{
pk_pw_callback = callback;
pk_pw_callback_userdata = userdata;
polkit_agent_session_response(pk_session, s);
}
#endif
int vpi_start_epilog()
{
int ret = VANILLA_ERR_GENERIC;
char ready_buf[500];
while (1) {
size_t read_count = 0;
int reached_eof = 0;
while (read_count + 1 < sizeof(ready_buf)) {
char c;
ssize_t read_size = read(err_pipes[0], &c, 1);
if (read_size < 0 && errno == EINTR) {
continue;
}
if (read_size != 1) {
reached_eof = 1;
break;
}
if (c == '\n') {
break;
}
ready_buf[read_count++] = c;
}
ready_buf[read_count] = 0;
if (!strcmp(ready_buf, "READY")) {
ret = VANILLA_SUCCESS;
pipe_input = in_pipes[1];
pipe_err = err_pipes[0];
pthread_create(&pipe_log_thread, 0, vpi_pipe_log_thread, (void *) (intptr_t) pipe_err);
pipe_pid = potential_pipe_pid;
break;
}
if (read_count) {
vpilog("%s\n", ready_buf);
}
if (reached_eof) {
break;
}
}
if (ret != VANILLA_SUCCESS) {
if (potential_pipe_pid != -1) {
kill(potential_pipe_pid, SIGKILL);
waitpid(potential_pipe_pid, NULL, 0);
potential_pipe_pid = -1;
}
close(in_pipes[1]);
close(err_pipes[0]);
}
#ifdef VANILLA_POLKIT_AVAILABLE
vpi_close_polkit_session();
#endif
return ret;
}
int vpi_start_pipe()
{
if (pipe_pid != -1) {
// Pipe is already active
return VANILLA_SUCCESS;
}
#ifdef VANILLA_POLKIT_AVAILABLE
{
PolkitAuthority *auth = polkit_authority_get_sync(NULL, NULL);
PolkitSubject *sub = polkit_unix_process_new_for_owner(getpid(), 0, -1);
PolkitAuthorizationResult *res =
polkit_authority_check_authorization_sync(
auth, sub, "com.mattkc.vanilla", NULL,
POLKIT_CHECK_AUTHORIZATION_FLAGS_NONE, NULL, NULL);
gboolean ok = res && polkit_authorization_result_get_is_authorized(res);
if (res) g_object_unref(res);
g_object_unref(sub);
g_object_unref(auth);
// gboolean ok = polkit_authorization_result_get_is_authorized(res);
// gboolean chall = polkit_authorization_result_get_is_challenge(res);
if (!ok) {
g_autoptr(GError) error = NULL;
g_autoptr(PolkitSubject) subject = polkit_unix_session_new_for_process_sync(getpid(), NULL, &error);
if (subject) {
pk_session = 0;
pk_attempts = 0;
// I don't think this is strictly necessary because according to
// documentation, subject == null when error is set, but
// just to be safe...
g_clear_error(&error);
gmainctx = g_main_context_new();
g_main_context_push_thread_default(gmainctx);
listener = g_object_new(vanilla_polkit_listener_get_type(), NULL);
polkit_handle = polkit_agent_listener_register(
POLKIT_AGENT_LISTENER(listener),
POLKIT_AGENT_REGISTER_FLAGS_NONE,
subject,
NULL, NULL, &error
);
g_main_context_pop_thread_default(gmainctx);
if (!polkit_handle) {
g_main_context_unref(gmainctx);
gmainctx = NULL;
g_clear_object(&listener);
vpilog("Failed to register Polkit listener: %s\n", error ? error->message : "unknown error");
} else {
vpilog("Registered Polkit listener\n");
}
} else {
vpilog("Failed to create session/subject for process: %s\n", error ? error->message : "unknown error");
}
}
}
#endif
// Set up pipes so child stdout can be read by the parent process
pipe(in_pipes);
pipe(err_pipes);
// Get parent pid (allows us to check if parent was terminated immediately after fork)
pid_t ppid_before_fork = getpid();
// Fork into parent/child processes
pid_t pid = fork();
if (pid == 0) {
// We are in the child. Set child to terminate when parent does.
int r = prctl(PR_SET_PDEATHSIG, SIGHUP);
if (r == -1) {
perror(0);
exit(1);
}
// See if parent pid is still the same. If not, it must have been terminated, so we will exit too.
if (getppid() != ppid_before_fork) {
exit(1);
}
// Set up pipes so our stdout can be read by the parent process
dup2(err_pipes[1], STDERR_FILENO);
dup2(in_pipes[0], STDIN_FILENO);
close(err_pipes[0]);
close(err_pipes[1]);
close(in_pipes[0]);
close(in_pipes[1]);
setsid();
// Execute process (this will replace the running code)
#ifdef ANDROID
// HACK: Use dladdr to get the path of us
Dl_info info;
if (!dladdr((void *) &vpi_start_pipe, &info) || !info.dli_fname) {
vpilog("Could not get running path of current Android executable\n");
_exit(1);
}
// Copy path into our own string
char our_path[PATH_MAX];
if (snprintf(our_path, sizeof(our_path), "%s", info.dli_fname) >= sizeof(our_path)) {
vpilog("Android library path too long - this is a bug, tell maintainers\n");
_exit(1);
}
// Get dirname of string and add libvanilla_pipe.so
char *library_dir = dirname(our_path);
char pipe_path[PATH_MAX];
if (snprintf(pipe_path, sizeof(pipe_path), "%s/libvanilla_pipe.so", library_dir) >= sizeof(pipe_path)) {
vpilog("Android pipe path too long - this is a bug, tell maintainers\n");
_exit(1);
}
char command[PATH_MAX + 256];
snprintf(command, sizeof(command), "exec '%s' -local '%s'", pipe_path, vpi_config.wireless_interface);
r = execlp("su", "su", "-c", command, (const char *) 0);
#else
char exe[4096];
ssize_t link_len = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
if (link_len <= 0) {
_exit(1);
}
exe[link_len] = 0;
dirname(exe);
strcat(exe, "/vanilla-pipe");
#ifdef VANILLA_POLKIT_AVAILABLE
const char *pkexec = "pkexec";
r = execlp(pkexec, pkexec, exe, "-local", vpi_config.wireless_interface, (const char *) 0);
#else
r = execlp(exe, exe, "-local", vpi_config.wireless_interface, (const char *) 0);
#endif
#endif
// Handle failure to execute, use _exit so we don't interfere with the host
_exit(1);
} else if (pid < 0) {
// Fork error
close(in_pipes[0]);
close(in_pipes[1]);
close(err_pipes[0]);
close(err_pipes[1]);
return VANILLA_ERR_GENERIC;
} else {
// Continuation of parent
int ret = VANILLA_ERR_GENERIC;
// Only the child writes stderr and reads stdin. Closing the parent's
// copies here lets startup detect a child that exits before READY.
close(err_pipes[1]);
close(in_pipes[0]);
struct sigaction sa;
sa.sa_handler = sigint_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, &old_sigint_action);
sigaction(SIGTERM, &sa, &old_sigterm_action);
potential_pipe_pid = pid;
#ifdef VANILLA_POLKIT_AVAILABLE
if (polkit_handle) {
// We'll need to handle this...
ret = VANILLA_REQUIRES_PASSWORD_HANDLING;
// Keep handling synchronous by waiting for polkit to send us a
// "request" signal before returning to the password prompt
while (!pk_session) {
g_main_context_iteration(gmainctx, 1);
}
} else {
#endif
// Polkit will be handled for us, wait for it to finish
ret = vpi_start_epilog();
#ifdef VANILLA_POLKIT_AVAILABLE
}
#endif
// Tell lib our wireless interface
vanilla_set_wireless_interface(vpi_config.wireless_interface);
return ret;
}
}
void vpi_update_pipe()
{
#ifdef VANILLA_POLKIT_AVAILABLE
if (gmainctx) {
while (g_main_context_pending(gmainctx)) {
g_main_context_iteration(gmainctx, 0);
}
}
#endif
}
void vpi_stop_pipe()
{
if (pipe_pid != -1) {
// Signal to pipe to quit. We must send it through stdin because the pipe
// runs under root, so we have no permission to send it SIGINT or SIGTERM.
ssize_t s = write(pipe_input, "QUIT\n", 5);
close(pipe_input);
// Wait for our log thread to quit
pthread_join(pipe_log_thread, 0);
close(pipe_err);
waitpid(pipe_pid, 0, 0);
pipe_pid = -1;
// Restore old sigaction
sigaction(SIGINT, &old_sigint_action, NULL);
sigaction(SIGTERM, &old_sigterm_action, NULL);
}
#ifdef VANILLA_POLKIT_AVAILABLE
vpi_close_polkit_session();
#endif
}
#else
// Empty function
void vpi_stop_pipe()
{
}
// Empty function
void vpi_update_pipe()
{
}
#endif