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
- Break the cycle: return a value the caller acts on, instead of calling back into it
- Move shared logic into a third application that neither one re-enters
- 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
- Design cross-application flows acyclically: A may call B, B must not call A
- Return results to the caller instead of notifying it via a call-back
- Audit the application call graph for cycles whenever adding a new cross-app call
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
- Contracts cannot call themselves
- BytecodeTooLarge
- MaximumFuelExceeded
- CrossApplicationCallInFinalize
- StreamNameTooLong
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/3d1cfa6a415bdafa.
Report an issue: GitHub.