Version
7.2.16
Host OS Type
all
Host OS name + version
Ubuntu 24.04 LTS
Host Architecture
x86
Guest OS Type
all
Guest Architecture
x86
Guest OS name + version
Ubuntu 24.04 LTS
Component
Storage and Virtual Disks
What happened?
Impact / symptoms
VMs that use VDI and snapshots can silently lose data: a 1 MiB region that the guest wrote as all zeroes reads back correctly while the VM is running, but after the next power-off / power-on it contains whatever the parent image had at that location. Deleting the snapshot afterwards makes this permanent.
Symptoms: the Bun-compiled single-file binaries Claude Code (claude 2.1.246) and opencode (1.18.23) worked fine, then segfaulted after the VM was rebooted (esp. after previous snapshots were deleted and merged into the current one). Compared with known-good copies, each binary differed in exactly one 1 MiB-aligned, 1 MiB-long region that should be all zeroes (Bun places file-backed .bss there) and instead contained JavaScript source / x86 code from an older binary that had occupied the same disk blocks in previous snapshots. Everything else was byte-identical.
What happens
vdiWrite() has an optimization: when the guest writes a full block (default 1 MiB) of all zeroes to a block that is not yet allocated, it sets the block's BAT entry to VDI_IMAGE_BLOCK_ZERO in memory only and never writes it to the image file.
In a base image that is harmless because FREE and ZERO are both read as zeroes. In a differencing image when using snapshots it is not: FREE means "read from the parent". So after the image is reopened the block reverts to the parent's stale content.
Root cause
VDI.cpp, vdiWrite(), lines 1721–1739:
if (!IS_VDI_IMAGE_BLOCK_ALLOCATED(pImage->paBlocks[uBlock]))
{
if ( !(pImage->uOpenFlags & VD_OPEN_FLAGS_HONOR_ZEROES)
&& ( pImage->paBlocks[uBlock] == VDI_IMAGE_BLOCK_ZERO
|| cbToWrite == getImageBlockSize(&pImage->Header)))
{
if (vdIfIoIntIoCtxIsZero(pImage->pIfIo, pIoCtx, cbToWrite, true))
{
pImage->paBlocks[uBlock] = VDI_IMAGE_BLOCK_ZERO; /* in memory only */
*pcbPreRead = 0;
*pcbPostRead = 0;
break; /* no vdiUpdateBlockInfoAsync() */
}
}
vdiFlushImage() / vdiFlushImageIoCtx() only write the header. vdiClose() → vdiFreeImage() flushes the header and then RTMemFree(paBlocks).
- Every other
paBlocks[] mutation in the backend (discard, block alloc, compact, resize) is followed by vdiUpdateBlockInfo[Async]() or a whole-BAT write. This is the only one that isn't.
vdiRead() (line 1655) returns VERR_VD_BLOCK_FREE for FREE, and VD.cpp:1675 then walks the parent chain; for ZERO it returns zeroes.
- The optimization is on by default:
HonorZeroWrites defaults to false (DrvVD.cpp:4771). Only VDI uses vdIfIoIntIoCtxIsZero() this way, so other formats are unaffected.
Note: a partial zero write does not trigger this, because the generic layer first merges the parent's data into a full block. The guest must issue one request covering a full aligned block of zeroes — routine for Linux writeback of large files.
Prior reports (same bug, never diagnosed)
- #17624 "Files containing only zeroes gets modified after restart" (2018, open, no developer response).
dd if=/dev/zero files in an openSUSE guest on a dynamically allocated differencing VDI, SATA/AHCI; after a VM restart they contain other data from the disk. Identical symptom.
- #16540 "Deleting older snapshot reproducibly, deterministically corrupts newer snapshot" (2017, open). Reduced test case shows a 1 MiB chunk of zeroes being replaced by the base image's content after a snapshot merge — the permanent form of this bug. Reporter reproduced it back to 4.3.4.
Fix
Persist the FREE → ZERO transition; skip the write if the entry is already ZERO:
--- a/src/VBox/Storage/VDI.cpp
+++ b/src/VBox/Storage/VDI.cpp
@@ -1731,9 +1731,15 @@
if (vdIfIoIntIoCtxIsZero(pImage->pIfIo, pIoCtx, cbToWrite, true))
{
- pImage->paBlocks[uBlock] = VDI_IMAGE_BLOCK_ZERO;
*pcbPreRead = 0;
*pcbPostRead = 0;
+ /* FREE and ZERO are not equivalent in a differencing image:
+ * FREE defers to the parent. Make the change durable. */
+ if (pImage->paBlocks[uBlock] != VDI_IMAGE_BLOCK_ZERO)
+ {
+ pImage->paBlocks[uBlock] = VDI_IMAGE_BLOCK_ZERO;
+ rc = vdiUpdateBlockInfoAsync(pImage, uBlock, pIoCtx,
+ false /* fUpdateHdr */);
+ }
break;
}
fUpdateHdr = false because cBlocksAllocated does not change (same as the discard path at line 1271). A VERR_VD_ASYNC_IO_IN_PROGRESS return is already handled by the generic write loop in VD.cpp. Applies to 7.2.16 with patch -p1.
Workaround
Disable the optimization for the affected disk (adjust controller name / LUN):
VBoxManage setextradata "<VM>" "VBoxInternal/Devices/ahci/0/LUN#0/Config/HonorZeroWrites" 1
Zero blocks then get allocated and the image grows faster, but data stays correct.
Running without snapshots also avoids the bug.
How can we reproduce this?
Steps to reproduce
- VM with a dynamically allocated VDI, take a snapshot (VM now writes to a diff image).
- In the guest, overwrite a region that holds non-zero data with ≥ 1 MiB of zeroes so that at least one 1 MiB-aligned virtual-disk block is written as a single all-zero request (e.g.
dd if=/dev/zero over an old large file, or copy a large binary with long zero runs over an older one like updating Claude code or oppencode, which use Bun files).
- Read it back while the VM runs: file is correct (in-memory BAT).
- Shut down cleanly, start the VM again, read it back: the zero region now contains the old parent-image bytes and - depending on the data in the parent-image and the location of the corruption! - the binary is segfaulting. (Note: This is not 100% reproducible.)
Optional check after step 4: dump the diff image's BAT (offStartBlocks + uBlock*4).
The entry is 0xFFFFFFFF (FREE) instead of 0xFFFFFFFE (ZERO).
Did you upload all of your necessary log files, screenshots, etc.?
Version
7.2.16
Host OS Type
all
Host OS name + version
Ubuntu 24.04 LTS
Host Architecture
x86
Guest OS Type
all
Guest Architecture
x86
Guest OS name + version
Ubuntu 24.04 LTS
Component
Storage and Virtual Disks
What happened?
Impact / symptoms
VMs that use VDI and snapshots can silently lose data: a 1 MiB region that the guest wrote as all zeroes reads back correctly while the VM is running, but after the next power-off / power-on it contains whatever the parent image had at that location. Deleting the snapshot afterwards makes this permanent.
Symptoms: the Bun-compiled single-file binaries Claude Code (
claude2.1.246) and opencode (1.18.23) worked fine, then segfaulted after the VM was rebooted (esp. after previous snapshots were deleted and merged into the current one). Compared with known-good copies, each binary differed in exactly one 1 MiB-aligned, 1 MiB-long region that should be all zeroes (Bun places file-backed.bssthere) and instead contained JavaScript source / x86 code from an older binary that had occupied the same disk blocks in previous snapshots. Everything else was byte-identical.What happens
vdiWrite()has an optimization: when the guest writes a full block (default 1 MiB) of all zeroes to a block that is not yet allocated, it sets the block's BAT entry toVDI_IMAGE_BLOCK_ZEROin memory only and never writes it to the image file.In a base image that is harmless because
FREEandZEROare both read as zeroes. In a differencing image when using snapshots it is not:FREEmeans "read from the parent". So after the image is reopened the block reverts to the parent's stale content.Root cause
VDI.cpp,vdiWrite(), lines 1721–1739:vdiFlushImage()/vdiFlushImageIoCtx()only write the header.vdiClose()→vdiFreeImage()flushes the header and thenRTMemFree(paBlocks).paBlocks[]mutation in the backend (discard, block alloc, compact, resize) is followed byvdiUpdateBlockInfo[Async]()or a whole-BAT write. This is the only one that isn't.vdiRead()(line 1655) returnsVERR_VD_BLOCK_FREEforFREE, andVD.cpp:1675then walks the parent chain; forZEROit returns zeroes.HonorZeroWritesdefaults tofalse(DrvVD.cpp:4771). Only VDI usesvdIfIoIntIoCtxIsZero()this way, so other formats are unaffected.Note: a partial zero write does not trigger this, because the generic layer first merges the parent's data into a full block. The guest must issue one request covering a full aligned block of zeroes — routine for Linux writeback of large files.
Prior reports (same bug, never diagnosed)
dd if=/dev/zerofiles in an openSUSE guest on a dynamically allocated differencing VDI, SATA/AHCI; after a VM restart they contain other data from the disk. Identical symptom.Fix
Persist the
FREE → ZEROtransition; skip the write if the entry is alreadyZERO:fUpdateHdr = falsebecausecBlocksAllocateddoes not change (same as the discard path at line 1271). AVERR_VD_ASYNC_IO_IN_PROGRESSreturn is already handled by the generic write loop inVD.cpp. Applies to 7.2.16 withpatch -p1.Workaround
Disable the optimization for the affected disk (adjust controller name / LUN):
Zero blocks then get allocated and the image grows faster, but data stays correct.
Running without snapshots also avoids the bug.
How can we reproduce this?
Steps to reproduce
dd if=/dev/zeroover an old large file, or copy a large binary with long zero runs over an older one like updating Claude code or oppencode, which use Bun files).Optional check after step 4: dump the diff image's BAT (
offStartBlocks + uBlock*4).The entry is
0xFFFFFFFF(FREE) instead of0xFFFFFFFE(ZERO).Did you upload all of your necessary log files, screenshots, etc.?