[Partclone] Enhance CRC32 handling with support for legacy x64 bug and verification logic. Fixes #89

This commit is contained in:
2026-04-18 13:08:09 +01:00
parent 804cf874c5
commit d69ae633d9
5 changed files with 201 additions and 5 deletions

View File

@@ -34,7 +34,20 @@ namespace Aaru.Images;
public sealed partial class PartClone
{
const int CRC_SIZE = 4;
/// <summary>Regular size of the per-block CRC32 trailer in partclone v0001.</summary>
const int CRC_SIZE_NORMAL = 4;
/// <summary>
/// Size of the per-block CRC32 trailer in partclone v0001 images affected by the legacy 64-bit platform bug
/// (the CRC was written using <c>sizeof(unsigned long)</c>, producing 8 bytes on x86_64 instead of 4).
/// </summary>
const int CRC_SIZE_X64_BUG = 8;
/// <summary>Polynomial used by partclone's reflected CRC-32 (Ethernet / ISO).</summary>
const uint CRC32_POLYNOMIAL = 0xEDB88320;
/// <summary>Initial seed used by partclone's <c>init_crc32()</c>.</summary>
const uint CRC32_SEED = 0xFFFFFFFF;
/// <summary>Only partclone image format 0001 is supported.</summary>
const string SUPPORTED_VERSION = "0001";
const uint MAX_CACHE_SIZE = 16777216;
const uint MAX_CACHED_SECTORS = MAX_CACHE_SIZE / 512;
readonly byte[] _biTmAgIc = "BiTmAgIc"u8.ToArray();

View File

@@ -34,6 +34,60 @@ namespace Aaru.Images;
public sealed partial class PartClone
{
/// <summary>
/// Reflected CRC-32 lookup table (polynomial <see cref="CRC32_POLYNOMIAL" />) used by partclone's
/// <c>crc32_0001()</c> implementation. Kept private to the plugin because that routine carries a source-level
/// bug (see <see cref="UpdateCrc32_0001" />) that would be incorrect for any other CRC-32 consumer.
/// </summary>
static readonly uint[] _crc0001Table = BuildCrc0001Table();
static uint[] BuildCrc0001Table()
{
var table = new uint[256];
for(uint i = 0; i < 256; i++)
{
uint entry = i;
for(var j = 0; j < 8; j++)
{
if((entry & 1) != 0)
entry = entry >> 1 ^ CRC32_POLYNOMIAL;
else
entry >>= 1;
}
table[i] = entry;
}
return table;
}
/// <summary>
/// Advances a running CRC-32 seed the way partclone's <c>crc32_0001()</c> does for image format 0001. The
/// upstream routine has a long-standing bug: the input pointer is <em>never advanced</em>, so every iteration
/// folds the very first byte of <paramref name="buffer" /> into the CRC. This quirk is intentionally
/// reproduced here so that Aaru's verification matches the checksums actually stored on disk.
/// </summary>
/// <param name="crc">Previous cumulative CRC (<see cref="CRC32_SEED" /> at the start of the stream).</param>
/// <param name="buffer">Block buffer; only its first byte is used.</param>
/// <param name="size">Number of iterations to run (i.e. the block size in bytes).</param>
/// <returns>Updated cumulative CRC.</returns>
static uint UpdateCrc32_0001(uint crc, byte[] buffer, int size)
{
if(size <= 0 || buffer is null || buffer.Length == 0) return crc;
byte b = buffer[0];
for(var s = 0; s < size; s++)
{
uint tmp = crc ^ b;
crc = crc >> 8 ^ _crc0001Table[tmp & 0xFF];
}
return crc;
}
ulong BlockOffset(ulong sectorAddress)
{
_extents.GetStart(sectorAddress, out ulong extentStart);

View File

@@ -46,7 +46,9 @@ public sealed partial class PartClone : IMediaImage, IVerifiableImage
// The used block "bitmap" uses one byte per block
// TODO: Convert on-image bytemap to on-memory bitmap
byte[] _byteMap;
byte[] _byteMap;
/// <summary>Detected stride of the per-block CRC trailer (4 for normal images, 8 for x64-bug ones).</summary>
int _crcSize;
long _dataOff;
ExtentsULong _extents;
Dictionary<ulong, ulong> _extentsOff;

View File

@@ -70,6 +70,17 @@ public sealed partial class PartClone
AaruLogging.Debug(MODULE_NAME, "pHdr.totalBlocks = {0}", _pHdr.totalBlocks);
AaruLogging.Debug(MODULE_NAME, "pHdr.usedBlocks = {0}", _pHdr.usedBlocks);
string version = StringHandlers.CToString(_pHdr.version);
if(version != SUPPORTED_VERSION)
{
AaruLogging.Error(MODULE_NAME,
"Unsupported partclone image version '{0}', only '0001' is supported",
version);
return ErrorNumber.NotSupported;
}
_byteMap = new byte[_pHdr.totalBlocks];
AaruLogging.Debug(MODULE_NAME, Localization.Reading_bytemap_0_bytes, _byteMap.Length);
stream.EnsureRead(_byteMap, 0, _byteMap.Length);
@@ -89,6 +100,35 @@ public sealed partial class PartClone
_dataOff = stream.Position;
AaruLogging.Debug(MODULE_NAME, "pHdr.dataOff = {0}", _dataOff);
// Autodetect the legacy x64 CRC bug: on 64-bit platforms old partclone versions serialized
// the 4-byte CRC using sizeof(unsigned long), writing 8 bytes per block instead of 4.
// Compare the trailing data region length against both possibilities to pick the right stride.
long trailing = stream.Length - _dataOff;
var expectedNormal = (long)(_pHdr.usedBlocks * (_pHdr.blockSize + CRC_SIZE_NORMAL));
var expectedX64 = (long)(_pHdr.usedBlocks * (_pHdr.blockSize + CRC_SIZE_X64_BUG));
if(trailing == expectedX64 && trailing != expectedNormal)
{
_crcSize = CRC_SIZE_X64_BUG;
AaruLogging.Debug(MODULE_NAME,
"Detected partclone v0001 x64 CRC bug: using 8-byte CRC stride (trailing={0})",
trailing);
}
else
{
_crcSize = CRC_SIZE_NORMAL;
if(trailing != expectedNormal && _pHdr.usedBlocks > 0)
{
AaruLogging.Debug(MODULE_NAME,
"Partclone data region size {0} does not match either layout (normal={1}, x64={2}); assuming 4-byte CRC stride",
trailing,
expectedNormal,
expectedX64);
}
}
AaruLogging.Debug(MODULE_NAME, Localization.Filling_extents);
var extentFillStopwatch = new Stopwatch();
extentFillStopwatch.Start();
@@ -164,7 +204,7 @@ public sealed partial class PartClone
if(_sectorCache.TryGetValue(sectorAddress, out buffer)) return ErrorNumber.NoError;
long imageOff = _dataOff + (long)(BlockOffset(sectorAddress) * (_pHdr.blockSize + CRC_SIZE));
long imageOff = _dataOff + (long)(BlockOffset(sectorAddress) * (_pHdr.blockSize + (uint)_crcSize));
buffer = new byte[_pHdr.blockSize];
_imageStream.Seek(imageOff, SeekOrigin.Begin);

View File

@@ -30,15 +30,102 @@
// Copyright © 2011-2026 Natalia Portillo
// ****************************************************************************/
using System;
using System.IO;
using Aaru.Helpers;
using Aaru.Logging;
namespace Aaru.Images;
public sealed partial class PartClone
{
#region IVerifiableImage Members
// TODO: All blocks contain a CRC32 that's incompatible with current implementation. Need to check for compatibility.
/// <inheritdoc />
public bool? VerifyMediaImage() => null;
/// <summary>
/// Verifies a partclone v0001 image by replaying the cumulative CRC-32 stored after each used block. The
/// checksum is produced by partclone's buggy <c>crc32_0001()</c> routine (see <see cref="UpdateCrc32_0001" />)
/// and is never reseeded, so the running seed is propagated from one block to the next.
/// </summary>
public bool? VerifyMediaImage()
{
if(_imageStream is null) return null;
if(_pHdr.usedBlocks == 0) return true;
var blockSize = (int)_pHdr.blockSize;
if(blockSize <= 0 || _crcSize != CRC_SIZE_NORMAL && _crcSize != CRC_SIZE_X64_BUG) return null;
var dataBuffer = new byte[blockSize];
var crcBuffer = new byte[_crcSize];
var allOk = true;
try
{
_imageStream.Seek(_dataOff, SeekOrigin.Begin);
uint running = CRC32_SEED;
for(ulong blockIdx = 0; blockIdx < _pHdr.usedBlocks; blockIdx++)
{
int dataRead = _imageStream.EnsureRead(dataBuffer, 0, blockSize);
if(dataRead != blockSize)
{
AaruLogging.Error(MODULE_NAME,
"Short read while verifying partclone data block {0}: expected {1}, got {2}",
blockIdx,
blockSize,
dataRead);
return null;
}
running = UpdateCrc32_0001(running, dataBuffer, blockSize);
int crcRead = _imageStream.EnsureRead(crcBuffer, 0, _crcSize);
if(crcRead != _crcSize)
{
AaruLogging.Error(MODULE_NAME,
"Short read while reading CRC trailer at data block {0}: expected {1}, got {2}",
blockIdx,
_crcSize,
crcRead);
return null;
}
// The x64 bug stores the CRC as an 8-byte little-endian value whose lower 4 bytes are the actual
// CRC (source cast the seed via `*(uint32_t*)checksum`). Upper 4 bytes are padding and ignored.
var storedCrc = BitConverter.ToUInt32(crcBuffer, 0);
if(storedCrc != running)
{
AaruLogging.Error(MODULE_NAME,
"CRC mismatch at data block {0}: stored 0x{1:X8}, computed 0x{2:X8}",
blockIdx,
storedCrc,
running);
allOk = false;
}
else
{
AaruLogging.Debug(MODULE_NAME, "Data block {0} CRC 0x{1:X8} OK", blockIdx, storedCrc);
}
}
}
catch(IOException ex)
{
AaruLogging.Error(MODULE_NAME, "I/O error while verifying partclone data blocks: {0}", ex.Message);
return null;
}
return allOk;
}
#endregion
}