From 214baf10200f6e02cd42ac81f4de609667de8b53 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Fri, 19 Dec 2025 10:39:11 +0200 Subject: [PATCH] Fsync frame data in wallog before writing header Currently in logger.rs we are not issuing any fsyncs at all. We do `flush()` in `write_header()` but this is a no-op on Unix because there are no userspace buffers to flush since `write_all_at()` writes directly to kernel. Not fsyncing can cause the following bug: - We write X frames - We write wallog header (containing frame count X) - Kernel reorders writes so that header has been written out but not all of the frame data - Crash - Sqld starts up, reads wallog header which claims X frames exist in wallog, but only Y frames (Y < X) have been written - short read in read_frame_byte_offset_mut() returns an error and crashes the server It's also important to sync after writing frames but before writing the header so that there is no write reordering scenario where the header is persistent before the frame data is. --- libsql-server/src/replication/primary/logger.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libsql-server/src/replication/primary/logger.rs b/libsql-server/src/replication/primary/logger.rs index 6213f0da50..8eb268e4ed 100644 --- a/libsql-server/src/replication/primary/logger.rs +++ b/libsql-server/src/replication/primary/logger.rs @@ -177,6 +177,10 @@ impl LogFile { } pub fn commit(&mut self) -> anyhow::Result<()> { + // Ensure frame data is durable before updating the header. + // Without this, a crash could leave the header claiming more frames + // than actually exist on disk. + self.file.sync_data()?; self.header.frame_count += self.uncommitted_frame_count.into(); self.uncommitted_frame_count = 0; self.commited_checksum = self.uncommitted_checksum; @@ -193,6 +197,7 @@ impl LogFile { pub fn write_header(&mut self) -> anyhow::Result<()> { self.file.write_all_at(self.header.as_bytes(), 0)?; self.file.flush()?; + self.file.sync_all()?; Ok(()) } @@ -387,7 +392,7 @@ impl LogFile { ..self.header }; new_log_file.header = new_header; - new_log_file.write_header().unwrap(); + new_log_file.write_header()?; // swap old and new snapshot // FIXME(marin): the dest path never changes, store it somewhere. atomic_rename(&to_compact_log_path, path.join("wallog")).unwrap();