firecracker-microvm/firecracker · error

Error converting cpu id to Tid

Error message

Error converting cpu id to Tid

What it means

Panic in the GDB blocking event loop (event_loop.rs:63): `Tid::new(cpu_id).expect("Error converting cpu id to Tid")`. In gdbstub, `Tid::new` returns None for an id of 0 because the GDB remote protocol treats thread ids as nonzero. The code converts the vcpu's raw id directly — vcpu ids are 0-indexed, so a debug exit reported by vcpu 0 produces tid 0 and panics. Note `get_raw_tid` in target.rs correctly does `cpu_id + 1`; this call site does not.

Source

Thrown at src/vmm/src/gdb/event_loop.rs:63

    type StopReason = MultiThreadStopReason<u64>;

    /// Poll for events from either Vcpu's or packets from the GDB connection
    fn wait_for_stop_reason(
        target: &mut FirecrackerTarget,
        conn: &mut Self::Connection,
    ) -> Result<
        run_blocking::Event<MultiThreadStopReason<u64>>,
        run_blocking::WaitForStopReasonError<
            <Self::Target as Target>::Error,
            <Self::Connection as Connection>::Error,
        >,
    > {
        loop {
            match target.gdb_event.try_recv() {
                Ok(cpu_id) => {
                    // The Vcpu reports it's id from raw_id so we straight convert here
                    let tid = Tid::new(cpu_id).expect("Error converting cpu id to Tid");
                    // If notify paused returns false this means we were already debugging a single
                    // core, the target will track this for us to pick up later
                    target.set_paused_vcpu(tid);
                    trace!("Vcpu: {tid:?} paused from debug exit");

                    let stop_reason = target
                        .get_stop_reason(tid)
                        .map_err(WaitForStopReasonError::Target)?;

                    let Some(stop_response) = stop_reason else {
                        // If we returned None this is a break which should be handled by
                        // the guest kernel (e.g. kernel int3 self testing) so we won't notify
                        // GDB and instead inject this back into the guest
                        target
                            .inject_bp_to_guest(tid)
                            .map_err(WaitForStopReasonError::Target)?;
                        target
                            .resume_vcpu(tid)

View on GitHub (pinned to 0a745def42)

Solutions

  1. Apply the 1-indexing used elsewhere in the file: `Tid::new(get_raw_tid(cpu_id))` (i.e. cpu_id + 1) before constructing the Tid
  2. Until fixed, avoid the affected path (initial breakpoint on vcpu 0) — e.g. don't enable the gdb feature on versions with this bug
  3. Add a regression test that feeds cpu_id 0 through the event loop

Example fix

// before
let tid = Tid::new(cpu_id).expect("Error converting cpu id to Tid");

// after (vcpu ids are 0-indexed; GDB tids are 1-indexed — see get_raw_tid)
let tid = Tid::new(get_raw_tid(cpu_id)).expect("Error converting cpu id to Tid");
Defensive patterns

Strategy: type-guard

Validate before calling

// Map 0-indexed vcpu ids to 1-indexed GDB tids BEFORE constructing Tid
let tid_value = cpu_id + 1; // get_raw_tid semantics
if tid_value == 0 { /* unreachable after +1, kept as guard */ return; }

Type guard

fn to_valid_tid(cpu_id: usize) -> Option<Tid> {
    // GDB remote protocol thread ids are nonzero; vcpu ids are 0-indexed
    Tid::new(cpu_id.checked_add(1)?)
}

Prevention

When it happens

Trigger: Any KVM debug-exit event delivered by vcpu 0 (the typical case — the entry breakpoint is set on vcpu 0) reaches `Tid::new(0)`, which returns None and trips the expect.

Common situations: Connecting GDB and hitting the initial entry breakpoint on the first core; single-stepping or breaking on CPU 0 of a multi-vcpu guest. Anyone using the gdb feature on a single-vcpu microvm hits it deterministically.

Related errors


AI-assisted analysis of firecracker-microvm/firecracker@0a745def42 (2026-08-19). Data as JSON: /api/errors/1843a5f4abdcf45e. Report an issue: GitHub.