diff --git a/README.md b/README.md index 8a8a20746..97043973e 100644 --- a/README.md +++ b/README.md @@ -55,4 +55,4 @@ Joy-Con VR controllers port the arm model of Google's [Google VR SDK for Unity]( Camera hand tracking runs Google's [MediaPipe](https://github.com/google-ai-edge/mediapipe) Tasks Vision library with its Pose Landmarker (Lite) model, BlazePose GHUM 3D, and its Hand Landmarker model (all Apache-2.0), on frames from AndroidX [CameraX](https://developer.android.com/jetpack/androidx/releases/camera) (Apache-2.0); MediaPipe's usage reports are switched off. The design was checked against [@F8LEFT](https://github.com/F8LEFT)'s [JoyConVr](https://github.com/F8LEFT/JoyConVr) camera app (MIT), and the short Joy-Con movements follow the accelerometer mode of the [Google VR SDK for Unity](https://github.com/googlevr/gvr-unity-sdk)'s arm model, Eric Foxlin's zero-velocity updates and Frosio, Pedersini and Borghese's accelerometer autocalibration, and the camera and Joy-Con are joined by a Kalman filter (R. E. Kalman, 1960). Bare camera hands take their gestures after [ALVR](https://github.com/alvr-org/ALVR)'s `hand_gestures.rs` (MIT) and Meta Quest's menu pinch, with distances checked on photos from the [HaGRID](https://github.com/hukenovs/hagrid) gesture set (CC BY-SA 4.0, not included), fit the palm with Horn's quaternion method (1987) and smooth it with the 1€ filter (Casiez, Roussel and Vogel, 2012); the white glove model follows Valve's OpenVR render model reference and Index controller poses, is meshed with surface nets (Gibson, 1998; Mikola Lysenko) and blends and shades it with Inigo Quilez's smooth minimum and distance-field ambient occlusion, porting his polygon distance (MIT); see [docs/third-party/handtracking](docs/third-party/handtracking/README.md). -The Store installs and runs apps with [Flatpak](https://github.com/flatpak/flatpak) (LGPL-2.1). Its sandbox goes through DroidDeck's own bubblewrap shim, which follows how Flatpak hands bwrap its environment. +The Store installs and runs apps with [Flatpak](https://github.com/flatpak/flatpak) (LGPL-2.1). Its sandbox goes through DroidDeck's own bubblewrap shim, which follows how Flatpak hands bwrap its environment. Chromium and Electron apps from Flathub start their browser processes through [zypak](https://github.com/refi64/zypak) by [@refi64](https://github.com/refi64) (BSD-3-Clause); its source was read to see that it always spawns them through Flatpak's spawn portal, which is why DroidDeck starts them directly without Chromium's sandbox instead. diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt b/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt index e7f393fd8..482564678 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/BwrapSpawner.kt @@ -181,6 +181,7 @@ object BwrapSpawner { request.optJSONObject("env")?.let { e -> e.keys().forEach { k -> env[k] = e.getString(k) } } if (binds.any { it.guest == "/dev" }) gpu(rootfs, root, env)?.forEach { addBind(it) } preload(session, rootfs, env).forEach { addBind(it) } + zypakShims(binds, root, dir).forEach { addBind(it) } browserSandboxes(env) return Plan(root, binds, request.optString("cwd", "/").ifEmpty { "/" }, argv, env) } @@ -227,6 +228,20 @@ object BwrapSpawner { private const val PRELOAD_DIR = "/run/droiddeck-preload" + private const val ZYPAK_SHIM = "#!/bin/sh\nprogram=\$1\nshift\nexec \"\$program\" --no-sandbox \"\$@\"\n" + + private fun zypakShims(binds: List, root: File, dir: File): List { + val wrappers = listOf("/app/bin/zypak-wrapper.sh", "/app/bin/zypak-wrapper").filter { guest -> + val host = File(hostPath(binds, root.path, guest)) + host.isFile && !Files.isSymbolicLink(host.toPath()) + } + if (wrappers.isEmpty()) return emptyList() + val shim = File(dir, "zypak-wrapper") + shim.writeText(ZYPAK_SHIM) + chmod(shim, 0x1ed) + return wrappers.map { Bind(shim.path, it) } + } + private fun browserSandboxes(env: MutableMap) { env.putIfAbsent("ZYPAK_ZYGOTE_STRATEGY_SPAWN", "0") env.putIfAbsent("QTWEBENGINE_DISABLE_SANDBOX", "1") diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/BwrapSpawnerTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/BwrapSpawnerTest.kt index e1e6827ab..2cb5e5edd 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/BwrapSpawnerTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/BwrapSpawnerTest.kt @@ -118,4 +118,31 @@ class BwrapSpawnerPlanTest { assertEquals("/app", plan.cwd) assertEquals("org.example.Game", plan.env["FLATPAK_ID"]) } + + @Test fun zypakLaunchesTheBrowserWithoutItsSandbox() { + val context = RuntimeEnvironment.getApplication() + val app = File(runtime, "app").apply { File(this, "bin").mkdirs() } + File(app, "bin/zypak-wrapper.sh").writeText("#!/usr/bin/bash\nexec zypak-helper host - \"\$@\"\n") + Files.createSymbolicLink(File(app, "bin/zypak-wrapper").toPath(), java.nio.file.Paths.get("/app/bin/zypak-wrapper.sh")) + val plan = BwrapSpawner.plan(context, request( + op("t" to "bind", "src" to "/runtime-files/app", "dest" to "/app", "ro" to true), + ), dir) + + val shim = plan.binds.single { it.guest == "/app/bin/zypak-wrapper.sh" } + assertTrue(shim.host.startsWith(dir.path)) + assertTrue(File(shim.host).readText().contains("exec \"\$program\" --no-sandbox \"\$@\"")) + assertFalse(plan.binds.any { it.guest == "/app/bin/zypak-wrapper" }) + assertTrue(File(app, "bin/zypak-wrapper.sh").readText().contains("zypak-helper")) + } + + @Test fun appsWithoutZypakKeepTheirFiles() { + val context = RuntimeEnvironment.getApplication() + File(runtime, "app/bin").mkdirs() + val plan = BwrapSpawner.plan(context, request( + op("t" to "bind", "src" to "/runtime-files/app", "dest" to "/app", "ro" to true), + ), dir) + + assertFalse(plan.binds.any { it.guest.startsWith("/app/bin/") }) + assertFalse(File(dir, "zypak-wrapper").exists()) + } } diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-appimage-run b/tools/linuxfs/overlay/usr/local/bin/droiddeck-appimage-run index 20e6bbba4..b23023b2e 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-appimage-run +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-appimage-run @@ -17,7 +17,7 @@ if [ -s /etc/ld.so.preload ]; then export LD_PRELOAD="${preload%:}${LD_PRELOAD:+:$LD_PRELOAD}" SHARUN_ALLOW_LD_PRELOAD=1 fi flags=() -if [ -e "$APPDIR/chrome-sandbox" ] || compgen -G "$APPDIR/*/chrome-sandbox" >/dev/null; then +if [ -n "$(find -H "$APPDIR" -maxdepth 4 \( -name chrome-sandbox -o -name chrome_100_percent.pak -o -name v8_context_snapshot.bin \) -print -quit 2>/dev/null)" ]; then flags+=(--no-sandbox) if [ -n "${DISPLAY:-}" ]; then export XDG_SESSION_TYPE=x11 diff --git a/tools/linuxfs/preload/tracer.c b/tools/linuxfs/preload/tracer.c index 132af5134..7d8c741c4 100644 --- a/tools/linuxfs/preload/tracer.c +++ b/tools/linuxfs/preload/tracer.c @@ -22,15 +22,17 @@ static const char *skip_digits(const char *s) { return end == s ? NULL : end; } -static int is_status_path(const char *path) { +static long status_pid(const char *path) { const char *rest; - if (path == NULL || strncmp(path, "/proc/", 6) != 0) return 0; + long pid = 0; + if (path == NULL || strncmp(path, "/proc/", 6) != 0) return -1; path += 6; if (strncmp(path, "self/", 5) == 0) rest = path + 4; else if (strncmp(path, "thread-self/", 12) == 0) rest = path + 11; - else if ((rest = skip_digits(path)) == NULL) return 0; - if (strncmp(rest, "/task/", 6) == 0 && (rest = skip_digits(rest + 6)) == NULL) return 0; - return strcmp(rest, "/status") == 0; + else if ((rest = skip_digits(path)) == NULL) return -1; + else pid = strtol(path, NULL, 10); + if (strncmp(rest, "/task/", 6) == 0 && (rest = skip_digits(rest + 6)) == NULL) return -1; + return strcmp(rest, "/status") == 0 ? pid : -1; } static int is_web_helper(const void *caller) { @@ -56,13 +58,26 @@ static int write_all(int fd, const char *data, size_t len) { return 0; } +static int read_only_copy(const char *data, size_t len, int flags) { + char path[] = "/tmp/.droiddeck-status-XXXXXX"; + int rw = mkostemp(path, O_CLOEXEC); + if (rw < 0) return -1; + int ro = -1; + if (write_all(rw, data, len) == 0) + ro = (int)syscall(SYS_openat, AT_FDCWD, path, O_RDONLY | (flags & O_CLOEXEC)); + unlink(path); + close(rw); + return ro; +} + __attribute__((visibility("hidden"))) int bl_status_without_tracer(const char *path, int flags, const void *caller) { char text[STATUS_MAX]; size_t len = 0; int saved = errno; int out = -1; - if ((flags & O_ACCMODE) != O_RDONLY || !is_status_path(path) || is_web_helper(caller)) return -1; + long pid; + if ((flags & O_ACCMODE) != O_RDONLY || (pid = status_pid(path)) < 0 || is_web_helper(caller)) return -1; int in = (int)syscall(SYS_openat, AT_FDCWD, path, O_RDONLY | O_CLOEXEC); if (in < 0) goto done; for (;;) { @@ -78,13 +93,16 @@ __attribute__((visibility("hidden"))) int bl_status_without_tracer(const char *p if (field == NULL) goto done; char *value = field + strlen(TRACER_FIELD); char *line_end = strchr(value, '\n'); - if (line_end == NULL) goto done; + if (line_end == NULL || line_end == value) goto done; + + memmove(value + 1, line_end, len - (size_t)(line_end - text) + 1); + *value = '0'; + len = (size_t)(value + 1 - text) + strlen(value + 1); + if (pid != 0 && pid != getpid() && (out = read_only_copy(text, len, flags)) >= 0) goto done; out = memfd_create("status", (flags & O_CLOEXEC) ? MFD_CLOEXEC : 0); if (out < 0) goto done; - if (write_all(out, text, (size_t)(value - text)) != 0 || write_all(out, "0", 1) != 0 - || write_all(out, line_end, len - (size_t)(line_end - text)) != 0 - || lseek(out, 0, SEEK_SET) != 0) { + if (write_all(out, text, len) != 0 || lseek(out, 0, SEEK_SET) != 0) { close(out); out = -1; }