How to Fix: Cranelift: wasmtime tests crash when logging is enabled

7 min read

When RUST_LOG=trace makes Wasmtime tests crash, the failure is usually not in the string roundtrip logic at all—it is a re-entrancy or environment-sensitive bug triggered by logging during test execution.

The symptom is easy to reproduce on current main with a single-threaded test run:

RUST_LOG=trace cargo test -j 1 "component_model::strings::roundtrip"

At first glance this looks like a regression in Cranelift or the component model, but the real issue is typically that enabling very verbose logs changes execution timing, allocation patterns, and sometimes host/runtime interactions deeply enough to expose code paths that are otherwise silent. In the Wasmtime test stack, that can surface as a crash when tracing is emitted during compilation, instantiation, or lowering/lifting of component values.

Reproduce the crash

Start by confirming the problem under the narrowest possible conditions. Running with -j 1 is important because it removes parallel test noise and makes the logging interaction easier to reason about.

git checkout main
cargo clean
RUST_LOG=trace cargo test -j 1 "component_model::strings::roundtrip" -- --nocapture

If the crash only appears with logging enabled, that is a strong sign the bug is tied to instrumentation side effects rather than the semantic behavior of the test itself.

Understanding the Root Cause

This class of failure usually happens because trace-level logging is not a passive observer. In a Wasmtime and Cranelift code path, enabling logging can change several low-level behaviors:

  • Re-entrancy during formatting: trace logs often format complex values. If a logged value indirectly touches runtime state, symbolization, or debug helpers, it can execute code in a context that was assumed to be non-reentrant.
  • Allocation pressure: trace logging allocates strings, buffers, and metadata. That can expose latent memory-safety issues, invalid assumptions about borrowing, or ordering bugs.
  • Timing and sequencing changes: code that appears deterministic without logs may behave differently once heavy tracing shifts when drops, locks, or host calls happen.
  • Global logger interactions: tests often share process-wide logging configuration. If a test or helper initializes logging more than once, or uses a subscriber that is not safe for the exact call path, crashes can appear only when logging is active.
  • Compiler/runtime debug hooks: Cranelift and Wasmtime both have internal debug instrumentation. Some logging paths may inspect intermediate compiler structures or component data in ways that are safe only outside a fragile execution window.

For this specific scenario, the most practical interpretation is: the roundtrip test is exposing a logging-sensitive path in the runtime or compiler pipeline. The test itself is just the trigger.

That is why the issue title points at Cranelift even though the failing test lives under Wasmtime’s component_model::strings suite. The crash is often downstream of compilation or runtime machinery that logging touches.

Step-by-Step Solution

The safest fix strategy is to reduce the logging surface, isolate the subsystem, and then patch or guard the problematic instrumentation.

1. Verify whether the crash is caused by broad trace logging

Instead of enabling trace logs for the entire process, scope logging to the relevant modules only.

RUST_LOG=wasmtime=trace cargo test -j 1 "component_model::strings::roundtrip"
RUST_LOG=cranelift=trace cargo test -j 1 "component_model::strings::roundtrip"
RUST_LOG=info,wasmtime::component=trace cargo test -j 1 "component_model::strings::roundtrip"

If one of these works while global trace crashes, the bug is likely in another module’s formatting or subscriber path.

2. Capture a backtrace and preserve stderr

You need a stack trace before changing code. Run the test with backtraces enabled:

RUST_BACKTRACE=1 RUST_LOG=trace cargo test -j 1 "component_model::strings::roundtrip" -- --nocapture

For deeper stack information:

RUST_BACKTRACE=full RUST_LOG=trace cargo test -j 1 "component_model::strings::roundtrip" -- --nocapture

Look for frames involving:

  • tracing or log subscribers
  • Cranelift debug or IR formatting
  • Wasmtime component model string lifting/lowering
  • host calls, allocation routines, or panics inside formatting code

3. Bisect the logging path in code

If you are working on the repository directly, temporarily disable suspicious trace statements near compilation or component string conversion boundaries. Focus on logs that print complex structures with {:?} or custom Display/Debug implementations.

// Example pattern to search for:
trace!("lowering string value: {:?}", value);
trace!("compiled function: {:?}", func);
trace!("component state: {:?}", state);

Then replace them with safer, cheaper logging:

trace!("lowering string value; len={}", value.len());
trace!("compiled function generated");
trace!("component state updated");

This matters because formatting rich internal structures can trigger deep traversal, borrow conflicts, or accidental recursion.

4. Guard logging from execution-sensitive code paths

If a trace statement sits inside a path that must not allocate or re-enter runtime machinery, move it outward or convert it into a lighter event.

// Risky: logs while interacting with fragile runtime state
fn roundtrip_string(value: &str) {
    trace!("roundtrip start: {:?}", value);
    // lowering/lifting/runtime interaction
}

// Safer: log metadata before entering the sensitive section
fn roundtrip_string(value: &str) {
    let len = value.len();
    trace!("roundtrip start; len={}", len);
    // lowering/lifting/runtime interaction
}

5. Confirm logger initialization is not happening multiple times

In Rust test environments, a common hidden problem is repeated initialization of the global logger or subscriber. Make sure test helpers use a Once-style guard.

use std::sync::Once;

static INIT: Once = Once::new();

fn init_logging() {
    INIT.call_once(|| {
        let _ = env_logger::try_init();
    });
}

If the project uses tracing_subscriber:

use std::sync::Once;

static INIT: Once = Once::new();

fn init_tracing() {
    INIT.call_once(|| {
        let _ = tracing_subscriber::fmt()
            .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
            .try_init();
    });
}

Even when the immediate symptom is a crash rather than a clean panic, duplicated global initialization can produce unstable behavior in tests that exercise runtime internals.

6. Narrow the issue to Cranelift versus component-model runtime

Try disabling or reducing compilation-related debug output if possible, then compare behavior. If the crash disappears when compiler logs are reduced, the issue is probably tied to Cranelift IR/debug formatting. If it remains only in component string tests, focus on lifting/lowering and memory access boundaries.

# Narrow to runtime-oriented logs
RUST_LOG=wasmtime::component=trace,wasmtime::runtime=trace cargo test -j 1 "component_model::strings::roundtrip"

# Narrow to compiler-oriented logs
RUST_LOG=cranelift_codegen=trace,cranelift_wasm=trace cargo test -j 1 "component_model::strings::roundtrip"

7. Apply the durable fix

Once the offending path is identified, the long-term fix is usually one of these:

  • remove or reduce trace logs that format fragile internal state
  • log only primitive metadata such as lengths, IDs, offsets, and counts
  • move logging outside execution-critical regions
  • avoid logging values whose Debug implementation walks runtime-owned data
  • ensure global logger/subscriber initialization is one-time and test-safe

After patching, rerun:

cargo test -j 1 "component_model::strings::roundtrip"
RUST_LOG=trace cargo test -j 1 "component_model::strings::roundtrip"
RUST_LOG=trace cargo test -j 1 component_model::strings -- --nocapture

Common Edge Cases

  • Only trace fails, debug works: this strongly suggests a specific high-volume or deep-formatting log statement is responsible.
  • The crash disappears under gdb or lldb: debugger timing changes can mask race-adjacent or re-entrancy-sensitive behavior.
  • The failure only happens with –nocapture: stdout/stderr handling can affect timing and buffering enough to expose the bug differently.
  • The issue reproduces only in debug builds: optimization level changes stack layout, inlining, and drop timing; do not assume release behavior rules out a real bug.
  • Backtrace points into formatting code: do not dismiss that as incidental. In logging-sensitive crashes, formatting code is often the direct trigger.
  • Only one specific test fails: string roundtrip tests are good at exposing memory boundary and component ABI issues because they cross lowering/lifting layers repeatedly.

FAQ

Why does the test pass without logging but crash with RUST_LOG=trace?

Because trace logging changes program behavior in subtle but real ways: extra allocations, formatting, lock usage, and timing can expose latent bugs in runtime or compiler code paths.

Is this definitely a Cranelift bug?

No. The issue title points at Cranelift because compiler internals are often involved, but the root cause may live in Wasmtime runtime logging, component string handling, or a shared tracing path. You need the backtrace and log scoping tests to confirm ownership.

What is the safest immediate workaround while waiting for a proper fix?

Avoid global RUST_LOG=trace. Use narrower filters such as RUST_LOG=wasmtime=debug or module-specific trace targets so you still get useful diagnostics without activating the crashing path.

The key takeaway is simple: when a Wasmtime test crashes only with logging enabled, treat the logger as part of the execution environment. In this issue, the practical fix is to identify the specific trace path that formats or touches unstable internal state, reduce that instrumentation, and keep logging outside sensitive Cranelift or component-runtime transitions.

Leave a Reply

Your email address will not be published. Required fields are marked *