fix(ntfs): validate first attribute bounds in FILE records

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
N0zoM1z0andgensokyo-dev authored and gensokyo-dev committed 2026-11-11 10:03:35 +08:00
1 parent 7712e3185e
commit 8d45cb45c4
3 files changed
+29 -9

No files matched your search

+1 -1
View File
@@ -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,
+16 -2
View File
@@ -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())),
+12 -6
View File
@@ -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,
};