Skip to content

dash-spv: SPV storage init fails on Android — File::try_lock() is unsupported on target_os="android" #840

Description

@QuantumExplorer

Summary

dash-spv fails to initialize its storage on Android (target_os = "android") because LockFile::new() calls std::fs::File::try_lock(), which the Rust standard library does not implement on Android — it is a compile-time stub that unconditionally returns ErrorKind::Unsupported. dash-spv treats that error as fatal, so SPV sync can never start on Android.

Observed at runtime on a real device/emulator as:

SPV error: Write failed: Failed to acquire lock: try_lock() not supported

This blocks the entire Core (SPV) layer on Android — no header/filter sync, no balances, no L1 send, and by extension anything that depends on a funded Core wallet (asset-lock funding, etc.). iOS and desktop are unaffected.

Environment

  • Target: aarch64-linux-android (also affects x86_64-linux-android), Android 15 / API 35
  • Rust: 1.92.0 (behavior is the same on every std since file-locking stabilized in 1.89)
  • Consumer: Android JNI build of dash-spv (via dashpay/platform rs-unified-sdk-jnirs-platform-walletdash-spv)

Root cause

1. std::fs::File::try_lock() is unsupported on target_os = "android"

The flock-based implementation in std (library/std/src/sys/fs/unix.rs) is gated on a fixed target list that includes linux and Apple, but not android:

#[cfg(any(
    target_os = "freebsd",
    target_os = "fuchsia",
    target_os = "linux",      // <-- Android is a SEPARATE target_os, not covered
    target_os = "netbsd",
    target_os = "openbsd",
    target_os = "cygwin",
    target_os = "illumos",
    target_vendor = "apple",  // <-- this is why iOS/macOS work
))]
pub fn try_lock(&self) -> Result<(), TryLockError> { /* real flock(LOCK_EX|LOCK_NB) */ }

Android therefore hits the fallback stub:

#[cfg(not(any(/* ...same list... */)))]
pub fn try_lock(&self) -> Result<(), TryLockError> {
    Err(TryLockError::Error(io::const_error!(
        io::ErrorKind::Unsupported,
        "try_lock() not supported",
    )))
}

So on Android this is a compile-time guarantee of failure, not a runtime filesystem quirk. (lock, lock_shared, try_lock_shared are gated identically and are also unsupported on Android.)

2. dash-spv treats the Unsupported error as fatal

dash-spv/src/storage/lockfile.rs:

file.try_lock().map_err(|e| match e {
    std::fs::TryLockError::WouldBlock => StorageError::DirectoryLocked(format!(
        "Data directory '{}' is already in use by another process",
        path.parent().map(|p| p.display().to_string()).unwrap_or_default()
    )),
    std::fs::TryLockError::Error(io_err) => {
        StorageError::WriteFailed(format!("Failed to acquire lock: {}", io_err))  // <-- Android lands here
    }
})?;

The TryLockError::Error(io_err) arm does not distinguish ErrorKind::Unsupported from a genuine I/O failure, so storage init returns Err and SPV never starts.

Impact

  • Android: Core/SPV completely non-functional — storage cannot be opened.
  • iOS, macOS, Linux desktop: unaffected (their targets implement flock).

The lock file is a best-effort guard against multiple processes sharing one data directory. On mobile the SDK runs as a single app process that owns its data dir, so the advisory lock provides no value there and its absence is safe.

Proposed fix

Treat Unsupported as non-fatal in LockFile::new() — skip the advisory lock (with a warning) and continue:

match file.try_lock() {
    Ok(()) => {}
    Err(std::fs::TryLockError::WouldBlock) => {
        return Err(StorageError::DirectoryLocked(format!(
            "Data directory '{}' is already in use by another process",
            path.parent().map(|p| p.display().to_string()).unwrap_or_default()
        )));
    }
    // Platforms without advisory file locking (e.g. Android) — single-process,
    // so proceeding without the cross-process guard is safe.
    Err(std::fs::TryLockError::Error(io_err))
        if io_err.kind() == std::io::ErrorKind::Unsupported =>
    {
        tracing::warn!(
            "advisory file lock not supported on this platform ({io_err}); \
             continuing without multi-process protection"
        );
    }
    Err(std::fs::TryLockError::Error(io_err)) => {
        return Err(StorageError::WriteFailed(format!(
            "Failed to acquire lock: {io_err}"
        )));
    }
}

Alternatives considered:

  • #[cfg(target_os = "android")]-gate the whole lock — works, but handling ErrorKind::Unsupported generically is more robust (covers any non-flock target and keeps the fast path unchanged on desktop).

Happy to open a PR with the graceful-degradation approach plus a small test if that's the preferred direction.

Repro

  1. Build dash-spv for aarch64-linux-android and open a storage instance whose data dir triggers LockFile::new().
  2. Storage init returns StorageError::WriteFailed("Failed to acquire lock: try_lock() not supported").

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions