diff --git a/src/artifacts/NtfsMftArtifactReader.test.ts b/src/artifacts/NtfsMftArtifactReader.test.ts index 8678621511..a5990944d2 100644 --- a/src/artifacts/NtfsMftArtifactReader.test.ts +++ b/src/artifacts/NtfsMftArtifactReader.test.ts @@ -106,7 +106,7 @@ it("reports incomplete coverage when FILE extents contradict the record slots", ); expect(inventory.coverage.status).toBe("unknown"); expect(inventory.limitations).toContainEqual( - "2 FILE record(s) declare inconsistent used or total byte counts, beginning at offset 0", + "2 FILE record(s) declare inconsistent record extents, beginning at offset 0", ); expect(inventory.occurrences[0]?.format_metadata).toMatchObject({ record_count: 2, diff --git a/src/domain/windows/ntfsMft.test.ts b/src/domain/windows/ntfsMft.test.ts index 9ed0712657..9439cec8ff 100644 --- a/src/domain/windows/ntfsMft.test.ts +++ b/src/domain/windows/ntfsMft.test.ts @@ -56,9 +56,11 @@ const recordOf = (options: RecordOptions = {}): Uint8Array => { view.setUint16(16, sequenceNumber, true); view.setUint16(18, options.hardLinkCount ?? 1, true); - view.setUint16(20, 56, true); + const firstAttributeOffset = + Math.ceil((48 + Math.max(0, usaEntries) * 2) / 8) * 8; + view.setUint16(20, firstAttributeOffset, true); view.setUint16(22, options.flags ?? 0x0001, true); - view.setUint32(24, 56, true); + view.setUint32(24, firstAttributeOffset, true); view.setUint32(28, recordBytes, true); view.setBigUint64(32, options.baseRecordReference ?? 0n, true); view.setUint16(40, 0, true); @@ -121,6 +123,18 @@ describe("MFT header facts", () => { }); }); + it.each([0, 49, 64])( + "reports an invalid first attribute offset %i", + (offset) => { + const record = recordOf(); + new DataView(record.buffer).setUint16(20, offset, true); + const parsed = parseMftImage(record); + if (!parsed.ok) throw new Error(parsed.error.message); + expect(parsed.value.records[0]?.extentValid).toBe(false); + expect(parsed.value.limitations.join(" ")).toContain("inconsistent"); + }, + ); + it("keeps the inline header window separate from the parsed record count", () => { const parsed = parseMftImage( streamOf(...Array.from({ length: 33 }, () => recordOf())), diff --git a/src/domain/windows/ntfsMft.ts b/src/domain/windows/ntfsMft.ts index bc901e5e6d..3ecfc5d89e 100644 --- a/src/domain/windows/ntfsMft.ts +++ b/src/domain/windows/ntfsMft.ts @@ -301,7 +301,7 @@ const scanMft = ( if (firstInvalidExtentOffset !== undefined) limitations.push( - `${String(invalidExtents)} FILE record(s) declare inconsistent used or total byte counts, beginning at offset ${String(firstInvalidExtentOffset)}`, + `${String(invalidExtents)} FILE record(s) declare inconsistent record extents, beginning at offset ${String(firstInvalidExtentOffset)}`, ); const truncatedTailBytes = bytes.length % recordBytes; @@ -359,6 +359,10 @@ const readRecord = ( const fixups = verifyFixups(record, view, usaOffset); const usedBytes = view.getUint32(RECORD_HEADER.usedBytes, true); const totalBytes = view.getUint32(RECORD_HEADER.totalBytes, true); + const firstAttributeOffset = view.getUint16( + RECORD_HEADER.firstAttributeOffset, + true, + ); return { index, @@ -374,7 +378,12 @@ const readRecord = ( totalBytes === recordBytes && usedBytes >= NTFS_MFT_MIN_RECORD_BYTES && usedBytes % 8 === 0 && - usedBytes <= totalBytes, + usedBytes <= totalBytes && + firstAttributeOffset % 8 === 0 && + firstAttributeOffset >= NTFS_MFT_MIN_RECORD_BYTES && + firstAttributeOffset <= usedBytes && + (fixups.malformed || + firstAttributeOffset >= usaOffset + fixups.entries * 2), baseRecordReference: baseRecordReference === 0n ? null @@ -386,10 +395,7 @@ const readRecord = ( logFileSequenceNumber: view .getBigUint64(RECORD_HEADER.logFileSequenceNumber, true) .toString(), - firstAttributeOffset: view.getUint16( - RECORD_HEADER.firstAttributeOffset, - true, - ), + firstAttributeOffset, nextAttributeId: view.getUint16(RECORD_HEADER.nextAttributeId, true), fixups, };