Files
MobileGL/tools/spikes/server_stub/main.cpp
T
swung0x48 8a239177ac [Feat] (Build, TraceApp): ship and exec a second native binary on android
- PLAN-B.md §11 P0 lists spike A (the Android delivery chain) as a P0 deliverable,
  inherited verbatim from PLAN.md §15 P0; §8.1 inherits PLAN.md §11.1-§11.6, whose
  Android path needs a second process. Android gives an application no writable
  exec-able directory, so the only supported route is to name the binary lib*.so, let
  the packager put it in lib/<abi>/, and exec it out of
  getApplicationInfo().nativeLibraryDir. This builds that route end to end so the
  spike can be answered with evidence instead of folklore.
- New root option MOBILEGL_BUILD_SERVER_SPIKE (OFF, ANDROID-only) adds the
  MobileGLServer target from tools/spikes/server_stub/main.cpp with PREFIX "lib" /
  SUFFIX ".so" and -fPIE/-pie: an .so name does not exempt the file from Android's
  PIE requirement. Its RUNTIME_OUTPUT_DIRECTORY is pointed at
  CMAKE_LIBRARY_OUTPUT_DIRECTORY, because AGP packages what lands in the per-ABI
  library output directory and CMake would otherwise put an executable elsewhere.
- The option is opt-in on both sides. The plugin flavour cannot turn it on at all,
  and the trace flavour builds it only when asked, with
  `-Pmobilegl.buildServerSpike=ON` or MOBILEGL_BUILD_SERVER_SPIKE=ON in the
  environment; a flavour that silently carries an executable nothing loads is the
  kind of thing nobody notices until it ships. Verified both ways:
  assembleTraceDebug -Pmobilegl.buildServerSpike=ON packages
  lib/arm64-v8a/libMobileGLServer.so and `file` reports "ELF 64-bit LSB pie
  executable, ARM aarch64 ... interpreter /system/bin/linker64, for Android 26";
  the same task with no property packages only libMobileGL.so and
  libtrace_replay_runner.so.
- The stub prints one line to stdout and writes the same line to the file named by
  argv[1], then exits 0. The line carries pid/ppid/uid/gid and, decisively, the
  child's own /proc/self/attr/current: only `u:r:untrusted_app:...` proves an
  ordinary app process did the exec. An `adb run-as` shell runs in a different
  SELinux domain, so a success there would prove nothing.
- RunSpawnSpike() starts the stub with argv [serverPath, markerPath], redirects the
  child's stdout/stderr into a captured file (an app process has stdout on
  /dev/null, so a printed line would otherwise vanish), waits for it, and reports
  exit status, signal, the exec errno, the parent's own SELinux context, the marker
  content and the captured stdout - to logcat, to the returned string, and to a
  <marker>.report file, because the Activity finishes immediately afterwards.
- The child reports the errno of a REFUSED execve through a close-on-exec pipe.
  Without it the one datum the spike exists to produce is lost: the parent only ever
  sees a wait status, in which every reason has already been flattened into one exit
  code, and EACCES (SELinux, or a noexec mount) versus ENOEXEC (a packager that
  mangled the file) are opposite verdicts for the design. A successful exec closes
  the write end for free, so the parent reads EOF and reports execErrno=0.
- fork/execve only. The earlier draft also carried a posix_spawn arm behind
  `__ANDROID_API__ >= 28`, which was dead code in every configuration this repo can
  build - bionic declares posix_spawn from API 28 and the root CMakeLists.txt pins
  MOBILEGL_ANDROID_API_LEVEL to 26 and refuses to configure lower - and would have
  silently become the production path, untested, on a minSdk bump. Keeping the arm
  that actually ships means the spike measures the code the server would really use.
  Nothing happens between fork and execve except open/dup2/execve/write/_exit, all
  async-signal-safe, because the parent is a multi-threaded JVM process.
- The spike lives in its own TU, spawn_spike.cpp/.hpp, listed only by the trace
  APK's CMakeLists. Its sibling trace_replay_core.cpp is compiled verbatim by the
  DESKTOP mobilegl_trace_replay runner (tools/trace_replay/CMakeLists.txt names the
  same file), where <android/log.h> does not exist, so nothing Android-only may live
  there; spawn_spike.cpp carries an #error for anyone who adds it to that list.
- The Activity runs the spike, and nothing else, when launched with the
  `mobilegl_spike_spawn` intent extra; that mode needs no trace, no golden and no
  render surface. It is a separate JNI entry point rather than another parameter on
  the 30-argument replay call, which it shares nothing with.
- Not yet run on a device: both device locks are held by another campaign. The
  on-device verdict is the coordinator's step.
2026-09-05 20:49:57 -04:00

90 lines
3.5 KiB
C++

// P0 spike A - the Android delivery chain for a second native executable.
//
// The disaggregated design (PLAN-B.md §8.1, inheriting PLAN.md §11.1-§11.6) needs a
// second process on Android. Android has no exec-able install location an application
// can write to, so the only supported way to ship an executable inside an APK is to
// name it lib*.so, let the packager put it in lib/<abi>/ and exec it out of
// getApplicationInfo().nativeLibraryDir. Whether that actually works from the app's own
// untrusted_app SELinux domain - as opposed to from an adb `run-as` shell, which runs in
// a different domain and proves nothing - is the question this spike answers.
//
// This binary is deliberately the smallest thing that can answer it: it prints one line
// describing the process it ended up being (pid, uid, and its own SELinux context) to
// stdout and writes the same line to the file named by argv[1], then exits 0. The parent
// reads both back; see RunSpawnSpike() in
// android-plugin/app/src/trace/cpp/trace_replay_core.cpp.
//
// It is built only when MOBILEGL_BUILD_SERVER_SPIKE=ON on an ANDROID configure, so it is
// absent from every shipping build. It is not the future server, and nothing links it.
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <unistd.h>
namespace {
// The single fact that makes this spike conclusive rather than suggestive: the exec'd
// child reports the domain it is running in. `u:r:untrusted_app:s0:...` means an ordinary
// application process really did exec this file; anything else (shell, adb, a platform
// domain) means the test was run the wrong way and its verdict does not transfer.
void ReadSelinuxContext(char* out, size_t size) {
out[0] = '\0';
FILE* file = std::fopen("/proc/self/attr/current", "r");
if (file == nullptr) {
std::snprintf(out, size, "<unreadable>");
return;
}
const size_t read = std::fread(out, 1, size - 1, file);
std::fclose(file);
out[read] = '\0';
// The kernel returns the context NUL-terminated inside the read; trim anything after.
for (size_t index = 0; index < read; ++index) {
if (out[index] == '\n' || out[index] == '\0') {
out[index] = '\0';
break;
}
}
if (out[0] == '\0') {
std::snprintf(out, size, "<empty>");
}
}
} // namespace
int main(int argc, char** argv) {
char context[256];
ReadSelinuxContext(context, sizeof(context));
char line[1024];
std::snprintf(line, sizeof(line),
"MobileGLServer-spike ok argv0=%s pid=%d ppid=%d uid=%d gid=%d argc=%d "
"selinux=%s\n",
argc > 0 && argv[0] != nullptr ? argv[0] : "<null>",
static_cast<int>(getpid()), static_cast<int>(getppid()),
static_cast<int>(getuid()), static_cast<int>(getgid()), argc, context);
std::fputs(line, stdout);
std::fflush(stdout);
if (argc < 2 || argv[1] == nullptr || argv[1][0] == '\0') {
std::fputs("MobileGLServer-spike: argv[1] (marker path) missing\n", stderr);
return 2;
}
FILE* marker = std::fopen(argv[1], "w");
if (marker == nullptr) {
std::fprintf(stderr, "MobileGLServer-spike: cannot open marker %s: %s\n", argv[1],
std::strerror(errno));
return 3;
}
std::fputs(line, marker);
if (std::fclose(marker) != 0) {
std::fprintf(stderr, "MobileGLServer-spike: cannot write marker %s: %s\n", argv[1],
std::strerror(errno));
return 4;
}
return 0;
}