mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
build: update Arti to 2.6.0, jni to 0.22, NDK r30, Rust 1.98.1
Moves every pin in tools/arti-build forward and rebuilds all three shipped libraries from them. Arti 2.5.0 carried two medium-severity fixes that a client reaches in normal use, so the shipped 2.3.0 was the reason to do this now: TROVE-2026-24, where a malicious directory mirror crashes the tor-netdoc parser and eventually stops tor-dirmgr, and TROVE-2026-27, an inefficient algorithm an attacker can drive into a CPU stall. Pins: - Arti 2.3.0 -> 2.6.0 (arti-client / tor-rtcompat 0.42 -> 0.46). New MSRV is 1.91, satisfied by the pinned toolchain. - Android NDK 27.3.13750724 (r27d) -> 30.0.16248370 (r30), the current LTS. clang and lld move 18.0.4 -> 21.0.0. - rustc 1.94.1 -> 1.98.1. - jni 0.21 -> 0.22. - Cargo.lock regenerated, so every transitive dependency moves to its latest semver-compatible release. cargo-ndk was already on the pinned 4.1.2. Source changes the upgrades required: - Arti 2.4.0 made every TorClient constructor return an Arc<TorClient> and dropped Clone from TorClient, so the wrapper no longer wraps it itself. - jni 0.22 splits the FFI environment pointer (EnvUnowned) from the API type (Env), which is only borrowed inside a closure. Native methods now acquire it via with_env and map failures through an ErrorPolicy instead of unwinding out of extern "C", which aborts. initialize() reads everything JNI-owned up front and resolves through Option<String>, because the policy default for jint is 0, the value that API reports as success. GlobalRef became Global<JObject>, thread attachment takes a closure (which also scopes a local-reference frame per log line), and the method name and signature are encoded at compile time via jni_str! / jni_sig!. Verification: - verify-reproducible.sh: two clean builds byte-for-byte identical, JNI symbols exported, 16 KiB LOAD alignment kept, both ABIs stamped r30, same libc/libm/libdl dependency set as before. - JVM tier-3 smoke test passes against the rebuilt host shim, and a scratch harness drove setLogCallback, getVersion, initialize and destroy through real JNI: log lines arrive over the migrated callback and initialize returns 0. - cargo audit: rsa 0.9.10 (RUSTSEC-2023-0071) remains, with no fixed version published upstream; it arrives via ssh-key-fork-arti and needs RSA private key operations, which a client without hosted onion services never does. The event-listener unsound and spin yanked warnings are gone. Not verified here: the network-dependent integration tier and the on-device instrumented test. This container blocks most outbound TCP (directory authority port 9131 among them), so Tor circuits time out regardless of which library is loaded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011cSuXeu4bUTNRAUCZJcLLW
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
||||
27.3.13750724
|
||||
30.0.16248370
|
||||
|
||||
@@ -1 +1 @@
|
||||
arti-v2.3.0
|
||||
arti-v2.6.0
|
||||
|
||||
Generated
+821
-597
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "arti-android"
|
||||
version = "2.3.0"
|
||||
version = "2.6.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
@@ -9,20 +9,20 @@ crate-type = ["cdylib"]
|
||||
[workspace]
|
||||
|
||||
[dependencies]
|
||||
arti-client = { version = "0.42", default-features = false, features = [
|
||||
arti-client = { version = "0.46", default-features = false, features = [
|
||||
"tokio",
|
||||
"rustls",
|
||||
"compression",
|
||||
"onion-service-client",
|
||||
"static-sqlite",
|
||||
] }
|
||||
tor-rtcompat = { version = "0.42", default-features = false, features = ["tokio", "rustls"] }
|
||||
tor-rtcompat = { version = "0.46", default-features = false, features = ["tokio", "rustls"] }
|
||||
# Direct dep on rustls so we can install the `ring` crypto provider ourselves —
|
||||
# arti-v2.3.0's tor-rtcompat no longer installs one implicitly. `ring` matches
|
||||
# what arti-v2.2.0 effectively used and avoids the Android build pain of
|
||||
# aws-lc-rs (which became Arti's default in 2.3.0).
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
||||
jni = "0.21"
|
||||
jni = "0.22"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "net", "io-util", "time", "macros"] }
|
||||
anyhow = "1"
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ JNI wrapper built directly from Arti source.
|
||||
| **Size** | ~140MB | ~11MB |
|
||||
| **16KB pages** | No | Yes (rustc aligns Android targets to 16 KiB) |
|
||||
| **Stop/restart** | Broken (state file lock) | Works (TorClient persists, only SOCKS proxy stops) |
|
||||
| **Version** | Behind | Pinned to latest (currently 1.9.0) |
|
||||
| **Version** | Behind | Pinned to latest (see [`ARTI_VERSION`](ARTI_VERSION)) |
|
||||
|
||||
## Quick start
|
||||
|
||||
@@ -98,7 +98,7 @@ repo is checked out.
|
||||
```
|
||||
|
||||
4. **Android NDK** — the exact revision in [`ANDROID_NDK_VERSION`](ANDROID_NDK_VERSION)
|
||||
(currently **27.3.13750724**, r27d). Any other revision is refused: it would
|
||||
(currently **30.0.16248370**, r30). Any other revision is refused: it would
|
||||
produce a `.so` that does not match the committed one.
|
||||
```bash
|
||||
# Via Android Studio: SDK Manager → SDK Tools → NDK (Side by side)
|
||||
@@ -168,7 +168,7 @@ readelf -p .note.android.ident amethyst/src/main/jniLibs/arm64-v8a/libarti_andro
|
||||
readelf -p .comment amethyst/src/main/jniLibs/arm64-v8a/libarti_android.so
|
||||
```
|
||||
|
||||
For the pinned toolchain that prints `r27d` / `13750724`, clang 18.0.4 and the
|
||||
For the pinned toolchain that prints `r30` / `16248370`, clang 21.0.0 and the
|
||||
`rustc` version from `rust-toolchain.toml`. `build-arti.sh` runs the first check
|
||||
itself after every build.
|
||||
|
||||
@@ -177,7 +177,7 @@ itself after every build.
|
||||
```
|
||||
tools/arti-build/
|
||||
├── README.md # This file
|
||||
├── ARTI_VERSION # Pinned Arti git tag (e.g., arti-v1.9.0)
|
||||
├── ARTI_VERSION # Pinned Arti git tag (e.g., arti-v2.6.0)
|
||||
├── ANDROID_NDK_VERSION # Pinned NDK revision — enforced by build-arti.sh (reproducibility)
|
||||
├── CARGO_NDK_VERSION # cargo-ndk release the pinned output was verified with
|
||||
├── rust-toolchain.toml # Pinned rustc version + Android targets (reproducibility)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# build scripts. Bump this in lockstep with ARTI_VERSION / Cargo.lock and
|
||||
# re-verify (see README.md → "Reproducible builds").
|
||||
[toolchain]
|
||||
channel = "1.94.1"
|
||||
channel = "1.98.1"
|
||||
profile = "minimal"
|
||||
components = ["rustc", "cargo", "rust-std"]
|
||||
# Only the two ABIs we actually ship libarti_android.so for (see jniLibs/). If
|
||||
|
||||
+94
-54
@@ -1,6 +1,7 @@
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JString, JObject, GlobalRef};
|
||||
use jni::sys::{jint, jstring};
|
||||
use jni::{jni_sig, jni_str, EnvUnowned};
|
||||
use jni::errors::{LogErrorAndDefault, Result as JniResult, ThrowRuntimeExAndDefault};
|
||||
use jni::objects::{Global, JClass, JObject, JString};
|
||||
use jni::sys::jint;
|
||||
use jni::JavaVM;
|
||||
|
||||
use arti_client::{BootstrapBehavior, TorClient};
|
||||
@@ -21,7 +22,7 @@ use anyhow::Result;
|
||||
static ARTI_CLIENT: Mutex<Option<Arc<TorClient<PreferredRuntime>>>> = Mutex::new(None);
|
||||
static TOKIO_RUNTIME: Mutex<Option<tokio::runtime::Runtime>> = Mutex::new(None);
|
||||
static JAVA_VM: Mutex<Option<JavaVM>> = Mutex::new(None);
|
||||
static LOG_CALLBACK: Mutex<Option<GlobalRef>> = Mutex::new(None);
|
||||
static LOG_CALLBACK: Mutex<Option<Global<JObject<'static>>>> = Mutex::new(None);
|
||||
static SOCKS_TASK: Mutex<Option<tokio::task::JoinHandle<()>>> = Mutex::new(None);
|
||||
// The background directory download started by initialize(). It holds an Arc<TorClient>, so
|
||||
// destroy() must abort it too — otherwise the client cannot drop, the state file lock is never
|
||||
@@ -42,16 +43,21 @@ fn send_log_to_java(message: String) {
|
||||
let callback_opt = LOG_CALLBACK.lock().unwrap();
|
||||
|
||||
if let (Some(vm), Some(callback)) = (vm_opt.as_ref(), callback_opt.as_ref()) {
|
||||
if let Ok(mut env) = vm.attach_current_thread() {
|
||||
// jni 0.22 only hands out an `Env` inside a closure, borrowed from an
|
||||
// attachment pinned to the stack; it also pushes a local-reference frame
|
||||
// per call, so `jmessage` is released when the closure returns instead of
|
||||
// accumulating on this long-lived logging thread.
|
||||
let _ = vm.attach_current_thread(|env| -> JniResult<()> {
|
||||
if let Ok(jmessage) = env.new_string(&message) {
|
||||
let _ = env.call_method(
|
||||
callback.as_obj(),
|
||||
"onLogLine",
|
||||
"(Ljava/lang/String;)V",
|
||||
&[(&jmessage).into()]
|
||||
&**callback,
|
||||
jni_str!("onLogLine"),
|
||||
jni_sig!("(Ljava/lang/String;)V"),
|
||||
&[(&jmessage).into()],
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,52 +80,87 @@ macro_rules! log_error {
|
||||
// ============================================================================
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_getVersion(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
) -> jstring {
|
||||
if JAVA_VM.lock().unwrap().is_none() {
|
||||
if let Ok(vm) = env.get_java_vm() {
|
||||
*JAVA_VM.lock().unwrap() = Some(vm);
|
||||
pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_getVersion<'caller>(
|
||||
mut env: EnvUnowned<'caller>,
|
||||
_class: JClass<'caller>,
|
||||
) -> JString<'caller> {
|
||||
// jni 0.22: the raw environment pointer is FFI-only (`EnvUnowned`); JNI calls
|
||||
// need the `Env` that `with_env` borrows for the closure. `resolve` maps an
|
||||
// error to the policy — here a Java RuntimeException plus a null return —
|
||||
// instead of unwinding out of `extern "C"`, which aborts the process.
|
||||
env.with_env(|env| -> JniResult<JString<'caller>> {
|
||||
if JAVA_VM.lock().unwrap().is_none() {
|
||||
if let Ok(vm) = env.get_java_vm() {
|
||||
*JAVA_VM.lock().unwrap() = Some(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let version = format!("Arti {} (custom build with rustls)", env!("CARGO_PKG_VERSION"));
|
||||
let output = env.new_string(version).expect("Couldn't create java string!");
|
||||
output.into_raw()
|
||||
let version = format!("Arti {} (custom build with rustls)", env!("CARGO_PKG_VERSION"));
|
||||
env.new_string(version)
|
||||
})
|
||||
.resolve::<ThrowRuntimeExAndDefault>()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_setLogCallback(
|
||||
env: JNIEnv,
|
||||
_class: JClass,
|
||||
callback: JObject,
|
||||
pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_setLogCallback<'caller>(
|
||||
mut env: EnvUnowned<'caller>,
|
||||
_class: JClass<'caller>,
|
||||
callback: JObject<'caller>,
|
||||
) {
|
||||
if JAVA_VM.lock().unwrap().is_none() {
|
||||
if let Ok(vm) = env.get_java_vm() {
|
||||
*JAVA_VM.lock().unwrap() = Some(vm);
|
||||
env.with_env(|env| -> JniResult<()> {
|
||||
if JAVA_VM.lock().unwrap().is_none() {
|
||||
if let Ok(vm) = env.get_java_vm() {
|
||||
*JAVA_VM.lock().unwrap() = Some(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(global_ref) = env.new_global_ref(callback) {
|
||||
*LOG_CALLBACK.lock().unwrap() = Some(global_ref);
|
||||
log_info!("Log callback registered");
|
||||
}
|
||||
if let Ok(global_ref) = env.new_global_ref(&callback) {
|
||||
*LOG_CALLBACK.lock().unwrap() = Some(global_ref);
|
||||
log_info!("Log callback registered");
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.resolve::<ThrowRuntimeExAndDefault>()
|
||||
}
|
||||
|
||||
/// Initialize Arti runtime and bootstrap the TorClient.
|
||||
/// The TorClient is created once and reused for the app's lifetime.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_initialize(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
data_dir: JString,
|
||||
pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_initialize<'caller>(
|
||||
mut env: EnvUnowned<'caller>,
|
||||
_class: JClass<'caller>,
|
||||
data_dir: JString<'caller>,
|
||||
) -> jint {
|
||||
if JAVA_VM.lock().unwrap().is_none() {
|
||||
if let Ok(vm) = env.get_java_vm() {
|
||||
*JAVA_VM.lock().unwrap() = Some(vm);
|
||||
}
|
||||
}
|
||||
// Everything JNI-owned is read inside this closure; the rest of the function
|
||||
// is pure Rust that blocks on Tokio, which must not hold an `Env`.
|
||||
//
|
||||
// The policy only applies to a panic or an `Err` returned here, and `None`
|
||||
// is `Option::default()`, so both of those land on the same `-1` the old
|
||||
// `Err` arm returned. A jint policy default would have been `0`, which this
|
||||
// API reports as success.
|
||||
let data_dir_str: Option<String> = env
|
||||
.with_env(|env| -> JniResult<Option<String>> {
|
||||
if JAVA_VM.lock().unwrap().is_none() {
|
||||
if let Ok(vm) = env.get_java_vm() {
|
||||
*JAVA_VM.lock().unwrap() = Some(vm);
|
||||
}
|
||||
}
|
||||
|
||||
// Already initialized — the caller-visible "reuse" path still has to
|
||||
// run below, so signal it with an empty string rather than here.
|
||||
if ARTI_CLIENT.lock().unwrap().is_some() {
|
||||
return Ok(Some(String::new()));
|
||||
}
|
||||
|
||||
Ok(match data_dir.try_to_string(env) {
|
||||
Ok(s) => Some(s),
|
||||
Err(e) => {
|
||||
log_error!("Failed to convert data_dir: {:?}", e);
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.resolve::<LogErrorAndDefault>();
|
||||
|
||||
// Already initialized — skip
|
||||
if ARTI_CLIENT.lock().unwrap().is_some() {
|
||||
@@ -127,12 +168,9 @@ pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_initialize(
|
||||
return 0;
|
||||
}
|
||||
|
||||
let data_dir_str: String = match env.get_string(&data_dir) {
|
||||
Ok(s) => s.into(),
|
||||
Err(e) => {
|
||||
log_error!("Failed to convert data_dir: {:?}", e);
|
||||
return -1;
|
||||
}
|
||||
let data_dir_str: String = match data_dir_str {
|
||||
Some(s) => s,
|
||||
None => return -1,
|
||||
};
|
||||
|
||||
log_info!("Initializing Arti with data directory: {}", data_dir_str);
|
||||
@@ -220,7 +258,9 @@ pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_initialize(
|
||||
.create_unbootstrapped_async()
|
||||
.await
|
||||
{
|
||||
Ok(c) => Arc::new(c),
|
||||
// Arti 2.4.0 made every TorClient constructor return an Arc<TorClient>
|
||||
// (TorClient itself is no longer Clone), so there is nothing to wrap here.
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log_error!("Failed to create Tor client: {:?}", e);
|
||||
return -3;
|
||||
@@ -267,7 +307,7 @@ pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_initialize(
|
||||
/// Can be called multiple times — stops any existing listener first.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_startSocksProxy(
|
||||
_env: JNIEnv,
|
||||
_env: EnvUnowned,
|
||||
_class: JClass,
|
||||
port: jint,
|
||||
) -> jint {
|
||||
@@ -494,7 +534,7 @@ async fn handle_socks_connection(
|
||||
/// Stop the SOCKS proxy listener. The TorClient stays alive.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_stopSocksProxy(
|
||||
_env: JNIEnv,
|
||||
_env: EnvUnowned,
|
||||
_class: JClass,
|
||||
) -> jint {
|
||||
log_info!("Stopping SOCKS proxy...");
|
||||
@@ -532,7 +572,7 @@ pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_stopSocksPro
|
||||
/// measurable signal for a guess.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_bootstrapProgressPermille(
|
||||
_env: JNIEnv,
|
||||
_env: EnvUnowned,
|
||||
_class: JClass,
|
||||
) -> jint {
|
||||
match ARTI_CLIENT.lock().unwrap().as_ref() {
|
||||
@@ -550,7 +590,7 @@ pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_bootstrapPro
|
||||
/// leaving the UI wrong and the exit-rotation self-heal disabled.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_isBootstrapped(
|
||||
_env: JNIEnv,
|
||||
_env: EnvUnowned,
|
||||
_class: JClass,
|
||||
) -> jint {
|
||||
match ARTI_CLIENT.lock().unwrap().as_ref() {
|
||||
@@ -573,7 +613,7 @@ pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_isBootstrapp
|
||||
/// (and re-bootstrap).
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_com_vitorpamplona_amethyst_ui_tor_ArtiNative_destroy(
|
||||
_env: JNIEnv,
|
||||
_env: EnvUnowned,
|
||||
_class: JClass,
|
||||
) -> jint {
|
||||
log_info!("Destroying Arti client");
|
||||
|
||||
Reference in New Issue
Block a user