diff --git a/Library/DiscUtils.Ntfs/MasterFileTable.cs b/Library/DiscUtils.Ntfs/MasterFileTable.cs index 05cb09cef..a56d77e96 100644 --- a/Library/DiscUtils.Ntfs/MasterFileTable.cs +++ b/Library/DiscUtils.Ntfs/MasterFileTable.cs @@ -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.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.Shared.Return(allocated); } } } diff --git a/Tests/LibraryTests/Ntfs/NtfsFileSystemTest.cs b/Tests/LibraryTests/Ntfs/NtfsFileSystemTest.cs index 4cc79494b..341dbbbfd 100644 --- a/Tests/LibraryTests/Ntfs/NtfsFileSystemTest.cs +++ b/Tests/LibraryTests/Ntfs/NtfsFileSystemTest.cs @@ -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}"); + } + } }