From 97488d22199dd962aa5419c128355180b2f773f0 Mon Sep 17 00:00:00 2001 From: Alessandro Decina Date: Wed, 27 Apr 2022 20:50:52 +1000 Subject: [PATCH] Add support for static syscalls (#296) This adds support for static, relocation-less syscalls using the same mechanism as upstream BPF: - CALL_IMMs with src=0 are considered syscalls, where the .imm field is the murmur32 hash of the syscall name - CALL_IMMs with src=1 are considered bpf-to-bpf calls --- src/assembler.rs | 4 +- src/elf.rs | 24 +++++++- src/jit.rs | 77 ++++++++++++++--------- src/vm.rs | 88 +++++++++++++++++---------- tests/assembler.rs | 2 +- tests/elfs/elf.ld | 4 +- tests/elfs/elfs.sh | 8 +++ tests/elfs/syscall_static.c | 11 ++++ tests/elfs/syscall_static.so | Bin 0 -> 1816 bytes tests/elfs/syscall_static_unknown.c | 10 +++ tests/elfs/syscall_static_unknown.so | Bin 0 -> 1568 bytes tests/ubpf_execution.rs | 76 ++++++++++++++++++++++- 12 files changed, 237 insertions(+), 67 deletions(-) create mode 100644 tests/elfs/syscall_static.c create mode 100755 tests/elfs/syscall_static.so create mode 100644 tests/elfs/syscall_static_unknown.c create mode 100755 tests/elfs/syscall_static_unknown.so diff --git a/src/assembler.rs b/src/assembler.rs index dffc123f..0ebe970f 100644 --- a/src/assembler.rs +++ b/src/assembler.rs @@ -326,7 +326,7 @@ pub fn assemble( &label, Some(target_pc), )?; - insn(opc, 0, 0, 0, hash as i32 as i64) + insn(opc, 0, 1, 0, hash as i32 as i64) } (CallReg, [Register(dst)]) => insn(opc, 0, 0, 0, *dst), (JumpConditional, [Register(dst), Register(src), Label(label)]) => insn( @@ -359,7 +359,7 @@ pub fn assemble( label, None, )?; - insn(opc, 0, 0, 0, hash as i32 as i64) + insn(opc, 0, 1, 0, hash as i32 as i64) } (Endian(size), [Register(dst)]) => insn(opc, *dst, 0, 0, size), (LoadImm, [Register(dst), Integer(imm)]) => { diff --git a/src/elf.rs b/src/elf.rs index 3596270d..7ad2fd19 100644 --- a/src/elf.rs +++ b/src/elf.rs @@ -561,7 +561,10 @@ impl Executable { .ok_or(ElfError::ValueOutOfBounds)?; for i in 0..instruction_count { let mut insn = ebpf::get_insn(elf_bytes, i); - if insn.opc == ebpf::CALL_IMM && insn.imm != -1 { + if insn.opc == ebpf::CALL_IMM + && insn.imm != -1 + && !(config.static_syscalls && insn.src == 0) + { let target_pc = (i as isize) .saturating_add(1) .saturating_add(insn.imm as isize); @@ -1794,6 +1797,25 @@ mod test { .expect("validation failed"); } + #[test] + #[should_panic(expected = r#"validation failed: RelativeJumpOutOfBounds(29)"#)] + fn test_static_syscall_disabled() { + let elf_bytes = + std::fs::read("tests/elfs/syscall_static_unknown.so").expect("failed to read elf file"); + + // when config.static_syscalls=false, all CALL_IMMs are treated as relative + // calls for backwards compatibility + ElfExecutable::load( + Config { + static_syscalls: false, + ..Config::default() + }, + &elf_bytes, + syscall_registry(), + ) + .expect("validation failed"); + } + #[cfg(all(not(windows), target_arch = "x86_64"))] #[test] fn test_size() { diff --git a/src/jit.rs b/src/jit.rs index b634af61..03dddde2 100644 --- a/src/jit.rs +++ b/src/jit.rs @@ -1329,36 +1329,57 @@ impl JitCompiler { // For JIT, syscalls MUST be registered at compile time. They can be // updated later, but not created after compiling (we need the address of the // syscall function in the JIT-compiled program). - if let Some(syscall) = executable.get_syscall_registry().lookup_syscall(insn.imm as u32) { - if self.config.enable_instruction_meter { - emit_validate_and_profile_instruction_count(self, true, Some(0))?; + + let mut resolved = false; + let (syscalls, calls) = if self.config.static_syscalls { + (insn.src == 0, insn.src != 0) + } else { + (true, true) + }; + + if syscalls { + if let Some(syscall) = executable.get_syscall_registry().lookup_syscall(insn.imm as u32) { + if self.config.enable_instruction_meter { + emit_validate_and_profile_instruction_count(self, true, Some(0))?; + } + X86Instruction::load_immediate(OperandSize::S64, R11, syscall.function as *const u8 as i64).emit(self)?; + X86Instruction::load(OperandSize::S64, R10, RAX, X86IndirectAccess::Offset((SYSCALL_CONTEXT_OBJECTS_OFFSET + syscall.context_object_slot) as i32 * 8 + self.program_argument_key)).emit(self)?; + emit_call(self, TARGET_PC_SYSCALL)?; + if self.config.enable_instruction_meter { + emit_undo_profile_instruction_count(self, 0)?; + } + // Throw error if the result indicates one + X86Instruction::cmp_immediate(OperandSize::S64, R11, 0, Some(X86IndirectAccess::Offset(0))).emit(self)?; + X86Instruction::load_immediate(OperandSize::S64, R11, self.pc as i64).emit(self)?; + emit_jcc(self, 0x85, TARGET_PC_RUST_EXCEPTION)?; + + resolved = true; } - X86Instruction::load_immediate(OperandSize::S64, R11, syscall.function as *const u8 as i64).emit(self)?; - X86Instruction::load(OperandSize::S64, R10, RAX, X86IndirectAccess::Offset((SYSCALL_CONTEXT_OBJECTS_OFFSET + syscall.context_object_slot) as i32 * 8 + self.program_argument_key)).emit(self)?; - emit_call(self, TARGET_PC_SYSCALL)?; - if self.config.enable_instruction_meter { - emit_undo_profile_instruction_count(self, 0)?; + } + + if calls { + if let Some(target_pc) = executable.lookup_bpf_function(insn.imm as u32) { + emit_bpf_call(self, Value::Constant64(target_pc as i64, false))?; + resolved = true; + } + } + + if !resolved { + if self.config.disable_unresolved_symbols_at_runtime { + X86Instruction::load_immediate(OperandSize::S64, R11, self.pc as i64).emit(self)?; + emit_jmp(self, TARGET_PC_CALL_UNSUPPORTED_INSTRUCTION)?; + } else { + emit_validate_instruction_count(self, true, Some(self.pc))?; + // executable.report_unresolved_symbol(self.pc)?; + // Workaround for unresolved symbols in ELF: Report error at runtime instead of compiletime + emit_rust_call(self, Value::Constant64(Executable::::report_unresolved_symbol as *const u8 as i64, false), &[ + Argument { index: 2, value: Value::Constant64(self.pc as i64, false) }, + Argument { index: 1, value: Value::Constant64(&*executable.as_ref() as *const _ as i64, false) }, + Argument { index: 0, value: Value::RegisterIndirect(RBP, slot_on_environment_stack(self, EnvironmentStackSlot::OptRetValPtr), false) }, + ], None, true)?; + X86Instruction::load_immediate(OperandSize::S64, R11, self.pc as i64).emit(self)?; + emit_jmp(self, TARGET_PC_RUST_EXCEPTION)?; } - // Throw error if the result indicates one - X86Instruction::cmp_immediate(OperandSize::S64, R11, 0, Some(X86IndirectAccess::Offset(0))).emit(self)?; - X86Instruction::load_immediate(OperandSize::S64, R11, self.pc as i64).emit(self)?; - emit_jcc(self, 0x85, TARGET_PC_RUST_EXCEPTION)?; - } else if let Some(target_pc) = executable.lookup_bpf_function(insn.imm as u32) { - emit_bpf_call(self, Value::Constant64(target_pc as i64, false))?; - } else if self.config.disable_unresolved_symbols_at_runtime { - X86Instruction::load_immediate(OperandSize::S64, R11, self.pc as i64).emit(self)?; - emit_jmp(self, TARGET_PC_CALL_UNSUPPORTED_INSTRUCTION)?; - } else { - emit_validate_instruction_count(self, true, Some(self.pc))?; - // executable.report_unresolved_symbol(self.pc)?; - // Workaround for unresolved symbols in ELF: Report error at runtime instead of compiletime - emit_rust_call(self, Value::Constant64(Executable::::report_unresolved_symbol as *const u8 as i64, false), &[ - Argument { index: 2, value: Value::Constant64(self.pc as i64, false) }, - Argument { index: 1, value: Value::Constant64(&*executable.as_ref() as *const _ as i64, false) }, - Argument { index: 0, value: Value::RegisterIndirect(RBP, slot_on_environment_stack(self, EnvironmentStackSlot::OptRetValPtr), false) }, - ], None, true)?; - X86Instruction::load_immediate(OperandSize::S64, R11, self.pc as i64).emit(self)?; - emit_jmp(self, TARGET_PC_RUST_EXCEPTION)?; } }, ebpf::CALL_REG => { diff --git a/src/vm.rs b/src/vm.rs index c9ab9306..af180e4a 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -231,6 +231,8 @@ pub struct Config { pub enable_sdiv: bool, /// Avoid copying read only sections when possible pub optimize_rodata: bool, + /// Support syscalls via pseudo calls (insn.src = 0) + pub static_syscalls: bool, } impl Config { @@ -261,6 +263,7 @@ impl Default for Config { dynamic_stack_frames: true, enable_sdiv: true, optimize_rodata: true, + static_syscalls: true, } } } @@ -711,9 +714,11 @@ impl<'a, E: UserDefinedError, I: InstructionMeter> EbpfVm<'a, E, I> { } // Check config outside of the instruction loop - let instruction_meter_enabled = self.executable.get_config().enable_instruction_meter; - let instruction_tracing_enabled = self.executable.get_config().enable_instruction_tracing; - let dynamic_stack_frames = self.executable.get_config().dynamic_stack_frames; + let config = self.executable.get_config(); + let instruction_meter_enabled = config.enable_instruction_meter; + let instruction_tracing_enabled = config.enable_instruction_tracing; + let dynamic_stack_frames = config.dynamic_stack_frames; + let static_syscalls = config.static_syscalls; // Loop on instructions let entry = self.executable.get_entrypoint_instruction_offset()?; @@ -1031,36 +1036,57 @@ impl<'a, E: UserDefinedError, I: InstructionMeter> EbpfVm<'a, E, I> { // Do not delegate the check to the verifier, since registered functions can be // changed after the program has been verified. ebpf::CALL_IMM => { - if let Some(syscall) = self.executable.get_syscall_registry().lookup_syscall(insn.imm as u32) { - if instruction_meter_enabled { - let _ = instruction_meter.consume(self.last_insn_count); + let mut resolved = false; + let (syscalls, calls) = if static_syscalls { + (insn.src == 0, insn.src != 0) + } else { + (true, true) + }; + + if syscalls { + if let Some(syscall) = self.executable.get_syscall_registry().lookup_syscall(insn.imm as u32) { + resolved = true; + + if instruction_meter_enabled { + let _ = instruction_meter.consume(self.last_insn_count); + } + total_insn_count += self.last_insn_count; + self.last_insn_count = 0; + let mut result: ProgramResult = Ok(0); + (unsafe { std::mem::transmute::>(syscall.function) })( + self.syscall_context_objects[SYSCALL_CONTEXT_OBJECTS_OFFSET + syscall.context_object_slot], + reg[1], + reg[2], + reg[3], + reg[4], + reg[5], + &self.memory_mapping, + &mut result, + ); + reg[0] = result?; + if instruction_meter_enabled { + remaining_insn_count = instruction_meter.get_remaining(); + } } - total_insn_count += self.last_insn_count; - self.last_insn_count = 0; - let mut result: ProgramResult = Ok(0); - (unsafe { std::mem::transmute::>(syscall.function) })( - self.syscall_context_objects[SYSCALL_CONTEXT_OBJECTS_OFFSET + syscall.context_object_slot], - reg[1], - reg[2], - reg[3], - reg[4], - reg[5], - &self.memory_mapping, - &mut result, - ); - reg[0] = result?; - if instruction_meter_enabled { - remaining_insn_count = instruction_meter.get_remaining(); + } + + if calls { + if let Some(target_pc) = self.executable.lookup_bpf_function(insn.imm as u32) { + resolved = true; + + // make BPF to BPF call + reg[ebpf::FRAME_PTR_REG] = + self.stack.push(®[ebpf::FIRST_SCRATCH_REG..ebpf::FIRST_SCRATCH_REG + ebpf::SCRATCH_REGS], next_pc)?; + next_pc = self.check_pc(pc, target_pc)?; + } + } + + if !resolved { + if config.disable_unresolved_symbols_at_runtime { + return Err(EbpfError::UnsupportedInstruction(pc + ebpf::ELF_INSN_DUMP_OFFSET)); + } else { + self.executable.report_unresolved_symbol(pc)?; } - } else if let Some(target_pc) = self.executable.lookup_bpf_function(insn.imm as u32) { - // make BPF to BPF call - reg[ebpf::FRAME_PTR_REG] = - self.stack.push(®[ebpf::FIRST_SCRATCH_REG..ebpf::FIRST_SCRATCH_REG + ebpf::SCRATCH_REGS], next_pc)?; - next_pc = self.check_pc(pc, target_pc)?; - } else if self.executable.get_config().disable_unresolved_symbols_at_runtime { - return Err(EbpfError::UnsupportedInstruction(pc + ebpf::ELF_INSN_DUMP_OFFSET)); - } else { - self.executable.report_unresolved_symbol(pc)?; } } diff --git a/tests/assembler.rs b/tests/assembler.rs index 12189e56..d7dd4f24 100644 --- a/tests/assembler.rs +++ b/tests/assembler.rs @@ -130,7 +130,7 @@ fn test_call_reg() { fn test_call_imm() { assert_eq!( asm("call 300"), - Ok(vec![insn(0, ebpf::CALL_IMM, 0, 0, 0, -1090069135)]) + Ok(vec![insn(0, ebpf::CALL_IMM, 0, 1, 0, -1090069135)]) ); } diff --git a/tests/elfs/elf.ld b/tests/elfs/elf.ld index c0fe6302..86b71670 100644 --- a/tests/elfs/elf.ld +++ b/tests/elfs/elf.ld @@ -9,11 +9,11 @@ SECTIONS { . = SIZEOF_HEADERS; .text : { *(.text) } :text - .rodata : { *(.rodata) } :rodata + .rodata : { *(.rodata*) } :rodata .dynamic : { *(.dynamic) } :dynamic .dynsym : { *(.dynsym) } :dynamic .dynstr : { *(.dynstr) } :dynamic .gnu.hash : { *(.gnu.hash) } :dynamic .rel.dyn : { *(.rel.dyn) } :dynamic .hash : { *(.hash) } :dynamic -} \ No newline at end of file +} diff --git a/tests/elfs/elfs.sh b/tests/elfs/elfs.sh index b6c23845..7be16e3c 100755 --- a/tests/elfs/elfs.sh +++ b/tests/elfs/elfs.sh @@ -54,3 +54,11 @@ rm bss_section.o "$LLVM_DIR"clang -Werror -target bpf -O2 -fno-builtin -fPIC -o rodata.o -c rodata.c "$LLVM_DIR"ld.lld -z notext -shared --Bdynamic -entry entrypoint --script elf.ld -o rodata.so rodata.o rm rodata.o + +"$LLVM_DIR"clang -Werror -target bpf -O2 -fno-builtin -fPIC -o syscall_static.o -c syscall_static.c +"$LLVM_DIR"ld.lld -z notext -shared --Bdynamic -entry entrypoint --script elf.ld -o syscall_static.so syscall_static.o +rm syscall_static.o + +"$LLVM_DIR"clang -Werror -target bpf -O2 -fno-builtin -fPIC -o syscall_static_unknown.o -c syscall_static_unknown.c +"$LLVM_DIR"ld.lld -z notext -shared --Bdynamic -entry entrypoint --script elf.ld -o syscall_static_unknown.so syscall_static_unknown.o +rm syscall_static_unknown.o \ No newline at end of file diff --git a/tests/elfs/syscall_static.c b/tests/elfs/syscall_static.c new file mode 100644 index 00000000..ae8c2d64 --- /dev/null +++ b/tests/elfs/syscall_static.c @@ -0,0 +1,11 @@ +typedef unsigned long int uint64_t; +typedef unsigned char uint8_t; + +// 1811268606 = murmur32("log"); +static void (*log)(const char*, uint64_t) = (void *) 1811268606; + +extern uint64_t entrypoint(const uint8_t *input) { + log("foo\n", 4); + + return 0; +} diff --git a/tests/elfs/syscall_static.so b/tests/elfs/syscall_static.so new file mode 100755 index 0000000000000000000000000000000000000000..8c425a0c9f729568dad47f76f986e1d8b8198b8d GIT binary patch literal 1816 zcmbtU&1w`u5bo^8-zaJX34$^wiHFQivLU<1OM+2UR*{?(grsMtlU*}E%S=bIC-LAT z_!i#t03N+~^bvB@LlivhS~XMOund9+3wpn*uj{Y7r}o{$-lN9MjDj@PFU8i05F3Ku znxYVFsIF?#?`1Vt_?{sS{&n;u45+S_1lO3QwIHSI$T?Ynh-(mcmX;%Nb$ls2@H{6W z;ryt<_Aw>m`X6YGKc5~PGfDEX1hto@7g@b6Hv6(%D?z>BMo8vMKzr(X&Hg=^q)bU$ zMel5X7h;YLl7$kg^E#5hXoB`G$^H#N!0EWm?}QRqtgIn~jF) zAhAUX_QSeKdP7dm-1{%%ZN%f=xeZ>tKP|mMa$vG`r`OwZmfNo9dd_0s+F`!lYV8kf z|Il}XG;ZZ-q!YaqX+Lj8(c5@wn5Az_U|sTY{J?aL>4aV1_`deKD}KiWq2JMg7n;3$ z?X}RXuC>F}yVHehI`=iJeoSh#y2V7JS$${N=lA~WJ-OB#S>>dW6tF;6^O7z4~35dCvJr$#*H&>@a%Bi<1Oh?0oY33S8} z^NRY2=X<+A3-S2B0pgtXc<}crmd}RxT~UBIC*te4NwIt;+!LvV97&!8gdoljd`diO wPrII=4kh=T$us*4{>?8J|gy$87pQP8L1 zx6r1iq2wn}h63nv?0Dz1EDH%KE3LoTZ)bKMyWzAU}BqbZ-q@N!@Ol=hYL>J$FFCvsO~gr-5%p4KUC$GJ09=44+qkdo_jaedRlGu`}>KGXMqfh ztX~zWpZja6A5{G`eV46G%i=(VS{fZU2-QfbVKfR<5cuBcQ7}|t6b${)i`3iA!A7Lk zHwMxAy|&|yOmVGH=9xuZ3b~)pWb9Y5 z;IGLpGqn`bXlXw%7+1Qql0{L?tI$u=Nu_2JJ8Mg0Wvis)c&WmS{yYZiQ zP3h9ayTttin|HttdL2iX*tTh3)Q|C)_3 syscalls::BpfSyscallString::init::; syscalls::BpfSyscallString::call, @@ -2899,8 +2904,13 @@ fn test_relative_call() { #[test] fn test_bpf_to_bpf_scratch_registers() { + let config = Config { + static_syscalls: false, + ..Config::default() + }; test_interpreter_and_jit_elf!( "tests/elfs/scratch_registers.so", + config, [1], ( b"log_64" => syscalls::BpfSyscallU64::init::; syscalls::BpfSyscallU64::call, @@ -3160,10 +3170,14 @@ fn test_err_dynamic_jmp_lddw() { #[test] fn test_bpf_to_bpf_depth() { - let config = Config::default(); + let config = Config { + static_syscalls: false, + ..Config::default() + }; for i in 0..config.max_call_depth { test_interpreter_and_jit_elf!( "tests/elfs/multiple_file.so", + config, [i as u8], ( b"log" => syscalls::BpfSyscallString::init::; syscalls::BpfSyscallString::call, @@ -3177,9 +3191,13 @@ fn test_bpf_to_bpf_depth() { #[test] fn test_err_bpf_to_bpf_too_deep() { - let config = Config::default(); + let config = Config { + static_syscalls: false, + ..Config::default() + }; test_interpreter_and_jit_elf!( "tests/elfs/multiple_file.so", + config, [config.max_call_depth as u8], ( b"log" => syscalls::BpfSyscallString::init::; syscalls::BpfSyscallString::call, @@ -3467,8 +3485,13 @@ fn test_nested_vm_syscall() { #[test] fn test_load_elf() { + let config = Config { + static_syscalls: false, + ..Config::default() + }; test_interpreter_and_jit_elf!( "tests/elfs/noop.so", + config, [], ( b"log" => syscalls::BpfSyscallString::init::; syscalls::BpfSyscallString::call, @@ -3482,8 +3505,13 @@ fn test_load_elf() { #[test] fn test_load_elf_empty_noro() { + let config = Config { + static_syscalls: false, + ..Config::default() + }; test_interpreter_and_jit_elf!( "tests/elfs/noro.so", + config, [], ( b"log_64" => syscalls::BpfSyscallU64::init::; syscalls::BpfSyscallU64::call, @@ -3496,8 +3524,13 @@ fn test_load_elf_empty_noro() { #[test] fn test_load_elf_empty_rodata() { + let config = Config { + static_syscalls: false, + ..Config::default() + }; test_interpreter_and_jit_elf!( "tests/elfs/empty_rodata.so", + config, [], ( b"log_64" => syscalls::BpfSyscallU64::init::; syscalls::BpfSyscallU64::call, @@ -3672,12 +3705,17 @@ fn test_instruction_count_syscall() { #[test] fn test_err_instruction_count_syscall_capped() { + let config = Config { + static_syscalls: false, + ..Config::default() + }; test_interpreter_and_jit_asm!( " mov64 r2, 0x5 call 0 mov64 r0, 0x0 exit", + config, [72, 101, 108, 108, 111], ( b"BpfSyscallString" => syscalls::BpfSyscallString::init::; syscalls::BpfSyscallString::call, @@ -3977,6 +4015,40 @@ fn test_err_unresolved_elf() { ); } +#[test] +fn test_syscall_static() { + test_interpreter_and_jit_elf!( + "tests/elfs/syscall_static.so", + [], + ( + b"log" => syscalls::BpfSyscallString::init::; syscalls::BpfSyscallString::call, + ), + 0, + { |_vm, res: Result| { res.unwrap() == 0 } }, + 5 + ); +} + +#[test] +fn test_syscall_unknown_static() { + // Check that unknown static syscalls result in UnsupportedInstruction (or + // would be UnresolvedSymbol with + // config.disable_unresolved_symbols_at_runtime=false). + // + // See also elf::test::test_static_syscall_disabled for the corresponding + // check with config.syscalls_static=false. + test_interpreter_and_jit_elf!( + "tests/elfs/syscall_static_unknown.so", + [], + ( + b"log" => syscalls::BpfSyscallString::init::; syscalls::BpfSyscallString::call, + ), + 0, + { |_vm, res: Result| { matches!(res.unwrap_err(), EbpfError::UnsupportedInstruction(29)) } }, + 1 + ); +} + // Programs #[test]