linera-io/linera-protocol · error · ExecutionError

ReentrantCall

ReentrantCall

Error message

ExecutionError::ReentrantCall(application_id)

What it means

check_for_reentrancy keeps the set of applications currently on the call stack (active_applications); if the callee id is already in that set, the call fails with ReentrantCall (runtime.rs:391). Linera forbids any application from appearing twice in one call chain, direct self-calls and longer cycles alike.

Source

Thrown at linera-execution/src/runtime.rs:391

    /// Ensures the application's ID is also removed from the `active_applications` set.
    ///
    /// # Panics
    ///
    /// If the call stack is empty.
    fn pop_application(&mut self) -> ApplicationStatus {
        let status = self
            .call_stack
            .pop()
            .expect("Can't remove application from empty call stack");
        assert!(self.active_applications.remove(&status.id));
        status
    }

    /// Ensures that a call to `application_id` is not-reentrant.
    ///
    /// Returns an error if there already is an entry for `application_id` in the call stack.
    fn check_for_reentrancy(&self, application_id: ApplicationId) -> Result<(), ExecutionError> {
        ensure!(
            !self.active_applications.contains(&application_id),
            ExecutionError::ReentrantCall(application_id)
        );
        Ok(())
    }
}

impl SyncRuntimeInternal<UserContractInstance> {
    /// Loads a contract instance, initializing it with this runtime if needed.
    #[instrument(skip_all, fields(application_id = %id))]
    fn load_contract_instance(
        &mut self,
        this: SyncRuntimeHandle<UserContractInstance>,
        id: ApplicationId,
    ) -> Result<LoadedApplication<UserContractInstance>, ExecutionError> {
        match self.loaded_applications.entry(id) {
            hash_map::Entry::Occupied(entry) => Ok(entry.get().clone()),

View on GitHub (pinned to 6c226ddcb3)

Solutions

  1. Break the cycle: return a value the caller acts on, instead of calling back into it
  2. Move shared logic into a third application that neither one re-enters
  3. Audit the cross-application call graph for cycles before adding a new edge

Example fix

// before: B calls back into A (A -> B -> A)
let result = runtime.try_call_application(a_id, payload)?;

// after: B returns or records the payload; A continues on its own
self.emit_outcome(payload);
Defensive patterns

Strategy: validation

Validate before calling

// Track applications already on the call stack; refuse cycles before calling
struct CallGuard { active: HashSet<ApplicationId> }
impl CallGuard {
    fn may_call(&self, callee: ApplicationId) -> Result<(), String> {
        if self.active.contains(&callee) {
            return Err(format!("call cycle through application {:?}", callee));
        }
        Ok(())
    }
}

Type guard

fn is_reentrant_call(err: &ExecutionError) -> bool {
    matches!(err, ExecutionError::ReentrantCall(_))
}

Try / catch

match runtime.try_call_application(callee, arg) {
    Ok(out) => out,
    Err(ref e) if is_reentrant_call(e) => {
        // cycle detected: return data instead of calling back
        Ok(default_response_for(arg))
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: try_call_application (via prepare_for_call) where the target application id is already active on the call stack: app A calls B which calls back into A, or A calls itself through the runtime.

Common situations: Circular application designs such as a token app calling a marketplace that calls back the token; callback or notification patterns where the callee informs the caller; refactors that introduce a call cycle between cooperating applications.

Related errors


AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22). Data as JSON: /api/errors/3d1cfa6a415bdafa. Report an issue: GitHub.