Skip to content

Commit 27b25a2

Browse files
committed
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
1 parent 14e32d9 commit 27b25a2

2 files changed

Lines changed: 197 additions & 7 deletions

File tree

src/elf/mod.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,11 @@ if_sylvan! {
271271
let ctx = misc.ctx;
272272
let permissive = opts.parse_mode.is_permissive();
273273

274+
// MIPS64 little-endian uses a non-standard relocation info layout
275+
let is_mips64el = header.e_machine == header::EM_MIPS
276+
&& ctx.container == Container::Big
277+
&& ctx.le.is_little();
278+
274279
let program_headers = ProgramHeader::parse(bytes, header.e_phoff as usize, header.e_phnum as usize, ctx)?;
275280

276281
let mut interpreter = None;
@@ -375,14 +380,14 @@ if_sylvan! {
375380
}
376381
}
377382
// parse the dynamic relocations
378-
dynrelas = RelocSection::parse(bytes, dyn_info.rela, dyn_info.relasz, true, ctx)
383+
dynrelas = RelocSection::parse_inner(bytes, dyn_info.rela, dyn_info.relasz, true, ctx, is_mips64el)
379384
.or_permissive_and_default(permissive, "Failed to parse dynamic RELA relocations")?;
380385

381-
dynrels = RelocSection::parse(bytes, dyn_info.rel, dyn_info.relsz, false, ctx)
386+
dynrels = RelocSection::parse_inner(bytes, dyn_info.rel, dyn_info.relsz, false, ctx, is_mips64el)
382387
.or_permissive_and_default(permissive, "Failed to parse dynamic REL relocations")?;
383388

384389
let is_rela = dyn_info.pltrel as u64 == dynamic::DT_RELA;
385-
pltrelocs = RelocSection::parse(bytes, dyn_info.jmprel, dyn_info.pltrelsz, is_rela, ctx)
390+
pltrelocs = RelocSection::parse_inner(bytes, dyn_info.jmprel, dyn_info.pltrelsz, is_rela, ctx, is_mips64el)
386391
.or_permissive_and_default(permissive, "Failed to parse PLT relocations")?;
387392

388393
let mut num_syms = if let Some(gnu_hash) = dyn_info.gnu_hash {
@@ -410,7 +415,7 @@ if_sylvan! {
410415
if is_rela || section.sh_type == section_header::SHT_REL {
411416

412417
section.check_size_with_opts(bytes.len(), permissive)?;
413-
let sh_relocs_opt = RelocSection::parse(bytes, section.sh_offset as usize, section.sh_size as usize, is_rela, ctx)
418+
let sh_relocs_opt = RelocSection::parse_inner(bytes, section.sh_offset as usize, section.sh_size as usize, is_rela, ctx, is_mips64el)
414419
.map(Some)
415420
.or_permissive_and_default(
416421
permissive,

src/elf/reloc.rs

Lines changed: 188 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,28 @@ pub mod reloc64 {
266266
(sym << 32) + typ
267267
}
268268

269+
/// Convert a MIPS64 little-endian `r_info` value to the standard ELF64 format.
270+
///
271+
/// MIPS64 ELF uses a non-standard relocation info layout:
272+
/// - `r_sym` (32 bits) | `r_ssym` (8 bits) | `r_type3` (8 bits) | `r_type2` (8 bits) | `r_type` (8 bits)
273+
///
274+
/// On little-endian systems, when this struct is read as a single `u64`, the byte order
275+
/// causes the fields to be scrambled compared to the standard `(sym << 32) | type` layout.
276+
/// This function rearranges the bytes so that the standard [`r_sym`] and [`r_type`] functions
277+
/// return correct values.
278+
///
279+
/// 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)
280+
/// and [LLVM's implementation](https://github.com/llvm/llvm-project/blob/119bf57ab6de49a3e61b9200c917a6d30ac6f0ad/llvm/include/llvm/Object/ELFTypes.h#L435-L444)
281+
/// for reference.
282+
#[inline(always)]
283+
pub fn mips64el_r_info(info: u64) -> u64 {
284+
(info << 32)
285+
| ((info >> 8) & 0xff000000)
286+
| ((info >> 24) & 0x00ff0000)
287+
| ((info >> 40) & 0x0000ff00)
288+
| ((info >> 56) & 0x000000ff)
289+
}
290+
269291
elf_rela_std_impl!(u64, i64);
270292
}
271293

@@ -298,6 +320,19 @@ if_alloc! {
298320
use scroll::ctx::SizeWith;
299321
Reloc::size_with(&(is_rela, ctx))
300322
}
323+
324+
/// Fix up `r_sym` and `r_type` for MIPS64 little-endian binaries.
325+
///
326+
/// MIPS64 ELF uses a non-standard relocation info layout that causes
327+
/// `r_sym` and `r_type` to be incorrectly extracted on little-endian systems.
328+
/// This method reconstructs the original `r_info`, applies the MIPS64 LE
329+
/// byte transformation, and re-extracts the correct values.
330+
fn fixup_mips64el(&mut self) {
331+
let r_info = ((self.r_sym as u64) << 32) | (self.r_type as u64);
332+
let fixed = reloc64::mips64el_r_info(r_info);
333+
self.r_sym = reloc64::r_sym(fixed) as usize;
334+
self.r_type = reloc64::r_type(fixed);
335+
}
301336
}
302337

303338
type RelocCtx = (bool, Ctx);
@@ -394,6 +429,7 @@ if_alloc! {
394429
ctx: RelocCtx,
395430
start: usize,
396431
end: usize,
432+
is_mips64el: bool,
397433
}
398434

399435
impl<'a> fmt::Debug for RelocSection<'a> {
@@ -412,6 +448,17 @@ if_alloc! {
412448
#[cfg(feature = "endian_fd")]
413449
/// Parse a REL or RELA section of size `filesz` from `offset`.
414450
pub fn parse(bytes: &'a [u8], offset: usize, filesz: usize, is_rela: bool, ctx: Ctx) -> crate::error::Result<RelocSection<'a>> {
451+
Self::parse_inner(bytes, offset, filesz, is_rela, ctx, false)
452+
}
453+
454+
#[cfg(feature = "endian_fd")]
455+
/// Parse a REL or RELA section of size `filesz` from `offset`, with MIPS64
456+
/// little-endian relocation info handling.
457+
///
458+
/// When `is_mips64el` is `true`, the MIPS64 little-endian byte transformation
459+
/// is applied to the `r_info` field of each relocation entry, which corrects
460+
/// the `r_sym` and `r_type` extraction for MIPS64 LE binaries.
461+
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>> {
415462
// TODO: better error message when too large (see symtab implementation)
416463
let bytes = if filesz != 0 {
417464
bytes.pread_with::<&'a [u8]>(offset, filesz)?
@@ -420,11 +467,12 @@ if_alloc! {
420467
};
421468

422469
Ok(RelocSection {
423-
bytes: bytes,
470+
bytes,
424471
count: filesz / Reloc::size(is_rela, ctx),
425472
ctx: (is_rela, ctx),
426473
start: offset,
427474
end: offset + filesz,
475+
is_mips64el,
428476
})
429477
}
430478

@@ -434,7 +482,11 @@ if_alloc! {
434482
if index >= self.count {
435483
None
436484
} else {
437-
Some(self.bytes.pread_with(index * Reloc::size_with(&self.ctx), self.ctx).unwrap())
485+
let mut reloc: Reloc = self.bytes.pread_with(index * Reloc::size_with(&self.ctx), self.ctx).unwrap();
486+
if self.is_mips64el {
487+
reloc.fixup_mips64el();
488+
}
489+
Some(reloc)
438490
}
439491
}
440492

@@ -473,6 +525,7 @@ if_alloc! {
473525
index: 0,
474526
count: self.count,
475527
ctx: self.ctx,
528+
is_mips64el: self.is_mips64el,
476529
}
477530
}
478531
}
@@ -483,6 +536,7 @@ if_alloc! {
483536
index: usize,
484537
count: usize,
485538
ctx: RelocCtx,
539+
is_mips64el: bool,
486540
}
487541

488542
impl<'a> fmt::Debug for RelocIterator<'a> {
@@ -506,7 +560,11 @@ if_alloc! {
506560
None
507561
} else {
508562
self.index += 1;
509-
Some(self.bytes.gread_with(&mut self.offset, self.ctx).unwrap())
563+
let mut reloc: Reloc = self.bytes.gread_with(&mut self.offset, self.ctx).unwrap();
564+
if self.is_mips64el {
565+
reloc.fixup_mips64el();
566+
}
567+
Some(reloc)
510568
}
511569
}
512570
}
@@ -518,3 +576,130 @@ if_alloc! {
518576
}
519577
}
520578
} // end if_alloc
579+
580+
#[cfg(test)]
581+
mod tests {
582+
use super::reloc64;
583+
584+
#[test]
585+
fn test_mips64el_r_info() {
586+
// Test case from issue #274: a MIPS64 LE binary with r_info bytes
587+
// [00 00 00 00 00 00 12 03] which, read as LE u64, gives 0x0312000000000000.
588+
//
589+
// Without the fix:
590+
// r_sym = 0x0312000000000000 >> 32 = 0x03120000 = 51511296 (WRONG)
591+
// r_type = 0x0312000000000000 & 0xFFFFFFFF = 0 (WRONG)
592+
//
593+
// The actual MIPS64 struct contains:
594+
// r_sym = 0, r_ssym = 0, r_type3 = 0, r_type2 = 0x12 (R_MIPS_64), r_type = 0x03 (R_MIPS_REL32)
595+
let info: u64 = 0x0312000000000000;
596+
let fixed = reloc64::mips64el_r_info(info);
597+
assert_eq!(reloc64::r_sym(fixed), 0, "r_sym should be 0");
598+
assert_eq!(
599+
reloc64::r_type(fixed),
600+
0x00001203,
601+
"r_type should contain composite MIPS64 type"
602+
);
603+
assert_eq!(
604+
reloc64::r_type(fixed) & 0xFF,
605+
3,
606+
"primary r_type should be R_MIPS_REL32 (3)"
607+
);
608+
assert_eq!(
609+
(reloc64::r_type(fixed) >> 8) & 0xFF,
610+
0x12,
611+
"r_type2 should be R_MIPS_64 (18)"
612+
);
613+
}
614+
615+
#[test]
616+
fn test_mips64el_r_info_with_sym() {
617+
// Test case from issue #274: last reloc entry with sym=0x27
618+
// Raw bytes in file: [27 00 00 00 00 00 12 03]
619+
// As LE u64: 0x0312000000000027
620+
let info: u64 = 0x0312000000000027;
621+
let fixed = reloc64::mips64el_r_info(info);
622+
assert_eq!(reloc64::r_sym(fixed), 0x27, "r_sym should be 0x27 (39)");
623+
assert_eq!(
624+
reloc64::r_type(fixed) & 0xFF,
625+
3,
626+
"primary r_type should be R_MIPS_REL32 (3)"
627+
);
628+
}
629+
630+
#[test]
631+
fn test_mips64el_r_info_zero() {
632+
// All-zero r_info should remain all-zero
633+
let info: u64 = 0;
634+
let fixed = reloc64::mips64el_r_info(info);
635+
assert_eq!(reloc64::r_sym(fixed), 0);
636+
assert_eq!(reloc64::r_type(fixed), 0);
637+
}
638+
639+
#[test]
640+
fn test_standard_r_sym_r_type_unchanged() {
641+
// Ensure the standard r_sym/r_type functions still work for non-MIPS
642+
let info: u64 = (42u64 << 32) | 7u64;
643+
assert_eq!(reloc64::r_sym(info), 42);
644+
assert_eq!(reloc64::r_type(info), 7);
645+
}
646+
647+
/// Test that RelocSection correctly applies MIPS64 LE fixup when parsing
648+
/// raw relocation bytes through the full pipeline.
649+
#[test]
650+
#[cfg(feature = "endian_fd")]
651+
fn test_mips64el_reloc_section_parse() {
652+
use super::RelocSection;
653+
use crate::container::{Container, Ctx};
654+
655+
let ctx = Ctx::new(Container::Big, scroll::Endian::Little);
656+
657+
// Construct raw bytes for a REL entry (r_offset + r_info, each 8 bytes).
658+
// r_offset = 0x150f0 (LE bytes: f0 50 01 00 00 00 00 00)
659+
// r_info as MIPS64 struct: r_sym=0, r_ssym=0, r_type3=0, r_type2=0x12, r_type=0x03
660+
// In file bytes: 00 00 00 00 00 00 12 03
661+
let rel_bytes: Vec<u8> = vec![
662+
// r_offset (LE u64 = 0x150f0)
663+
0xf0, 0x50, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // r_info (MIPS64 LE layout)
664+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x03,
665+
];
666+
667+
// Prepend enough zeros so the offset parameter works
668+
let mut bytes = vec![0u8; 64];
669+
let offset = bytes.len();
670+
bytes.extend_from_slice(&rel_bytes);
671+
672+
// Parse without MIPS64 fixup - should give wrong values
673+
let section_no_fix =
674+
RelocSection::parse(&bytes, offset, rel_bytes.len(), false, ctx).unwrap();
675+
let reloc_no_fix = section_no_fix.get(0).unwrap();
676+
assert_eq!(reloc_no_fix.r_offset, 0x150f0);
677+
// Without fixup, r_sym is garbage (51511296) and r_type is wrong (0)
678+
assert_eq!(
679+
reloc_no_fix.r_sym, 51511296,
680+
"Without fixup, r_sym should be 51511296 (wrong)"
681+
);
682+
assert_eq!(
683+
reloc_no_fix.r_type, 0,
684+
"Without fixup, r_type should be 0 (wrong)"
685+
);
686+
687+
// Parse with MIPS64 fixup - should give correct values
688+
let section_fixed =
689+
RelocSection::parse_inner(&bytes, offset, rel_bytes.len(), false, ctx, true).unwrap();
690+
let reloc_fixed = section_fixed.get(0).unwrap();
691+
assert_eq!(reloc_fixed.r_offset, 0x150f0);
692+
assert_eq!(reloc_fixed.r_sym, 0, "With fixup, r_sym should be 0");
693+
assert_eq!(
694+
reloc_fixed.r_type & 0xFF,
695+
3,
696+
"With fixup, primary r_type should be R_MIPS_REL32 (3)"
697+
);
698+
699+
// Also test iteration
700+
let relocs: Vec<_> = section_fixed.iter().collect();
701+
assert_eq!(relocs.len(), 1);
702+
assert_eq!(relocs[0].r_sym, 0);
703+
assert_eq!(relocs[0].r_type & 0xFF, 3);
704+
}
705+
}

0 commit comments

Comments
 (0)