Skip to content

Commit

Permalink
Implements strict ELF loader.
Browse files Browse the repository at this point in the history
  • Loading branch information
Lichtso committed Oct 16, 2024
1 parent 5fc2bdd commit d0fee43
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 25 deletions.
180 changes: 173 additions & 7 deletions src/elf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ use crate::{
ebpf::{self, EF_SBPF_V2, HOST_ALIGN, INSN_SIZE},
elf_parser::{
consts::{
ELFCLASS64, ELFDATA2LSB, ELFOSABI_NONE, EM_BPF, EM_SBPF, ET_DYN, R_X86_64_32,
R_X86_64_64, R_X86_64_NONE, R_X86_64_RELATIVE,
ELFCLASS64, ELFDATA2LSB, ELFOSABI_NONE, EM_BPF, EM_SBPF, ET_DYN, PF_R, PF_W, PF_X,
PT_GNU_STACK, PT_LOAD, R_X86_64_32, R_X86_64_64, R_X86_64_NONE, R_X86_64_RELATIVE,
STT_FUNC,
},
types::{Elf64Phdr, Elf64Shdr, Elf64Word},
types::{Elf64Phdr, Elf64Shdr, Elf64Sym, Elf64Word},
Elf64, ElfParserError,
},
error::EbpfError,
Expand Down Expand Up @@ -382,10 +383,160 @@ impl<C: ContextObject> Executable<C> {
aligned = AlignedMemory::<{ HOST_ALIGN }>::from_slice(bytes);
aligned.as_slice()
};
Self::load_with_parser(&Elf64::parse(bytes)?, bytes, loader)
let elf = Elf64::parse(bytes)?;
let enabled_sbpf_versions = loader.get_config().enabled_sbpf_versions.clone();
let sbpf_version = if *enabled_sbpf_versions.end() == SBPFVersion::V1 {
// Emulates a bug in the version dispatcher until we enable the first other version
if elf.file_header().e_flags == 0x20 {
SBPFVersion::V2
} else {
SBPFVersion::V1
}
} else {
match elf.file_header().e_flags {
0 => SBPFVersion::V1,
EF_SBPF_V2 => SBPFVersion::V2,
_ => SBPFVersion::V3,
}
};
if !enabled_sbpf_versions.contains(&sbpf_version) {
return Err(ElfError::UnsupportedSBPFVersion);
}
if sbpf_version == SBPFVersion::V1 {
Self::load_with_lenient_parser(&elf, bytes, loader)
} else {
Self::load_with_strict_parser(&elf, bytes, loader).map_err(|err| err.into())
}
}

fn load_with_parser(
fn load_with_strict_parser(
elf: &Elf64,
bytes: &[u8],
loader: Arc<BuiltinProgram<C>>,
) -> Result<Self, ElfParserError> {
const EXPECTED_PROGRAM_HEADERS: [(u32, u32, u64); 5] = [
(PT_LOAD, PF_R | PF_X, ebpf::MM_BYTECODE_START), // byte code
(PT_LOAD, PF_R, ebpf::MM_RODATA_START), // read only data
(PT_GNU_STACK, PF_R | PF_W, ebpf::MM_STACK_START), // stack
(PT_LOAD, PF_R | PF_W, ebpf::MM_HEAP_START), // heap
(PT_LOAD, PF_R, 0xFFFFFFFF00000000), // dynamic symbol table
];

let file_header = elf.file_header();
if file_header.e_ident.ei_osabi != ELFOSABI_NONE
|| file_header.e_ident.ei_abiversion != 0x00
|| file_header.e_ident.ei_pad != [0x00; 7]
|| file_header.e_type != ET_DYN
|| file_header.e_machine != EM_SBPF
|| elf.program_header_table().len() < EXPECTED_PROGRAM_HEADERS.len()
{
return Err(ElfParserError::InvalidFileHeader);
}
for (program_header, (p_type, p_flags, p_vaddr)) in elf
.program_header_table()
.iter()
.zip(EXPECTED_PROGRAM_HEADERS.iter())
{
let p_filesz = if (*p_flags & PF_W) != 0 {
0
} else {
program_header.p_memsz
};
if program_header.p_type != *p_type
|| program_header.p_flags != *p_flags
|| program_header.p_paddr != *p_vaddr
|| program_header.p_vaddr != *p_vaddr
|| program_header.p_memsz >= ebpf::MM_REGION_SIZE
|| program_header.p_filesz != p_filesz
{
return Err(ElfParserError::InvalidProgramHeader);
}
}
let config = loader.get_config();
let bytecode_header = &elf.program_header_table()[0];
let rodata_header = &elf.program_header_table()[1];
let dynamic_symbol_table: &[Elf64Sym] =
elf.slice_from_program_header(&elf.program_header_table()[4])?;
let mut function_registry = FunctionRegistry::<usize>::default();
let mut expected_symbol_address = bytecode_header.p_vaddr;
for symbol in dynamic_symbol_table {
if symbol.st_info & STT_FUNC == 0 {
continue;
}
if symbol.st_size == 0 {
return Err(ElfParserError::InvalidSize);
}
if symbol.st_value != expected_symbol_address {
return Err(ElfParserError::SectionNotInOrder);
}
if symbol.st_value.checked_rem(ebpf::INSN_SIZE as u64) != Some(0) {
return Err(ElfParserError::InvalidAlignment);
}
expected_symbol_address = symbol.st_value.saturating_add(symbol.st_size);
let target_pc = symbol
.st_value
.saturating_sub(bytecode_header.p_vaddr)
.checked_div(ebpf::INSN_SIZE as u64)
.unwrap_or_default() as usize;
function_registry
.register_function(
target_pc as u32,
if config.enable_symbol_and_section_labels {
elf.symbol_name(symbol.st_name as Elf64Word)?
} else {
&[]
},
target_pc,
)
.unwrap();
}
if expected_symbol_address != bytecode_header.vm_range().end {
return Err(ElfParserError::OutOfBounds);
}
if expected_symbol_address.checked_rem(ebpf::INSN_SIZE as u64) != Some(0) {
return Err(ElfParserError::InvalidAlignment);
}
if !bytecode_header
.vm_range()
.contains(&elf.file_header().e_entry)
{
return Err(ElfParserError::InvalidFileHeader);
}
if elf
.file_header()
.e_entry
.checked_rem(ebpf::INSN_SIZE as u64)
!= Some(0)
{
return Err(ElfParserError::InvalidFileHeader);
}
let entry_pc = elf
.file_header()
.e_entry
.saturating_sub(bytecode_header.p_vaddr)
.checked_div(ebpf::INSN_SIZE as u64)
.unwrap_or_default() as usize;
if function_registry.lookup_by_key(entry_pc as u32).is_none() {
return Err(ElfParserError::InvalidFileHeader);
}
Ok(Self {
elf_bytes: AlignedMemory::from_slice(bytes), // TODO: borrow
sbpf_version: SBPFVersion::V2,
ro_section: Section::Borrowed(
rodata_header.p_vaddr as usize,
rodata_header.file_range().unwrap_or_default(),
),
text_section_vaddr: bytecode_header.p_vaddr,
text_section_range: bytecode_header.file_range().unwrap_or_default(),
entry_pc,
function_registry,
loader,
#[cfg(all(feature = "jit", not(target_os = "windows"), target_arch = "x86_64"))]
compiled_program: None,
})
}

fn load_with_lenient_parser(
elf: &Elf64,
bytes: &[u8],
loader: Arc<BuiltinProgram<C>>,
Expand Down Expand Up @@ -1941,14 +2092,29 @@ mod test {

#[test]
fn test_long_section_name() {
let loader = BuiltinProgram::new_loader(
Config {
enabled_sbpf_versions: SBPFVersion::V1..=SBPFVersion::V1,
reject_broken_elfs: true,
..Config::default()
},
FunctionRegistry::default(),
);
let elf_bytes = std::fs::read("tests/elfs/long_section_name.so").unwrap();
assert_error!(
Elf64::parse(&elf_bytes),
"StringTooLong({:?}, {})",
ElfExecutable::load(&elf_bytes, Arc::new(loader)),
"FailedToParse(\"Section or symbol name `{}` is longer than `{}` bytes\")",
".bss.__rust_no_alloc_shim_is_unstable"
.get(0..SECTION_NAME_LENGTH_MAXIMUM)
.unwrap(),
SECTION_NAME_LENGTH_MAXIMUM
);
}

#[test]
fn test_strict_header() {
let elf_bytes =
std::fs::read("tests/elfs/strict_header.so").expect("failed to read elf file");
ElfExecutable::load(&elf_bytes, loader()).expect("validation failed");
}
}
8 changes: 8 additions & 0 deletions src/elf_parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ pub enum ElfParserError {
}

impl Elf64Phdr {
/// Returns the byte range the section spans in the file.
pub fn file_range(&self) -> Option<Range<usize>> {
(self.p_type == PT_LOAD).then(|| {
let offset = self.p_offset as usize;
offset..offset.saturating_add(self.p_filesz as usize)
})
}

/// Returns the segment virtual address range.
pub fn vm_range(&self) -> Range<Elf64Addr> {
let addr = self.p_vaddr;
Expand Down
35 changes: 20 additions & 15 deletions tests/elfs/elf.ld
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
PHDRS
{
text PT_LOAD ;
text PT_LOAD ;
rodata PT_LOAD ;
data PT_LOAD ;
dynamic PT_DYNAMIC ;
stack PT_GNU_STACK ;
heap PT_LOAD ;
dynsym PT_LOAD ;
other PT_NULL ;
}

SECTIONS
{
. = SIZEOF_HEADERS;
.text : { *(.text*) } :text
.rodata : { *(.rodata*) } :rodata
.data.rel.ro : { *(.data.rel.ro*) } :rodata
.dynamic : { *(.dynamic) } :dynamic
.dynsym : { *(.dynsym) } :data
.dynstr : { *(.dynstr) } :data
.rel.dyn : { *(.rel.dyn) } :data
.data : { *(.data*) } :data
.bss : { *(.bss*) } :data
.text 0x000000000 : { *(.text*) } :text
.rodata 0x100000000 : { *(.rodata*) } :rodata
.bss.stack 0x200000000 : { *(.bss.stack*) } :stack
.bss.heap 0x300000000 : { *(.bss.heap*) } :heap
.dynsym 0xFFFFFFFF00000000 : { *(.dynsym) } :dynsym
.dynstr : { *(.dynstr) } :other
.dynamic : { *(.dynamic) } :other
.symtab : { *(.symtab) } :other
.shstrtab : { *(.shstrtab) } :other
.strtab : { *(.strtab) } :other
/DISCARD/ : {
*(.comment*)
*(.eh_frame*)
*(.gnu.hash*)
*(.hash*)
*(*hash*)
*(.bss*)
*(.data*)
*(.rel.dyn*)
}
}
26 changes: 26 additions & 0 deletions tests/elfs/elf_sbpfv1.ld
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
PHDRS
{
text PT_LOAD ;
rodata PT_LOAD ;
data PT_LOAD ;
dynamic PT_DYNAMIC ;
}

SECTIONS
{
. = SIZEOF_HEADERS;
.text : { *(.text*) } :text
.rodata : { *(.rodata*) } :rodata
.data.rel.ro : { *(.data.rel.ro*) } :rodata
.dynamic : { *(.dynamic) } :dynamic
.dynsym : { *(.dynsym) } :data
.dynstr : { *(.dynstr) } :data
.rel.dyn : { *(.rel.dyn) } :data
.data : { *(.data*) } :data
.bss : { *(.bss*) } :data
/DISCARD/ : {
*(.eh_frame*)
*(.gnu.hash*)
*(.hash*)
}
}
9 changes: 6 additions & 3 deletions tests/elfs/elfs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ TOOLCHAIN=../../../agave/sdk/sbf/dependencies/platform-tools
RC_COMMON="$TOOLCHAIN/rust/bin/rustc --target sbf-solana-solana --crate-type lib -C panic=abort -C opt-level=2"
RC="$RC_COMMON -C target_cpu=sbfv2"
RC_V1="$RC_COMMON -C target_cpu=generic"
LD_COMMON="$TOOLCHAIN/llvm/bin/ld.lld -z notext -shared --Bdynamic -entry entrypoint --script elf.ld"
LD="$LD_COMMON --section-start=.text=0x100000000"
LD_V1=$LD_COMMON
LD_COMMON="$TOOLCHAIN/llvm/bin/ld.lld -z notext -shared --Bdynamic -entry entrypoint"
LD="$LD_COMMON --script elf.ld"
LD_V1="$LD_COMMON --script elf_sbpfv1.ld"

$RC -o strict_header.o strict_header.rs
$LD -o strict_header.so strict_header.o

$RC_V1 -o relative_call.o relative_call.rs
$LD_V1 -o relative_call_sbpfv1.so relative_call.o
Expand Down
13 changes: 13 additions & 0 deletions tests/elfs/strict_header.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#[link_section = ".bss.stack"]
pub static _STACK: [u8; 0x1000] = [0; 0x1000];
#[link_section = ".bss.heap"]
pub static _HEAP: [u8; 0x1000] = [0; 0x1000];

static _VAL_A: u64 = 41;
static VAL_B: u64 = 42;
static _VAL_C: u64 = 43;

#[no_mangle]
pub fn entrypoint() -> u64 {
return unsafe { core::ptr::read_volatile(&VAL_B) };
}
Binary file added tests/elfs/strict_header.so
Binary file not shown.
11 changes: 11 additions & 0 deletions tests/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2717,6 +2717,17 @@ fn test_nested_vm_syscall() {
assert_error!(result, "CallDepthExceeded");
}

#[test]
fn test_static_syscall() {
test_interpreter_and_jit_elf!(
"tests/elfs/strict_header.so",
[],
(),
TestContextObject::new(4),
ProgramResult::Ok(42),
);
}

// Instruction Meter Limit

#[test]
Expand Down

0 comments on commit d0fee43

Please sign in to comment.