tursodatabase/turso · error

invalid MVCC logical log frame magic at offset

Error message

invalid MVCC logical log frame magic at offset {offset}: {frame_magic:#x}

What it means

While walking frames, read_mvcc_frame_boundary requires the u32 at the current offset to be either MVCC_TX_FRAME_MAGIC or MVCC_TX_EXT_FRAME_MAGIC. A different value means the walk landed on bytes that are not a frame start. Note the distinction: a torn final frame (not enough bytes for header+trailer) returns Ok(None) and the scan stops cleanly, so this error means the bytes are present but wrong - real mid-log corruption, not truncation.

Solutions

  1. Restore the MVCC log from a known-good backup
  2. Regenerate the log (delete + resync via replace-base fallback)
  3. Ensure exactly one sync-server process owns the log file and writes are atomic (write to temp, fsync, rename/append)
Defensive patterns

Strategy: try-catch

Try / catch

match scan_mvcc_log(&log) {
    Ok(snapshot) => serve(snapshot),
    Err(err) if err.to_string().starts_with("invalid MVCC logical log frame magic") => {
        warn!("mid-log corruption at {}: {err}", err);
        // truncate to last known-good prefix is NOT safe to improvise:
        // restore from backup or regenerate the log
        restore_or_rebuild()?;
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: scan_mvcc_log iterates offsets after the 56-byte header; at some offset the first u32 is neither frame magic - e.g. garbage appended to the log, a frame overwritten mid-way, or a payload-size error in a previous frame that desynced the walk.

Common situations: Two processes appending to the same logical log concurrently; a crash mid-append followed by an unrelated write; copying the log with a tool that altered bytes; disk-level corruption.

Related errors


AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-08-20). Data as JSON: /api/errors/92c41f0424c1650e. Report an issue: GitHub.

Appendix: source

Thrown at cli/sync_server.rs:1154

            .try_into()
            .expect("fixed-size salt slice"),
    );
    Ok(crc32c::crc32c(&salt.to_le_bytes()))
}

fn read_mvcc_frame_boundary(
    log: &[u8],
    offset: usize,
    running_crc: u32,
) -> Result<Option<(usize, u32)>> {
    if log.len() - offset < MVCC_TX_HEADER_SIZE + MVCC_TX_TRAILER_SIZE {
        return Ok(None);
    }
    let frame_magic = read_u32_le(log, offset)?;
    let has_extension_header = frame_magic == MVCC_TX_EXT_FRAME_MAGIC;
    if frame_magic != MVCC_TX_FRAME_MAGIC && !has_extension_header {
        return Err(anyhow!(
            "invalid MVCC logical log frame magic at offset {offset}: {frame_magic:#x}"
        ));
    }
    let header_size = if has_extension_header {
        MVCC_TX_EXT_HEADER_SIZE
    } else {
        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 {

View on GitHub (pinned to 492c4a71cd)