tursodatabase/turso · error · anyhow::Error

MVCC logical log extension record count without extension bl

Error message

MVCC logical log extension record count without extension block at offset {offset}

What it means

An extension-header frame with extension_size == 0 (u64 at +24) must also have extension_record_count == 0 (u32 at +32). The two fields describe the same optional extension block; when they disagree the frame header is internally inconsistent, so the validator stops the scan rather than trust a trailer offset computed from contradictory metadata.

Source

Thrown at cli/sync_server.rs:1063

        MVCC_TX_HEADER_SIZE
    };
    if log.len() - offset < header_size + MVCC_TX_TRAILER_SIZE {
        return Ok(None);
    }
    let payload_size = usize::try_from(read_u64_le(log, offset + 4)?)
        .map_err(|_| anyhow!("MVCC logical log payload size overflows usize"))?;
    let extension_size = if has_extension_header {
        let extension_size = usize::try_from(read_u64_le(log, offset + 24)?)
            .map_err(|_| anyhow!("MVCC logical log extension size overflows usize"))?;
        let extension_record_count = read_u32_le(log, offset + 32)?;
        let frame_flags = read_u32_le(log, offset + 36)?;
        if frame_flags & !MVCC_TX_FRAME_FLAG_HAS_EXTENSION_BLOCK != 0 {
            return Err(anyhow!(
                "unsupported MVCC logical log frame flags at offset {offset}: {frame_flags:#x}"
            ));
        }
        if extension_size == 0 && extension_record_count != 0 {
            return Err(anyhow!(
                "MVCC logical log extension record count without extension block at offset {offset}"
            ));
        }
        if extension_size > 0 && frame_flags & MVCC_TX_FRAME_FLAG_HAS_EXTENSION_BLOCK == 0 {
            return Err(anyhow!(
                "MVCC logical log extension block missing flag at offset {offset}"
            ));
        }
        extension_size
    } else {
        0
    };
    let trailer_start = offset
        .checked_add(header_size)
        .and_then(|value| value.checked_add(payload_size))
        .and_then(|value| value.checked_add(extension_size))
        .ok_or_else(|| anyhow!("MVCC logical log frame offset overflow"))?;
    let frame_end = trailer_start

View on GitHub (pinned to bad083fafb)

Solutions

  1. Re-pull or re-bootstrap the log; size/count disagreement is unrecoverable corruption.
  2. If you write a log producer, zero extension_record_count whenever extension_size is 0.
  3. Check whether a partial write crashed mid-frame on the writer side (producer logs, fsync failures).

Example fix

// before
let snapshot = scan_mvcc_log(&log)?; // errors: extension record count without extension block

// after (log producer): keep the two extension fields consistent when building frames
if extension_bytes.is_empty() {
    extension_record_count = 0;
}
write_frame_header(&mut out, payload, extension_bytes, extension_record_count, flags);
Defensive patterns

Strategy: try-catch

Validate before calling

fn extension_fields_consistent(log: &[u8], offset: usize) -> bool {
    let size = log.get(offset + 24..offset + 32)
        .map(|b| u64::from_le_bytes(b.try_into().unwrap()));
    let count = log.get(offset + 32..offset + 36)
        .map(|b| u32::from_le_bytes(b.try_into().unwrap()));
    match (size, count) {
        (Some(0), Some(c)) => c == 0,
        (Some(_), Some(_)) => true,
        _ => false,
    }
}

Type guard

fn extension_count_matches_size(log: &[u8], offset: usize) -> bool {
    let size = log.get(offset + 24..offset + 32)
        .map(|b| u64::from_le_bytes(b.try_into().unwrap()));
    let count = log.get(offset + 32..offset + 36)
        .map(|b| u32::from_le_bytes(b.try_into().unwrap()));
    !(size == Some(0) && count.is_some_and(|c| c != 0))
}

Try / catch

match scan_mvcc_log(&log) {
    Ok(snapshot) => { /* serve deltas */ }
    Err(err) if err.to_string().contains("extension record count without extension block") => {
        // internally inconsistent frame header: re-bootstrap the log
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: Scanning an extension frame where extension_size is 0 but extension_record_count is nonzero — corruption of those bytes, or a hand-rolled log producer (test tool, migration script) that writes a record count without emitting an extension block.

Common situations: Test and tooling code that builds extension frames field-by-field; partial frame writes after a crash; fuzzing the extension-header path with mismatched fields.

Related errors


AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16). Data as JSON: /api/errors/af998367bdb2f3f4. Report an issue: GitHub.