From 9fc89a7258edff99c03b7dd5694390736a741747 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Wed, 12 Nov 2025 15:58:51 -0800 Subject: [PATCH] fix: resolve BlobStorage compression race condition causing data corruption (v5.7.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL BUG FIX - VFS file corruption since v5.0.0 Root cause: BlobStorage.initCompression() is async but called without await in constructor, creating race window where write() happens before zstd loads. Result: Data written uncompressed, metadata says "compressed" → read attempts decompression → garbage → hash mismatch → "Blob integrity check failed" error. Impact: VFS files (Workshop) written in first 100-500ms after BlobStorage creation are permanently corrupted and unreadable. Data loss for users. Fix (3 changes in BlobStorage.ts): 1. Added compressionReady flag to track init state (line 108) 2. Added ensureCompressionReady() to await init before write (lines 162-170) 3. Store ACTUAL compression state in metadata, not intended (line 227) This ensures: - Compression fully initialized before first write - Metadata accurately reflects what actually happened - No corruption even if zstd fails to load Tests: All 30 BlobStorage unit tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/storage/cow/BlobStorage.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/storage/cow/BlobStorage.ts b/src/storage/cow/BlobStorage.ts index 57f743c6..21998e99 100644 --- a/src/storage/cow/BlobStorage.ts +++ b/src/storage/cow/BlobStorage.ts @@ -105,6 +105,7 @@ export class BlobStorage { // Compression (lazily loaded) private zstdCompress?: (data: Buffer) => Promise private zstdDecompress?: (data: Buffer) => Promise + private compressionReady = false // Configuration private readonly CACHE_MAX_SIZE = 100 * 1024 * 1024 // 100MB default @@ -158,6 +159,16 @@ export class BlobStorage { } } + /** + * v5.7.5: Ensure compression is ready before write operations + * Fixes race condition where write happens before async compression init completes + */ + private async ensureCompressionReady(): Promise { + if (this.compressionReady) return + await this.initCompression() + this.compressionReady = true + } + /** * Compute SHA-256 hash of data * @@ -194,6 +205,10 @@ export class BlobStorage { return hash } + // v5.7.5: Ensure compression is initialized before writing + // Fixes race condition where write happens before async init completes + await this.ensureCompressionReady() + // Determine compression strategy const compression = this.selectCompression(data, options) @@ -207,11 +222,14 @@ export class BlobStorage { } // Create metadata + // v5.7.5: Store ACTUAL compression state, not intended + // Prevents corruption if compression failed to initialize + const actualCompression = finalData === data ? 'none' : compression const metadata: BlobMetadata = { hash, size: data.length, compressedSize, - compression, + compression: actualCompression, type: options.type || 'blob', // CRITICAL FIX: Use 'blob' default to match storage prefix createdAt: Date.now(), refCount: 1