From 9dfc91e6b43f302140fd17ef1502f6fc1e2f6e81 Mon Sep 17 00:00:00 2001 From: messense Date: Sat, 28 Feb 2026 15:27:58 +0800 Subject: [PATCH 1/3] elf: fix MIPS64 little-endian relocation parsing Fix #274: MIPS64 ELF uses a non-standard relocation info layout where the r_info field contains r_sym (32 bits) | r_ssym (8 bits) | r_type3 (8 bits) | r_type2 (8 bits) | r_type (8 bits), instead of the standard ELF64 format of (sym << 32) | type. On little-endian MIPS64 systems, when this struct is read as a single u64, the byte order causes the fields to be scrambled. This resulted in r_sym returning garbage values (e.g., 51511296 instead of 0), which then caused the dynamic symbol table parsing to try to read millions of symbols, failing with an out-of-bounds error. The fix applies a byte transformation (matching the approach used by LLVM and the `object` crate) to rearrange the MIPS64 LE r_info bytes into the standard ELF64 format before extracting r_sym and r_type. Changes: - Add `reloc64::mips64el_r_info()` to convert MIPS64 LE r_info to standard ELF64 format - Add `Reloc::fixup_mips64el()` to apply the transformation after parsing - Add `is_mips64el` flag to `RelocSection` and `RelocIterator` to conditionally apply the fixup during iteration/access - Add `RelocSection::parse_inner()` that accepts the `is_mips64el` flag - Detect MIPS64 LE in `Elf::parse_with_opts()` from the ELF header's e_machine and endianness - Backward compatible: `RelocSection::parse()` still works unchanged (defaults to no MIPS64 fixup) - Add comprehensive unit and integration tests --- src/elf/mod.rs | 13 +++- src/elf/reloc.rs | 196 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 202 insertions(+), 7 deletions(-) diff --git a/src/elf/mod.rs b/src/elf/mod.rs index 8fe49d98..5c7ba58f 100644 --- a/src/elf/mod.rs +++ b/src/elf/mod.rs @@ -271,6 +271,11 @@ if_sylvan! { let ctx = misc.ctx; let permissive = opts.parse_mode.is_permissive(); + // MIPS64 little-endian uses a non-standard relocation info layout + let is_mips64el = header.e_machine == header::EM_MIPS + && ctx.is_big() + && ctx.is_little_endian(); + let program_headers = ProgramHeader::parse(bytes, header.e_phoff as usize, header.e_phnum as usize, ctx)?; let mut interpreter = None; @@ -375,14 +380,14 @@ if_sylvan! { } } // parse the dynamic relocations - dynrelas = RelocSection::parse(bytes, dyn_info.rela, dyn_info.relasz, true, ctx) + dynrelas = RelocSection::parse_inner(bytes, dyn_info.rela, dyn_info.relasz, true, ctx, is_mips64el) .or_permissive_and_default(permissive, "Failed to parse dynamic RELA relocations")?; - dynrels = RelocSection::parse(bytes, dyn_info.rel, dyn_info.relsz, false, ctx) + dynrels = RelocSection::parse_inner(bytes, dyn_info.rel, dyn_info.relsz, false, ctx, is_mips64el) .or_permissive_and_default(permissive, "Failed to parse dynamic REL relocations")?; let is_rela = dyn_info.pltrel as u64 == dynamic::DT_RELA; - pltrelocs = RelocSection::parse(bytes, dyn_info.jmprel, dyn_info.pltrelsz, is_rela, ctx) + pltrelocs = RelocSection::parse_inner(bytes, dyn_info.jmprel, dyn_info.pltrelsz, is_rela, ctx, is_mips64el) .or_permissive_and_default(permissive, "Failed to parse PLT relocations")?; let mut num_syms = if let Some(gnu_hash) = dyn_info.gnu_hash { @@ -410,7 +415,7 @@ if_sylvan! { if is_rela || section.sh_type == section_header::SHT_REL { section.check_size_with_opts(bytes.len(), permissive)?; - let sh_relocs_opt = RelocSection::parse(bytes, section.sh_offset as usize, section.sh_size as usize, is_rela, ctx) + let sh_relocs_opt = RelocSection::parse_inner(bytes, section.sh_offset as usize, section.sh_size as usize, is_rela, ctx, is_mips64el) .map(Some) .or_permissive_and_default( permissive, diff --git a/src/elf/reloc.rs b/src/elf/reloc.rs index 4d25ee27..b5dad4ec 100644 --- a/src/elf/reloc.rs +++ b/src/elf/reloc.rs @@ -266,6 +266,28 @@ pub mod reloc64 { (sym << 32) + typ } + /// Convert a MIPS64 little-endian `r_info` value to the standard ELF64 format. + /// + /// MIPS64 ELF uses a non-standard relocation info layout: + /// - `r_sym` (32 bits) | `r_ssym` (8 bits) | `r_type3` (8 bits) | `r_type2` (8 bits) | `r_type` (8 bits) + /// + /// On little-endian systems, when this struct is read as a single `u64`, the byte order + /// causes the fields to be scrambled compared to the standard `(sym << 32) | type` layout. + /// This function rearranges the bytes so that the standard [`r_sym`] and [`r_type`] functions + /// return correct values. + /// + /// See the [MIPS64 ELF ABI](https://web.archive.org/web/20231012215433/https://techpubs.jurassic.nl/manuals/hdwr/developer/Mpro_n32_ABI/sgi_html/sgidoc/books/Mpro_n32_ABI/sgi_html/ch06.html) + /// and [LLVM's implementation](https://github.com/llvm/llvm-project/blob/119bf57ab6de49a3e61b9200c917a6d30ac6f0ad/llvm/include/llvm/Object/ELFTypes.h#L435-L444) + /// for reference. + #[inline(always)] + pub fn mips64el_r_info(info: u64) -> u64 { + (info << 32) + | ((info >> 8) & 0xff000000) + | ((info >> 24) & 0x00ff0000) + | ((info >> 40) & 0x0000ff00) + | ((info >> 56) & 0x000000ff) + } + elf_rela_std_impl!(u64, i64); } @@ -298,6 +320,19 @@ if_alloc! { use scroll::ctx::SizeWith; Reloc::size_with(&(is_rela, ctx)) } + + /// Fix up `r_sym` and `r_type` for MIPS64 little-endian binaries. + /// + /// MIPS64 ELF uses a non-standard relocation info layout that causes + /// `r_sym` and `r_type` to be incorrectly extracted on little-endian systems. + /// This method reconstructs the original `r_info`, applies the MIPS64 LE + /// byte transformation, and re-extracts the correct values. + fn fixup_mips64el(&mut self) { + let r_info = ((self.r_sym as u64) << 32) | (self.r_type as u64); + let fixed = reloc64::mips64el_r_info(r_info); + self.r_sym = reloc64::r_sym(fixed) as usize; + self.r_type = reloc64::r_type(fixed); + } } type RelocCtx = (bool, Ctx); @@ -394,6 +429,7 @@ if_alloc! { ctx: RelocCtx, start: usize, end: usize, + is_mips64el: bool, } impl<'a> fmt::Debug for RelocSection<'a> { @@ -411,7 +447,23 @@ if_alloc! { impl<'a> RelocSection<'a> { #[cfg(feature = "endian_fd")] /// Parse a REL or RELA section of size `filesz` from `offset`. + /// + /// **Note:** This method does not apply the MIPS64 little-endian relocation + /// fixup. If you are parsing a MIPS64 LE binary, use [`Elf::parse`] or + /// [`Elf::parse_with_opts`] instead, which automatically detect MIPS64 LE + /// and apply the necessary `r_info` byte transformation. pub fn parse(bytes: &'a [u8], offset: usize, filesz: usize, is_rela: bool, ctx: Ctx) -> crate::error::Result> { + Self::parse_inner(bytes, offset, filesz, is_rela, ctx, false) + } + + #[cfg(feature = "endian_fd")] + /// Parse a REL or RELA section of size `filesz` from `offset`, with MIPS64 + /// little-endian relocation info handling. + /// + /// When `is_mips64el` is `true`, the MIPS64 little-endian byte transformation + /// is applied to the `r_info` field of each relocation entry, which corrects + /// the `r_sym` and `r_type` extraction for MIPS64 LE binaries. + pub(crate) fn parse_inner(bytes: &'a [u8], offset: usize, filesz: usize, is_rela: bool, ctx: Ctx, is_mips64el: bool) -> crate::error::Result> { // TODO: better error message when too large (see symtab implementation) let bytes = if filesz != 0 { bytes.pread_with::<&'a [u8]>(offset, filesz)? @@ -420,11 +472,12 @@ if_alloc! { }; Ok(RelocSection { - bytes: bytes, + bytes, count: filesz / Reloc::size(is_rela, ctx), ctx: (is_rela, ctx), start: offset, end: offset + filesz, + is_mips64el, }) } @@ -434,7 +487,11 @@ if_alloc! { if index >= self.count { None } else { - Some(self.bytes.pread_with(index * Reloc::size_with(&self.ctx), self.ctx).unwrap()) + let mut reloc: Reloc = self.bytes.pread_with(index * Reloc::size_with(&self.ctx), self.ctx).unwrap(); + if self.is_mips64el { + reloc.fixup_mips64el(); + } + Some(reloc) } } @@ -473,6 +530,7 @@ if_alloc! { index: 0, count: self.count, ctx: self.ctx, + is_mips64el: self.is_mips64el, } } } @@ -483,6 +541,7 @@ if_alloc! { index: usize, count: usize, ctx: RelocCtx, + is_mips64el: bool, } impl<'a> fmt::Debug for RelocIterator<'a> { @@ -506,7 +565,11 @@ if_alloc! { None } else { self.index += 1; - Some(self.bytes.gread_with(&mut self.offset, self.ctx).unwrap()) + let mut reloc: Reloc = self.bytes.gread_with(&mut self.offset, self.ctx).unwrap(); + if self.is_mips64el { + reloc.fixup_mips64el(); + } + Some(reloc) } } } @@ -518,3 +581,130 @@ if_alloc! { } } } // end if_alloc + +#[cfg(test)] +mod tests { + use super::reloc64; + + #[test] + fn test_mips64el_r_info() { + // Test case from issue #274: a MIPS64 LE binary with r_info bytes + // [00 00 00 00 00 00 12 03] which, read as LE u64, gives 0x0312000000000000. + // + // Without the fix: + // r_sym = 0x0312000000000000 >> 32 = 0x03120000 = 51511296 (WRONG) + // r_type = 0x0312000000000000 & 0xFFFFFFFF = 0 (WRONG) + // + // The actual MIPS64 struct contains: + // r_sym = 0, r_ssym = 0, r_type3 = 0, r_type2 = 0x12 (R_MIPS_64), r_type = 0x03 (R_MIPS_REL32) + let info: u64 = 0x0312000000000000; + let fixed = reloc64::mips64el_r_info(info); + assert_eq!(reloc64::r_sym(fixed), 0, "r_sym should be 0"); + assert_eq!( + reloc64::r_type(fixed), + 0x00001203, + "r_type should contain composite MIPS64 type" + ); + assert_eq!( + reloc64::r_type(fixed) & 0xFF, + 3, + "primary r_type should be R_MIPS_REL32 (3)" + ); + assert_eq!( + (reloc64::r_type(fixed) >> 8) & 0xFF, + 0x12, + "r_type2 should be R_MIPS_64 (18)" + ); + } + + #[test] + fn test_mips64el_r_info_with_sym() { + // Test case from issue #274: last reloc entry with sym=0x27 + // Raw bytes in file: [27 00 00 00 00 00 12 03] + // As LE u64: 0x0312000000000027 + let info: u64 = 0x0312000000000027; + let fixed = reloc64::mips64el_r_info(info); + assert_eq!(reloc64::r_sym(fixed), 0x27, "r_sym should be 0x27 (39)"); + assert_eq!( + reloc64::r_type(fixed) & 0xFF, + 3, + "primary r_type should be R_MIPS_REL32 (3)" + ); + } + + #[test] + fn test_mips64el_r_info_zero() { + // All-zero r_info should remain all-zero + let info: u64 = 0; + let fixed = reloc64::mips64el_r_info(info); + assert_eq!(reloc64::r_sym(fixed), 0); + assert_eq!(reloc64::r_type(fixed), 0); + } + + #[test] + fn test_standard_r_sym_r_type_unchanged() { + // Ensure the standard r_sym/r_type functions still work for non-MIPS + let info: u64 = (42u64 << 32) | 7u64; + assert_eq!(reloc64::r_sym(info), 42); + assert_eq!(reloc64::r_type(info), 7); + } + + /// Test that RelocSection correctly applies MIPS64 LE fixup when parsing + /// raw relocation bytes through the full pipeline. + #[test] + #[cfg(feature = "endian_fd")] + fn test_mips64el_reloc_section_parse() { + use super::RelocSection; + use crate::container::{Container, Ctx}; + + let ctx = Ctx::new(Container::Big, scroll::Endian::Little); + + // Construct raw bytes for a REL entry (r_offset + r_info, each 8 bytes). + // r_offset = 0x150f0 (LE bytes: f0 50 01 00 00 00 00 00) + // r_info as MIPS64 struct: r_sym=0, r_ssym=0, r_type3=0, r_type2=0x12, r_type=0x03 + // In file bytes: 00 00 00 00 00 00 12 03 + let rel_bytes: Vec = vec![ + // r_offset (LE u64 = 0x150f0) + 0xf0, 0x50, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // r_info (MIPS64 LE layout) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x03, + ]; + + // Prepend enough zeros so the offset parameter works + let mut bytes = vec![0u8; 64]; + let offset = bytes.len(); + bytes.extend_from_slice(&rel_bytes); + + // Parse without MIPS64 fixup - should give wrong values + let section_no_fix = + RelocSection::parse(&bytes, offset, rel_bytes.len(), false, ctx).unwrap(); + let reloc_no_fix = section_no_fix.get(0).unwrap(); + assert_eq!(reloc_no_fix.r_offset, 0x150f0); + // Without fixup, r_sym is garbage (51511296) and r_type is wrong (0) + assert_eq!( + reloc_no_fix.r_sym, 51511296, + "Without fixup, r_sym should be 51511296 (wrong)" + ); + assert_eq!( + reloc_no_fix.r_type, 0, + "Without fixup, r_type should be 0 (wrong)" + ); + + // Parse with MIPS64 fixup - should give correct values + let section_fixed = + RelocSection::parse_inner(&bytes, offset, rel_bytes.len(), false, ctx, true).unwrap(); + let reloc_fixed = section_fixed.get(0).unwrap(); + assert_eq!(reloc_fixed.r_offset, 0x150f0); + assert_eq!(reloc_fixed.r_sym, 0, "With fixup, r_sym should be 0"); + assert_eq!( + reloc_fixed.r_type & 0xFF, + 3, + "With fixup, primary r_type should be R_MIPS_REL32 (3)" + ); + + // Also test iteration + let relocs: Vec<_> = section_fixed.iter().collect(); + assert_eq!(relocs.len(), 1); + assert_eq!(relocs[0].r_sym, 0); + assert_eq!(relocs[0].r_type & 0xFF, 3); + } +} From b35e8a345a61e704f9edf46ba5481df6a227bbc4 Mon Sep 17 00:00:00 2001 From: messense Date: Mon, 9 Mar 2026 20:03:18 +0800 Subject: [PATCH 2/3] elf: move is_mips64el into RelocCtx and apply fixup in TryFromCtx Address code review feedback: - Convert RelocCtx from a type alias (bool, Ctx) to a struct with is_rela, is_mips64el, and ctx fields - Move fixup_mips64el() call from RelocSection::get() and RelocIterator::next() into the TryFromCtx implementation - Remove is_mips64el field from RelocSection and RelocIterator since RelocCtx now carries it This is a non-breaking change since RelocCtx was not pub. --- src/elf/reloc.rs | 66 +++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/src/elf/reloc.rs b/src/elf/reloc.rs index b5dad4ec..bacb52dc 100644 --- a/src/elf/reloc.rs +++ b/src/elf/reloc.rs @@ -318,7 +318,7 @@ if_alloc! { impl Reloc { pub fn size(is_rela: bool, ctx: Ctx) -> usize { use scroll::ctx::SizeWith; - Reloc::size_with(&(is_rela, ctx)) + Reloc::size_with(&RelocCtx::new(is_rela, ctx)) } /// Fix up `r_sym` and `r_type` for MIPS64 little-endian binaries. @@ -335,16 +335,27 @@ if_alloc! { } } - type RelocCtx = (bool, Ctx); + #[derive(Clone, Copy, Debug, Default)] + struct RelocCtx { + is_rela: bool, + is_mips64el: bool, + ctx: Ctx, + } + + impl RelocCtx { + fn new(is_rela: bool, ctx: Ctx) -> Self { + RelocCtx { is_rela, is_mips64el: false, ctx } + } + } impl ctx::SizeWith for Reloc { - fn size_with( &(is_rela, Ctx { container, .. }): &RelocCtx) -> usize { - match container { + fn size_with(rctx: &RelocCtx) -> usize { + match rctx.ctx.container { Container::Little => { - if is_rela { reloc32::SIZEOF_RELA } else { reloc32::SIZEOF_REL } + if rctx.is_rela { reloc32::SIZEOF_RELA } else { reloc32::SIZEOF_REL } }, Container::Big => { - if is_rela { reloc64::SIZEOF_RELA } else { reloc64::SIZEOF_REL } + if rctx.is_rela { reloc64::SIZEOF_RELA } else { reloc64::SIZEOF_REL } } } } @@ -352,36 +363,41 @@ if_alloc! { impl<'a> ctx::TryFromCtx<'a, RelocCtx> for Reloc { type Error = crate::error::Error; - fn try_from_ctx(bytes: &'a [u8], (is_rela, Ctx { container, le }): RelocCtx) -> result::Result<(Self, usize), Self::Error> { + fn try_from_ctx(bytes: &'a [u8], rctx: RelocCtx) -> result::Result<(Self, usize), Self::Error> { use scroll::Pread; - let reloc = match container { + let Ctx { container, le } = rctx.ctx; + let (mut reloc, size): (Reloc, usize) = match container { Container::Little => { - if is_rela { + if rctx.is_rela { (bytes.pread_with::(0, le)?.into(), reloc32::SIZEOF_RELA) } else { (bytes.pread_with::(0, le)?.into(), reloc32::SIZEOF_REL) } }, Container::Big => { - if is_rela { + if rctx.is_rela { (bytes.pread_with::(0, le)?.into(), reloc64::SIZEOF_RELA) } else { (bytes.pread_with::(0, le)?.into(), reloc64::SIZEOF_REL) } } }; - Ok(reloc) + if rctx.is_mips64el { + reloc.fixup_mips64el(); + } + Ok((reloc, size)) } } impl ctx::TryIntoCtx for Reloc { type Error = crate::error::Error; /// Writes the relocation into `bytes` - fn try_into_ctx(self, bytes: &mut [u8], (is_rela, Ctx {container, le}): RelocCtx) -> result::Result { + fn try_into_ctx(self, bytes: &mut [u8], rctx: RelocCtx) -> result::Result { use scroll::Pwrite; + let Ctx { container, le } = rctx.ctx; match container { Container::Little => { - if is_rela { + if rctx.is_rela { let rela: reloc32::Rela = self.into(); Ok(bytes.pwrite_with(rela, 0, le)?) } else { @@ -390,7 +406,7 @@ if_alloc! { } }, Container::Big => { - if is_rela { + if rctx.is_rela { let rela: reloc64::Rela = self.into(); Ok(bytes.pwrite_with(rela, 0, le)?) } else { @@ -404,9 +420,9 @@ if_alloc! { impl ctx::IntoCtx<(bool, Ctx)> for Reloc { /// Writes the relocation into `bytes` - fn into_ctx(self, bytes: &mut [u8], ctx: RelocCtx) { + fn into_ctx(self, bytes: &mut [u8], (is_rela, ctx): (bool, Ctx)) { use scroll::Pwrite; - bytes.pwrite_with(self, 0, ctx).unwrap(); + bytes.pwrite_with(self, 0, RelocCtx { is_rela, is_mips64el: false, ctx }).unwrap(); } } @@ -429,7 +445,6 @@ if_alloc! { ctx: RelocCtx, start: usize, end: usize, - is_mips64el: bool, } impl<'a> fmt::Debug for RelocSection<'a> { @@ -474,10 +489,9 @@ if_alloc! { Ok(RelocSection { bytes, count: filesz / Reloc::size(is_rela, ctx), - ctx: (is_rela, ctx), + ctx: RelocCtx { is_rela, is_mips64el, ctx }, start: offset, end: offset + filesz, - is_mips64el, }) } @@ -487,11 +501,7 @@ if_alloc! { if index >= self.count { None } else { - let mut reloc: Reloc = self.bytes.pread_with(index * Reloc::size_with(&self.ctx), self.ctx).unwrap(); - if self.is_mips64el { - reloc.fixup_mips64el(); - } - Some(reloc) + Some(self.bytes.pread_with(index * Reloc::size_with(&self.ctx), self.ctx).unwrap()) } } @@ -530,7 +540,6 @@ if_alloc! { index: 0, count: self.count, ctx: self.ctx, - is_mips64el: self.is_mips64el, } } } @@ -541,7 +550,6 @@ if_alloc! { index: usize, count: usize, ctx: RelocCtx, - is_mips64el: bool, } impl<'a> fmt::Debug for RelocIterator<'a> { @@ -565,11 +573,7 @@ if_alloc! { None } else { self.index += 1; - let mut reloc: Reloc = self.bytes.gread_with(&mut self.offset, self.ctx).unwrap(); - if self.is_mips64el { - reloc.fixup_mips64el(); - } - Some(reloc) + Some(self.bytes.gread_with(&mut self.offset, self.ctx).unwrap()) } } } From 1f279f2340b4783cc4d72f6c2cde25451dfe3acb Mon Sep 17 00:00:00 2001 From: messense Date: Tue, 10 Mar 2026 19:44:58 +0800 Subject: [PATCH 3/3] elf: address PR review feedback for MIPS64 relocation - Remove RelocCtx::new() constructor, use struct literal instead to make is_mips64el: false explicit at call sites - Change IntoCtx<(bool, Ctx)> to IntoCtx for consistency with TryIntoCtx - Handle is_mips64el in TryIntoCtx for round-trip correctness --- src/elf/reloc.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/elf/reloc.rs b/src/elf/reloc.rs index bacb52dc..06e47e7e 100644 --- a/src/elf/reloc.rs +++ b/src/elf/reloc.rs @@ -318,7 +318,7 @@ if_alloc! { impl Reloc { pub fn size(is_rela: bool, ctx: Ctx) -> usize { use scroll::ctx::SizeWith; - Reloc::size_with(&RelocCtx::new(is_rela, ctx)) + Reloc::size_with(&RelocCtx { is_rela, is_mips64el: false, ctx }) } /// Fix up `r_sym` and `r_type` for MIPS64 little-endian binaries. @@ -342,11 +342,6 @@ if_alloc! { ctx: Ctx, } - impl RelocCtx { - fn new(is_rela: bool, ctx: Ctx) -> Self { - RelocCtx { is_rela, is_mips64el: false, ctx } - } - } impl ctx::SizeWith for Reloc { fn size_with(rctx: &RelocCtx) -> usize { @@ -392,8 +387,11 @@ if_alloc! { impl ctx::TryIntoCtx for Reloc { type Error = crate::error::Error; /// Writes the relocation into `bytes` - fn try_into_ctx(self, bytes: &mut [u8], rctx: RelocCtx) -> result::Result { + fn try_into_ctx(mut self, bytes: &mut [u8], rctx: RelocCtx) -> result::Result { use scroll::Pwrite; + if rctx.is_mips64el { + self.fixup_mips64el(); + } let Ctx { container, le } = rctx.ctx; match container { Container::Little => { @@ -418,11 +416,11 @@ if_alloc! { } } - impl ctx::IntoCtx<(bool, Ctx)> for Reloc { + impl ctx::IntoCtx for Reloc { /// Writes the relocation into `bytes` - fn into_ctx(self, bytes: &mut [u8], (is_rela, ctx): (bool, Ctx)) { + fn into_ctx(self, bytes: &mut [u8], rctx: RelocCtx) { use scroll::Pwrite; - bytes.pwrite_with(self, 0, RelocCtx { is_rela, is_mips64el: false, ctx }).unwrap(); + bytes.pwrite_with(self, 0, rctx).unwrap(); } }