tursodatabase/turso · error · anyhow::Error

invalid MVCC logical log frame end magic at offset {offset}

Error message

invalid MVCC logical log frame end magic at offset {offset}

What it means

After the chained CRC check passes, the 8-byte trailer must end with magic 0x4554564D (MVCC_TX_END_MAGIC), read at trailer_start + 4. A CRC that matches while the end magic fails pinpoints corruption in the trailer's magic bytes themselves (the CRC does not cover the trailer), or a producer writing the two trailer fields in the wrong order. The scan stops at that frame.

Source

Thrown at cli/sync_server.rs:1096

        .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
        .checked_add(MVCC_TX_TRAILER_SIZE)
        .ok_or_else(|| anyhow!("MVCC logical log frame end overflow"))?;
    if frame_end > log.len() {
        return Ok(None);
    }
    let expected_crc = crc32c::crc32c_append(running_crc, &log[offset..trailer_start]);
    let stored_crc = read_u32_le(log, trailer_start)?;
    if stored_crc != expected_crc {
        return Err(anyhow!(
            "MVCC logical log frame checksum mismatch at offset {offset}"
        ));
    }
    let end_magic = read_u32_le(log, trailer_start + 4)?;
    if end_magic != MVCC_TX_END_MAGIC {
        return Err(anyhow!(
            "invalid MVCC logical log frame end magic at offset {offset}"
        ));
    }
    Ok(Some((frame_end, stored_crc)))
}

fn read_u32_le(buf: &[u8], offset: usize) -> Result<u32> {
    let bytes = buf
        .get(offset..offset + 4)
        .ok_or_else(|| anyhow!("buffer too short for u32 at offset {offset}"))?;
    Ok(u32::from_le_bytes(bytes.try_into().unwrap()))
}

fn read_u64_le(buf: &[u8], offset: usize) -> Result<u64> {
    let bytes = buf
        .get(offset..offset + 8)
        .ok_or_else(|| anyhow!("buffer too short for u64 at offset {offset}"))?;
    Ok(u64::from_le_bytes(bytes.try_into().unwrap()))

View on GitHub (pinned to bad083fafb)

Solutions

  1. Re-pull the log; the trailer is corrupt even though the frame body verified.
  2. Hexdump the 8-byte trailer at the reported offset: crc32c u32 then 0x4554564D.
  3. If writing a test producer, append the trailer in the fixed order CRC then end magic.

Example fix

// before (log producer)
trailer.extend_from_slice(&MVCC_TX_END_MAGIC.to_le_bytes()); // end magic first
trailer.extend_from_slice(&frame_crc.to_le_bytes());

// after
trailer.extend_from_slice(&frame_crc.to_le_bytes()); // CRC first
trailer.extend_from_slice(&MVCC_TX_END_MAGIC.to_le_bytes());
Defensive patterns

Strategy: try-catch

Type guard

fn frame_end_magic_ok(log: &[u8], trailer_start: usize) -> bool {
    log.get(trailer_start + 4..trailer_start + 8)
        .map(|b| u32::from_le_bytes(b.try_into().unwrap()))
        .is_some_and(|m| m == MVCC_TX_END_MAGIC)
}

Try / catch

match scan_mvcc_log(&log) {
    Ok(snapshot) => { /* serve deltas */ }
    Err(err) if err.to_string().contains("frame end magic") => {
        // trailer corrupt despite passing CRC: re-pull the log
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: A frame whose stored trailer CRC matches but whose second trailer u32 is not 0x4554564D — trailer magic bytes damaged after writing, or a hand-built producer that writes end-magic before CRC in the 8-byte trailer.

Common situations: Tail-of-file damage (the trailer is the last 8 bytes written); producer code swapping trailer field order; partial writes of the 8-byte trailer after a crash; fuzzed trailers.

Related errors


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