Merge branch 'main' into fix/gamespace-launcher

This commit is contained in:
MaxsTechReview authored and GitHub committed 2026-10-22 16:41:22 -04:00
commit 727dcd45d9
30 files changed
+692 -38

No files matched your search

+1 -1
View File
@@ -47,7 +47,7 @@ Compatibility and performance vary by device; hardware validation is limited. De
## Credits and licence
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. The registry entries a GOG game's install script (`goggame-<id>.script`) sets are read the way [Heroic Games Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher)'s GOG setup (GPL-3.0) reads them. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence, which also starts the arm64 emulator AppImages whose own runtime cannot run its extractor on Android. The Steam client's audio goes through [@The412Banner](https://github.com/The412Banner)'s [DirectAudio](https://github.com/The412Banner/directaudio) PulseAudio sink (LGPL-2.1-or-later), built from `tools/directaudio/sink` with its request size kept to its ring so clients are not asked to queue 100 ms ahead. mmap ring-buffer controller input was developed by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)). Why the x86-64 Steam client refused the arm64 client's sign-in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest). x86 programs that link OpenSSL 3 under FEX use the copy in the [Steam Linux Runtime 4.0](https://gitlab.steamos.cloud/steamrt/steamrt) platform. Hooks written with `WriteProcessMemory` into code [FEX](https://github.com/FEX-Emu/FEX) has translated go through `/proc/<pid>/mem` again when the kernel refuses `process_vm_writev`, as [Wine](https://gitlab.winehq.org/wine/wine)'s `server/ptrace.c` wrote them before Wine 11. x86 Linux games under FEX get the same `/etc/asound.conf` that [pressure-vessel](https://gitlab.steamos.cloud/steamrt/steam-runtime-tools) writes into the Steam Linux Runtime, so ALSA plays through PulseAudio. Games render with [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's own per-render-pass choice between tile memory and system memory, while the Steam client keeps `TU_DEBUG=sysmem`; [FEX](https://github.com/FEX-Emu/FEX)'s code disk cache was checked in its source and is already on through Proton. See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. The registry entries a GOG game's install script (`goggame-<id>.script`) sets are read the way [Heroic Games Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher)'s GOG setup (GPL-3.0) reads them. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence, which also starts the arm64 emulator AppImages whose own runtime cannot run its extractor on Android. The Steam client's audio goes through [@The412Banner](https://github.com/The412Banner)'s [DirectAudio](https://github.com/The412Banner/directaudio) PulseAudio sink (LGPL-2.1-or-later), built from `tools/directaudio/sink` with its request size kept to its ring so clients are not asked to queue 100 ms ahead. mmap ring-buffer controller input was developed by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)). Why the x86-64 Steam client refused the arm64 client's sign-in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest). x86 programs that link OpenSSL 3 under FEX use the copy in the [Steam Linux Runtime 4.0](https://gitlab.steamos.cloud/steamrt/steamrt) platform. Hooks written with `WriteProcessMemory` into code [FEX](https://github.com/FEX-Emu/FEX) has translated go through `/proc/<pid>/mem` again when the kernel refuses `process_vm_writev`, as [Wine](https://gitlab.winehq.org/wine/wine)'s `server/ptrace.c` wrote them before Wine 11. x86 Linux games under FEX get the same `/etc/asound.conf` that [pressure-vessel](https://gitlab.steamos.cloud/steamrt/steam-runtime-tools) writes into the Steam Linux Runtime, so ALSA plays through PulseAudio. Games render with [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's own per-render-pass choice between tile memory and system memory, while the Steam client keeps `TU_DEBUG=sysmem`; [FEX](https://github.com/FEX-Emu/FEX)'s code disk cache was checked in its source and is already on through Proton. The proot fast path picks the guest name of a host file bound under several names in the order [PRoot](https://github.com/proot-me/proot)'s `path/binding.c` (GPL-2.0-or-later) sorts its bindings, so links and `/proc/self/fd` read back as they do through proot. The session's [PipeWire](https://gitlab.freedesktop.org/pipewire/pipewire) (MIT) opens its sockets with the per-socket access settings documented in its `module-access.c`, and [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber) (MIT) keeps those clients at full permissions through its `access.rules`. Wine write watches use the userfaultfd support in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/ntdll/unix/virtual.c` (LGPL-2.1-or-later): libblsession gives it the arm64 syscall number and answers its `PAGEMAP_SCAN` from `/proc/self/pagemap` where Android refuses the ioctl. See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
XR glasses support builds on WinNative's VITURE glasses work. VITURE protocol details were checked against [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver) by [@wheaney](https://github.com/wheaney), including the VITURE protocol header it ships, [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) by [@badicsalex](https://github.com/badicsalex) and the [go-macos/iokit VITURE package](https://pkg.go.dev/github.com/go-macos/iokit/viture); the optional VITURE SDK belongs to VITURE and is not included. RayNeo support adapts [@informalTechCode](https://github.com/informalTechCode)'s [RayNeo-Air-Series-3DoF-Scaffold](https://github.com/informalTechCode/RayNeo-Air-Series-3DoF-Scaffold) (Apache-2.0) and references [@verncat](https://github.com/verncat)'s [RayNeo-Air-3S-Pro-OpenVR](https://github.com/verncat/RayNeo-Air-3S-Pro-OpenVR) (MIT) and [Monado](https://gitlab.freedesktop.org/monado/monado) (BSL-1.0); see [docs/third-party/rayneo](docs/third-party/rayneo/README.md). XREAL 1S support references [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver) and [xrealOneDeviceKit](https://github.com/wheaney/xrealOneDeviceKit) by @wheaney, [xreal_one_driver](https://github.com/rohitsangwan01/xreal_one_driver) by [@rohitsangwan01](https://github.com/rohitsangwan01) (MIT) and [xr-tools](https://github.com/0xcaff/xr-tools) by [@0xcaff](https://github.com/0xcaff). Rokid support follows the Rokid protocol notes in [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) and @badicsalex's [Void Computing write-ups](https://voidcomputing.hu/blog/worse-better-prettier/), and [Monado](https://gitlab.freedesktop.org/monado/monado)'s Rokid driver. The REDMAGIC glasses-display findings draw on [@mekhontsev](https://github.com/mekhontsev)'s [MagicDesk investigation](https://github.com/mekhontsev/magicdesk/blob/main/docs/xr-resolution-fix.md) and [@Coding-BR](https://github.com/Coding-BR)'s [decompiled Nubia kernel](https://github.com/Coding-BR/android_kernel_nubia_sm8850_qwjujube). VITURE, RayNeo, XREAL, Rokid and REDMAGIC are trademarks of their owners; this project is not affiliated with them.
+2 -2
View File
@@ -122,8 +122,8 @@ android {
applicationId "com.droiddeck.launcher"
minSdkVersion 26
targetSdkVersion 28
versionCode 14
versionName "0.5.0"
versionCode 15
versionName "0.6.0"
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\""
buildConfigField "int", "PR_NUMBER", "${prNumber}"
@@ -24,6 +24,11 @@ object StoreLogFiles {
Thread({ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); r.run() }, "store-log-flush").apply { isDaemon = true }
}
private val CLOCK = SimpleDateFormat("HH:mm:ss.SSS", Locale.US)
/** Appends [text] with a time stamp. Works in any of the app's processes. */
fun stamped(context: Context, text: String) = append(context, "${synchronized(CLOCK) { CLOCK.format(Date()) }} $text")
fun append(context: Context, line: String) {
val app = context.applicationContext
pending.add(line)
@@ -10,9 +10,6 @@ import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.droiddeck.launcher.stores.download.DownloadEntry
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
object StoresState {
private const val TAG = "Stores"
@@ -262,7 +259,7 @@ object StoresState {
val text = StoreLog.redactLine(raw)
Log.i(TAG, text)
val app = appContext ?: return
StoreLogFiles.append(app, "${synchronized(CLOCK) { CLOCK.format(Date()) }} $text")
StoreLogFiles.stamped(app, text)
}
private var appContext: Context? = null
@@ -295,8 +292,6 @@ object StoresState {
}
}, name).start()
}
private val CLOCK = SimpleDateFormat("HH:mm:ss.SSS", Locale.US)
}
interface StoreBackend {
@@ -29,10 +29,16 @@ internal class XboxCredentials(private val app: Context, expectedGeneration: Str
}
private fun rejected(reason: String): Boolean {
android.util.Log.w("XboxCredentials", "Rejected credential peer: $reason")
note("Rejected credential peer: $reason")
return false
}
/** Also written to the stores log: sign-in runs in its own process, where only that file reaches a shared log. */
private fun note(text: String) {
android.util.Log.w("XboxCredentials", text)
com.droiddeck.launcher.stores.StoreLogFiles.stamped(app, "Xbox credentials: $text")
}
private fun authorized(pid: Int): Boolean {
if (!bound.await(5, java.util.concurrent.TimeUnit.SECONDS) || closed) return rejected("unbound")
val process = backend ?: return rejected("missing-child")
@@ -88,7 +94,7 @@ internal class XboxCredentials(private val app: Context, expectedGeneration: Str
answer(request)
}
}.getOrElse {
android.util.Log.w("XboxCredentials", "Credential request rejected: ${it.javaClass.simpleName}")
note("Credential request rejected: ${it.javaClass.simpleName}")
JSONObject().put("ok", false)
}
runCatching { it.outputStream.write((reply.toString() + "\n").toByteArray()) }
@@ -63,7 +63,8 @@ class XboxLoginActivity : ComponentActivity() {
Xodus.start(applicationContext, listOf("--external-webview-stdio", "login"), credentialGeneration = accountGeneration).use { process ->
child = process
if (closed) return@use
Xodus.diagnostics(process)
// StoresState.logLine writes no file in this process, so the reason would miss the shared log.
Xodus.diagnostics(process) { logStep(it) }
progress("waiting for the Xbox helper", "Waiting for the Xbox helper…")
val timer = Thread({
runCatching { if (!process.process.waitFor(10, TimeUnit.MINUTES)) process.process.destroy() }
@@ -153,8 +154,7 @@ class XboxLoginActivity : ComponentActivity() {
private fun logStep(line: String) {
val text = StoreLog.redactLine("Xbox sign-in: $line")
Log.i(TAG, text)
val time = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", java.util.Locale.US).format(java.util.Date())
StoreLogFiles.append(applicationContext, "$time $text")
StoreLogFiles.stamped(applicationContext, text)
}
@SuppressLint("SetJavaScriptEnabled")
@@ -133,11 +133,12 @@ internal object Xodus {
}
}
fun diagnostics(child: Child): Thread = Thread({
/** Passes the backend's known stderr diagnostics to [report]; other stderr text never leaves the process. */
fun diagnostics(child: Child, report: (String) -> Unit = { com.droiddeck.launcher.stores.StoresState.logLine("Xbox: $it") }): Thread = Thread({
runCatching {
child.process.errorStream.bufferedReader().useLines { lines -> lines.forEach { line ->
if (line.matches(Regex("NEXUS_CREDENTIAL_ERROR_V1 [a-z-]{1,48}|NEXUS_LIBRARY_DIAGNOSTIC_V1 [a-z-]{1,48} [0-9]{1,3} [0-9]{1,6}|NEXUS_SESSION_DIAGNOSTIC_V1 [a-z-]{1,64}|Account library unavailable \\([a-z-]{1,64}\\); credentials and installed games are unchanged\\.|Account library timed out; installed games are unchanged\\.|Xbox profile is temporarily unavailable \\([a-z-]{1,48}\\); stored account is unchanged\\.|Xbox profile request timed out; stored account is unchanged\\.")))
com.droiddeck.launcher.stores.StoresState.logLine("Xbox: $line")
report(line)
} }
}
}, "xbox-diagnostics").apply { start() }
+172
View File
@@ -0,0 +1,172 @@
# DroidDeck 0.6.0
<p align="center"><a href="https://discord.gg/JRGAvawjsm"><img src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white" alt="Join the DroidDeck Discord"></a></p>
<!-- Maintainer check: Steam, Proton downloads and the games themselves come from Valve and the stores and are not part of DroidDeck; the intro below only claims DroidDeck's own code and the packs it builds. -->
> **DroidDeck is 100% open source. No ads. No telemetry. No source withheld.**
>
> DroidDeck is a community project in the truest sense. Because all of its code is public, the people who use it can read it, fix it and improve it, and in 0.6.0 you all sure did:
>
> **153 pull requests · 23 contributors · 10 first-timers · 73% of the pull requests came from the community.**
>
> Thank you to everyone who sent code, translations, device testing and bug reports. Here’s everything we did together for this release:
### Features
<!-- GIF: stores-capsules -->
<img src="media/0.6.0/stores-capsules.gif" width="50%" alt="The Epic library switching from cards to the capsule view, the Xbox library, an itch.io store search, then an installed game's page with its Play button. Account names are blurred">
- Added experimental **Xbox and PC Game Pass** as a store. Sign in with your Microsoft account to see your owned and Game Pass games, then download them with pause and resume, update, verify and manage add-on content. Games launch through Proton or FEX from the Games tab or straight from Steam. Search results open with their artwork and details, and Game Pass titles you have not installed yet can be installed from their page. (@xXJSONDeruloXx, @moronigranja, [#1156](https://github.com/Droid-Deck/DroidDeck/pull/1156), [#1164](https://github.com/Droid-Deck/DroidDeck/pull/1164), [#1170](https://github.com/Droid-Deck/DroidDeck/pull/1170), [#1193](https://github.com/Droid-Deck/DroidDeck/pull/1193), [#1279](https://github.com/Droid-Deck/DroidDeck/pull/1279))
- Added an **itch.io** store. Sign in with your itch.io API key to see your library, then search the store or browse the newest, on-sale, free and top-rated shelves. DRM-free Windows builds install and launch through Steam like games from the other stores. (@kingokksa, [#1064](https://github.com/Droid-Deck/DroidDeck/pull/1064))
- **Redesigned store pages.** Each store has **Library** and **Store** tabs, and the library lists installed games first. A new **capsule view** shows portrait DVD-case art in every store. LB and RB switch between stores, and installed games get a **Play** button on their page. You can reorder and hide store tabs in the stores settings, and stores you are not signed in to are dimmed. (@xXJSONDeruloXx, @kingokksa, [#1116](https://github.com/Droid-Deck/DroidDeck/pull/1116), [#1117](https://github.com/Droid-Deck/DroidDeck/pull/1117), [#1121](https://github.com/Droid-Deck/DroidDeck/pull/1121), [#1125](https://github.com/Droid-Deck/DroidDeck/pull/1125), [#1236](https://github.com/Droid-Deck/DroidDeck/pull/1236))
<!-- GIF: touch-gestures -->
<img src="media/0.6.0/touch-gestures.gif" width="50%" alt="Setting Touch to Gestures, going through the one-, two- and three-finger tabs and the searchable action picker, turning on Show recognized gesture, then two-finger drags moving through the Steam library and a pinch zoom, each gesture named at the top">
- Added configurable **touchscreen gestures**: taps, double taps, holds, one-, two- and three-finger drags and pinch zoom. Each one can be mapped to an action from a searchable list, with defaults and per-game overrides. (@Nightwalker743, @kingokksa, [#940](https://github.com/Droid-Deck/DroidDeck/pull/940), [#1214](https://github.com/Droid-Deck/DroidDeck/pull/1214))
- **Steam Remote Play from DroidDeck** now streams video as well as audio and input. The picture is captured through PipeWire, as on SteamOS, and encoded by the phone's hardware encoder. On a Thor that takes 7.3–8.0 ms per frame, down from 50–90 ms with software encoding. Touch on the client can tap and drag, and games are heard on the stream. (@xXJSONDeruloXx, [#1139](https://github.com/Droid-Deck/DroidDeck/pull/1139))
<!-- Chart: remote-play-encode -->
<img src="media/0.6.0/remote-play-encode.png" width="80%" alt="Remote Play encode time per frame on an AYN Thor: 50 to 90 ms with software x264, 7.3 to 8.0 ms with the hardware encoder.">
<!-- GIF: boot-movies -->
<img src="media/0.6.0/boot-movies.gif" width="50%" alt="Opening SteamDeckRepo, filtering to Steam Deck and Most liked, installing Handheld History by KnightMD and pressing Use, then starting Steam with the new boot movie. Waits are cut">
- **Boot movies from SteamDeckRepo.** Search SteamDeckRepo, filter by device and browse the Trending, Most downloaded, Most liked and Newest tabs, then install a movie and use it. Installed movies are listed with covers. Imported movies go into Steam's own movie folder, and the sound has volume and length sliders. (@FX192, [#1036](https://github.com/Droid-Deck/DroidDeck/pull/1036), [#1229](https://github.com/Droid-Deck/DroidDeck/pull/1229))
<!-- GIF: performance-hud -->
<img src="media/0.6.0/performance-hud.gif" width="50%" alt="In Valheim's intro, turning on the battery temperature and the frame-time graph, picking the Amber color and raising the opacity, then a close-up of the HUD over the game">
- The **performance HUD** can show a colored frame-time graph and the battery temperature. Each part can be shown or hidden, and its colors and opacity can be changed. (@0x1Nawaf, [#1256](https://github.com/Droid-Deck/DroidDeck/pull/1256))
<!-- GIF: gradient-theme -->
<img src="media/0.6.0/gradient-theme.gif" width="50%" alt="Choosing the Gradient theme, dragging the primary color around the wheel, typing a hex code for the secondary color, then the Games and Steam tabs in the new colors">
- Added a custom **Gradient theme**, with primary and secondary colors chosen from a color wheel, sliders or a hex code, and a **light app icon** option. (@FX192, @xXJSONDeruloXx, [#1309](https://github.com/Droid-Deck/DroidDeck/pull/1309), [#1104](https://github.com/Droid-Deck/DroidDeck/pull/1104))
- **SteamVR controller types.** Without VR controllers, the gamepad can act as Valve Index controllers (the default), Meta Quest 3 Touch Plus, HTC Vive wands, or a regular gamepad. The sticks move you in the game by default, and pressing both sticks switches them to moving the hands. The type can be changed while SteamVR runs. With glasses, the phone becomes a control panel with Recenter, the SteamVR menu, the keyboard, brightness and battery. Flat games started from VR open as a SteamVR panel. (@xXJSONDeruloXx, [#1268](https://github.com/Droid-Deck/DroidDeck/pull/1268))
- Added **Rokid Max and Air** glasses, with 3DoF head tracking, SBS modes, 120 Hz on the Max and brightness control, and experimental head tracking for **VITURE Pro 2** without the vendor SDK. (@xXJSONDeruloXx, @informalTechCode, @kingokksa, [#1266](https://github.com/Droid-Deck/DroidDeck/pull/1266), [#1115](https://github.com/Droid-Deck/DroidDeck/pull/1115))
- **Easy Anti-Cheat games with an offline mode** start without the anti-cheat bootstrapper. This is on by default and can be turned off per game. Online modes that need Easy Anti-Cheat stay unavailable. (@xXJSONDeruloXx, [#1039](https://github.com/Droid-Deck/DroidDeck/pull/1039))
- The **built-in controller can be turned off** (Steam settings > Controller > None), so a connected controller becomes player 1 for games that only read the first pad. (@kingokksa, [#1207](https://github.com/Droid-Deck/DroidDeck/pull/1207), [#1304](https://github.com/Droid-Deck/DroidDeck/pull/1304))
- On foldables, the **unfolded controls can stay while a controller is connected**, for players who use a telescopic controller and still want the trackpads and back buttons. (@ovurrsl, [#996](https://github.com/Droid-Deck/DroidDeck/pull/996))
- The component manager offers **nightly DXVK, VKD3D-Proton and FEX builds** from DroidDeck-Components, with a **Stable | Nightly** switch on each row. (@The412Banner, [#1284](https://github.com/Droid-Deck/DroidDeck/pull/1284))
- When a game started from the Games tab closes, DroidDeck **returns you to where you launched it** instead of leaving you in Big Picture. (@The412Banner, [#395](https://github.com/Droid-Deck/DroidDeck/pull/395))
- **Decky Loader** installs from the upstream SteamDeckHomebrew release, has a Stable or Prerelease update channel, and shows Decky's own third-party warning before a plugin ZIP is installed. (@xXJSONDeruloXx, [#1090](https://github.com/Droid-Deck/DroidDeck/pull/1090), [#1263](https://github.com/Droid-Deck/DroidDeck/pull/1263), [#1264](https://github.com/Droid-Deck/DroidDeck/pull/1264))
- **Desktop mode can be uninstalled** from beside Open desktop, including a desktop install that was interrupted. Games, Steam, added apps and settings are kept. (@xXJSONDeruloXx, [#1311](https://github.com/Droid-Deck/DroidDeck/pull/1311))
- **GOG and Epic installs can use your language.** The install dialog has a **Game language** choice, which follows the app's language by default, and the stores settings can pin a default. GOG games no longer download only their English files. (@LooLL, [#1199](https://github.com/Droid-Deck/DroidDeck/pull/1199))
- Games can be **uninstalled or removed from their page in the Games tab**. Store games use their store's uninstaller. Steam games delete their install but keep their saves and Proton prefix. Added games only lose their shortcut, so their files stay. (@kingokksa, [#1190](https://github.com/Droid-Deck/DroidDeck/pull/1190))
### Improvements
- **Performance**
- **Faster games and session starts on Adreno.** Games now use Turnip's tile rendering while the Steam interface keeps system memory. In PEAK on an Adreno 840 tablet, the frame rate rose 11.7% and frames slower than 25 ms dropped by 26%. Steam is ready 35.9% sooner after a session starts (5.57 s to 3.57 s). (@maxjivi05, [#1188](https://github.com/Droid-Deck/DroidDeck/pull/1188))
<!-- Chart: adreno-peak-and-steam-start -->
<img src="media/0.6.0/adreno-peak-and-steam-start.png" width="80%" alt="PEAK on an Adreno 840 tablet: 46.8 fps before and 52.3 fps in 0.6.0 (+11.7%). Frames shown for longer than 25 ms fall from 12.90% to 9.53%. Steam is ready 5.57 s after session start before and 3.57 s in 0.6.0 (-35.9%).">
- **Faster game launches.** Steam's install-script step is skipped once it has finished for a game. In testing, Moonlighter went from 10.5 s to 7.7 s from Play to the game window. FEX's file lookups skip proot, so Graveyard Keeper reaches its first scene 0.57 s sooner with 48% fewer proot stops. (@maxjivi05, [#1273](https://github.com/Droid-Deck/DroidDeck/pull/1273), [#1283](https://github.com/Droid-Deck/DroidDeck/pull/1283))
- **Games on an SD card library load far faster.** proot's fast path now names files and follows links inside a library the same way proot does, instead of handing every lookup back to proot. In testing, Red Dead Redemption 2's Story mode went from still loading after 16 minutes to the opening scene in about 2 minutes 10 seconds. (@maxjivi05, [#1331](https://github.com/Droid-Deck/DroidDeck/pull/1331))
- **Less overhead from the performance overlay.** gamescope's CPU use dropped from about 12.9% to 7.7% of a core with the overlay hidden, and a 60 fps game on a 120 Hz panel no longer gets extra composites while the overlay is shown. Wine's network queries also cost less. (@maxjivi05, [#1272](https://github.com/Droid-Deck/DroidDeck/pull/1272), [#1274](https://github.com/Droid-Deck/DroidDeck/pull/1274), [#1275](https://github.com/Droid-Deck/DroidDeck/pull/1275))
<!-- Chart: speed-launch-load-overlay -->
<img src="media/0.6.0/speed-launch-load-overlay.png" width="80%" alt="Moonlighter from Play to game window: 10.5 s before, 7.7 s in 0.6.0. Graveyard Keeper to its first scene: 9.70 s before, 9.13 s in 0.6.0. Red Dead Redemption 2 Story mode from an SD card library: still loading after 16 minutes before (a lower bound), about 2 minutes 10 seconds in 0.6.0. gamescope CPU with Moonlighter at 120 fps: 12.85% of a core before and 7.65% in 0.6.0 with the overlay hidden, 19.05% and 10.35% with it visible.">
- The **Steam client responds sooner**: it paces itself to the panel's real refresh rate, and its interface sounds no longer queue about 112 ms of audio. The DirectAudio sink is now built from @The412Banner's [DirectAudio](https://github.com/The412Banner/directaudio) source. (@maxjivi05, [#1094](https://github.com/Droid-Deck/DroidDeck/pull/1094))
<!-- Chart: steam-client-responsiveness -->
<img src="media/0.6.0/steam-client-responsiveness.png" width="80%" alt="Steam client on a REDMAGIC Astra: per-run median press-to-response 41.0 to 41.2 ms before and 28.5 to 33.0 ms in 0.6.0. The interface frame interval falls from 10.8 ms (about 92 fps) to 8.3 ms (120 fps), and the interface sound queue from 105 to 120 ms to 28 to 45 ms.">
- The **GOG tab opens in seconds**, because the whole library comes from one listing instead of a request per game. (@kingokksa, [#786](https://github.com/Droid-Deck/DroidDeck/pull/786))
- **Reduced the APK size** from 124.4 MB to 66.4 MB. (@xXJSONDeruloXx, [#1198](https://github.com/Droid-Deck/DroidDeck/pull/1198), [#1204](https://github.com/Droid-Deck/DroidDeck/pull/1204), [#1265](https://github.com/Droid-Deck/DroidDeck/pull/1265))
- **Better battery life while a session is suspended.** The device can now really sleep: every guest thread stops and the audio stream is stopped. Before, a Thor did not suspend once in 13 minutes. Steam downloads keep running with the Manual policy, including downloads to SD-card libraries, and a switched-off Steam Controller no longer wakes Bluetooth all night. (@xXJSONDeruloXx, @Detinator10, [#1220](https://github.com/Droid-Deck/DroidDeck/pull/1220), [#1262](https://github.com/Droid-Deck/DroidDeck/pull/1262), [#1312](https://github.com/Droid-Deck/DroidDeck/pull/1312))
- **zRAM is always on** where supported, and a session is paged out to zRAM before it is suspended, so Android is less likely to close it overnight. In testing, Bulletstorm's resident memory went from 1,886 MB to 318 MB while suspended, with the rest moved to zRAM. The zRAM setting has been removed. (@xXJSONDeruloXx, [#1317](https://github.com/Droid-Deck/DroidDeck/pull/1317))
<!-- Chart: suspend-sleep-and-memory -->
<img src="media/0.6.0/suspend-sleep-and-memory.png" width="80%" alt="On an AYN Thor, guest threads still running while suspended: Steam 59 of 72 and gamescope 8 of 15 before, none in 0.6.0. Bulletstorm's resident memory while suspended falls from 1,886 MB to 318 MB, with 1,570 MB moved to zRAM.">
- Sessions on an **external display or DeX** keep running while the phone's screen is off. (@maxjivi05, @xXJSONDeruloXx, [#1211](https://github.com/Droid-Deck/DroidDeck/pull/1211), [#1320](https://github.com/Droid-Deck/DroidDeck/pull/1320))
- **Game cores** now apply to games started with **Linux x86 (FEX)** too. (@coffeemeist3r, [#1294](https://github.com/Droid-Deck/DroidDeck/pull/1294))
- **GPU drivers:** Auto's picks come from a manifest in Drivers-CI, so a default can change without an app update. The Snapdragon SM8975 is recognized and Adreno 850 support is marked experimental. A trial driver that crashes at startup now rolls back automatically. (@xXJSONDeruloXx, @Danbro007, @ovurrsl, [#977](https://github.com/Droid-Deck/DroidDeck/pull/977), [#1249](https://github.com/Droid-Deck/DroidDeck/pull/1249), [#1031](https://github.com/Droid-Deck/DroidDeck/pull/1031))
- The KDE desktop is updated to **KWin 6.8.0**. (@xXJSONDeruloXx, [#1328](https://github.com/Droid-Deck/DroidDeck/pull/1328), [#1330](https://github.com/Droid-Deck/DroidDeck/pull/1330))
- New sessions default to **landscape**, and the Deck controls open on **both trackpads**. (@xXJSONDeruloXx, [#1287](https://github.com/Droid-Deck/DroidDeck/pull/1287), [#1295](https://github.com/Droid-Deck/DroidDeck/pull/1295))
- **Accessibility:** buttons, tiles and list rows are announced as buttons by screen readers, the Electric theme's buttons meet WCAG contrast, and section headings are capitalized correctly in the app's language. (@ovurrsl, [#891](https://github.com/Droid-Deck/DroidDeck/pull/891), [#1149](https://github.com/Droid-Deck/DroidDeck/pull/1149), [#1145](https://github.com/Droid-Deck/DroidDeck/pull/1145), [#1148](https://github.com/Droid-Deck/DroidDeck/pull/1148))
- **Translations:** Turkish covers every new screen, every language has the plural forms it needs, and English left in the translations is translated. New features in this release ship in all 32 languages. The KDE desktop's translations are now built as per-language packs, ready for a translated desktop. (@ovurrsl, @maxjivi05, @kingokksa, [#989](https://github.com/Droid-Deck/DroidDeck/pull/989), [#1102](https://github.com/Droid-Deck/DroidDeck/pull/1102), [#942](https://github.com/Droid-Deck/DroidDeck/pull/942))
- **Building and testing DroidDeck yourself** is easier: local builds find Java 17 installed with mise, reuse add-on packs a CI build installed, and `tools/device-smoke.sh` checks a build on a real device over plain adb. Session logs time each setup step to the millisecond. (@ugreg, @xXJSONDeruloXx, @ovurrsl, @maxjivi05, [#1231](https://github.com/Droid-Deck/DroidDeck/pull/1231), [#1265](https://github.com/Droid-Deck/DroidDeck/pull/1265), [#911](https://github.com/Droid-Deck/DroidDeck/pull/911), [#993](https://github.com/Droid-Deck/DroidDeck/pull/993), [#1091](https://github.com/Droid-Deck/DroidDeck/pull/1091), [#1151](https://github.com/Droid-Deck/DroidDeck/pull/1151))
### Fixed
- Fixed **Ubisoft games** stopping with "Error at hooking API" (for example Immortals Fenyx Rising), and other programs that patch their own code under FEX. (@maxjivi05, [#1163](https://github.com/Droid-Deck/DroidDeck/pull/1163))
- Fixed **Steam going back to sleep** right after a long-paused session resumed. (@maxjivi05, [#1162](https://github.com/Droid-Deck/DroidDeck/pull/1162))
- Fixed **controllers in Rewired (Unity) games** such as Drova and Graveyard Keeper, and in **Factorio**'s ARM64 build. (@Delete228, @maxjivi05, [#1195](https://github.com/Droid-Deck/DroidDeck/pull/1195), [#1222](https://github.com/Droid-Deck/DroidDeck/pull/1222), [#1227](https://github.com/Droid-Deck/DroidDeck/pull/1227))
- Fixed **Linux games under FEX**: games such as Stardew Valley start again, have sound, find OpenSSL 3 and reach FEXServer. CJK text shows in x86 apps such as the Epic Games Launcher, and clearing a game's compatibility tool in Steam keeps its Linux build. x86 Proton runs in the Steam runtime it requires. (@maxjivi05, @xXJSONDeruloXx, [#1100](https://github.com/Droid-Deck/DroidDeck/pull/1100), [#1166](https://github.com/Droid-Deck/DroidDeck/pull/1166), [#1167](https://github.com/Droid-Deck/DroidDeck/pull/1167), [#1213](https://github.com/Droid-Deck/DroidDeck/pull/1213), [#1168](https://github.com/Droid-Deck/DroidDeck/pull/1168), [#1250](https://github.com/Droid-Deck/DroidDeck/pull/1250), [#1034](https://github.com/Droid-Deck/DroidDeck/pull/1034))
- Fixed the **x86-64 Steam client** staying on a blank screen, asking for the password again and listing games twice. (@maxjivi05, [#1096](https://github.com/Droid-Deck/DroidDeck/pull/1096))
- Fixed **garbled Chinese, Japanese and Korean text** in Steam's interface. (@zooluis, @xXJSONDeruloXx, [#1319](https://github.com/Droid-Deck/DroidDeck/pull/1319))
- Fixed **flicker and green flashes on Samsung One UI devices with a Snapdragon 8 Gen 2** (Galaxy S23 series, Tab S9). (@xXJSONDeruloXx, [#1128](https://github.com/Droid-Deck/DroidDeck/pull/1128))
- Fixed the **GPU load reading 0%** in the performance overlay on Adreno phones. (@angelrenefm, @xXJSONDeruloXx, [#1326](https://github.com/Droid-Deck/DroidDeck/pull/1326), [#1327](https://github.com/Droid-Deck/DroidDeck/pull/1327))
- Fixed gamescope running out of file handles in long sessions with lots of touch input. (@asdf17128, [#1189](https://github.com/Droid-Deck/DroidDeck/pull/1189))
- Fixed **GOG games missing their registry entries**, such as Fallout: New Vegas saying it is not installed, and the "already installed" markers of Windows redistributables never reaching the user registry. (@maxjivi05, @kaosjammo, [#1159](https://github.com/Droid-Deck/DroidDeck/pull/1159), [#1286](https://github.com/Droid-Deck/DroidDeck/pull/1286))
- Fixed a store game's **first launch missing its cloud saves**, and manual cloud-save Upload and Download reporting success when nothing was synced. (@maxjivi05, @ovurrsl, [#1215](https://github.com/Droid-Deck/DroidDeck/pull/1215), [#1068](https://github.com/Droid-Deck/DroidDeck/pull/1068))
- Fixed the **on-screen pad covering Steam's keyboard** and DroidDeck's PC keyboard. (@maxjivi05, @ovurrsl, [#1217](https://github.com/Droid-Deck/DroidDeck/pull/1217), [#956](https://github.com/Droid-Deck/DroidDeck/pull/956), [#1221](https://github.com/Droid-Deck/DroidDeck/pull/1221))
- Fixed **Steam touch layouts**: third-party and added games load their own layout, layouts of non-Steam shortcuts such as the Epic Games Launcher can be edited and saved, and held controls stay pressed across action-set and layer changes. Labels with quotes no longer break the config. (@kingokksa, @maxjivi05, @ovurrsl, [#1058](https://github.com/Droid-Deck/DroidDeck/pull/1058), [#1171](https://github.com/Droid-Deck/DroidDeck/pull/1171), [#1143](https://github.com/Droid-Deck/DroidDeck/pull/1143), [#1017](https://github.com/Droid-Deck/DroidDeck/pull/1017))
- Fixed touch input: long-press right-click in Direct touch, the saved touch mode coming back when Steam Touch is turned off, and the touchpad cursor jumping after a two-finger scroll. (@xXJSONDeruloXx, @ovurrsl, [#677](https://github.com/Droid-Deck/DroidDeck/pull/677), [#1142](https://github.com/Droid-Deck/DroidDeck/pull/1142))
- Fixed controller input: moving a stick no longer releases a held D-pad direction or trigger, Xiaomi HyperOS's virtual input device no longer takes player 1, and back-to-back reports to a Bluetooth Steam Controller are no longer dropped. (@ovurrsl, [#1060](https://github.com/Droid-Deck/DroidDeck/pull/1060), [#954](https://github.com/Droid-Deck/DroidDeck/pull/954), [#1032](https://github.com/Droid-Deck/DroidDeck/pull/1032))
- Fixed Steam's **"Steam Controller detected!" tour** opening at every start, and the emulated Steam Controller looking out of date to Steam. (@xXJSONDeruloXx, [#1321](https://github.com/Droid-Deck/DroidDeck/pull/1321), [#1323](https://github.com/Droid-Deck/DroidDeck/pull/1323))
- Fixed **SteamVR starting in a normal Steam session** after a VR session had been used. (@xXJSONDeruloXx, [#1137](https://github.com/Droid-Deck/DroidDeck/pull/1137))
- Fixed **adding games from an SD card**, both a whole games folder and a single EXE, and adding a game whose folder sits inside another added game's folder. (@The412Banner, @maxjivi05, [#1153](https://github.com/Droid-Deck/DroidDeck/pull/1153), [#1173](https://github.com/Droid-Deck/DroidDeck/pull/1173), [#1155](https://github.com/Droid-Deck/DroidDeck/pull/1155))
- Fixed the games-folder scan hiding games such as Reach, Breach, Crashday and Observer, and Unreal Engine games being named "BootstrapPackagedGame". (@ovurrsl, @charan-rathore, [#1023](https://github.com/Droid-Deck/DroidDeck/pull/1023), [#1020](https://github.com/Droid-Deck/DroidDeck/pull/1020))
- Added games start sooner on large Steam libraries, and game files keep the full game name. (@ovurrsl, @maxjivi05, [#994](https://github.com/Droid-Deck/DroidDeck/pull/994), [#1230](https://github.com/Droid-Deck/DroidDeck/pull/1230))
- Fixed a game's **Components** button opening the default profile instead of the game's own. (@jmarti326, [#1146](https://github.com/Droid-Deck/DroidDeck/pull/1146))
- Fixed **desktop apps and emulators**: RPCS3 opens from the emulator rail, RetroArch fills the screen, Chromium browsers start, browsers added as non-Steam games show their window, Flatpak apps open in the renamed app variants, user apps open at the Desktop's resolution, and programs that need an executable stack start. Apps started from the front end show their own cursor, and Steam shortcuts are kept out of the desktop's controls. (@maxjivi05, @Delete228, @xXJSONDeruloXx, [#1191](https://github.com/Droid-Deck/DroidDeck/pull/1191), [#1194](https://github.com/Droid-Deck/DroidDeck/pull/1194), [#1280](https://github.com/Droid-Deck/DroidDeck/pull/1280), [#1243](https://github.com/Droid-Deck/DroidDeck/pull/1243), [#1192](https://github.com/Droid-Deck/DroidDeck/pull/1192), [#1172](https://github.com/Droid-Deck/DroidDeck/pull/1172), [#1208](https://github.com/Droid-Deck/DroidDeck/pull/1208), [#1154](https://github.com/Droid-Deck/DroidDeck/pull/1154), [#1318](https://github.com/Droid-Deck/DroidDeck/pull/1318))
- Fixed **store sign-in**: GOG lets you sign in with a different account, and one failing image or script no longer replaces the Epic sign-in page with an SSL error. (@maxjivi05, [#1235](https://github.com/Droid-Deck/DroidDeck/pull/1235), [#1238](https://github.com/Droid-Deck/DroidDeck/pull/1238))
- Fixed the **file manager**: copy and move keep symbolic links, Cancel really stops a paste, a double tap no longer starts two pastes, selection follows the search filter, and a slow folder no longer replaces the one you moved to. (@ovurrsl, [#1027](https://github.com/Droid-Deck/DroidDeck/pull/1027), [#1029](https://github.com/Droid-Deck/DroidDeck/pull/1029), [#1025](https://github.com/Droid-Deck/DroidDeck/pull/1025), [#1021](https://github.com/Droid-Deck/DroidDeck/pull/1021), [#1028](https://github.com/Droid-Deck/DroidDeck/pull/1028))
- Fixed an interrupted desktop or package install breaking the Linux runtime, which used to need an app reinstall. (@xXJSONDeruloXx, [#1078](https://github.com/Droid-Deck/DroidDeck/pull/1078))
- A failed Steam client download now says your network or VPN may be blocking Steam and offers Retry, instead of silently ending the session. (@ovurrsl, [#997](https://github.com/Droid-Deck/DroidDeck/pull/997))
- Fixed **in-app updates on Android 9 and 10**. (@ovurrsl, [#1066](https://github.com/Droid-Deck/DroidDeck/pull/1066))
- Fixed crashes with a full disk while fetching cover art and with an empty App ID in the game environment editor. (@ovurrsl, [#1030](https://github.com/Droid-Deck/DroidDeck/pull/1030), [#1014](https://github.com/Droid-Deck/DroidDeck/pull/1014))
- Fixed DirectAudio setup changing a game's audio choice during Steam's path queries and install scripts. (@xXJSONDeruloXx, [#1044](https://github.com/Droid-Deck/DroidDeck/pull/1044), [#1046](https://github.com/Droid-Deck/DroidDeck/pull/1046))
- Hidden and private Steam games no longer show on the Steam home art wall. (@xXJSONDeruloXx, [#1097](https://github.com/Droid-Deck/DroidDeck/pull/1097))
- Storage asks before bulk-removing unused components, and store search no longer reopens the keyboard when you scroll back up. (@ovurrsl, [#1140](https://github.com/Droid-Deck/DroidDeck/pull/1140), [#1141](https://github.com/Droid-Deck/DroidDeck/pull/1141))
- Moving a mouse no longer redraws the whole session overlay. (@maxjivi05, [#1253](https://github.com/Droid-Deck/DroidDeck/pull/1253))
### Community
DroidDeck is built in the open by the people who use it. These are everyone whose code is in 0.6.0:
| Contributor | Pull requests |
|---|---|
| @xXJSONDeruloXx | 48: Xbox and Game Pass, store redesign, Remote Play, add-on packs, SteamVR controller types, Rokid glasses, suspend and zRAM, Decky, KWin 6.8 and more |
| @maxjivi05 | 41: performance, FEX and Linux games, Steam client, Ubisoft and GOG fixes, Rewired pads, desktop apps and emulators |
| @ovurrsl | 33: file manager, controller and touch fixes, security, accessibility, Turkish, cloud saves, device smoke tests |
| @kingokksa | 10: itch.io store, uninstall from Games, store tab order, controller off, GOG speed-up, Steam touch layouts |
| @The412Banner | 3: return to launcher, SD-card games folders, nightly components (and the DirectAudio sink) |
| @FX192 | 3: SteamDeckRepo boot movies, Gradient theme |
| @Nightwalker743 | [#940](https://github.com/Droid-Deck/DroidDeck/pull/940): touchscreen gestures |
| @0x1Nawaf | [#1256](https://github.com/Droid-Deck/DroidDeck/pull/1256): performance HUD graph and options |
| @LooLL | [#1199](https://github.com/Droid-Deck/DroidDeck/pull/1199): game language for GOG and Epic |
| @moronigranja | [#1279](https://github.com/Droid-Deck/DroidDeck/pull/1279): Xbox search results and Game Pass installs |
| @informalTechCode | [#1115](https://github.com/Droid-Deck/DroidDeck/pull/1115): VITURE Pro 2 head tracking |
| @Delete228 | [#1195](https://github.com/Droid-Deck/DroidDeck/pull/1195), [#1318](https://github.com/Droid-Deck/DroidDeck/pull/1318): Factorio controller input, second-screen controls in the desktop |
| @Danbro007 | [#1249](https://github.com/Droid-Deck/DroidDeck/pull/1249): SM8975 and Adreno 850 detection |
| @coffeemeist3r | [#1294](https://github.com/Droid-Deck/DroidDeck/pull/1294): Game cores for FEX games |
| @angelrenefm | [#1326](https://github.com/Droid-Deck/DroidDeck/pull/1326): GPU load in the overlay |
| @asdf17128 | [#1189](https://github.com/Droid-Deck/DroidDeck/pull/1189): gamescope cursor buffer leak |
| @kaosjammo | [#1286](https://github.com/Droid-Deck/DroidDeck/pull/1286): Windows redistributable registry markers |
| @jmarti326 | [#1146](https://github.com/Droid-Deck/DroidDeck/pull/1146): Components opens the game's profile |
| @charan-rathore | [#1020](https://github.com/Droid-Deck/DroidDeck/pull/1020): Unreal Engine game names |
| @wallentx | [#1063](https://github.com/Droid-Deck/DroidDeck/pull/1063): readable library names in logs |
| @ugreg | [#1231](https://github.com/Droid-Deck/DroidDeck/pull/1231): Java 17 through mise for local builds |
| @zooluis | [#1319](https://github.com/Droid-Deck/DroidDeck/pull/1319): CJK fonts in Steam |
| @Detinator10 | [#1220](https://github.com/Droid-Deck/DroidDeck/pull/1220): full session freeze for sleep |
<!-- Full PR lists per contributor are in contributors.md. -->
### New contributors
- @Nightwalker743 made their first contribution in [#940](https://github.com/Droid-Deck/DroidDeck/pull/940).
- @charan-rathore made their first contribution in [#1020](https://github.com/Droid-Deck/DroidDeck/pull/1020).
- @Delete228 made their first contribution in [#1195](https://github.com/Droid-Deck/DroidDeck/pull/1195).
- @LooLL made their first contribution in [#1199](https://github.com/Droid-Deck/DroidDeck/pull/1199).
- @Detinator10 made their first contribution in [#1220](https://github.com/Droid-Deck/DroidDeck/pull/1220).
- @ugreg made their first contribution in [#1231](https://github.com/Droid-Deck/DroidDeck/pull/1231).
- @Danbro007 made their first contribution in [#1249](https://github.com/Droid-Deck/DroidDeck/pull/1249).
- @moronigranja made their first contribution in [#1279](https://github.com/Droid-Deck/DroidDeck/pull/1279).
- @coffeemeist3r made their first contribution in [#1294](https://github.com/Droid-Deck/DroidDeck/pull/1294).
- @zooluis made their first contribution in [#1319](https://github.com/Droid-Deck/DroidDeck/pull/1319).
[Full changelog](https://github.com/Droid-Deck/DroidDeck/compare/0.5.0...0.6.0)
Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

@@ -21,6 +21,30 @@ unpack() {
{ rm -rf "$dir.new"; echo "droiddeck-pipewire: could not unpack $bundle" >&2; return 1; }
}
access_config() {
mkdir -p "$dir/share/pipewire/pipewire.conf.d" "$dir/share/wireplumber/wireplumber.conf.d" || return 1
cat > "$dir/share/pipewire/pipewire.conf.d/droiddeck-access.conf" <<'EOF' || return 1
context.properties = {
module.access = false
}
context.modules = [
{ name = libpipewire-module-access
args = {
access.socket = { pipewire-0 = "unrestricted", pipewire-0-manager = "unrestricted" }
}
}
]
EOF
cat > "$dir/share/wireplumber/wireplumber.conf.d/droiddeck-access.conf" <<'EOF' || return 1
access.rules = [
{
matches = [ { pipewire.access = "unrestricted" } ]
actions = { update-props = { default_permissions = "all" } }
}
]
EOF
}
stop() {
for pid in $(cat "$state/pids" 2>/dev/null); do kill "$pid" 2>/dev/null; done
rm -f "$state/pids"
@@ -32,6 +56,7 @@ env)
;;
start)
unpack || exit 1
access_config || { echo "droiddeck-pipewire: could not write the access settings" >&2; exit 1; }
stop
mkdir -p "$state"
eval "$(client_env)"
+3
View File
@@ -409,6 +409,8 @@ int bl_drm_gem_close(int fd, unsigned long request, void *arg, int *rc) __attrib
int bl_venc_ioctl(int fd, unsigned long request, void *arg, int *rc) __attribute__((visibility("hidden")));
int bl_winebus_rumble_ioctl(int (*real)(int, unsigned long, void *), int fd, unsigned long request,
void *arg, void *caller, int *rc) __attribute__((visibility("hidden")));
int bl_writewatch_ioctl(int (*real)(int, unsigned long, void *), int fd, unsigned long request, void *arg,
int *rc) __attribute__((visibility("hidden")));
int ioctl(int fd, unsigned long request, ...) {
typedef int (*ioctl_fn)(int, unsigned long, void *);
@@ -424,6 +426,7 @@ int ioctl(int fd, unsigned long request, ...) {
if (bl_drm_gem_close(fd, request, arg, &rc)) return rc;
if (real == NULL) real = (ioctl_fn) dlsym(RTLD_NEXT, "ioctl");
if (bl_winebus_rumble_ioctl(real, fd, request, arg, __builtin_return_address(0), &rc)) return rc;
if (bl_writewatch_ioctl(real, fd, request, arg, &rc)) return rc;
if (!bl_kgsl_poll(real, fd, request, arg, &rc)) rc = quest_kgsl_ioctl(real, fd, request, arg);
bl_kgsl_chip_id_fixup(request, arg, rc);
if (arg == NULL || !is_link_query(request)) return rc;
+5
View File
@@ -329,6 +329,8 @@ static int execveat_is_execve(const long *args) {
return path[0] == '/' || (int)args[0] == AT_FDCWD;
}
int bl_writewatch_syscall(long number, const long *args, long (*real)(long, ...), long *rc) __attribute__((visibility("hidden")));
long syscall(long number, ...) {
va_list ap;
long args[6];
@@ -354,6 +356,9 @@ long syscall(long number, ...) {
if (!real_syscall) {
real_syscall = (long (*)(long, ...)) dlsym(RTLD_NEXT, "syscall");
}
if (bl_writewatch_syscall(number, args, real_syscall, &handled)) {
return handled;
}
if (number == SYS_openat2) {
if (!__atomic_load_n(&openat2_missing, __ATOMIC_RELAXED)) {
long fd = real_syscall(number, args[0], args[1], args[2], args[3]);
+113
View File
@@ -0,0 +1,113 @@
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/fs.h>
#include <linux/userfaultfd.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <unistd.h>
#define WINE_ARM64_USERFAULTFD 374
#define PM_UFFD_WP (1ULL << 57)
#define PM_SWAP (1ULL << 62)
#define PM_PRESENT (1ULL << 63)
#if defined(PAGEMAP_SCAN) && defined(UFFD_USER_MODE_ONLY)
static int ww_uffd = -1;
static int scan_blocked;
#endif
__attribute__((visibility("hidden"))) int bl_writewatch_syscall(long number, const long *args, long (*real)(long, ...), long *rc) {
#if defined(__aarch64__) && defined(PAGEMAP_SCAN) && defined(UFFD_USER_MODE_ONLY)
if (number != WINE_ARM64_USERFAULTFD || (args[0] & ~(long)(O_CLOEXEC | O_NONBLOCK | UFFD_USER_MODE_ONLY))) return 0;
*rc = real(SYS_userfaultfd, args[0]);
if (*rc >= 0) __atomic_store_n(&ww_uffd, (int)*rc, __ATOMIC_RELEASE);
return 1;
#else
(void)number;
(void)args;
(void)real;
(void)rc;
return 0;
#endif
}
#if defined(PAGEMAP_SCAN) && defined(UFFD_USER_MODE_ONLY)
static int protect(int uffd, uint64_t start, uint64_t end) {
if (start == end) return 0;
struct uffdio_writeprotect wp = { .range = { start, end - start }, .mode = UFFDIO_WRITEPROTECT_MODE_WP };
return ioctl(uffd, UFFDIO_WRITEPROTECT, &wp);
}
static long emulate_scan(int fd, int uffd, struct pm_scan_arg *a) {
uint64_t page = (uint64_t)getpagesize();
if (a->size < sizeof(*a) || (a->flags & ~(uint64_t)PM_SCAN_WP_MATCHING) || a->category_inverted || a->category_anyof_mask
|| a->category_mask != PAGE_IS_WRITTEN || (a->return_mask & ~(uint64_t)PAGE_IS_WRITTEN) || a->end < a->start
|| ((a->start | a->end) & (page - 1)) || (a->vec_len && !a->vec)) {
errno = EINVAL;
return -1;
}
struct page_region *vec = (struct page_region *)(uintptr_t)a->vec;
uint64_t regions = 0, pages = 0, addr = a->start, wp_start = 0, wp_end = 0;
uint64_t entries[512];
int wp = (a->flags & PM_SCAN_WP_MATCHING) != 0;
while (addr < a->end) {
uint64_t want = (a->end - addr) / page;
if (want > 512) want = 512;
ssize_t got = pread(fd, entries, want * sizeof(entries[0]), (off_t)(addr / page * sizeof(entries[0])));
if (got < 0) return -1;
if (got == 0) break;
for (uint64_t i = 0; i < (uint64_t)got / sizeof(entries[0]); i++, addr += page) {
uint64_t e = entries[i];
if (!(e & (PM_PRESENT | PM_SWAP)) || (e & PM_UFFD_WP)) continue;
if (a->max_pages && pages >= a->max_pages) goto done;
if (a->vec_len) {
if (regions && vec[regions - 1].end == addr) {
vec[regions - 1].end += page;
} else {
if (regions == a->vec_len) goto done;
vec[regions].start = addr;
vec[regions].end = addr + page;
vec[regions].categories = PAGE_IS_WRITTEN & a->return_mask;
regions++;
}
}
pages++;
if (!wp) continue;
if (wp_end != addr) {
if (protect(uffd, wp_start, wp_end) < 0) return -1;
wp_start = addr;
}
wp_end = addr + page;
}
}
done:
if (wp && protect(uffd, wp_start, wp_end) < 0) return -1;
a->walk_end = addr < a->end ? addr : a->end;
return (long)regions;
}
#endif
__attribute__((visibility("hidden"))) int bl_writewatch_ioctl(int (*real)(int, unsigned long, void *), int fd, unsigned long request,
void *arg, int *rc) {
#if defined(PAGEMAP_SCAN) && defined(UFFD_USER_MODE_ONLY)
if (request != PAGEMAP_SCAN) return 0;
int uffd = __atomic_load_n(&ww_uffd, __ATOMIC_ACQUIRE);
if (uffd < 0) return 0;
if (!__atomic_load_n(&scan_blocked, __ATOMIC_RELAXED)) {
*rc = real(fd, request, arg);
if (*rc >= 0 || errno != EACCES) return 1;
__atomic_store_n(&scan_blocked, 1, __ATOMIC_RELAXED);
}
*rc = (int)emulate_scan(fd, uffd, (struct pm_scan_arg *)arg);
return 1;
#else
(void)real;
(void)fd;
(void)request;
(void)arg;
(void)rc;
return 0;
#endif
}
+59 -19
View File
@@ -337,34 +337,73 @@ static int to_guest(const char *h, char *g, size_t size) {
return 0;
}
static int guest_text(const char *h, char *g) {
int in = h[0] == '/' && under(h, root, rootlen);
for (int i = 0; h[0] == '/' && !in && i < nbinds; i++) {
int live = held(&binds[i]);
if (live < 0) return -1;
in = live && under(h, binds[i].host, binds[i].hlen);
static int host_order[MAX_BINDS + 1], norder, ordered;
static int contains(const char *a, const char *b) {
size_t la = strlen(a);
return strlen(b) > la && under(b, a, la);
}
static const char *side_of(int i, int host) {
return host ? (i < 0 ? root : binds[i].host) : (i < 0 ? "/" : binds[i].guest);
}
static int insort(int *list, int m, int i, int host) {
const char *p = side_of(i, host);
int prev = -1;
for (int j = 0; j < m; j++) {
const char *q = side_of(list[j], host);
if (!strcmp(p, q) && !host) {
list[j] = i;
return m;
}
if (!strcmp(p, q) || contains(p, q)) prev = j;
}
if (in) return to_guest(h, g, PATH_MAX);
memcpy(g, h, strlen(h) + 1);
memmove(list + prev + 2, list + prev + 1, (m - prev - 1) * sizeof *list);
list[prev + 1] = i;
return m + 1;
}
static int order_binds(void) {
if (__atomic_load_n(&ordered, __ATOMIC_ACQUIRE)) return 0;
for (int i = 0; i < nbinds; i++)
if (held(&binds[i]) < 0) return -1;
if (pthread_mutex_trylock(&bind_lock) != 0) return -1;
if (!ordered) {
int pending[MAX_BINDS + 1], m = 1;
pending[0] = -1;
for (int i = 0; i < nbinds; i++)
if (!binds[i].dropped) m = insort(pending, m, i, 0);
norder = 0;
for (int j = m - 1; j >= 0; j--) norder = insort(host_order, norder, pending[j], 1);
__atomic_store_n(&ordered, 1, __ATOMIC_RELEASE);
}
pthread_mutex_unlock(&bind_lock);
return 0;
}
static int host_bind(const char *h) {
int found = -1;
for (int i = 0; i < nbinds; i++) {
int live = held(&binds[i]);
if (live < 0) return -2;
if (!live || !under(h, binds[i].host, binds[i].hlen)) continue;
if (found < 0 || binds[i].hlen > binds[found].hlen) found = i;
else if (binds[i].hlen == binds[found].hlen && strcmp(binds[i].guest, binds[found].guest) != 0) found = -3;
}
return found == -3 ? -2 : found;
if (order_binds() < 0) return -2;
for (int j = 0; j < norder; j++)
if (host_order[j] >= 0 && under(h, binds[host_order[j]].host, binds[host_order[j]].hlen)) return host_order[j];
return -1;
}
static int guest_text(const char *h, char *g) {
if (h[0] == '/' && under(h, root, rootlen)) return to_guest(h, g, PATH_MAX);
int i = h[0] == '/' ? host_bind(h) : -1;
if (i == -2) return -1;
if (i >= 0)
return snprintf(g, PATH_MAX, "%s%s", binds[i].guest, h + binds[i].hlen) >= PATH_MAX || reserved(g) ? -1 : 0;
memcpy(g, h, strlen(h) + 1);
return 0;
}
static long detranslated(char *text, const char *at) {
if (text[0] != '/') return 0;
if (!under(at, root, rootlen) && !under(text, root, rootlen)) {
int own = host_bind(at), to = host_bind(text);
if (to == -1) return 0;
if (own == -2 || to == -2) return FP_SLOW;
if (own >= 0 && to >= 0 && strcmp(binds[own].host, binds[to].host) == 0) {
const struct bind *bd = &binds[to];
@@ -635,8 +674,9 @@ static long resolve_in(const char *base, const char *path, int follow, struct re
char norm[PATH_MAX];
size_t o = 0;
int dotdot = 0;
size_t plen = strlen(path);
int trailing = plen > 1 && path[plen - 1] == '/';
size_t plen = strlen(path), end = plen;
while (end > 1 && path[end - 1] == '/') end--;
int trailing = (plen > 1 && path[plen - 1] == '/') || (end > 0 && path[end - 1] == '.' && (end == 1 || path[end - 2] == '/'));
for (int part = base ? 0 : 1; part < 2; part++) {
for (const char *c = part ? path : base; *c;) {
while (*c == '/') c++;
+139
View File
@@ -0,0 +1,139 @@
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
FASTPATH = Path(__file__).resolve().parents[1] / "proot/fastpath/fastpath.c"
PROGRAM = r"""
#include "FASTPATH"
static long host_sys(long nr, long a, long b, long c, long d, long e, long f) {
long r = syscall(nr, a, b, c, d, e, f);
return r < 0 ? -errno : r;
}
int main(int argc, char **argv) {
static char hosts[MAX_BINDS][PATH_MAX], guests[MAX_BINDS][512];
fp_sys = host_sys;
snprintf(root, sizeof root, "%s", argv[1]);
rootlen = strlen(root);
int i = 2;
for (; i + 1 < argc && strcmp(argv[i], "--"); i += 2) {
struct bind *bd = &binds[nbinds];
bd->host = hosts[nbinds];
bd->guest = guests[nbinds];
snprintf(bd->host, PATH_MAX, "%s", argv[i]);
snprintf(bd->guest, sizeof guests[0], "%s", argv[i + 1]);
bd->hlen = strlen(bd->host);
bd->glen = strlen(bd->guest);
nbinds++;
}
pthread_once(&init_once, fp_init);
fp_on = 1;
for (i++; i < argc; i++) {
char text[PATH_MAX];
if (!strcmp(argv[i], "name")) {
if (guest_text(argv[++i], text) < 0) strcpy(text, "SLOW");
puts(text);
} else if (!strcmp(argv[i], "link")) {
snprintf(text, sizeof text, "%s", argv[i + 2]);
if (detranslated(text, argv[i + 1]) == FP_SLOW) strcpy(text, "SLOW");
i += 2;
puts(text);
} else if (!strcmp(argv[i], "lstat")) {
struct stat st;
long r = fstatat(AT_FDCWD, argv[++i], &st, AT_SYMLINK_NOFOLLOW);
puts(r < 0 ? "error" : S_ISDIR(st.st_mode) ? "dir" : S_ISLNK(st.st_mode) ? "link" : "other");
}
}
return 0;
}
"""
class FastPathBindNamesTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
if shutil.which("gcc") is None:
raise unittest.SkipTest("gcc is not available")
cls.work = Path(tempfile.mkdtemp())
source = cls.work / "names.c"
source.write_text(PROGRAM.replace("FASTPATH", str(FASTPATH)))
cls.program = cls.work / "names"
subprocess.run(["gcc", "-O1", "-w", "-pthread", "-o", str(cls.program), str(source), "-ldl"], check=True)
t = cls.work / "tree"
for d in ["root/etc", "root/mnt", "root/opt", "root/d/real", "lib/common/game", "fonts", "a", "b"]:
(t / d).mkdir(parents=True, exist_ok=True)
for f in ["root/etc/x", "lib/f", "lib/common/game/f", "fonts/f", "a/f", "b/f"]:
(t / f).touch()
os.symlink(t / "lib/f", t / "lib/l2")
os.symlink(t / "lib/common", t / "lib/l")
os.symlink("real", t / "root/d/link")
cls.tree = t
cls.binds = [
(t / "lib", "/mnt/droiddeck-sd"),
(t / "lib", "/mnt/bannerlator-sd"),
(t / "lib", "/mnt/DroidDeck-SD"),
(t / "lib/common", "/opt/common"),
(t / "fonts", "/usr/share/fonts/android"),
(t / "a", "/opt/g"),
(t / "b", "/opt/g"),
(t / "fonts", "/mnt/x"),
]
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls.work, ignore_errors=True)
def ask(self, *queries):
argv = [str(self.program), str(self.tree / "root")]
for host, guest in self.binds:
argv += [str(host), guest]
argv.append("--")
for q in queries:
argv += [str(x) for x in q]
out = subprocess.run(argv, capture_output=True, text=True, timeout=20, check=True).stdout
return out.splitlines()
def test_a_host_file_takes_the_name_proot_gives_it(self):
t = self.tree
self.assertEqual(self.ask(
("name", t / "lib/f"),
("name", t / "lib/common/game/f"),
("name", t / "fonts/f"),
("name", t / "b/f"),
("name", t / "a/f"),
("name", t / "root/etc/x"),
), [
"/mnt/droiddeck-sd/f",
"/opt/common/game/f",
"/usr/share/fonts/android/f",
"/opt/g/f",
str(t / "a/f"),
"/etc/x",
])
def test_a_link_inside_a_library_reads_back_like_proot(self):
t = self.tree
self.assertEqual(self.ask(
("link", t / "lib/l2", t / "lib/f"),
("link", t / "lib/l", t / "lib/common"),
), [
"/mnt/droiddeck-sd/f",
str(t / "lib/common"),
])
def test_a_final_dot_follows_the_link(self):
self.assertEqual(self.ask(
("lstat", "/d/link"),
("lstat", "/d/link/."),
("lstat", "/d/link/./"),
("lstat", "/d/link/"),
), ["link", "dir", "dir", "dir"])
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -319,7 +319,7 @@ class FsyncPreloadTest(unittest.TestCase):
source = base / "fsync-test.c"
source.write_text(PROGRAM)
subprocess.run(["cc", "-O2", "-Wall", "-fPIC", "-shared", "-pthread", "-o", str(cls.lib),
str(PRELOAD / "fsync.c"), str(PRELOAD / "robust.c"), str(PRELOAD / "noexec.c"), "-ldl"], check=True)
str(PRELOAD / "fsync.c"), str(PRELOAD / "robust.c"), str(PRELOAD / "noexec.c"), str(PRELOAD / "writewatch.c"), "-ldl"], check=True)
subprocess.run(["cc", "-O2", "-Wall", "-pthread", "-o", str(cls.exe), str(source)], check=True)
@classmethod
+1 -1
View File
@@ -43,7 +43,7 @@ class NoexecPreloadTest(unittest.TestCase):
cls.tmp = Path(tempfile.mkdtemp())
cls.lib = cls.tmp / "libnoexec.so"
subprocess.run(["cc", "-O2", "-Wall", "-Werror", "-fPIC", "-shared", "-pthread", "-DNOEXEC_TEST", "-o", str(cls.lib),
str(PRELOAD / "noexec.c"), str(PRELOAD / "fsync.c"), str(PRELOAD / "robust.c"), "-ldl"], check=True)
str(PRELOAD / "noexec.c"), str(PRELOAD / "fsync.c"), str(PRELOAD / "robust.c"), str(PRELOAD / "writewatch.c"), "-ldl"], check=True)
source = cls.tmp / "program.c"
source.write_text(PROGRAM)
for name in ("FEX", "game"):
+1 -1
View File
@@ -230,7 +230,7 @@ class Openat2PreloadTest(unittest.TestCase):
cls.tmp = Path(tempfile.mkdtemp())
cls.lib = cls.tmp / "libopenat2.so"
subprocess.run(["cc", "-O2", "-Wall", "-Werror", "-fPIC", "-shared", "-pthread", "-o", str(cls.lib),
str(PRELOAD / "robust.c"), str(PRELOAD / "fsync.c"), str(PRELOAD / "noexec.c"), "-ldl"], check=True)
str(PRELOAD / "robust.c"), str(PRELOAD / "fsync.c"), str(PRELOAD / "noexec.c"), str(PRELOAD / "writewatch.c"), "-ldl"], check=True)
source = cls.tmp / "openat2.c"
source.write_text(PROGRAM)
subprocess.run(["cc", "-O2", "-Wall", "-Werror", "-o", str(cls.tmp / "openat2"), str(source), "-ldl"], check=True)
+30
View File
@@ -74,6 +74,36 @@ class PipeWireHelperTest(unittest.TestCase):
self.assertIn('did not open', result.stderr)
self.assertFalse((self.runtime / 'droiddeck-pipewire/pids').exists())
def test_clients_get_full_access_without_the_flatpak_check(self):
self.pack('python3 -c "import socket,sys; s=socket.socket(socket.AF_UNIX); s.bind(sys.argv[1]); '
'import time; time.sleep(30)" "$XDG_RUNTIME_DIR/pipewire-0"')
for _ in range(2):
result = self.run_helper('start')
self.assertEqual(result.returncode, 0, result.stderr)
daemon = (self.dir / 'share/pipewire/pipewire.conf.d/droiddeck-access.conf').read_text()
self.assertIn('module.access = false', daemon)
self.assertIn('name = libpipewire-module-access', daemon)
self.assertIn('access.socket = { pipewire-0 = "unrestricted", pipewire-0-manager = "unrestricted" }',
daemon)
self.assertNotIn('access.legacy', daemon)
manager = (self.dir / 'share/wireplumber/wireplumber.conf.d/droiddeck-access.conf').read_text()
self.assertIn('matches = [ { pipewire.access = "unrestricted" } ]', manager)
self.assertIn('default_permissions = "all"', manager)
def test_start_fails_cleanly_when_the_access_settings_cannot_be_written(self):
self.pack('exec sleep 30')
self.run_helper('start')
self.run_helper('stop')
conf = self.dir / 'share/pipewire/pipewire.conf.d'
for child in conf.iterdir():
child.unlink()
conf.rmdir()
conf.write_text('')
result = self.run_helper('start')
self.assertNotEqual(result.returncode, 0)
self.assertIn('could not write the access settings', result.stderr)
self.assertFalse((self.runtime / 'droiddeck-pipewire/pids').exists())
def test_unchanged_bundle_is_not_unpacked_again(self):
self.pack('exec sleep 30')
self.run_helper('start')
+1
View File
@@ -32,6 +32,7 @@ int probe(void) {
#include <assert.h>
#include <dlfcn.h>
int bl_fsync_syscall(long n, const long *a, long *r) { (void)n; (void)a; (void)r; return 0; }
int bl_writewatch_syscall(long n, const long *a, long (*real)(long, ...), long *r) { (void)n; (void)a; (void)real; (void)r; return 0; }
#include """ + '"' + robust.as_posix() + '"' + r"""
int main(int argc, char **argv) {
assert(argc == 3);
+119
View File
@@ -0,0 +1,119 @@
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload"
PROGRAM = r"""
#include "WRITEWATCH"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#define PAGES 96
static int uffd, pagemap;
static uint64_t page;
static char *region(void) {
char *m = mmap(NULL, PAGES * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
madvise(m, PAGES * page, MADV_NOHUGEPAGE);
struct uffdio_register reg = { .range = { (uintptr_t)m, PAGES * page }, .mode = UFFDIO_REGISTER_MODE_WP };
if (m == MAP_FAILED || ioctl(uffd, UFFDIO_REGISTER, &reg)) exit(3);
protect(uffd, (uintptr_t)m, (uintptr_t)m + PAGES * page);
return m;
}
static long scan(char *m, int emulated, uint64_t flags, uint64_t vec_len, uint64_t max_pages, uint64_t from, uint64_t to,
struct page_region *vec, uint64_t *walk_end) {
struct pm_scan_arg a = { .size = sizeof a, .flags = flags, .start = (uintptr_t)m + from * page, .end = (uintptr_t)m + to * page,
.vec = vec_len ? (uintptr_t)vec : 0, .vec_len = vec_len, .max_pages = max_pages,
.category_mask = PAGE_IS_WRITTEN, .return_mask = PAGE_IS_WRITTEN };
long n = emulated ? emulate_scan(pagemap, uffd, &a) : ioctl(pagemap, PAGEMAP_SCAN, &a);
*walk_end = (a.walk_end - (uintptr_t)m) / page;
return n;
}
int main(int argc, char **argv) {
page = (uint64_t)getpagesize();
uffd = syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK | UFFD_USER_MODE_ONLY);
struct uffdio_api api = { .api = UFFD_API, .features = UFFD_FEATURE_WP_ASYNC | UFFD_FEATURE_WP_UNPOPULATED };
pagemap = open("/proc/self/pagemap", O_RDONLY | O_CLOEXEC);
if (uffd < 0 || ioctl(uffd, UFFDIO_API, &api) || pagemap < 0) { puts("unsupported"); return 0; }
char *kernel = region(), *ours = region();
struct page_region probe[1];
uint64_t end;
if (scan(kernel, 0, 0, 1, 0, 0, PAGES, probe, &end) < 0) { puts("unsupported"); return 0; }
srand(atoi(argv[1]));
int rounds = atoi(argv[2]), differing = 0;
for (int r = 0; r < rounds; r++) {
int writes = rand() % 24;
for (int w = 0; w < writes; w++) {
int p = rand() % PAGES;
if (rand() % 4 == 0) {
volatile char x = kernel[p * page] + ours[p * page];
(void)x;
continue;
}
kernel[p * page + rand() % page] = (char)r;
ours[p * page + rand() % page] = (char)r;
}
uint64_t flags = rand() % 2 ? PM_SCAN_WP_MATCHING : 0;
uint64_t vec_len = rand() % 3 == 0 ? 0 : 1 + rand() % 5;
uint64_t max_pages = rand() % 3 == 0 ? 1 + rand() % 8 : 0;
if (!vec_len) flags = PM_SCAN_WP_MATCHING, max_pages = 0;
uint64_t from = rand() % PAGES, to = from + rand() % (PAGES - from + 1);
struct page_region kv[8], ov[8];
uint64_t kend, oend;
long kn = scan(kernel, 0, flags, vec_len, max_pages, from, to, kv, &kend);
long on = scan(ours, 1, flags, vec_len, max_pages, from, to, ov, &oend);
int same = kn == on && (kn < 0 || kend == oend);
for (long i = 0; same && i < kn; i++)
same = kv[i].start - (uintptr_t)kernel == ov[i].start - (uintptr_t)ours
&& kv[i].end - (uintptr_t)kernel == ov[i].end - (uintptr_t)ours && kv[i].categories == ov[i].categories;
struct page_region kall[PAGES], oall[PAGES];
long ka = scan(kernel, 0, 0, PAGES, 0, 0, PAGES, kall, &kend), oa = scan(ours, 1, 0, PAGES, 0, 0, PAGES, oall, &oend);
same = same && ka == oa;
for (long i = 0; same && i < ka; i++)
same = kall[i].start - (uintptr_t)kernel == oall[i].start - (uintptr_t)ours && kall[i].end - (uintptr_t)kernel == oall[i].end - (uintptr_t)ours;
if (!same) {
differing++;
printf("round %d flags %llu vec %llu max %llu range %llu-%llu: kernel %ld end %llu, ours %ld end %llu\n", r,
(unsigned long long)flags, (unsigned long long)vec_len, (unsigned long long)max_pages, (unsigned long long)from,
(unsigned long long)to, kn, (unsigned long long)kend, on, (unsigned long long)oend);
}
}
printf("rounds %d differing %d\n", rounds, differing);
return 0;
}
"""
class WriteWatchPreloadTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
if shutil.which("gcc") is None:
raise unittest.SkipTest("gcc is not available")
cls.work = Path(tempfile.mkdtemp())
source = cls.work / "writewatch.c"
source.write_text(PROGRAM.replace("WRITEWATCH", str(PRELOAD / "writewatch.c")))
cls.program = cls.work / "writewatch"
subprocess.run(["gcc", "-O1", "-Wall", "-o", str(cls.program), str(source)], check=True)
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls.work, ignore_errors=True)
def test_a_scan_android_refuses_answers_like_the_kernel_one(self):
for seed in range(4):
out = subprocess.run([str(self.program), str(seed), "400"], capture_output=True, text=True, timeout=60).stdout
if out.strip() == "unsupported":
self.skipTest("this kernel has no asynchronous userfaultfd write protection")
self.assertTrue(out.rstrip().endswith("differing 0"), out)
if __name__ == "__main__":
unittest.main()