astrid-runtime/astrid · error

FUSE lease is expired

Error message

FUSE lease is expired

What it means

Every mount lease carries an expiry timestamp (`expires_at_epoch_secs`). Before starting the FUSE service, the helper compares it to the current UNIX time and refuses to mount with a lease that has already expired, ensuring stale grants cannot create mounts.

Source

Thrown at crates/astrid-storage-provider-fuse/src/service.rs:216

    if parent.start_identity.is_none() {
        bail!("FUSE service parent start identity is required on Linux");
    }
    Ok(())
}

fn validate_lease(lease: &StorageMountLeaseV1) -> Result<()> {
    if lease.lease_token.len() < 16
        || lease.lease_token.len() > 4096
        || lease.lease_token.chars().any(char::is_control)
    {
        bail!("invalid FUSE callback token");
    }
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .context("read system clock")?
        .as_secs();
    if lease.expires_at_epoch_secs < now {
        bail!("FUSE lease is expired");
    }
    if !lease.resource_path.is_absolute() || !lease.callback_path.is_absolute() {
        bail!("FUSE lease paths must be absolute");
    }
    if lease.callback_path != lease.resource_path.join("control.sock") {
        bail!("FUSE callback path is not the kernel lease endpoint");
    }
    platform_fs::validate_private_directory(&lease.resource_path)
        .context("validate private FUSE lease resource")?;
    platform_fs::verify_no_redirects(&lease.resource_path)
        .context("reject redirected FUSE lease resource")?;
    let manifest_path = lease.resource_path.join("lease.json");
    platform_fs::validate_private_file(&manifest_path)
        .context("validate private FUSE lease manifest")?;
    let manifest = std::fs::read(&manifest_path).context("read FUSE lease manifest")?;
    if manifest.len() > 64 * 1024 {
        bail!("FUSE lease manifest exceeds the bounded size");
    }

View on GitHub (pinned to affd8760f4)

Solutions

  1. Request a fresh lease from the broker and retry the mount.
  2. Fix system clock (NTP sync) if skew or resume drift caused the mismatch.
  3. Re-issue the lease with a longer TTL for slow/unreliable startup paths.
  4. Check that the expiry epoch is computed in seconds (not millis) when the lease is created.
  5. Don't cache lease.json across runs — reload it at mount time.

Example fix

// before
expires_at_epoch_secs: now + 60, // expires during slow startup
// after
expires_at_epoch_secs: now + 3600, // longer TTL
// or: refresh the lease before mounting
let lease = broker.refresh_lease(&mount_id).await?;
Defensive patterns

Strategy: validation

Validate before calling

let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs();
if lease.expires_at_epoch_secs < now + 60 {
    lease = broker.refresh_lease(&lease).await?; // refresh if near expiry
}

Type guard

fn lease_is_current(lease: &StorageMountLeaseV1) -> bool {
    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
    lease.expires_at_epoch_secs >= now
}

Try / catch

match mount(&lease).await {
    Err(e) if e.to_string().contains("lease is expired") => {
        let fresh = broker.refresh_lease(&lease).await?;
        mount(&fresh).await
    }
    other => other,
}

Prevention

When it happens

Trigger: `validate_lease` computes `now` from `SystemTime::now()` and finds `lease.expires_at_epoch_secs < now` during `validate_launch`.

Common situations: Clock skew between the machine issuing the lease and the machine mounting (VM/container clock drift); a cached lease.json reused after its TTL; suspended/resumed laptop with a short-lived lease; lease generated with a wrong (past) expiry.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/092f8cda66211b92. Report an issue: GitHub.