Documents
RAII Scope Guards For Evaluator Context
RAII Scope Guards For Evaluator Context
Type
Topic
Status
Published
Created
Apr 25, 2026
Updated
Apr 25, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

RAII Scope Guards for Evaluator Context#

libmagic-rs's evaluator uses three Drop-based RAII guards—SubroutineScope, AnchorScope, and RecursionGuard—to save and restore EvaluationContext fields during scoped operations. The guards ensure that context state is restored on all exit paths, including early returns triggered by Rust's ? operator. SubroutineScope and AnchorScope live in src/evaluator/engine/mod.rs; RecursionGuard lives in src/evaluator/mod.rs. The pattern was introduced in issue #42, PR #230.

Why Manual Save/Restore Breaks With ?#

Rust's ? operator is early-return sugar: when a fallible function returns Err, ? immediately returns that error from the enclosing function. Any lines below the ? are unreachable on the error path. This is not a logic bug—it is a structural property of the operator.

When a function manually saves context state (let saved_x = context.x()), modifies it (context.set_x(new_val)), then calls a fallible operation (operation()?), any manual restore line after the ? (context.set_x(saved_x)) is silently skipped on every error path. The restore only fires when the fallible operation succeeds, leaving the context corrupted on errors like LibmagicError::Timeout or RecursionLimitExceeded.

This bug shipped in libmagic-rs passing 1348 tests because no test exercised RecursionLimitExceeded or Timeout inside a MetaType::Use body. The guards fix this by moving the restore logic into a Drop implementation, which Rust guarantees fires on normal return, ?-triggered early return, and panic unwind.

Guard Summary#

GuardFileSavesUsed By
SubroutineScopeengine/mod.rs last_match_end, base_offsetMetaType::Use via evaluate_use_rule
AnchorScopeengine/mod.rs last_match_end, base_offsetMetaType::Indirect re-entry
RecursionGuardmod.rs recursion_depthAll child rule evaluation

The Guards#

SubroutineScope#

SubroutineScope manages context state during MetaType::Use subroutine evaluation . The struct holds saved copies of last_match_end and base_offset . The enter method saves both fields and seeds them with the use-site offset:

fn enter(context: &'a mut EvaluationContext, use_site: usize) -> Self {
    let saved_anchor = context.last_match_end();
    let saved_base = context.base_offset();
    context.set_last_match_end(use_site);
    context.set_base_offset(use_site);
    Self { context, saved_anchor, saved_base }
}

The Drop implementation restores both fields:

impl Drop for SubroutineScope<'_> {
    fn drop(&mut self) {
        self.context.set_last_match_end(self.saved_anchor);
        self.context.set_base_offset(self.saved_base);
    }
}

SubroutineScope is used in evaluate_use_rule:

let (subroutine_matches, terminal_anchor) = {
    let mut scope = SubroutineScope::enter(context, absolute_offset);
    let mut guard = RecursionGuard::enter(scope.context())?;
    let matches = evaluate_rules(&subroutine_rules, buffer, guard.context())?;
    let terminal = guard.context().last_match_end();
    (matches, terminal)
};

The guard wraps the fallible RecursionGuard::enter and evaluate_rules calls. If either returns Err, the ? operator triggers an early return through SubroutineScope::drop, which restores the caller's anchor and base offset.

AnchorScope#

AnchorScope manages context state during MetaType::Indirect re-entry . The struct saves last_match_end and base_offset, seeds the anchor with a new value, and resets the base offset to 0 :

fn enter(context: &'a mut EvaluationContext, new_anchor: usize) -> Self {
    let saved_anchor = context.last_match_end();
    let saved_base = context.base_offset();
    context.set_last_match_end(new_anchor);
    context.set_base_offset(0);
    Self { context, saved_anchor, saved_base }
}

The Drop implementation restores both fields. AnchorScope is used for MetaType::Indirect re-entry to ensure that the root rule list is evaluated with top-level semantics (base_offset = 0). Without this guard, a non-zero base_offset from an outer MetaType::Use subroutine would leak into the re-entered database, biasing absolute offsets and causing incorrect reads .

RecursionGuard#

RecursionGuard manages recursion depth during child rule evaluation . The struct holds a mutable reference to the context . The enter method increments recursion depth and can fail with RecursionLimitExceeded:

pub(crate) fn enter(context: &'a mut EvaluationContext) -> Result<Self, LibmagicError> {
    context.increment_recursion_depth()?;
    Ok(Self { context })
}

The Drop implementation decrements the depth and uses debug_assert to verify the invariant that the depth was successfully incremented by the corresponding enter call:

impl Drop for RecursionGuard<'_> {
    fn drop(&mut self) {
        let result = self.context.decrement_recursion_depth();
        debug_assert!(
            result.is_ok(),
            "RecursionGuard invariant violated: decrement failed after successful enter()"
        );
    }
}

EvaluationContext Fields Under Guard#

The guards protect two fields in EvaluationContext:

  • last_match_end : the GNU file anchor for relative offset resolution. Updated to the end of the most recent successful match. The value may increase or decrease as successive rules match at different positions; it is not a high-watermark.

  • base_offset : normally 0. During a MetaType::Use body evaluation, set to the use-site offset so that the subroutine's OffsetSpec::Absolute(n) rules resolve to base + n. The doc comment explicitly references SubroutineScope as the guard responsible for restore.

Canonical Smell Pattern#

The canonical smell for this bug is a three-part sequence:

  1. let saved_x = context.x() — save shared mutable state
  2. context.set_x(new_val) — modify the state
  3. Any ? operator — fallible operation
  4. context.set_x(saved_x) — manual restore (unreachable on Err)

The single checklist question is: "Is shared mutable state modified before a fallible operation and restored manually afterward?"

A Semgrep rule could detect this shape: let saved_$X = ...; ...; ...?; ...; ..set_$X(saved_$X). Clippy has no built-in lint for this pattern.

Ghost-Reference Anti-Pattern#

The base_offset doc comment names SubroutineScope—this is correct in the current code. The original version of this doc comment named a BaseOffsetScope guard that never existed. The comment was written as future-tense design intent before the guard was implemented.

The rule: grep for every type named in a doc comment. If the type is absent, the doc comment is design intent, not implementation. Any type reference in a doc comment should be validated against the codebase.

AtomicBool::swap Once-Guard Idiom#

USE_WITHOUT_RULE_ENV_WARNED is a process-local once-guard :

static USE_WITHOUT_RULE_ENV_WARNED: AtomicBool = AtomicBool::new(false);

The guard is used at lines 303–309:

if USE_WITHOUT_RULE_ENV_WARNED.swap(true, Ordering::Relaxed) {
    debug!("use directive '{name}' evaluated without a rule environment; no-op");
} else {
    warn!(
        "use directive '{name}' evaluated without a rule environment; treating as no-op (subsequent occurrences suppressed)"
    );
}

AtomicBool::swap(true, Ordering::Relaxed) returns the previous value. A false return means this is the first call, so the code emits warn!. A true return means a subsequent call, so the code emits debug!. Common review false positive: reading swap(true) as "returns true" rather than "returns the previous value".

The idiomatic form is:

let already_warned = X.swap(true, Ordering::Relaxed);
if already_warned {
    debug!(...)
} else {
    warn!(...)
}

Extension Rule#

When adding a field to EvaluationContext that participates in scoped evaluation, first ask: "Does a RAII guard already exist?" If yes, add the field to the existing guard. If no, create the guard before writing any manual save/restore logic.

  • docs/solutions/integration-issues/meta-type-subroutine-dispatch-architecture.md — sibling document that predates SubroutineScope; needs pointer update to the canonical implementation
  • docs/solutions/security-issues/pstring-anchor-poisoning.md — different failure mode of the same last_match_end field
  • GOTCHAS S3.8, S3.10, S14.2
RAII Scope Guards For Evaluator Context | Dosu