Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/elf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
230 changes: 212 additions & 18 deletions src/elf/reloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -296,57 +318,86 @@ 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.
///
/// 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);
#[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<RelocCtx> 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 }
}
}
}
}

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::<reloc32::Rela>(0, le)?.into(), reloc32::SIZEOF_RELA)
} else {
(bytes.pread_with::<reloc32::Rel>(0, le)?.into(), reloc32::SIZEOF_REL)
}
},
Container::Big => {
if is_rela {
if rctx.is_rela {
(bytes.pread_with::<reloc64::Rela>(0, le)?.into(), reloc64::SIZEOF_RELA)
} else {
(bytes.pread_with::<reloc64::Rel>(0, le)?.into(), reloc64::SIZEOF_REL)
}
}
};
Ok(reloc)
if rctx.is_mips64el {
reloc.fixup_mips64el();
}
Ok((reloc, size))
}
}

impl ctx::TryIntoCtx<RelocCtx> 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<usize, Self::Error> {
fn try_into_ctx(self, bytes: &mut [u8], rctx: RelocCtx) -> result::Result<usize, Self::Error> {
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 {
Expand All @@ -355,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 {
Expand All @@ -369,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();
}
}

Expand Down Expand Up @@ -411,7 +462,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<RelocSection<'a>> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we want to make this a breaking change I think the better api here is to have caller construct the reloc ctx, so we'd make RelocCtx public and do something like:

pub fn parse(bytes: &'a [u8], offset: usize, filesz: usize, ctx: RelocCtx) -> crate::error::Result<RelocSection<'a>> 

I don't think we should do this though in this patch, if this change isn't already breaking (which it doesn't appear to be, even with suggestion about doing the mips logic in the Ctx)

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<RelocSection<'a>> {
// TODO: better error message when too large (see symtab implementation)
let bytes = if filesz != 0 {
bytes.pread_with::<&'a [u8]>(offset, filesz)?
Expand All @@ -420,9 +487,9 @@ if_alloc! {
};

Ok(RelocSection {
bytes: bytes,
bytes,
count: filesz / Reloc::size(is_rela, ctx),
ctx: (is_rela, ctx),
ctx: RelocCtx { is_rela, is_mips64el, ctx },
start: offset,
end: offset + filesz,
})
Expand Down Expand Up @@ -518,3 +585,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<u8> = 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);
}
}