fix(disk-transform): erronous offset in DiskLargerBlock when copying data

the offset must be relative to the start of THIS large block, not to the
start of the disk — otherwise every large block after index 0 lands past
the end of destinationBlock and copy() silently writes nothing (zeroed block).

affect qcow2 incremental backup with NBD disable

NBD + qcow2 and Vhd backups don't go through this code path
This commit is contained in:
Florent BEAUCHAMP
2026-07-24 03:56:44 -04:00
committed by Florent BEAUCHAMP
parent f683817dec
commit 9388fa4bb6
3 changed files with 32 additions and 5 deletions

View File

@@ -33,9 +33,10 @@ export class DiskLargerBlock extends RandomAccessDisk {
async readBlock(index: number): Promise<DiskBlock> {
// @todo handle partial block at the end
const source = this.source
const destinationBlock = Buffer.alloc(this.getBlockSize(), 0)
const destinationBlockData = Buffer.alloc(this.getBlockSize(), 0)
const blockRatio = this.#blockSize / source.getBlockSize()
for (let i = index * blockRatio; i < (index + 1) * blockRatio; i++) {
const firstSourceBlockIndex = index * blockRatio
for (let i = firstSourceBlockIndex; i < firstSourceBlockIndex + blockRatio; i++) {
let data: Buffer | undefined
if (source.hasBlock(i)) {
data = (await source.readBlock(i)).data
@@ -51,12 +52,12 @@ export class DiskLargerBlock extends RandomAccessDisk {
}
}
if (data !== undefined) {
data.copy(destinationBlock, i * source.getBlockSize())
data.copy(destinationBlockData, (i - firstSourceBlockIndex) * source.getBlockSize())
}
}
return {
index,
data: destinationBlock,
data: destinationBlockData,
}
}

View File

@@ -118,6 +118,29 @@ test('readBlock with simple block mapping', async () => {
assert(result.data.subarray(512).equals(block2))
})
test('readBlock at index > 0 places source data at block-relative offsets', async () => {
// Regression test: a large block after index 0 must be assembled from its own
// source blocks at offsets relative to the start of that large block.
// With distinct, non-zero patterns this catches the case where copy() targets an
// absolute (disk-wide) offset and silently produces a zeroed block.
const blockC = createPatternBuffer(512, 'C')
const blockD = createPatternBuffer(512, 'D')
const source = new MockDisk(512, 4096, [
[2, blockC],
[3, blockD],
])
await source.init()
const disk = new DiskLargerBlock(source, 1024) // blockRatio = 2, so large block 1 = source blocks 2 & 3
await disk.init()
const result = await disk.readBlock(1)
assert.strictEqual(result.index, 1)
assert.strictEqual(result.data.length, 1024)
assert(result.data.subarray(0, 512).equals(blockC))
assert(result.data.subarray(512).equals(blockD))
})
test('hasBlock behavior', async () => {
const source = new MockDisk(512, 2048, [
[0, Buffer.alloc(512)],

View File

@@ -14,6 +14,9 @@
### Bug fixes
> Users must be able to say: "I had this issue, happy to know it's fixed"
- [Backups] Fix qcow2 transfer without NBD (PR [#10164](https://github.com/vatesfr/xen-orchestra/pull/10164))
### Packages to release
@@ -30,5 +33,5 @@
> Keep this list alphabetically ordered to avoid merge conflicts
<!--packages-start-->
- @xen-orchestra/disk-transform patch
<!--packages-end-->