diff --git a/src/artifacts/apple/KeyedArchiveReader.test.ts b/src/artifacts/apple/KeyedArchiveReader.test.ts index eb571c7efa..9dd1ce6a9e 100644 --- a/src/artifacts/apple/KeyedArchiveReader.test.ts +++ b/src/artifacts/apple/KeyedArchiveReader.test.ts @@ -723,6 +723,61 @@ describe("binary keyed archive dates", () => { }); }); +describe("binary keyed archive post-table objects", () => { + it("preserves exact integer decimals when an archive string follows the offset table", () => { + const original = Buffer.from( + buildBinary({ + $archiver: "NSKeyedArchiver", + $version: 100000, + $top: { root: { UID: 2 } }, + $objects: [ + "$null", + "hello", + 9007199254740993n as unknown as PlistValue, + ], + }), + ); + const intMarkerOffset = original.indexOf(0x13); + expect(intMarkerOffset).toBeGreaterThan(0); + // Overwrite the 8-byte integer payload with exact 9007199254740993n + original.writeBigInt64BE(9007199254740993n, intMarkerOffset + 1); + + const stringBytes = Buffer.from("\x55hello", "latin1"); + const stringOffset = original.indexOf(stringBytes); + expect(stringOffset).toBeGreaterThan(0); + const trailerOffset = original.length - 32; + const tableOffset = Number(original.readBigUInt64BE(trailerOffset + 24)); + const count = Number(original.readBigUInt64BE(trailerOffset + 8)); + const width = original[trailerOffset + 6] ?? 1; + + const post = Buffer.concat([ + original.subarray(0, trailerOffset), + stringBytes, + original.subarray(trailerOffset), + ]); + const newTrailerOffset = post.length - 32; + for (let i = 0; i < count; i += 1) { + const offset = post.readUIntBE(tableOffset + i * width, width); + if (offset === stringOffset) { + post.writeUIntBE(trailerOffset, tableOffset + i * width, width); + } + } + post.writeBigUInt64BE(BigInt(tableOffset), newTrailerOffset + 24); + + const graph = decodeKeyedArchiveBytes(post, { offset: 0, limit: 10 }); + expect(graph.objects[2]).toMatchObject({ + id: 2, + status: "observed", + value: { $plist_type: "integer", decimal: "9007199254740993" }, + }); + expect( + graph.limitations.some((lim) => + lim.includes("could not be completely observed"), + ), + ).toBe(false); + }); +}); + describe("binary plist date object region", () => { it("refuses a binary date that extends past the object region", () => { // Strict bounds end objects at the offset table. Four payload bytes diff --git a/src/artifacts/apple/PlistScalars.test.ts b/src/artifacts/apple/PlistScalars.test.ts index a3d0d97fb6..869e1d2dfa 100644 --- a/src/artifacts/apple/PlistScalars.test.ts +++ b/src/artifacts/apple/PlistScalars.test.ts @@ -620,3 +620,53 @@ it.each([4n, 0xffffffffffffffffn])( ).toThrow("binary plist trailer or object table is invalid"); }, ); + +it("preserves exact pre-table integer precision when a string follows the offset table", () => { + const stringRecord = Buffer.from("\x55hello", "latin1"); + const intRecord = int64(9007199254740993n); + const intOffset = 8; + const tableOffset = intOffset + intRecord.length; + const stringOffset = tableOffset + 2; + const trailerOffset = stringOffset + stringRecord.length; + const bytes = Buffer.alloc(trailerOffset + 32); + Buffer.from("bplist00", "ascii").copy(bytes, 0); + intRecord.copy(bytes, intOffset); + stringRecord.copy(bytes, stringOffset); + bytes[tableOffset] = intOffset; + bytes[tableOffset + 1] = stringOffset; + bytes[trailerOffset + 6] = 1; + bytes[trailerOffset + 7] = 1; + bytes.writeBigUInt64BE(2n, trailerOffset + 8); + bytes.writeBigUInt64BE(0n, trailerOffset + 16); + bytes.writeBigUInt64BE(BigInt(tableOffset), trailerOffset + 24); + + const metadata = scanBinaryPlistScalars(bytes); + expect(metadata.literals.incomplete).toBe(false); + expect(metadata.literals.unsafeIntegerLiterals).toBe(1); + const decimals = metadata.literals.integers.get(Number(9007199254740993n)); + expect(decimals).toBeDefined(); + expect([...decimals!]).toEqual(["9007199254740993"]); +}); + +it("marks literals incomplete when a numeric scalar follows the offset table", () => { + const intRecord = int64(9007199254740993n); + const intOffset = 8; + const postInt = int64(42n); + const tableOffset = intOffset + intRecord.length; + const postOffset = tableOffset + 2; + const trailerOffset = postOffset + postInt.length; + const bytes = Buffer.alloc(trailerOffset + 32); + Buffer.from("bplist00", "ascii").copy(bytes, 0); + intRecord.copy(bytes, intOffset); + postInt.copy(bytes, postOffset); + bytes[tableOffset] = intOffset; + bytes[tableOffset + 1] = postOffset; + bytes[trailerOffset + 6] = 1; + bytes[trailerOffset + 7] = 1; + bytes.writeBigUInt64BE(2n, trailerOffset + 8); + bytes.writeBigUInt64BE(0n, trailerOffset + 16); + bytes.writeBigUInt64BE(BigInt(tableOffset), trailerOffset + 24); + + const metadata = scanBinaryPlistScalars(bytes); + expect(metadata.literals.incomplete).toBe(true); +}); diff --git a/src/artifacts/apple/PlistScalars.ts b/src/artifacts/apple/PlistScalars.ts index 6fd530886f..64b57ab3b5 100644 --- a/src/artifacts/apple/PlistScalars.ts +++ b/src/artifacts/apple/PlistScalars.ts @@ -268,13 +268,19 @@ export const scanBinaryPlistScalars = ( const capacity = Math.floor((trailer - offsetTable) / offsetWidth); if (countRaw > BigInt(capacity)) return result(true); const count = Number(countRaw); + const tableEnd = offsetTable + count * offsetWidth; if (referenceWidth < 1) return result(true); const objectOffsets: number[] = []; let incomplete = false; for (let index = 0; index < count; index += 1) { const rawOffset = sourceLayout.objectOffset(index); const offset = rawOffset > safeMaximum ? undefined : Number(rawOffset); - if (offset === undefined || offset < 8 || offset >= offsetTable) { + if ( + offset === undefined || + offset < 8 || + (offset >= offsetTable && offset < tableEnd) || + offset >= trailer + ) { incomplete = true; continue; } @@ -324,6 +330,7 @@ export const scanBinaryPlistScalars = ( const marker = bytes[offset] ?? 0; const type = marker >> 4; if (type === 3 && marker === 0x33) { + if (offset + 9 > offsetTable) incomplete = true; continue; } if (type !== 1 && type !== 2) continue;