mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Force SSBS for Proton games (libssbs.so), switch under FEX preset (#328)
* Force SSBS for a game's Wine: libssbs.so, opt in per game Wine's arm64 signal code resumes threads from a Windows CONTEXT whose Cpsr never carries PSTATE.SSBS, so every signal return leaves the thread running with speculative store bypass disabled - slower on cores such as Oryon. Bannerlator fixes it in its own Wine builds (force_ssbs.diff, Sonicadvance1) and with an LD_PRELOAD adapter for layers it cannot rebuild. DroidDeck's Protons (Valve, GE, CachyOS ARM64) come prebuilt from Steam but run the same aarch64 Wine signal path, so the adapter applies here: tools/linuxfs/ssbs/ssbs_adapter.c, the same source built for glibc (+ sysv_signal), staged to /usr/local/lib/libssbs.so. droiddeck-game-env preloads it when the game's (or the shared) environment sets DROIDDECK_FORCE_SSBS=1, and only into an ARM64 Proton. Off unless asked for: the gain is unproven on Snapdragon 8 Gen 3. For testing: SSBS_ADAPTER_OBSERVE=1 counts without changing anything (the baseline) and SSBS_ADAPTER_DEBUG=1 logs the fixes; both also count signals that ARRIVE while a thread runs without SSBS, which shows whether anything besides signal returns clears it. Checked on an ARM64 Linux kernel: a handler that clears SSBS left 4 of 5 later signals arriving with it clear; with the adapter, 0 of 5. * Force SSBS: a switch under the FEX preset in Steam settings Steam settings > Games gets "Force SSBS" right under the FEX preset, off unless turned on. Like the preset it goes into the game environment's defaults (DROIDDECK_FORCE_SSBS=1), published at once and read by droiddeck-game-env on the next game launch, so a game's own environment can still set it to 0. Measured on the device with the adapter's counters (AYANEO Pocket FIT, DiRT 3 on GE-Proton11-7-aarch64): observing only, 8192 of 8277 signals found the thread running without SSBS; with the adapter, none did - it keeps the bit set everywhere Wine clears it, as a rebuilt ntdll would. The speed-up itself is reported on Oryon cores and was not measurable on the FIT's 8 Gen 3, hence off by default. --------- Co-authored-by: The412Banner <[email protected]>
This commit is contained in:
1 parent
a1b01a04a8
commit
bc01b77752
13 files changed
+283
-6
No files matched your search
@@ -173,7 +173,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: app/src/main/assets/linuxfs
|
||||
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/build-x86-preloads.sh', 'tools/linuxfs/fex/*', 'tools/linuxfs/clipboard/*.c', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/linuxfs/uruntime.env', 'tools/linuxfs/licenses/*', 'tools/mangoapp/*') }}
|
||||
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/build-x86-preloads.sh', 'tools/linuxfs/fex/*', 'tools/linuxfs/clipboard/*.c', 'tools/linuxfs/ssbs/*.c', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/linuxfs/uruntime.env', 'tools/linuxfs/licenses/*', 'tools/mangoapp/*') }}
|
||||
|
||||
- name: Build libfakeinput.so and libblsession.so (glibc aarch64)
|
||||
if: steps.preloads.outputs.cache-hit != 'true'
|
||||
@@ -195,6 +195,10 @@ jobs:
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
|
||||
-o "$D/libblfastpath.so" tools/proot/fastpath/fastpath.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/libblfastpath.so"
|
||||
# Force-SSBS adapter, preloaded into a game's Wine only when the game asks for it.
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
|
||||
-o "$D/libssbs.so" tools/linuxfs/ssbs/ssbs_adapter.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/libssbs.so"
|
||||
mkdir -p "$D/usr/local/bin"
|
||||
aarch64-linux-gnu-gcc -O2 -Wall -Wextra -o "$D/usr/local/bin/droiddeck-clipboard" tools/linuxfs/clipboard/clipboard.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/bin/droiddeck-clipboard"
|
||||
@@ -362,6 +366,10 @@ jobs:
|
||||
for sym in socket bind getsockname setsockopt statfs statvfs syscall shm_open shm_unlink mmap mmap64 munmap drmPrimeFDToHandle drmCloseBufferHandle drmIoctl; do
|
||||
grep -qx "$sym" <<<"$SESSION" || { echo "::error::libblsession.so does not export $sym"; exit 1; }
|
||||
done
|
||||
SSBS=$(syms "$D/libssbs.so")
|
||||
for sym in sigaction signal bsd_signal sysv_signal; do
|
||||
grep -qx "$sym" <<<"$SSBS" || { echo "::error::libssbs.so does not export $sym"; exit 1; }
|
||||
done
|
||||
test -f "$D/usr/local/bin/droiddeck-session"
|
||||
test -f "$D/usr/local/bin/droiddeck-proton-extra"
|
||||
|
||||
|
||||
@@ -311,6 +311,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var settingsMode by mutableStateOf<String?>(null)
|
||||
private var resolution by mutableStateOf(com.droiddeck.launcher.session.SessionDisplay.DEFAULT_RESOLUTION)
|
||||
private var fexPreset by mutableStateOf("")
|
||||
private var forceSsbs by mutableStateOf(false)
|
||||
private var steamChannel by mutableStateOf("steamdeck_publicbeta")
|
||||
private var runSteamAtStartup by mutableStateOf(false)
|
||||
private var theme by mutableStateOf("graphite")
|
||||
@@ -1159,6 +1160,7 @@ class MainActivity : ComponentActivity() {
|
||||
storageOptions = storageOptions,
|
||||
storageDiagnostics = mode == SessionService.MODE_STEAM && storageDiagnostics,
|
||||
fexPreset = if (mode == SessionService.MODE_STEAM) fexPreset else null,
|
||||
forceSsbs = forceSsbs,
|
||||
syncBackend = if (mode == SessionService.MODE_STEAM) SessionPrefs.syncBackendOf(fastSync, fsyncFirst, syncFallback) else null,
|
||||
steamChannel = if (mode == SessionService.MODE_STEAM) steamChannel else null,
|
||||
steamDeckMode = mode == SessionService.MODE_STEAM && steamDeckMode,
|
||||
@@ -1232,6 +1234,7 @@ class MainActivity : ComponentActivity() {
|
||||
storageDiagnostics = on
|
||||
},
|
||||
onFexPreset = { id -> SessionPrefs.setFexPreset(this, id); fexPreset = id },
|
||||
onForceSsbs = { on -> SessionPrefs.setForceSsbs(this, on); forceSsbs = on },
|
||||
onSyncBackend = { id ->
|
||||
SessionPrefs.setSyncBackend(this, id)
|
||||
fastSync = SessionPrefs.fastSync(this)
|
||||
@@ -1353,6 +1356,7 @@ class MainActivity : ComponentActivity() {
|
||||
showMapping = false
|
||||
resolution = SessionPrefs.resolutionChoice(this, mode, com.droiddeck.launcher.session.SessionDisplay.panelSize(this))
|
||||
fexPreset = SessionPrefs.fexPreset(this)
|
||||
forceSsbs = SessionPrefs.forceSsbs(this)
|
||||
fastSync = SessionPrefs.fastSync(this)
|
||||
fsyncFirst = SessionPrefs.fsyncFirst(this)
|
||||
syncFallback = SessionPrefs.syncFallback(this)
|
||||
|
||||
@@ -25,7 +25,7 @@ object GameEnvironment {
|
||||
fun validValue(value: String) = '\u0000' !in value && value.length <= 8192
|
||||
fun validScope(scope: String) = scope.isEmpty() || (Regex("[1-9][0-9]*").matches(scope) && scope.toLongOrNull()?.let { it in 1..4294967295L } == true)
|
||||
|
||||
fun defaults(preset: String): Map<String, String?> = linkedMapOf<String, String?>(
|
||||
fun defaults(preset: String, forceSsbs: Boolean = false): Map<String, String?> = linkedMapOf<String, String?>(
|
||||
"MESA_SHADER_CACHE_DISABLE" to "false",
|
||||
"VKD3D_FEATURE_LEVEL" to "12_2",
|
||||
"VKD3D_SHADER_MODEL" to "6_9",
|
||||
@@ -37,8 +37,12 @@ object GameEnvironment {
|
||||
"DOTNET_EnableWriteXorExecute" to "0",
|
||||
).apply {
|
||||
FexPreset.env(preset).forEach { put(it.substringBefore('='), it.substringAfter('=')) }
|
||||
// Steam settings' Force SSBS: droiddeck-game-env preloads libssbs.so into the game's Wine.
|
||||
if (forceSsbs) put(FORCE_SSBS, "1")
|
||||
}
|
||||
|
||||
fun effective(config: Config, preset: String, scope: String): Map<String, String?> =
|
||||
defaults(preset) + config.shared + if (scope.isEmpty()) emptyMap() else config.games[scope].orEmpty()
|
||||
const val FORCE_SSBS = "DROIDDECK_FORCE_SSBS"
|
||||
|
||||
fun effective(config: Config, preset: String, scope: String, forceSsbs: Boolean = false): Map<String, String?> =
|
||||
defaults(preset, forceSsbs) + config.shared + if (scope.isEmpty()) emptyMap() else config.games[scope].orEmpty()
|
||||
}
|
||||
@@ -59,7 +59,7 @@ object GameEnvironmentStore {
|
||||
|
||||
@Synchronized
|
||||
fun publish(context: Context, config: GameEnvironment.Config = read(context)) {
|
||||
val resolved = config.copy(shared = GameEnvironment.defaults(SessionPrefs.fexPreset(context)) + config.shared)
|
||||
val resolved = config.copy(shared = GameEnvironment.defaults(SessionPrefs.fexPreset(context), SessionPrefs.forceSsbs(context)) + config.shared)
|
||||
val json = encode(resolved)
|
||||
// The Display page's texture filtering, as DXVK options the launcher appends to
|
||||
// DXVK_CONFIG after the profiles - a user's own DXVK_CONFIG entry keeps its options.
|
||||
|
||||
@@ -36,6 +36,7 @@ object SessionFiles {
|
||||
"libblsession.so" to "usr/local/lib/libblsession.so",
|
||||
"libfakeinput.so" to "usr/local/lib/libfakeinput.so",
|
||||
"libblfastpath.so" to "usr/local/lib/libblfastpath.so",
|
||||
"libssbs.so" to "usr/local/lib/libssbs.so",
|
||||
"usr/local/bin/droiddeck-session" to "usr/local/bin/droiddeck-session",
|
||||
"usr/local/bin/steam-compatibility" to "usr/local/bin/steam-compatibility",
|
||||
"usr/local/bin/droiddeck-clipboard" to "usr/local/bin/droiddeck-clipboard",
|
||||
|
||||
@@ -543,6 +543,22 @@ object SessionPrefs {
|
||||
|
||||
private const val DEFAULT_FEX_PRESET = "PERFORMANCE_TSO"
|
||||
|
||||
/**
|
||||
* Force SSBS for Proton games: Wine resumes threads from a Windows CONTEXT that never carries
|
||||
* PSTATE.SSBS, so they run with speculative store bypass disabled; libssbs.so keeps it set
|
||||
* (on DiRT 3 / GE-Proton: from ~99% of a game's threads running without it to none). The
|
||||
* speed-up is reported on Oryon cores (Snapdragon 8 Elite) and was not measurable on an
|
||||
* 8 Gen 3, so it is off unless turned on. A game's own environment can still say
|
||||
* DROIDDECK_FORCE_SSBS=0.
|
||||
*/
|
||||
fun forceSsbs(context: Context): Boolean = prefs(context).getBoolean("forceSsbs", false)
|
||||
|
||||
fun setForceSsbs(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("forceSsbs", on).apply()
|
||||
runCatching { GameEnvironmentStore.publish(context) }
|
||||
.onFailure { android.util.Log.e("GameEnvironment", "Could not update game environment", it) }
|
||||
}
|
||||
|
||||
fun setFexPreset(context: Context, id: String) {
|
||||
prefs(context).edit().putString("fexPreset", id).apply()
|
||||
runCatching { GameEnvironmentStore.publish(context) }
|
||||
|
||||
@@ -95,6 +95,7 @@ private fun GameEnvironmentEditor(byPad: Boolean, onClose: () -> Unit) {
|
||||
var error by remember { mutableStateOf(false) }
|
||||
var editing by remember { mutableStateOf<Pair<String, String>?>(null) }
|
||||
val preset = SessionPrefs.fexPreset(context)
|
||||
val forceSsbs = SessionPrefs.forceSsbs(context)
|
||||
val firstFocus = remember { FocusRequester() }
|
||||
var editByPad by remember { mutableStateOf(false) }
|
||||
val dialogInput = LocalInputModeManager.current
|
||||
@@ -159,7 +160,7 @@ private fun GameEnvironmentEditor(byPad: Boolean, onClose: () -> Unit) {
|
||||
if (current == null && !error) Small(stringResource(R.string.game_env_loading))
|
||||
if (current != null) {
|
||||
val own = current.entries(scope)
|
||||
val entries = GameEnvironment.effective(current, preset, scope).toSortedMap()
|
||||
val entries = GameEnvironment.effective(current, preset, scope, forceSsbs).toSortedMap()
|
||||
if (entries.isEmpty()) Small(stringResource(R.string.game_env_empty))
|
||||
else Panel {
|
||||
entries.entries.forEachIndexed { i, (name, value) ->
|
||||
|
||||
@@ -70,6 +70,8 @@ class ModeSettings(
|
||||
/** Steam only: record allocation and sampled storage timings in the next session's Share logs. */
|
||||
val storageDiagnostics: Boolean = false,
|
||||
val fexPreset: String? = null,
|
||||
/** Steam only: Force SSBS for Proton games (SessionPrefs.forceSsbs). */
|
||||
val forceSsbs: Boolean = false,
|
||||
/** Steam only: SessionPrefs.SYNC_* chosen for Proton games; null outside Steam. */
|
||||
val syncBackend: String? = null,
|
||||
/** Steam only: forces game windows fullscreen (null = not a Steam page). */
|
||||
@@ -131,6 +133,7 @@ class ModeSettingsActions(
|
||||
val onPickGameStorageFolder: () -> Unit = {},
|
||||
val onStorageDiagnostics: (Boolean) -> Unit = {},
|
||||
val onFexPreset: (String) -> Unit = {},
|
||||
val onForceSsbs: (Boolean) -> Unit = {},
|
||||
val onSyncBackend: (String) -> Unit = {},
|
||||
val onForceFullscreen: (Boolean) -> Unit = {},
|
||||
val onStretch16x9: (Boolean) -> Unit = {},
|
||||
@@ -459,6 +462,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
FexPreset.all.map { it.id to stringResource(it.label) }, s.fexPreset,
|
||||
note = stringResource(FexPreset.byId(s.fexPreset).detail), onPick = a.onFexPreset,
|
||||
)
|
||||
ToggleRow(host, "ssbs", stringResource(R.string.force_ssbs_title), stringResource(R.string.force_ssbs_hint), s.forceSsbs, onChange = a.onForceSsbs)
|
||||
GameEnvironmentRow()
|
||||
|
||||
}
|
||||
|
||||
@@ -1052,4 +1052,6 @@
|
||||
<string name="device_info_copy">Copy</string>
|
||||
<string name="device_info_copy_hint">Copy these details into a bug report.</string>
|
||||
<string name="device_info_copied">Copied.</string>
|
||||
<string name="force_ssbs_title">Force SSBS</string>
|
||||
<string name="force_ssbs_hint">Keeps a CPU speed setting on that Wine keeps turning off. Can make Windows games faster on Snapdragon 8 Elite; little change seen on older chips. Applies on the next game launch.</string>
|
||||
</resources>
|
||||
@@ -11,6 +11,14 @@ class GameEnvironmentTest {
|
||||
assertEquals("6_9", values["VKD3D_SHADER_MODEL"])
|
||||
}
|
||||
|
||||
@Test fun forceSsbsIsADefaultAGameCanTurnOff() {
|
||||
assertFalse(GameEnvironment.defaults("").containsKey(GameEnvironment.FORCE_SSBS))
|
||||
assertEquals("1", GameEnvironment.defaults("", forceSsbs = true)[GameEnvironment.FORCE_SSBS])
|
||||
val config = GameEnvironment.Config().withEntries("42", mapOf(GameEnvironment.FORCE_SSBS to "0"))
|
||||
assertEquals("0", GameEnvironment.effective(config, "", "42", forceSsbs = true)[GameEnvironment.FORCE_SSBS])
|
||||
assertEquals("1", GameEnvironment.effective(config, "", "43", forceSsbs = true)[GameEnvironment.FORCE_SSBS])
|
||||
}
|
||||
|
||||
@Test fun gameOverridesSharedAndPresetWithoutLeakingToOtherGames() {
|
||||
val config = GameEnvironment.Config(
|
||||
shared = mapOf("FEX_MULTIBLOCK" to "0", "VKD3D_FEATURE_LEVEL" to "12_2"),
|
||||
|
||||
@@ -196,6 +196,30 @@ def without_pack(chosen_env, env):
|
||||
return result
|
||||
|
||||
|
||||
SSBS_LIB = "/usr/local/lib/libssbs.so"
|
||||
|
||||
|
||||
def force_ssbs(command, env):
|
||||
"""DROIDDECK_FORCE_SSBS=1 (a game's or the shared environment): preload libssbs.so into this
|
||||
Proton's Wine, which then keeps PSTATE.SSBS set across signal returns - speculative store
|
||||
bypass stays allowed, as force_ssbs.diff does inside a rebuilt ntdll. ARM64 Protons only: the
|
||||
library is aarch64 and only arm64 Wine has the signal path it fixes."""
|
||||
if env.get("DROIDDECK_FORCE_SSBS", "").lower() not in ("1", "on", "true", "yes"):
|
||||
return env
|
||||
proton = os.path.dirname(command[0]) if command else ""
|
||||
if not os.path.isdir(os.path.join(proton, "files/lib/wine/aarch64-unix")):
|
||||
print("droiddeck-game-env: force SSBS asked for, but this Proton is not an ARM64 one; left off", file=sys.stderr)
|
||||
return env
|
||||
if not os.path.isfile(SSBS_LIB):
|
||||
print("droiddeck-game-env: force SSBS asked for, but %s is missing; left off" % SSBS_LIB, file=sys.stderr)
|
||||
return env
|
||||
parts = [p for p in re.split(r"[:\s]+", env.get("LD_PRELOAD", "")) if p]
|
||||
if SSBS_LIB not in parts:
|
||||
env = dict(env, LD_PRELOAD=":".join(parts + [SSBS_LIB]))
|
||||
print("droiddeck-game-env: force SSBS on (libssbs.so preloaded)", file=sys.stderr)
|
||||
return env
|
||||
|
||||
|
||||
def main():
|
||||
command = sys.argv[1:]
|
||||
if not command:
|
||||
@@ -223,6 +247,7 @@ def main():
|
||||
env = apply_config(env, {"version": 1}, app_id, fixes)
|
||||
except (OSError, ValueError, TypeError, AttributeError):
|
||||
print("droiddeck-game-env: invalid configuration; using inherited environment", file=sys.stderr)
|
||||
env = force_ssbs(command, env)
|
||||
sync = load_sync()
|
||||
launch, launch_env = select_sync(command, env, sync)
|
||||
if launch[0] != command[0]:
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* libssbs.so - keep PSTATE.SSBS set across signal returns, without rebuilding Wine.
|
||||
*
|
||||
* Wine's arm64 signal code writes PSTATE back from a Windows CONTEXT, whose Cpsr never carries
|
||||
* SSBS (bit 12). Every signal return through that code (exception fixups, NtSetContextThread on
|
||||
* another thread, the SIGUSR2 suspend path) therefore resumes the thread with SSBS clear, so the
|
||||
* CPU runs it with speculative store bypass disabled - slower on cores such as Oryon.
|
||||
* force_ssbs.diff (Sonicadvance1, FEX) fixes this inside ntdll; this library does the same from
|
||||
* outside, for Protons that cannot be rebuilt: the Valve, GE and CachyOS ARM64 builds DroidDeck
|
||||
* runs, which come prebuilt from Steam. Same source as Bannerlator's adapter (bionic), built here
|
||||
* for glibc.
|
||||
*
|
||||
* How: interpose sigaction()/signal() via LD_PRELOAD. Every handler is installed behind one
|
||||
* SA_SIGINFO trampoline that calls the real handler and then sets SSBS in the ucontext it is
|
||||
* about to return through. The kernel accepts SSBS from userspace on sigreturn and context-
|
||||
* switches it per thread, so the bit sticks until the next sigreturn. Handlers that never return
|
||||
* (siglongjmp) are unaffected either way.
|
||||
*
|
||||
* Env:
|
||||
* SSBS_ADAPTER_DISABLE=1 pass everything through untouched.
|
||||
* SSBS_ADAPTER_OBSERVE=1 wrap and count, but leave SSBS as the handler left it (the baseline).
|
||||
* SSBS_ADAPTER_DEBUG=1 log to stderr (implied by OBSERVE):
|
||||
* - "returned with SSBS clear": a handler was about to resume a thread without SSBS.
|
||||
* - "arrived with SSBS clear": a thread was RUNNING without SSBS when a signal reached it.
|
||||
* Under the adapter this should stop after each thread's first fixed return; if it keeps
|
||||
* counting, something other than a signal return is clearing the bit.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <signal.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ucontext.h>
|
||||
|
||||
#define PSTATE_SSBS 0x1000ULL
|
||||
#define MAX_SIG 65
|
||||
|
||||
typedef void (*sigact_fn)(int, siginfo_t *, void *);
|
||||
typedef void (*sighand_fn)(int);
|
||||
typedef int (*sigaction_fn)(int, const struct sigaction *, struct sigaction *);
|
||||
|
||||
/* Per signal: the handler the program asked for, and whether it wanted SA_SIGINFO. */
|
||||
static _Atomic(void *) real_handler[MAX_SIG];
|
||||
static atomic_bool real_siginfo[MAX_SIG];
|
||||
|
||||
static sigaction_fn next_sigaction;
|
||||
static int disabled;
|
||||
static int observe;
|
||||
static int debug;
|
||||
static atomic_ulong n_returns, n_fixed, n_arrived, n_arrived_clear;
|
||||
|
||||
static void init(void)
|
||||
{
|
||||
if (next_sigaction) return;
|
||||
next_sigaction = (sigaction_fn)dlsym(RTLD_NEXT, "sigaction");
|
||||
const char *e = getenv("SSBS_ADAPTER_DISABLE");
|
||||
disabled = e && *e == '1';
|
||||
e = getenv("SSBS_ADAPTER_OBSERVE");
|
||||
observe = e && *e == '1';
|
||||
e = getenv("SSBS_ADAPTER_DEBUG");
|
||||
debug = observe || (e && *e == '1');
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void ctor(void)
|
||||
{
|
||||
init();
|
||||
if (debug) fprintf(stderr, "libssbs: loaded (%s)\n", disabled ? "disabled" : observe ? "observing only" : "active");
|
||||
}
|
||||
|
||||
/* 1, 2, 4, 8, ...: enough lines to see a trend without flooding the log. */
|
||||
static int milestone(unsigned long n) { return (n & (n - 1)) == 0; }
|
||||
|
||||
static void trampoline(int sig, siginfo_t *info, void *uc_v)
|
||||
{
|
||||
ucontext_t *uc = uc_v;
|
||||
if (debug) {
|
||||
unsigned long a = atomic_fetch_add(&n_arrived, 1) + 1;
|
||||
if (!(uc->uc_mcontext.pstate & PSTATE_SSBS)) {
|
||||
unsigned long c = atomic_fetch_add(&n_arrived_clear, 1) + 1;
|
||||
if (milestone(c))
|
||||
fprintf(stderr, "libssbs: sig %d arrived with SSBS clear (%lu of %lu arrivals)\n", sig, c, a);
|
||||
}
|
||||
}
|
||||
void *h = atomic_load_explicit(&real_handler[sig], memory_order_acquire);
|
||||
if (h) {
|
||||
if (atomic_load_explicit(&real_siginfo[sig], memory_order_relaxed))
|
||||
((sigact_fn)h)(sig, info, uc_v);
|
||||
else
|
||||
((sighand_fn)h)(sig);
|
||||
}
|
||||
if (debug) {
|
||||
unsigned long r = atomic_fetch_add(&n_returns, 1) + 1;
|
||||
if (!(uc->uc_mcontext.pstate & PSTATE_SSBS)) {
|
||||
unsigned long f = atomic_fetch_add(&n_fixed, 1) + 1;
|
||||
if (milestone(f))
|
||||
fprintf(stderr, "libssbs: sig %d returned with SSBS clear, %s (%lu of %lu returns)\n",
|
||||
sig, observe ? "left as is" : "set it", f, r);
|
||||
}
|
||||
}
|
||||
if (!observe) uc->uc_mcontext.pstate |= PSTATE_SSBS;
|
||||
}
|
||||
|
||||
static int wrapped(void *h)
|
||||
{
|
||||
return h != (void *)SIG_DFL && h != (void *)SIG_IGN && h != (void *)SIG_ERR;
|
||||
}
|
||||
|
||||
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact)
|
||||
{
|
||||
init();
|
||||
if (disabled || sig <= 0 || sig >= MAX_SIG)
|
||||
return next_sigaction(sig, act, oldact);
|
||||
|
||||
struct sigaction mine;
|
||||
const struct sigaction *pass = act;
|
||||
void *new_h = NULL;
|
||||
int new_info = 0;
|
||||
if (act) {
|
||||
new_info = (act->sa_flags & SA_SIGINFO) != 0;
|
||||
new_h = new_info ? (void *)act->sa_sigaction : (void *)act->sa_handler;
|
||||
if (wrapped(new_h)) {
|
||||
mine = *act;
|
||||
mine.sa_flags |= SA_SIGINFO;
|
||||
mine.sa_sigaction = trampoline;
|
||||
pass = &mine;
|
||||
}
|
||||
}
|
||||
|
||||
void *prev_h = atomic_load(&real_handler[sig]);
|
||||
int prev_info = atomic_load(&real_siginfo[sig]);
|
||||
/* Publish before installing so the trampoline never runs with a stale handler. */
|
||||
if (act && wrapped(new_h)) {
|
||||
atomic_store(&real_siginfo[sig], new_info);
|
||||
atomic_store_explicit(&real_handler[sig], new_h, memory_order_release);
|
||||
}
|
||||
|
||||
int ret = next_sigaction(sig, pass, oldact);
|
||||
if (ret != 0 && act && wrapped(new_h)) {
|
||||
atomic_store(&real_siginfo[sig], prev_info);
|
||||
atomic_store(&real_handler[sig], prev_h);
|
||||
}
|
||||
|
||||
/* Report the caller's own handler, not the trampoline, so handler chaining keeps working. */
|
||||
if (ret == 0 && oldact && oldact->sa_sigaction == trampoline && prev_h) {
|
||||
if (prev_info) {
|
||||
oldact->sa_sigaction = (sigact_fn)prev_h;
|
||||
} else {
|
||||
oldact->sa_flags &= ~SA_SIGINFO;
|
||||
oldact->sa_handler = (sighand_fn)prev_h;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
sighandler_t signal(int sig, sighandler_t handler)
|
||||
{
|
||||
struct sigaction sa, old;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_handler = handler;
|
||||
sa.sa_flags = SA_RESTART;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
if (sigaction(sig, &sa, &old) != 0) return SIG_ERR;
|
||||
return (old.sa_flags & SA_SIGINFO) ? (sighandler_t)(void *)old.sa_sigaction : old.sa_handler;
|
||||
}
|
||||
|
||||
sighandler_t bsd_signal(int sig, sighandler_t handler) { return signal(sig, handler); }
|
||||
|
||||
/* glibc's System V flavour: a one-shot handler with no restart. */
|
||||
sighandler_t sysv_signal(int sig, sighandler_t handler)
|
||||
{
|
||||
struct sigaction sa, old;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_handler = handler;
|
||||
sa.sa_flags = SA_RESETHAND | SA_NODEFER;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
if (sigaction(sig, &sa, &old) != 0) return SIG_ERR;
|
||||
return (old.sa_flags & SA_SIGINFO) ? (sighandler_t)(void *)old.sa_sigaction : old.sa_handler;
|
||||
}
|
||||
@@ -461,3 +461,27 @@ class DirectAudioPrefixTest(unittest.TestCase):
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
class ForceSsbsTest(unittest.TestCase):
|
||||
def test_libssbs_is_preloaded_only_when_asked_and_only_into_arm64_proton(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
arm = Path(tmp) / "arm"
|
||||
(arm / "files/lib/wine/aarch64-unix").mkdir(parents=True)
|
||||
x86 = Path(tmp) / "x86"
|
||||
(x86 / "files/lib/wine/x86_64-unix").mkdir(parents=True)
|
||||
lib = Path(tmp) / "libssbs.so"
|
||||
lib.write_bytes(b"\x7fELF")
|
||||
g = MODULE["force_ssbs"].__globals__
|
||||
old = g["SSBS_LIB"]
|
||||
g["SSBS_LIB"] = str(lib)
|
||||
try:
|
||||
env = {"LD_PRELOAD": "/usr/local/lib/libblsession.so"}
|
||||
self.assertEqual(MODULE["force_ssbs"]([str(arm / "proton"), "waitforexitandrun"], env), env)
|
||||
on = dict(env, DROIDDECK_FORCE_SSBS="1")
|
||||
got = MODULE["force_ssbs"]([str(arm / "proton"), "waitforexitandrun"], on)
|
||||
self.assertEqual(got["LD_PRELOAD"], "/usr/local/lib/libblsession.so:" + str(lib))
|
||||
self.assertEqual(MODULE["force_ssbs"]([str(arm / "proton")], got)["LD_PRELOAD"], got["LD_PRELOAD"])
|
||||
self.assertEqual(MODULE["force_ssbs"]([str(x86 / "proton")], on), on)
|
||||
finally:
|
||||
g["SSBS_LIB"] = old
|
||||
Reference in new issue
Block a user