diff --git a/docs/ghidra-provider.md b/docs/ghidra-provider.md index 8c6ad61a62..8450cfbba4 100644 --- a/docs/ghidra-provider.md +++ b/docs/ghidra-provider.md @@ -261,3 +261,9 @@ next match. Coverage can overlap between pages; do not sum it as unique memory. page. These blocks are never searched. Cancellation returns a typed error, discards the active Ghidra database, and does not return a successful partial page. Hopper byte search remains unavailable until its implementation is verified. + +MIPS16 shared-image admission requires examined dynamic REL declarations. +RELR dynamic tags or nonempty `SHT_RELR` sections leave relocation coverage +unavailable and the profile unsupported; REA does not infer an empty relocation +set from the absence of REL declarations. RELR loader semantics have not been +verified for this lane. diff --git a/src/application/MipsElfMetadata.ts b/src/application/MipsElfMetadata.ts index 0cf8efdfec..76466ad481 100644 --- a/src/application/MipsElfMetadata.ts +++ b/src/application/MipsElfMetadata.ts @@ -179,7 +179,11 @@ export const readMipsElfMetadata = async ( gnuAttributes = parsed.value; continue; } - if (metadata.type === 3 && table.optional && (type === 4 || type === 9)) + if ( + metadata.type === 3 && + table.optional && + (type === 4 || type === 9 || type === 19) + ) relocationSections.push({ type, offset: u32(contents.value, position + 16), diff --git a/src/application/MipsElfSharedLoad.ts b/src/application/MipsElfSharedLoad.ts index 7c4dcf7553..2a4e4b7817 100644 --- a/src/application/MipsElfSharedLoad.ts +++ b/src/application/MipsElfSharedLoad.ts @@ -103,7 +103,7 @@ export const readMipsElfSharedLoad = async ( // Bound temporary storage, not admitted artifact size. ELF32 records are // exact eight-byte structures; every declared record is examined or refused. const chunkBytes = 64 * 1024; - const relevant = new Set([2, 7, 8, 9, 17, 18, 19, 20, 23]); + const relevant = new Set([2, 7, 8, 9, 17, 18, 19, 20, 23, 35, 36, 37]); const tags = new Map(); let terminated = false; for ( @@ -130,6 +130,10 @@ export const readMipsElfSharedLoad = async ( } } if (!terminated) return err("MIPS dynamic table has no DT_NULL terminator"); + if ([35, 36, 37].some((tag) => tags.has(tag))) + return err( + "MIPS shared RELR relocation declarations require separate verification", + ); if ([2, 7, 8, 9, 23].some((tag) => (tags.get(tag) ?? 0) !== 0)) return err( "MIPS shared RELA or PLT relocation declarations require separate verification", diff --git a/tests/boundary/filesystem/mipsSharedLoad.test.ts b/tests/boundary/filesystem/mipsSharedLoad.test.ts index ba88cdb780..41c5434f98 100644 --- a/tests/boundary/filesystem/mipsSharedLoad.test.ts +++ b/tests/boundary/filesystem/mipsSharedLoad.test.ts @@ -110,6 +110,76 @@ for (const little of [true, false]) { /Symbol-bearing R_MIPS_REL32/u, ); }); + + it.each([false, true])( + `leaves RELR dynamic declarations unverified with existing REL=%s (${little ? "LE" : "BE"})`, + async (withRel) => { + const bytes = fixture(little); + const write = (value: number, offset: number) => + little + ? bytes.writeUInt32LE(value, offset) + : bytes.writeUInt32BE(value, offset); + const start = withRel ? 280 : 256; + write(35, start); // DT_RELRSZ + write(4, start + 4); + write(36, start + 8); // DT_RELR + write(2048, start + 12); + write(37, start + 16); // DT_RELRENT + write(4, start + 20); + write(0, start + 24); + write(64, 100); + write(64, 104); + const target = await resolve(bytes); + assert.equal(target.mips.sharedLoad?.status, "unavailable"); + assert.match(ghidraMipsUnsupportedReason(target) ?? "", /RELR/u); + }, + ); + + it(`does not ignore an isolated zero-valued RELR declaration (${little ? "LE" : "BE"})`, async () => { + const bytes = fixture(little); + if (little) bytes.writeUInt32LE(37, 280); + else bytes.writeUInt32BE(37, 280); + if (little) { + bytes.writeUInt32LE(40, 100); + bytes.writeUInt32LE(40, 104); + } else { + bytes.writeUInt32BE(40, 100); + bytes.writeUInt32BE(40, 104); + } + const target = await resolve(bytes); + assert.equal(target.mips.sharedLoad?.status, "unavailable"); + assert.match(ghidraMipsUnsupportedReason(target) ?? "", /RELR/u); + }); + + it.each([false, true])( + `includes SHT_RELR in source coverage with existing REL=%s (${little ? "LE" : "BE"})`, + async (withRel) => { + const bytes = fixture(little); + const write = (value: number, offset: number) => + little + ? bytes.writeUInt32LE(value, offset) + : bytes.writeUInt32BE(value, offset); + if (!withRel) write(0, 256); + write(1024, 32); + if (little) { + bytes.writeUInt16LE(40, 46); + bytes.writeUInt16LE(2, 48); + } else { + bytes.writeUInt16BE(40, 46); + bytes.writeUInt16BE(2, 48); + } + write(19, 1068); // SHT_RELR + write(2048, 1080); + write(4, 1084); + write(4, 1100); + const target = await resolve(bytes); + assert.equal(target.mips.sharedLoad?.status, "unavailable"); + assert.match( + ghidraMipsUnsupportedReason(target) ?? "", + /section relocation declarations/u, + ); + }, + ); } it("examines relocation records beyond a read chunk before deciding completeness", async () => {