fix(soter): harden on-demand mount recovery

The supervisor mounted the forge on the happy path but could not
re-attempt: mount() returned silently on inject/handshake failure, and a
live-but-uninjected binding never died to trigger a rebind, stranding the
forge for the life of that soterserver process (audit F1).

Route every unmounted outcome through scheduleRetry(): inject failure,
post-inject handshake-null, and register failure now schedule a re-bind
instead of returning. Add onNullBinding (F2) and exponential backoff
capped at 30s, reset on a clean mount (F3). register() now returns
whether the transact succeeded so mount() retries on a false reply (F4);
existing keystore callers ignore the new return.

Audit remediation. compileDebugKotlin clean.
This commit is contained in:
Enginex0
2026-06-26 16:57:12 +01:00
parent a4875dc200
commit 5ddd8137df
2 changed files with 53 additions and 18 deletions
@@ -305,20 +305,26 @@ abstract class BinderInterceptor : Binder() {
target: IBinder,
interceptor: BinderInterceptor,
filteredCodes: IntArray = intArrayOf(),
) {
): Boolean {
val data = Parcel.obtain()
val reply = Parcel.obtain()
try {
return try {
data.writeStrongBinder(target)
data.writeStrongBinder(interceptor)
data.writeInt(filteredCodes.size)
for (code in filteredCodes) data.writeInt(code)
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
SystemLogger.info(
"Registered interceptor for target: $target (${filteredCodes.size} filtered codes)"
)
val ok = backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
if (ok) {
SystemLogger.info(
"Registered interceptor for target: $target (${filteredCodes.size} filtered codes)"
)
} else {
SystemLogger.error("Register transact returned false for target: $target")
}
ok
} catch (e: Exception) {
SystemLogger.error("Failed to register binder interceptor.", e)
false
} finally {
data.recycle()
reply.recycle()
@@ -45,9 +45,13 @@ object SoterProcessSupervisor {
"exec ./inject `pidof $SOTER_PACKAGE` libTEESimulator.so entry"
private const val REBIND_DELAY_MS = 1000L
private const val REBIND_MAX_MS = 30_000L
private val started = AtomicBoolean(false)
/** Re-bind backoff; doubles each failed (re)bind up to [REBIND_MAX_MS], resets on a clean mount. Handler-thread-confined. */
private var rebindDelay = REBIND_DELAY_MS
private lateinit var context: Context
private lateinit var handler: Handler
@@ -74,12 +78,17 @@ object SoterProcessSupervisor {
override fun onServiceDisconnected(name: ComponentName?) {
SystemLogger.debug("SOTER service disconnected (process died); rebinding")
rebind()
scheduleRetry()
}
override fun onBindingDied(name: ComponentName?) {
SystemLogger.debug("SOTER binding died; rebinding")
rebind()
scheduleRetry()
}
override fun onNullBinding(name: ComponentName?) {
SystemLogger.debug("SOTER onBind returned null; rebinding")
scheduleRetry()
}
}
@@ -97,13 +106,24 @@ object SoterProcessSupervisor {
SystemLogger.debug("SOTER bind requested (on-demand poke)")
} else {
SystemLogger.debug("SOTER bindService returned false; retrying")
handler.postDelayed({ rebind() }, REBIND_DELAY_MS)
scheduleRetry()
}
}
private fun rebind() {
runCatching { context.unbindService(connection) }
handler.postDelayed({ bind() }, REBIND_DELAY_MS)
bind()
}
/**
* Re-attempts the bind after the current backoff, then widens it (capped at [REBIND_MAX_MS]).
* Every path that fails to leave the forge mounted routes here, so a live-but-uninjected
* binding is re-attempted instead of stranding the forge. A clean [mount] resets the backoff.
*/
private fun scheduleRetry() {
val delay = rebindDelay
rebindDelay = (rebindDelay * 2).coerceAtMost(REBIND_MAX_MS)
handler.postDelayed({ rebind() }, delay)
}
/** Confirms injection via the `0xdeadbeef` handshake, injecting first if absent, then registers. */
@@ -112,21 +132,30 @@ object SoterProcessSupervisor {
if (backdoor == null) {
SystemLogger.debug("SOTER backdoor absent; injecting libTEESimulator.so")
if (!injectLibrary()) {
SystemLogger.debug("SOTER injection failed; will retry on next (re)bind")
SystemLogger.debug("SOTER injection failed; scheduling re-bind")
scheduleRetry()
return
}
backdoor = BinderInterceptor.getBackdoor(soterBinder)
}
if (backdoor == null) {
SystemLogger.debug("SOTER backdoor handshake failed after injection")
SystemLogger.debug("SOTER backdoor handshake failed after injection; scheduling re-bind")
scheduleRetry()
return
}
BinderInterceptor.register(
backdoor,
soterBinder,
SoterServiceInterceptor,
SoterServiceInterceptor.interceptedCodes,
)
val registered =
BinderInterceptor.register(
backdoor,
soterBinder,
SoterServiceInterceptor,
SoterServiceInterceptor.interceptedCodes,
)
if (!registered) {
SystemLogger.debug("SOTER register failed; scheduling re-bind")
scheduleRetry()
return
}
rebindDelay = REBIND_DELAY_MS
SystemLogger.debug("SOTER forge mounted; handshake ok")
}