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.

This commit is contained in:
Cory McWilliams 2024-07-04 13:02:39 -04:00
parent 71268636df
commit ed6bef6d24
8 changed files with 300 additions and 55 deletions

View File

@ -20,5 +20,10 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<service
android:name=".TildeFriendsSandboxService"
android:exported="false"
android:isolatedProcess="true"
android:process=":sandbox"/>
</application> </application>
</manifest> </manifest>

View File

@ -3,15 +3,18 @@ package com.unprompted.tildefriends;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.DownloadManager; import android.app.DownloadManager;
import android.content.ComponentName;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.Environment; 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.StrictMode;
import android.os.SystemClock;
import android.os.strictmode.Violation;
import android.util.Base64; import android.util.Base64;
import android.util.Log; import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
@ -29,19 +32,13 @@ import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest; import android.webkit.WebResourceRequest;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient; import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import java.io.BufferedInputStream;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.FileReader; import java.io.FileReader;
import java.io.InputStream; import java.io.OutputStream;
import java.lang.Process;
import java.lang.Thread;
import java.nio.file.FileSystems; import java.nio.file.FileSystems;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds; import java.nio.file.StandardWatchEventKinds;
@ -51,12 +48,13 @@ import java.nio.file.WatchService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class TildeFriendsActivity extends Activity { public class TildeFriendsActivity extends Activity {
static TildeFriendsActivity s_activity;
TildeFriendsWebView web_view; TildeFriendsWebView web_view;
String base_url; String base_url;
String port_file_path; String port_file_path;
Process process;
Thread thread; Thread thread;
Thread server_thread; Thread server_thread;
ServiceConnection service_connection;
private ValueCallback<Uri[]> upload_message; private ValueCallback<Uri[]> upload_message;
private final static int FILECHOOSER_RESULT = 1; private final static int FILECHOOSER_RESULT = 1;
@ -68,10 +66,12 @@ public class TildeFriendsActivity extends Activity {
Log.w("tildefriends", "system.loadLibrary() completed."); 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 @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
s_activity = this;
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedClosableObjects() .detectLeakedClosableObjects()
.penaltyLog() .penaltyLog()
@ -274,13 +274,8 @@ public class TildeFriendsActivity extends Activity {
@Override @Override
protected void onDestroy() protected void onDestroy()
{ {
if (process != null) {
Log.w("tildefriends", "Killing process.");
process.destroyForcibly();
Log.w("tildefriends", "Process killed.");
process = null;
}
super.onDestroy(); super.onDestroy();
s_activity = null;
} }
@Override @Override
@ -382,4 +377,49 @@ public class TildeFriendsActivity extends Activity {
web_view.setVisibility(View.VISIBLE); web_view.setVisibility(View.VISIBLE);
text_view.setVisibility(View.GONE); 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;
}
}
} }

View File

@ -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;
}
};
}
}

View File

@ -2,7 +2,6 @@ package com.unprompted.tildefriends;
import android.content.Context; import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log;
public class TildeFriendsWebView extends android.webkit.WebView { public class TildeFriendsWebView extends android.webkit.WebView {
boolean overscrolledY = false; boolean overscrolledY = false;

View File

@ -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[]) static int _tf_command_sandbox(const char* file, int argc, char* argv[])
{ {
bool show_usage = false; bool show_usage = false;
int fd = STDIN_FILENO;
while (!show_usage) while (!show_usage)
{ {
static const struct option k_options[] = { static const struct option k_options[] = {
{ "fd", required_argument, NULL, 'f' },
{ "help", no_argument, NULL, 'h' }, { "help", no_argument, NULL, 'h' },
{ 0 }, { 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) if (c == -1)
{ {
break; break;
@ -626,6 +628,10 @@ static int _tf_command_sandbox(const char* file, int argc, char* argv[])
default: default:
show_usage = true; show_usage = true;
break; 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("\nUsage: %s sandbox [options]\n\n", file);
tf_printf("options:\n"); tf_printf("options:\n");
tf_printf(" -h, --help Show this usage information.\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; return EXIT_FAILURE;
} }
@ -641,7 +648,7 @@ static int _tf_command_sandbox(const char* file, int argc, char* argv[])
prctl(PR_SET_PDEATHSIG, SIGHUP); prctl(PR_SET_PDEATHSIG, SIGHUP);
#endif #endif
tf_task_t* task = tf_task_create(); tf_task_t* task = tf_task_create();
tf_task_configure_from_fd(task, STDIN_FILENO); tf_task_configure_from_fd(task, fd);
_shed_privileges(); _shed_privileges();
/* The caller will trigger tf_task_activate with a message. */ /* The caller will trigger tf_task_activate with a message. */
tf_task_run(task); 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); tf_mem_startup(tracking);
g_backtrace_state = backtrace_create_state(argv[0], 0, _backtrace_error, NULL); 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 (
#if !defined(_WIN32) #if !defined(_WIN32)
signal(SIGSYS, _error_handler) == SIG_ERR || signal(SIGSYS, _error_handler) == SIG_ERR || signal(SIGABRT, _error_handler) == SIG_ERR ||
#endif #endif
signal(SIGSEGV, _error_handler) == SIG_ERR) signal(SIGSEGV, _error_handler) == SIG_ERR)
{ {
@ -730,8 +733,28 @@ static void _startup(int argc, char* argv[])
} }
#if defined(__ANDROID__) #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) 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"); tf_printf("This is tf_server_main main.\n");
_startup(0, (char*[]) { NULL }); _startup(0, (char*[]) { NULL });
tf_printf("That was startup.\n"); 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); 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); snprintf(port_file_arg, port_file_arg_length, "out_http_port_file=%s", out_port_file);
const char* args[] = const char* args[] = {
{ "run",
"-z", "-z",
apk, apk,
"-a", "-a",
port_file_arg, port_file_arg,
"-p", "-p",
"0", "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); 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, files_dir, files);
(*env)->ReleaseStringUTFChars(env, apk_path, apk); (*env)->ReleaseStringUTFChars(env, apk_path, apk);
(*env)->ReleaseStringUTFChars(env, out_port_file_path, out_port_file); (*env)->ReleaseStringUTFChars(env, out_port_file_path, out_port_file);
tf_printf("tf_server_main finished with %d.", result); 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; return result;
} }
@ -796,6 +846,7 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
tf_printf("Registering method.\n"); tf_printf("Registering method.\n");
static const JNINativeMethod methods[] = { static const JNINativeMethod methods[] = {
{ "tf_server_main", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I", _tf_server_main }, { "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)); int result = (*env)->RegisterNatives(env, c, methods, (int)_countof(methods));
if (result != JNI_OK) if (result != JNI_OK)

View File

@ -50,6 +50,9 @@
static JSClassID _import_class_id; static JSClassID _import_class_id;
static int _count; 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; extern struct backtrace_state* g_backtrace_state;
typedef struct _export_record_t export_record_t; typedef struct _export_record_t export_record_t;
@ -2160,3 +2163,19 @@ static JSValue _tf_task_pokeSandbox(JSContext* context, JSValueConst this_val, i
#endif #endif
return JS_NewInt32(context, WEXITSTATUS(result)); 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;
}

View File

@ -333,4 +333,35 @@ char* tf_task_get_debug(tf_task_t* task);
*/ */
char* tf_task_get_hitches(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();
/** @} */ /** @} */

View File

@ -154,41 +154,74 @@ static JSValue _taskstub_create(JSContext* context, JSValueConst this_val, int a
} }
else else
{ {
uv_pipe_t* pipe = tf_packetstream_get_pipe(stub->_stream); tf_android_start_service_t* start_service = tf_task_get_android_start_service();
memset(pipe, 0, sizeof(*pipe)); if (start_service)
if (uv_pipe_init(tf_task_get_loop(parent), pipe, 1) != 0)
{ {
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]; uv_pipe_t* pipe = tf_packetstream_get_pipe(stub->_stream);
io[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE | UV_WRITABLE_PIPE; *pipe = (uv_pipe_t) { 0 };
io[0].data.stream = (uv_stream_t*)pipe; if (uv_pipe_init(tf_task_get_loop(parent), pipe, 1) != 0)
io[1].flags = UV_INHERIT_FD; {
io[1].data.fd = STDOUT_FILENO; tf_printf("uv_pipe_init failed\n");
io[2].flags = UV_INHERIT_FD; }
io[2].data.fd = STDERR_FILENO;
uv_process_options_t options = { 0 }; int pipe_result = uv_pipe_open(pipe, fds[0]);
options.args = command_argv; if (pipe_result)
options.exit_cb = _taskstub_on_process_exit; {
options.stdio = io; tf_printf("uv_pipe_open: %s\n", uv_strerror(pipe_result));
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; start_service(fds[1]);
int spawn_result = uv_spawn(tf_task_get_loop(parent), &stub->_process, &options);
if (spawn_result == 0) stub->_process.data = stub;
{
tf_packetstream_set_on_receive(stub->_stream, tf_task_on_receive_packet, stub); tf_packetstream_set_on_receive(stub->_stream, tf_task_on_receive_packet, stub);
tf_packetstream_start(stub->_stream); tf_packetstream_start(stub->_stream);
result = taskObject; result = taskObject;
} }
else else
{ {
tf_printf("uv_spawn failed: %s\n", uv_strerror(spawn_result)); uv_pipe_t* pipe = tf_packetstream_get_pipe(stub->_stream);
JS_FreeValue(context, taskObject); 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); return JS_DupValue(context, result);
@ -443,7 +476,15 @@ JSValue tf_taskstub_kill(tf_taskstub_t* stub)
JSValue result = JS_UNDEFINED; JSValue result = JS_UNDEFINED;
if (!tf_task_get_one_proc(stub->_owner)) 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 else
{ {