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
- 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
- Until fixed, avoid the affected path (initial breakpoint on vcpu 0) — e.g. don't enable the gdb feature on versions with this bug
- 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
- Always use get_raw_tid(cpu_id) when crossing the vcpu-id <-> GDB-tid boundary
- Fuzz the event loop with cpu_id 0 in unit tests
- Document the 1-indexing invariant next to every Tid::new call site
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.