diff --git a/pkg/dotc1z/c1file.go b/pkg/dotc1z/c1file.go index 548968ce6..0ffd2ed94 100644 --- a/pkg/dotc1z/c1file.go +++ b/pkg/dotc1z/c1file.go @@ -276,6 +276,25 @@ func (c *C1File) CloseContext(ctx context.Context) error { if c.readOnly { return cleanupDbDir(c.dbFilePath, ErrReadOnly) } + + // CRITICAL: Ensure database file is synced to disk before reading for compression. + // On filesystems with aggressive caching (like ZFS with ARC), data written by + // sqlite during Close() may still be in kernel buffers. Without this explicit + // fsync, saveC1z() could read stale/incomplete data, producing a truncated + // zstd stream that appears valid but is missing the end-of-stream marker. + // Note: O_RDWR is required because Sync() on read-only fd fails on Windows. + dbFile, err := os.OpenFile(c.dbFilePath, os.O_RDWR, 0) + if err != nil { + return cleanupDbDir(c.dbFilePath, fmt.Errorf("open db for sync: %w", err)) + } + if err := dbFile.Sync(); err != nil { + dbFile.Close() + return cleanupDbDir(c.dbFilePath, fmt.Errorf("sync db before compress: %w", err)) + } + if err := dbFile.Close(); err != nil { + return cleanupDbDir(c.dbFilePath, fmt.Errorf("close db after sync: %w", err)) + } + err = saveC1z(c.dbFilePath, c.outputFilePath, c.encoderConcurrency) if err != nil { return cleanupDbDir(c.dbFilePath, err) diff --git a/pkg/dotc1z/manager/local/local.go b/pkg/dotc1z/manager/local/local.go index 3f3190795..3693579e9 100644 --- a/pkg/dotc1z/manager/local/local.go +++ b/pkg/dotc1z/manager/local/local.go @@ -168,11 +168,24 @@ func (l *localManager) SaveC1Z(ctx context.Context) error { } defer dstFile.Close() + // Get source file size before copy for validation + srcStat, err := tmpFile.Stat() + if err != nil { + return fmt.Errorf("failed to stat source file: %w", err) + } + expectedSize := srcStat.Size() + size, err := io.Copy(dstFile, tmpFile) if err != nil { return err } + // CRITICAL: Validate copy was complete. A truncated copy would result in + // a corrupt c1z file missing the zstd end-of-stream marker. + if size != expectedSize { + return fmt.Errorf("copy truncated: copied %d bytes, expected %d bytes", size, expectedSize) + } + // CRITICAL: Sync to ensure data is written before function returns. // This is especially important on ZFS ARC where writes may be cached. if err := dstFile.Sync(); err != nil {