From ed6bef6d24f332e17d53fe67b9e09816eda899e5 Mon Sep 17 00:00:00 2001 From: Cory McWilliams Date: Thu, 4 Jul 2024 13:02:39 -0400 Subject: [PATCH] Get android running its sandbox in a seprate, isolated service process. So that we support not extracting the native code from the APK, so that we support distributing as an .aab file, so that we may one day release on the app store. --- src/android/AndroidManifest.xml | 5 + .../tildefriends/TildeFriendsActivity.java | 76 +++++++++++---- .../TildeFriendsSandboxService.java | 59 ++++++++++++ .../tildefriends/TildeFriendsWebView.java | 1 - src/main.c | 71 ++++++++++++-- src/task.c | 19 ++++ src/task.h | 31 +++++++ src/taskstub.js.c | 93 +++++++++++++------ 8 files changed, 300 insertions(+), 55 deletions(-) create mode 100644 src/android/com/unprompted/tildefriends/TildeFriendsSandboxService.java diff --git a/src/android/AndroidManifest.xml b/src/android/AndroidManifest.xml index c3a6ef34..d6693e56 100644 --- a/src/android/AndroidManifest.xml +++ b/src/android/AndroidManifest.xml @@ -20,5 +20,10 @@ + diff --git a/src/android/com/unprompted/tildefriends/TildeFriendsActivity.java b/src/android/com/unprompted/tildefriends/TildeFriendsActivity.java index 1c7b1cb4..a117460f 100644 --- a/src/android/com/unprompted/tildefriends/TildeFriendsActivity.java +++ b/src/android/com/unprompted/tildefriends/TildeFriendsActivity.java @@ -3,15 +3,18 @@ package com.unprompted.tildefriends; import android.app.Activity; import android.app.AlertDialog; import android.app.DownloadManager; +import android.content.ComponentName; import android.content.DialogInterface; import android.content.Intent; +import android.content.ServiceConnection; import android.net.Uri; import android.os.Bundle; -import android.os.CountDownTimer; import android.os.Environment; +import android.os.IBinder; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; import android.os.StrictMode; -import android.os.SystemClock; -import android.os.strictmode.Violation; import android.util.Base64; import android.util.Log; import android.view.KeyEvent; @@ -29,19 +32,13 @@ import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebViewClient; -import android.widget.Button; import android.widget.TextView; import android.widget.Toast; -import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.OutputStream; import java.io.FileReader; -import java.io.InputStream; -import java.lang.Process; -import java.lang.Thread; +import java.io.OutputStream; import java.nio.file.FileSystems; import java.nio.file.Paths; import java.nio.file.StandardWatchEventKinds; @@ -51,12 +48,13 @@ import java.nio.file.WatchService; import java.util.concurrent.TimeUnit; public class TildeFriendsActivity extends Activity { + static TildeFriendsActivity s_activity; TildeFriendsWebView web_view; String base_url; String port_file_path; - Process process; Thread thread; Thread server_thread; + ServiceConnection service_connection; private ValueCallback upload_message; private final static int FILECHOOSER_RESULT = 1; @@ -68,10 +66,12 @@ public class TildeFriendsActivity extends Activity { Log.w("tildefriends", "system.loadLibrary() completed."); } - static native int tf_server_main(String files_dir, String apk_path, String out_port_file_path); + public static native int tf_server_main(String files_dir, String apk_path, String out_port_file_path); + public static native int tf_sandbox_main(int pipe_fd); @Override protected void onCreate(Bundle savedInstanceState) { + s_activity = this; StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedClosableObjects() .penaltyLog() @@ -274,13 +274,8 @@ public class TildeFriendsActivity extends Activity { @Override protected void onDestroy() { - if (process != null) { - Log.w("tildefriends", "Killing process."); - process.destroyForcibly(); - Log.w("tildefriends", "Process killed."); - process = null; - } super.onDestroy(); + s_activity = null; } @Override @@ -382,4 +377,49 @@ public class TildeFriendsActivity extends Activity { web_view.setVisibility(View.VISIBLE); text_view.setVisibility(View.GONE); } + + public static void start_sandbox(int pipe_fd) { + Log.w("tildefriends", "starting service with fd: " + pipe_fd); + Intent intent = new Intent(s_activity, TildeFriendsSandboxService.class); + s_activity.service_connection = new ServiceConnection() { + @Override + public void onBindingDied(ComponentName name) { + Log.w("tildefriends", "onBindingDied"); + } + + @Override + public void onNullBinding(ComponentName name) { + Log.w("tildefriends", "onNullBinding"); + } + + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + Log.w("tildefriends", "onServiceConnected"); + Parcel data = Parcel.obtain(); + ParcelFileDescriptor pfd = ParcelFileDescriptor.adoptFd(pipe_fd); + data.writeParcelable(pfd, 0); + try { + binder.transact(TildeFriendsSandboxService.START_CALL, data, null, IBinder.FLAG_ONEWAY); + } catch (RemoteException e) { + Log.w("tildefriends", "RemoteException"); + } finally { + data.recycle(); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.w("tildefriends", "onServiceDisconnected"); + } + }; + s_activity.bindService(intent, s_activity.service_connection, BIND_AUTO_CREATE); + } + + public static void stop_sandbox() { + Log.w("tildefriends", "stop_sandbox"); + if (s_activity.service_connection != null) { + s_activity.unbindService(s_activity.service_connection); + s_activity.service_connection = null; + } + } } diff --git a/src/android/com/unprompted/tildefriends/TildeFriendsSandboxService.java b/src/android/com/unprompted/tildefriends/TildeFriendsSandboxService.java new file mode 100644 index 00000000..4626f705 --- /dev/null +++ b/src/android/com/unprompted/tildefriends/TildeFriendsSandboxService.java @@ -0,0 +1,59 @@ +package com.unprompted.tildefriends; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import android.os.Parcel; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +public class TildeFriendsSandboxService extends Service { + public static final int START_CALL = IBinder.FIRST_CALL_TRANSACTION; + + Thread thread; + + public int onStartCommand(Intent intent, int flags, int start_id) { + Log.w("tildefriends", "TildeFriendsSandboxService: onStartCommand"); + return super.onStartCommand(intent, flags, start_id); + } + + public void onDestroy() { + Log.w("tildefriends", "TildeFriendsSandboxService: onDestroy"); + super.onDestroy(); + } + + private void start_thread(int pipe_fd) { + thread = new Thread(new Runnable() { + @Override + public void run() { + Log.w("tildefriends", "Calling tf_sandbox_main."); + int result = TildeFriendsActivity.tf_sandbox_main(pipe_fd); + Log.w("tildefriends", "tf_sandbox_main returned " + result + "."); + } + }); + thread.start(); + } + + @Override + public IBinder onBind(Intent intent) { + return new Binder() { + @Override + protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) { + if (code == START_CALL) { + ParcelFileDescriptor pfd = data.readParcelable(ParcelFileDescriptor.class.getClassLoader(), ParcelFileDescriptor.class); + if (pfd != null) { + Log.w("tildefriends", "fd is " + pfd.getFd()); + start_thread(pfd.detachFd()); + try { + pfd.close(); + } catch (java.io.IOException e) { + } + } + return true; + } + return false; + } + }; + } +} diff --git a/src/android/com/unprompted/tildefriends/TildeFriendsWebView.java b/src/android/com/unprompted/tildefriends/TildeFriendsWebView.java index e56bee09..340de874 100644 --- a/src/android/com/unprompted/tildefriends/TildeFriendsWebView.java +++ b/src/android/com/unprompted/tildefriends/TildeFriendsWebView.java @@ -2,7 +2,6 @@ package com.unprompted.tildefriends; import android.content.Context; import android.util.AttributeSet; -import android.util.Log; public class TildeFriendsWebView extends android.webkit.WebView { boolean overscrolledY = false; diff --git a/src/main.c b/src/main.c index e834383b..37b203d1 100644 --- a/src/main.c +++ b/src/main.c @@ -607,14 +607,16 @@ static int _tf_command_run(const char* file, int argc, char* argv[]) static int _tf_command_sandbox(const char* file, int argc, char* argv[]) { bool show_usage = false; + int fd = STDIN_FILENO; while (!show_usage) { static const struct option k_options[] = { + { "fd", required_argument, NULL, 'f' }, { "help", no_argument, NULL, 'h' }, { 0 }, }; - int c = getopt_long(argc, argv, "h", k_options, NULL); + int c = getopt_long(argc, argv, "f:h", k_options, NULL); if (c == -1) { break; @@ -626,6 +628,10 @@ static int _tf_command_sandbox(const char* file, int argc, char* argv[]) default: show_usage = true; break; + case 'f': + tf_printf("got -f %s\n", optarg); + fd = atoi(optarg); + break; } } @@ -634,6 +640,7 @@ static int _tf_command_sandbox(const char* file, int argc, char* argv[]) tf_printf("\nUsage: %s sandbox [options]\n\n", file); tf_printf("options:\n"); tf_printf(" -h, --help Show this usage information.\n"); + tf_printf(" -f, --fd File descriptor with which to communicate with parent process.\n"); return EXIT_FAILURE; } @@ -641,7 +648,7 @@ static int _tf_command_sandbox(const char* file, int argc, char* argv[]) prctl(PR_SET_PDEATHSIG, SIGHUP); #endif tf_task_t* task = tf_task_create(); - tf_task_configure_from_fd(task, STDIN_FILENO); + tf_task_configure_from_fd(task, fd); _shed_privileges(); /* The caller will trigger tf_task_activate with a message. */ tf_task_run(task); @@ -689,10 +696,6 @@ static void _startup(int argc, char* argv[]) } } -#if defined(__ANDROID__) - setenv("UV_USE_IO_URING", "0", 1); -#endif - tf_mem_startup(tracking); g_backtrace_state = backtrace_create_state(argv[0], 0, _backtrace_error, NULL); @@ -720,7 +723,7 @@ static void _startup(int argc, char* argv[]) { if ( #if !defined(_WIN32) - signal(SIGSYS, _error_handler) == SIG_ERR || + signal(SIGSYS, _error_handler) == SIG_ERR || signal(SIGABRT, _error_handler) == SIG_ERR || #endif signal(SIGSEGV, _error_handler) == SIG_ERR) { @@ -730,8 +733,28 @@ static void _startup(int argc, char* argv[]) } #if defined(__ANDROID__) +static JNIEnv* s_jni_env; + +static void _tf_service_start(int pipe_fd) +{ + tf_printf("_tf_service_start\n"); + jclass c = (*s_jni_env)->FindClass(s_jni_env, "com/unprompted/tildefriends/TildeFriendsActivity"); + jmethodID start_sandbox = (*s_jni_env)->GetStaticMethodID(s_jni_env, c, "start_sandbox", "(I)V"); + (*s_jni_env)->CallStaticVoidMethod(s_jni_env, c, start_sandbox, pipe_fd); +} + +static void _tf_service_stop() +{ + tf_printf("_tf_service_stop\n"); + jclass c = (*s_jni_env)->FindClass(s_jni_env, "com/unprompted/tildefriends/TildeFriendsActivity"); + jmethodID stop_sandbox = (*s_jni_env)->GetStaticMethodID(s_jni_env, c, "stop_sandbox", "()V"); + (*s_jni_env)->CallStaticVoidMethod(s_jni_env, c, stop_sandbox); +} + static jint _tf_server_main(JNIEnv* env, jobject this_object, jstring files_dir, jstring apk_path, jstring out_port_file_path) { + s_jni_env = env; + tf_printf("This is tf_server_main main.\n"); _startup(0, (char*[]) { NULL }); tf_printf("That was startup.\n"); @@ -754,24 +777,51 @@ static jint _tf_server_main(JNIEnv* env, jobject this_object, jstring files_dir, char* port_file_arg = alloca(port_file_arg_length); snprintf(port_file_arg, port_file_arg_length, "out_http_port_file=%s", out_port_file); - const char* args[] = - { + const char* args[] = { + "run", "-z", apk, "-a", port_file_arg, "-p", "0", - "-o", /* HACK! FIXME! */ }; + tf_task_set_android_service_callbacks(_tf_service_start, _tf_service_stop); result = _tf_command_run(apk, _countof(args), (char**)args); + tf_task_set_android_service_callbacks(NULL, NULL); (*env)->ReleaseStringUTFChars(env, files_dir, files); (*env)->ReleaseStringUTFChars(env, apk_path, apk); (*env)->ReleaseStringUTFChars(env, out_port_file_path, out_port_file); tf_printf("tf_server_main finished with %d.", result); + + s_jni_env = NULL; + return result; +} + +static jint _tf_sandbox_main(JNIEnv* env, jobject this_object, int pipe_fd) +{ + s_jni_env = env; + + tf_printf("This is tf_sandbox_main main (fd=%d).\n", pipe_fd); + _startup(0, (char*[]) { NULL }); + tf_printf("That was startup.\n"); + + char fd[32] = { 0 }; + snprintf(fd, sizeof(fd), "%d", pipe_fd); + const char* args[] = { + "sandbox", + "-f", + fd, + }; + + int result = _tf_command_sandbox(NULL, _countof(args), (char**)args); + + tf_printf("tf_sandbox_main finished with %d.", result); + + s_jni_env = NULL; return result; } @@ -796,6 +846,7 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) tf_printf("Registering method.\n"); static const JNINativeMethod methods[] = { { "tf_server_main", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I", _tf_server_main }, + { "tf_sandbox_main", "(I)I", _tf_sandbox_main }, }; int result = (*env)->RegisterNatives(env, c, methods, (int)_countof(methods)); if (result != JNI_OK) diff --git a/src/task.c b/src/task.c index dbf32710..b0d54f95 100644 --- a/src/task.c +++ b/src/task.c @@ -50,6 +50,9 @@ static JSClassID _import_class_id; static int _count; +static tf_android_start_service_t* s_android_start_service; +static tf_android_stop_service_t* s_android_stop_service; + extern struct backtrace_state* g_backtrace_state; typedef struct _export_record_t export_record_t; @@ -2160,3 +2163,19 @@ static JSValue _tf_task_pokeSandbox(JSContext* context, JSValueConst this_val, i #endif return JS_NewInt32(context, WEXITSTATUS(result)); } + +void tf_task_set_android_service_callbacks(tf_android_start_service_t* start_service, tf_android_stop_service_t* stop_service) +{ + s_android_start_service = start_service; + s_android_stop_service = stop_service; +} + +tf_android_start_service_t* tf_task_get_android_start_service() +{ + return s_android_start_service; +} + +tf_android_stop_service_t* tf_task_get_android_stop_service() +{ + return s_android_stop_service; +} diff --git a/src/task.h b/src/task.h index 61a2eb09..7d81d2c6 100644 --- a/src/task.h +++ b/src/task.h @@ -333,4 +333,35 @@ char* tf_task_get_debug(tf_task_t* task); */ char* tf_task_get_hitches(tf_task_t* task); +/** +** A callback used to start an Android service. +** @param pipe_fd A file descriptor with which to communicate with the invoking +** task. +*/ +typedef void(tf_android_start_service_t)(int pipe_fd); + +/** +** A callback used to stop an Android service. +*/ +typedef void(tf_android_stop_service_t)(); + +/** +** Set Android service callbacks. +** @param start_service Start service callback. +** @param stop_service Stop service callback. +*/ +void tf_task_set_android_service_callbacks(tf_android_start_service_t* start_service, tf_android_stop_service_t* stop_service); + +/** +** Get the callback registered for starting an Android service. +** @return the callback. +*/ +tf_android_start_service_t* tf_task_get_android_start_service(); + +/** +** Get the callback registered for stopping an Android service. +** @return the callback. +*/ +tf_android_stop_service_t* tf_task_get_android_stop_service(); + /** @} */ diff --git a/src/taskstub.js.c b/src/taskstub.js.c index 5fdb3414..e2071e12 100644 --- a/src/taskstub.js.c +++ b/src/taskstub.js.c @@ -154,41 +154,74 @@ static JSValue _taskstub_create(JSContext* context, JSValueConst this_val, int a } else { - uv_pipe_t* pipe = tf_packetstream_get_pipe(stub->_stream); - memset(pipe, 0, sizeof(*pipe)); - if (uv_pipe_init(tf_task_get_loop(parent), pipe, 1) != 0) + tf_android_start_service_t* start_service = tf_task_get_android_start_service(); + if (start_service) { - tf_printf("uv_pipe_init failed\n"); - } + uv_os_sock_t fds[2]; + int socketpair_result = uv_socketpair(SOCK_STREAM, 0, fds, 0, 0); + if (socketpair_result) + { + tf_printf("uv_socketpair: %s\n", uv_strerror(socketpair_result)); + } - uv_stdio_container_t io[3]; - io[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE | UV_WRITABLE_PIPE; - io[0].data.stream = (uv_stream_t*)pipe; - io[1].flags = UV_INHERIT_FD; - io[1].data.fd = STDOUT_FILENO; - io[2].flags = UV_INHERIT_FD; - io[2].data.fd = STDERR_FILENO; + uv_pipe_t* pipe = tf_packetstream_get_pipe(stub->_stream); + *pipe = (uv_pipe_t) { 0 }; + if (uv_pipe_init(tf_task_get_loop(parent), pipe, 1) != 0) + { + tf_printf("uv_pipe_init failed\n"); + } - uv_process_options_t options = { 0 }; - options.args = command_argv; - options.exit_cb = _taskstub_on_process_exit; - options.stdio = io; - options.stdio_count = sizeof(io) / sizeof(*io); - options.file = command_argv[0]; - options.env = (char*[]) { "ASAN_OPTIONS=detect_leaks=0", NULL }; + int pipe_result = uv_pipe_open(pipe, fds[0]); + if (pipe_result) + { + tf_printf("uv_pipe_open: %s\n", uv_strerror(pipe_result)); + } - stub->_process.data = stub; - int spawn_result = uv_spawn(tf_task_get_loop(parent), &stub->_process, &options); - if (spawn_result == 0) - { + start_service(fds[1]); + + stub->_process.data = stub; tf_packetstream_set_on_receive(stub->_stream, tf_task_on_receive_packet, stub); tf_packetstream_start(stub->_stream); result = taskObject; } else { - tf_printf("uv_spawn failed: %s\n", uv_strerror(spawn_result)); - JS_FreeValue(context, taskObject); + uv_pipe_t* pipe = tf_packetstream_get_pipe(stub->_stream); + memset(pipe, 0, sizeof(*pipe)); + if (uv_pipe_init(tf_task_get_loop(parent), pipe, 1) != 0) + { + tf_printf("uv_pipe_init failed\n"); + } + + uv_stdio_container_t io[3]; + io[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE | UV_WRITABLE_PIPE; + io[0].data.stream = (uv_stream_t*)pipe; + io[1].flags = UV_INHERIT_FD; + io[1].data.fd = STDOUT_FILENO; + io[2].flags = UV_INHERIT_FD; + io[2].data.fd = STDERR_FILENO; + + uv_process_options_t options = { 0 }; + options.args = command_argv; + options.exit_cb = _taskstub_on_process_exit; + options.stdio = io; + options.stdio_count = sizeof(io) / sizeof(*io); + options.file = command_argv[0]; + options.env = (char*[]) { "ASAN_OPTIONS=detect_leaks=0", NULL }; + + stub->_process.data = stub; + int spawn_result = uv_spawn(tf_task_get_loop(parent), &stub->_process, &options); + if (spawn_result == 0) + { + tf_packetstream_set_on_receive(stub->_stream, tf_task_on_receive_packet, stub); + tf_packetstream_start(stub->_stream); + result = taskObject; + } + else + { + tf_printf("uv_spawn failed: %s\n", uv_strerror(spawn_result)); + JS_FreeValue(context, taskObject); + } } } return JS_DupValue(context, result); @@ -443,7 +476,15 @@ JSValue tf_taskstub_kill(tf_taskstub_t* stub) JSValue result = JS_UNDEFINED; if (!tf_task_get_one_proc(stub->_owner)) { - uv_process_kill(&stub->_process, SIGKILL); + tf_android_stop_service_t* stop_service = tf_task_get_android_stop_service(); + if (stop_service) + { + stop_service(); + } + else + { + uv_process_kill(&stub->_process, SIGKILL); + } } else {