Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 43 additions & 19 deletions Library/DiscUtils.Ntfs/MasterFileTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -410,30 +410,54 @@ public void WriteRecord(FileRecord record)
throw new IOException("Attempting to write over-sized MFT record");
}

_recordStream.Position = record.MasterFileTableIndex * RecordSize;
record.ToStream(_recordStream, RecordSize);
_recordStream.Flush();

// We may have modified our own meta-data by extending the data stream, so
// make sure our records are up-to-date.
if (_self.MftRecordIsDirty)
// Serialize the record exactly once and write the identical bytes to both
// $MFT and $MFTMirr. FixupRecordBase.ToBytes calls ProtectBuffer, which
// increments the update sequence number on every serialization, so calling
// ToStream twice would store different USNs (and different fixup bytes) in
// each copy. NTFS implementations compare $MFTMirr to $MFT verbatim at mount
// time, so ntfs-3g would fail with "$MFTMirr does not match $MFT" and chkdsk
// would report corruption.
byte[] allocated = null;
var buffer = RecordSize <= 1024
? stackalloc byte[RecordSize]
: (allocated = ArrayPool<byte>.Shared.Rent(RecordSize)).AsSpan(0, RecordSize);
try
{
var dirEntry = _self.DirectoryEntry;
dirEntry?.UpdateFrom(_self);
buffer.Clear();
record.ToBytes(buffer);

_self.UpdateRecordInMft();
}
_recordStream.Position = record.MasterFileTableIndex * RecordSize;
_recordStream.Write(buffer);
_recordStream.Flush();

// We may have modified our own meta-data by extending the data stream, so
// make sure our records are up-to-date.
if (_self.MftRecordIsDirty)
{
var dirEntry = _self.DirectoryEntry;
dirEntry?.UpdateFrom(_self);

_self.UpdateRecordInMft();
}

// Need to update Mirror. OpenRaw is OK because this is short duration, and we don't
// extend or otherwise modify any meta-data, just the content of the Data stream.
if (record.MasterFileTableIndex < 4 && _self.Context.GetFileByIndex != null)
// Need to update Mirror. OpenRaw is OK because this is short duration, and we don't
// extend or otherwise modify any meta-data, just the content of the Data stream.
if (record.MasterFileTableIndex < 4 && _self.Context.GetFileByIndex != null)
{
var mftMirror = _self.Context.GetFileByIndex(MftMirrorIndex);
if (mftMirror != null)
{
using var s = mftMirror.OpenStream(AttributeType.Data, null, FileAccess.ReadWrite);
s.Position = record.MasterFileTableIndex * RecordSize;
s.Write(buffer);
}
}
}
finally
{
var mftMirror = _self.Context.GetFileByIndex(MftMirrorIndex);
if (mftMirror != null)
if (allocated != null)
{
using var s = mftMirror.OpenStream(AttributeType.Data, null, FileAccess.ReadWrite);
s.Position = record.MasterFileTableIndex * RecordSize;
record.ToStream(s, RecordSize);
ArrayPool<byte>.Shared.Return(allocated);
}
}
}
Expand Down
49 changes: 49 additions & 0 deletions Tests/LibraryTests/Ntfs/NtfsFileSystemTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1100,4 +1100,53 @@ public void Create4KSectorFileSystem()
Assert.Equal(dummyFileSize, fs.GetFileLength("test.bin"));
}
}

[Fact]
public void MftMirrorStaysInSyncWithMft()
{
var ms = new MemoryStream();
ms.SetLength(64 * 1024 * 1024);

var geometry = Geometry.FromCapacity(ms.Length);
using (NtfsFileSystem.Format(ms, "mirror", geometry, 0, ms.Length / geometry.BytesPerSector))
{
}

using (var fs = new NtfsFileSystem(ms))
{
fs.CreateDirectory("dir");
var content = new byte[4096];
for (var i = 0; i < 400; ++i)
{
using var file = fs.OpenFile($"dir\\file{i}.bin", FileMode.CreateNew);
file.Write(content, 0, content.Length);
}
}

var image = ms.ToArray();

// Parse the boot sector.
var bytesPerSector = EndianUtilities.ToUInt16LittleEndian(image, 0x0B);
var sectorsPerCluster = image[0x0D];
var clusterBytes = bytesPerSector * sectorsPerCluster;
var mftCluster = EndianUtilities.ToInt64LittleEndian(image, 0x30);
var mirrorCluster = EndianUtilities.ToInt64LittleEndian(image, 0x38);

var rawClustersPerRecord = (sbyte)image[0x40];
var recordSize = rawClustersPerRecord >= 0
? rawClustersPerRecord * clusterBytes
: 1 << (-rawClustersPerRecord);

var mftBase = mftCluster * clusterBytes;
var mirrorBase = mirrorCluster * clusterBytes;

for (var record = 0; record < 4; ++record)
{
var mftSlice = image.AsSpan((int)(mftBase + record * recordSize), recordSize);
var mirrorSlice = image.AsSpan((int)(mirrorBase + record * recordSize), recordSize);

Assert.True(mftSlice.SequenceEqual(mirrorSlice),
$"$MFTMirr does not match $MFT for record {record}");
}
}
}
Loading