fix: bound Arti bootstrap with a 60s timeout so a hostile network can't wedge Tor

A self-heal reset()/resetWithCleanState() (and the lifecycle serialization added
in the previous commit) can only recover Tor if initialize() actually returns.
ArtiNative.initialize() calls TorClient::create_bootstrapped, which on a hostile
network (unreachable guards, wiped consensus) retries internally for many
minutes. While it blocks it holds lifecycleMutex, so the watchdog's reset can
never run — Tor stays wedged at Connecting.

Wrap create_bootstrapped in a 60s tokio::time::timeout. On timeout the future is
dropped (tearing down the half-built client) and initialize() returns -4; the
JNI ABI is unchanged (still one String arg), so the checked-in CI host .so and
TorArtiNativeIntegrationTest keep working without a rebuild. TorService treats
-4 specially: drop the init flag and leave status Connecting (don't wipe+retry
inline under the lock, don't go Off) so TorManager's self-heal watchdog resets
and re-inits on its own cadence, and connectionFailure can still surface the
"use regular connection" dialog.

Rebuilt libarti_android.so for arm64-v8a + x86_64.

Verified on device: a no-network cold-start bootstrap timed out at exactly 60s
(previously hung 7+ min), released the lock, and on network restore the watchdog
re-init'd and Tor reached Active. Addresses #3225.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-06-16 10:41:09 -04:00
co-authored by Claude Opus 4.8
parent bdfc56cf23
commit 69b8ba9f23
4 changed files with 57 additions and 18 deletions
@@ -36,6 +36,13 @@ import java.util.concurrent.atomic.AtomicBoolean
private const val DEFAULT_SOCKS_PORT = 17392
private const val MAX_PORT_RETRIES = 10
/**
* Return code from [ArtiNative.initialize] when the native bootstrap exceeds its
* internal timeout. The native side has already torn down the half-built client;
* we treat this differently from a hard failure (see [TorService.start]).
*/
private const val ARTI_ERROR_BOOTSTRAP_TIMEOUT = -4
/**
* Manages the Arti Tor client via custom JNI bindings.
*
@@ -220,6 +227,19 @@ class TorService(
Log.d("TorService") { "Initializing Arti with data dir: $dataDir" }
var initResult = ArtiNative.initialize(dataDir)
if (initResult == ARTI_ERROR_BOOTSTRAP_TIMEOUT) {
// The native bootstrap hit its timeout (hostile network) and
// already tore down the half-built client. Don't wipe state or
// retry inline — that would hold lifecycleMutex for another full
// timeout. Drop the init flag and leave status at Connecting so
// TorManager's self-heal watchdog resets and retries on its own
// cadence (and the connection-failure dialog can still surface).
Log.w("TorService") { "Arti bootstrap timed out — leaving Connecting for the self-heal watchdog to retry" }
initialized.set(false)
return@withContext
}
if (initResult != 0) {
Log.e("TorService") { "Failed to initialize Arti: error $initResult, clearing data and retrying" }
clearAllArtiData()
Binary file not shown.
+37 -18
View File
@@ -167,7 +167,15 @@ pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_initialize(
std::fs::create_dir_all(&cache_dir).ok();
std::fs::create_dir_all(&state_dir).ok();
let result: Result<()> = runtime.block_on(async {
// Bound the bootstrap so a hostile network (unreachable guards, wiped
// consensus) can't block this JNI call — and the Kotlin-side lifecycle lock
// it holds — indefinitely. On timeout the `create_bootstrapped` future is
// dropped, tearing down the partially built client, and -4 is returned so
// TorService can leave status Connecting and let the self-heal watchdog
// retry instead of wedging. The ABI is unchanged (still one String arg).
const BOOTSTRAP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
let outcome: jint = runtime.block_on(async {
log_info!("Creating Arti client...");
let mut builder = TorClientConfigBuilder::from_directories(state_dir, cache_dir);
@@ -184,27 +192,38 @@ pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_initialize(
builder.storage().permissions().dangerously_trust_everyone();
}
let config = builder.build()?;
let config = match builder.build() {
Ok(c) => c,
Err(e) => {
log_error!("Failed to build Tor config: {:?}", e);
return -3;
}
};
let client = TorClient::create_bootstrapped(config).await?;
log_info!("Arti client created and bootstrapped");
*ARTI_CLIENT.lock().unwrap() = Some(Arc::new(client));
Ok(())
match tokio::time::timeout(BOOTSTRAP_TIMEOUT, TorClient::create_bootstrapped(config)).await {
Ok(Ok(client)) => {
log_info!("Arti client created and bootstrapped");
*ARTI_CLIENT.lock().unwrap() = Some(Arc::new(client));
0
}
Ok(Err(e)) => {
log_error!("Failed to bootstrap Tor client: {:?}", e);
-3
}
Err(_elapsed) => {
log_error!(
"Tor bootstrap timed out after {}s — aborting so the client can be retried",
BOOTSTRAP_TIMEOUT.as_secs()
);
-4
}
}
});
match result {
Ok(_) => {
log_info!("Arti initialized successfully");
0
}
Err(e) => {
log_error!("Failed to initialize Arti: {:?}", e);
-3
}
if outcome == 0 {
log_info!("Arti initialized successfully");
}
outcome
}
/// Start the SOCKS5 proxy on the specified port.