diff --git a/data/test-elfs/.gitignore b/data/test-elfs/.gitignore new file mode 100644 index 000000000..483958e83 --- /dev/null +++ b/data/test-elfs/.gitignore @@ -0,0 +1,2 @@ +*.bc +*.o \ No newline at end of file diff --git a/data/test-elfs/bss_section.so b/data/test-elfs/bss_section.so new file mode 100755 index 000000000..9df6d020a Binary files /dev/null and b/data/test-elfs/bss_section.so differ diff --git a/data/test-elfs/bss_section.zig b/data/test-elfs/bss_section.zig new file mode 100644 index 000000000..ddc9c961b --- /dev/null +++ b/data/test-elfs/bss_section.zig @@ -0,0 +1,7 @@ +// NOTE: the value is 0 in order to get the symbol into .bss +var x: u32 = 0; + +export fn entrypoint() u32 { + @as(*volatile u32, &x).* = 10; + return 0; +} diff --git a/data/test-elfs/data_section.so b/data/test-elfs/data_section.so new file mode 100755 index 000000000..3dc2e206b Binary files /dev/null and b/data/test-elfs/data_section.so differ diff --git a/data/test-elfs/data_section.zig b/data/test-elfs/data_section.zig new file mode 100644 index 000000000..d9fdad614 --- /dev/null +++ b/data/test-elfs/data_section.zig @@ -0,0 +1,6 @@ +var x: u32 = 42; + +export fn entrypoint() u32 { + @as(*volatile u32, &x).* = 10; + return 0; +} diff --git a/data/test-elfs/elf.ld b/data/test-elfs/elf.ld new file mode 100644 index 000000000..34ee22dfa --- /dev/null +++ b/data/test-elfs/elf.ld @@ -0,0 +1,26 @@ +PHDRS +{ + text PT_LOAD ; + rodata PT_LOAD ; + data PT_LOAD ; + dynamic PT_DYNAMIC ; +} +ENTRY ( entrypoint ) +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*) + } +} diff --git a/data/test-elfs/reloc_64_64.so b/data/test-elfs/reloc_64_64.so new file mode 100755 index 000000000..17711ae0a Binary files /dev/null and b/data/test-elfs/reloc_64_64.so differ diff --git a/data/test-elfs/reloc_64_64.zig b/data/test-elfs/reloc_64_64.zig new file mode 100644 index 000000000..2e87d7c79 --- /dev/null +++ b/data/test-elfs/reloc_64_64.zig @@ -0,0 +1,3 @@ +export fn entrypoint() u64 { + return @intFromPtr(&entrypoint); +} diff --git a/data/test-elfs/reloc_64_64_sbpfv1.so b/data/test-elfs/reloc_64_64_sbpfv1.so index a68fc3b74..964b73e9d 100755 Binary files a/data/test-elfs/reloc_64_64_sbpfv1.so and b/data/test-elfs/reloc_64_64_sbpfv1.so differ diff --git a/data/test-elfs/reloc_64_relative.so b/data/test-elfs/reloc_64_relative.so new file mode 100755 index 000000000..4ebd735ab Binary files /dev/null and b/data/test-elfs/reloc_64_relative.so differ diff --git a/data/test-elfs/reloc_64_relative.zig b/data/test-elfs/reloc_64_relative.zig new file mode 100644 index 000000000..3dca409d2 --- /dev/null +++ b/data/test-elfs/reloc_64_relative.zig @@ -0,0 +1,3 @@ +export fn entrypoint() u64 { + return @intFromPtr("entrypoint"); +} diff --git a/data/test-elfs/reloc_64_relative_data.so b/data/test-elfs/reloc_64_relative_data.so new file mode 100755 index 000000000..5f024fa1a Binary files /dev/null and b/data/test-elfs/reloc_64_relative_data.so differ diff --git a/data/test-elfs/reloc_64_relative_data.zig b/data/test-elfs/reloc_64_relative_data.zig new file mode 100644 index 000000000..6b15acb02 --- /dev/null +++ b/data/test-elfs/reloc_64_relative_data.zig @@ -0,0 +1,5 @@ +const DATA = "hello"; + +export fn entrypoint() u64 { + return @intFromPtr(&DATA); +} diff --git a/data/test-elfs/reloc_64_relative_data_sbpfv1.so b/data/test-elfs/reloc_64_relative_data_sbpfv1.so index 256662fc2..94739643c 100755 Binary files a/data/test-elfs/reloc_64_relative_data_sbpfv1.so and b/data/test-elfs/reloc_64_relative_data_sbpfv1.so differ diff --git a/data/test-elfs/reloc_64_relative_sbpfv1.so b/data/test-elfs/reloc_64_relative_sbpfv1.so index 086410148..c9e0717fc 100755 Binary files a/data/test-elfs/reloc_64_relative_sbpfv1.so and b/data/test-elfs/reloc_64_relative_sbpfv1.so differ diff --git a/data/test-elfs/rodata_section.so b/data/test-elfs/rodata_section.so new file mode 100755 index 000000000..197b3254f Binary files /dev/null and b/data/test-elfs/rodata_section.so differ diff --git a/data/test-elfs/rodata_section.zig b/data/test-elfs/rodata_section.zig new file mode 100644 index 000000000..3deab63a1 --- /dev/null +++ b/data/test-elfs/rodata_section.zig @@ -0,0 +1,7 @@ +const VAL_A: u64 = 41; +const VAL_B: u64 = 42; +const VAL_C: u64 = 43; + +export fn entrypoint() u64 { + return @as(*const volatile u64, &VAL_B).*; +} diff --git a/data/test-elfs/rodata_section_sbpfv1.so b/data/test-elfs/rodata_section_sbpfv1.so index d61014d5a..2b3b6410f 100755 Binary files a/data/test-elfs/rodata_section_sbpfv1.so and b/data/test-elfs/rodata_section_sbpfv1.so differ diff --git a/data/test-elfs/static_internal_call_sbpfv1.so b/data/test-elfs/static_internal_call_sbpfv1.so deleted file mode 100755 index 52f394aa8..000000000 Binary files a/data/test-elfs/static_internal_call_sbpfv1.so and /dev/null differ diff --git a/data/test-elfs/struct_func_pointer.so b/data/test-elfs/struct_func_pointer.so new file mode 100755 index 000000000..c45582bf3 Binary files /dev/null and b/data/test-elfs/struct_func_pointer.so differ diff --git a/data/test-elfs/struct_func_pointer.zig b/data/test-elfs/struct_func_pointer.zig new file mode 100644 index 000000000..ca7f35e58 --- /dev/null +++ b/data/test-elfs/struct_func_pointer.zig @@ -0,0 +1,17 @@ +const PubkeyLutEntry = extern struct { + fp: *const fn (u8) callconv(.C) u8, + key: u64, +}; + +fn f1(a: u8) callconv(.C) u8 { + return a + 1; +} + +export const entry: PubkeyLutEntry linksection(".data.rel.ro") = .{ + .fp = f1, + .key = 0x0102030405060708, +}; + +export fn entrypoint() u64 { + return entry.key; +} diff --git a/data/test-elfs/syscall_reloc_64_32.so b/data/test-elfs/syscall_reloc_64_32.so index c765a7cc9..159288586 100755 Binary files a/data/test-elfs/syscall_reloc_64_32.so and b/data/test-elfs/syscall_reloc_64_32.so differ diff --git a/data/test-elfs/syscall_reloc_64_32.zig b/data/test-elfs/syscall_reloc_64_32.zig new file mode 100644 index 000000000..3461ec900 --- /dev/null +++ b/data/test-elfs/syscall_reloc_64_32.zig @@ -0,0 +1,6 @@ +extern fn log(msg: [*]const u8, len: u64) void; + +export fn entrypoint() u64 { + log("foo\n", 4); + return 0; +} diff --git a/data/test-elfs/syscall_static.so b/data/test-elfs/syscall_static.so new file mode 100755 index 000000000..0667af4f1 Binary files /dev/null and b/data/test-elfs/syscall_static.so differ diff --git a/scripts/gen_svm_elfs.sh b/scripts/gen_svm_elfs.sh new file mode 100755 index 000000000..8bc13c47a --- /dev/null +++ b/scripts/gen_svm_elfs.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +CC="../toolchain/llvm/bin/clang" +LD="../toolchain/llvm/bin/ld.lld" +ZIG="zig" + + +LD_FLAGS="-z notext -shared --Bdynamic --script data/test-elfs/elf.ld" +C_BASE_FLAGS="-target sbf-solana-solana \ + -fno-builtin \ + -fPIC -fno-unwind-tables \ + -fomit-frame-pointer -fno-exceptions \ + -fno-asynchronous-unwind-tables \ + -std=c23 \ + -O2 \ + -Werror \ + -Wno-override-module" +C_FLAGS="${C_BASE_FLAGS} -mcpu=v1" +C_FLAGS_V2="${C_BASE_FLAGS} -mcpu=sbfv2" + +CC_V1="${CC} ${C_FLAGS}" +CC_V2="${CC} ${C_FLAGS_V2}" + +LD_V1="${LD} ${LD_FLAGS}" +LD_V2="${LD_V1} --section-start=.text=0x100000000" + +V1_FILES=(reloc_64_64 reloc_64_relative reloc_64_relative_data rodata_section) + +for ZIG_FILE in data/test-elfs/*.zig; do + BASE_NAME=$(basename "$ZIG_FILE" .zig) + + $ZIG build-obj "$ZIG_FILE" -OReleaseSmall -fstrip -fno-emit-bin -femit-llvm-bc="data/test-elfs/${BASE_NAME}.bc" + $CC_V2 "data/test-elfs/${BASE_NAME}.bc" -c -o "data/test-elfs/${BASE_NAME}.o" + $LD_V2 "data/test-elfs/${BASE_NAME}.o" -o "data/test-elfs/${BASE_NAME}.so" + + if [[ " ${V1_FILES[@]} " =~ " ${BASE_NAME} " ]]; then + $CC_V1 "data/test-elfs/${BASE_NAME}.bc" -c -o "data/test-elfs/${BASE_NAME}_sbpfv1.o" + $LD_V1 "data/test-elfs/${BASE_NAME}_sbpfv1.o" -o "data/test-elfs/${BASE_NAME}_sbpfv1.so" + fi +done + +rm data/test-elfs/*.o +rm data/test-elfs/*.bc diff --git a/src/svm/elf.zig b/src/svm/elf.zig index 3b5c7af8d..82913b865 100644 --- a/src/svm/elf.zig +++ b/src/svm/elf.zig @@ -8,6 +8,7 @@ const memory = @import("memory.zig"); const lib = @import("lib.zig"); const BuiltinProgram = lib.BuiltinProgram; +const Config = lib.Config; const Executable = lib.Executable; const Registry = lib.Registry; @@ -20,7 +21,9 @@ pub const Elf = struct { data: Data, entry_pc: u64, version: sbpf.SBPFVersion, - function_registry: Registry(u32), + function_registry: Registry(u64), + ro_section: Executable.Section, + config: Config, /// Contains immutable headers parsed from the ELF file. const Headers = struct { @@ -51,18 +54,18 @@ pub const Elf = struct { }; } - fn shdrSlice(self: Headers, index: u32) []const u8 { + fn shdrSlice(self: Headers, index: u32) ![]const u8 { const shdr = self.shdrs[index]; const sh_offset = shdr.sh_offset; const sh_size = shdr.sh_size; - return self.bytes[sh_offset..][0..sh_size]; + return try safeSlice(self.bytes, sh_offset, sh_size); } - fn phdrSlice(self: Headers, index: u32) []const u8 { + fn phdrSlice(self: Headers, index: u32) ![]const u8 { const phdr = self.phdrs[index]; const p_offset = phdr.p_offset; const p_filesz = phdr.p_filesz; - return self.bytes[p_offset..][0..p_filesz]; + return try safeSlice(self.bytes, p_offset, p_filesz); } fn getPhdrIndexByType(self: Headers, p_type: elf.Elf64_Word) ?u32 { @@ -83,7 +86,7 @@ pub const Elf = struct { const DynamicTable = [elf.DT_NUM]elf.Elf64_Xword; fn parse(headers: Headers) !Data { - const strtab = headers.shdrSlice(headers.header.e_shstrndx); + const strtab = try headers.shdrSlice(headers.header.e_shstrndx); const dynamic_table = try parseDynamic(headers); @@ -103,22 +106,25 @@ pub const Elf = struct { fn parseDynamic(headers: Headers) !?DynamicTable { var output_table: DynamicTable = .{0} ** elf.DT_NUM; - var dynamic_table: ?[]align(1) const elf.Elf64_Dyn = null; - if (headers.getPhdrIndexByType(elf.PT_DYNAMIC)) |index| { - dynamic_table = std.mem.bytesAsSlice(elf.Elf64_Dyn, headers.phdrSlice(index)); - } - - // if PT_DYNAMIC doesn't exist or is invalid, fallback to parsing - // SHT_DYNAMIC - if (dynamic_table == null) { - @panic("TODO: parse SHT_DYNAMIC"); + const dynamic_table = if (headers.getPhdrIndexByType(elf.PT_DYNAMIC)) |index| dt: { + const slice = try headers.phdrSlice(index); + if (slice.len % @sizeOf(elf.Elf64_Dyn) != 0) return error.InvalidSize; + break :dt std.mem.bytesAsSlice(elf.Elf64_Dyn, slice); + } else for (headers.shdrs, 0..) |shdr, i| { + // if PT_DYNAMIC doesn't exist or is invalid, fallback to parsing + // SHT_DYNAMIC + if (shdr.sh_type == elf.SHT_DYNAMIC) { + break std.mem.bytesAsSlice( + elf.Elf64_Dyn, + try headers.shdrSlice(@intCast(i)), + ); + } } - // if neither PT_DYNAMIC nor SHT_DYNAMIC exist, this is a state file. - if (dynamic_table == null) return null; + else return null; - for (dynamic_table.?) |dyn| { + for (dynamic_table) |dyn| { if (dyn.d_tag == elf.DT_NULL) break; if (dyn.d_tag >= elf.DT_NUM) continue; // we don't parse any reversed tags if (dyn.d_tag < 0) return error.InvalidDynamicTable; @@ -143,16 +149,13 @@ pub const Elf = struct { const size = dynamic_table[elf.DT_RELSZ]; if (size == 0) return error.InvalidDynamicSectionTable; - var offset: u64 = 0; - for (headers.phdrs) |phdr| { - const p_vaddr = phdr.p_vaddr; - const p_memsz = phdr.p_memsz; - - if (vaddr >= p_vaddr and vaddr < p_vaddr + p_memsz) { - offset = vaddr - p_vaddr + phdr.p_offset; - break; + const offset = for (headers.phdrs) |phdr| { + if (inRangeOfPhdrVm(phdr, vaddr)) { + break vaddr - phdr.p_vaddr + phdr.p_offset; } - } else @panic("invalid dynamic section, investigate special case"); + } else for (headers.shdrs) |shdr| { + if (shdr.sh_addr == vaddr) break shdr.sh_offset; + } else return error.InvalidDynamicSectionTable; return std.mem.bytesAsSlice(elf.Elf64_Rel, headers.bytes[offset..][0..size]); } @@ -169,10 +172,147 @@ pub const Elf = struct { if (shdr.sh_type != elf.SHT_SYMTAB and shdr.sh_type != elf.SHT_DYNSYM) { return error.InvalidSectionHeader; } - return std.mem.bytesAsSlice(elf.Elf64_Sym, headers.shdrSlice(@intCast(i))); + return std.mem.bytesAsSlice(elf.Elf64_Sym, try headers.shdrSlice(@intCast(i))); } else return error.InvalidDynamicSectionTable; } + fn parseRoSections( + self: Data, + headers: Headers, + config: Config, + allocator: std.mem.Allocator, + ) !Executable.Section { + const version = config.minimum_version; + const ro_names: []const []const u8 = &.{ + ".text", + ".rodata", + ".data.rel.ro", + ".eh_frame", + }; + + var lowest_addr: usize = std.math.maxInt(usize); + var highest_addr: usize = 0; + + var ro_fill_length: usize = 0; + + var ro_slices = try std.ArrayListUnmanaged(struct { + usize, + []const u8, + }).initCapacity(allocator, headers.shdrs.len); + defer ro_slices.deinit(allocator); + + var addr_file_offset: ?u64 = null; + var invalid_offsets: bool = false; + + var first_ro_section: usize = 0; + var last_ro_section: usize = 0; + var n_ro_sections: usize = 0; + + for (headers.shdrs, 0..) |shdr, i| { + const name = self.getString(shdr.sh_name); + for (ro_names) |ro_name| { + if (std.mem.eql(u8, ro_name, name)) break; + } else continue; + + if (n_ro_sections == 0) { + first_ro_section = i; + } + last_ro_section = i; + n_ro_sections = n_ro_sections +| 1; + + const section_addr = shdr.sh_addr; + + if (!invalid_offsets) { + if (version.enableElfVaddr()) { + assert(config.optimize_rodata); + if (section_addr < shdr.sh_offset) { + invalid_offsets = true; + } else { + const offset = try std.math.sub(u64, section_addr, shdr.sh_offset); + addr_file_offset = addr_file_offset orelse offset; + if (addr_file_offset.? != offset) { + invalid_offsets = true; + } + } + } else if (section_addr != shdr.sh_offset) { + invalid_offsets = true; + } + } + + var vaddr_end = if (version.enableElfVaddr() and + section_addr >= memory.PROGRAM_START) + section_addr + else + section_addr +| memory.PROGRAM_START; + if (version.rejectRodataStackOverlap()) { + vaddr_end +|= shdr.sh_size; + } + if ((config.reject_broken_elfs and invalid_offsets) or + vaddr_end > memory.STACK_START) + { + return error.ValueOutOfBounds; + } + + const section_data = try headers.shdrSlice(@intCast(i)); + lowest_addr = @min(lowest_addr, section_addr); + highest_addr = @max(highest_addr, section_addr +| section_data.len); + ro_fill_length +|= section_data.len; + + ro_slices.appendAssumeCapacity(.{ section_addr, section_data }); + } + + if (config.reject_broken_elfs and lowest_addr +| ro_fill_length > highest_addr) { + return error.ValueOutOfBounds; + } + + const can_borrow = !invalid_offsets and + last_ro_section +| 1 -| first_ro_section == n_ro_sections and + config.optimize_rodata; + const ro_section: Executable.Section = if (can_borrow) ro: { + const file_offset = addr_file_offset orelse 0; + const start = lowest_addr -| file_offset; + const end = highest_addr -| file_offset; + + if (lowest_addr >= memory.PROGRAM_START) { + break :ro .{ .borrowed = .{ + .offset = lowest_addr -| memory.PROGRAM_START, + .start = start, + .end = end, + } }; + } else { + if (version.enableElfVaddr()) { + return error.ValueOutOfBounds; + } + break :ro .{ .borrowed = .{ + .offset = lowest_addr, + .start = start, + .end = end, + } }; + } + } else ro: { + if (config.optimize_rodata) { + highest_addr -|= lowest_addr; + } else { + lowest_addr = 0; + } + + const buf_len = highest_addr; + if (buf_len > headers.bytes.len) { + return error.ValueOutOfBounds; + } + + const ro_section = try allocator.alloc(u8, buf_len); + @memset(ro_section, 0); + for (ro_slices.items) |ro_slice| { + const section_addr, const slice = ro_slice; + const buf_offset_start = section_addr -| lowest_addr; + @memcpy(ro_section[buf_offset_start..][0..slice.len], slice); + } + break :ro .{ .owned = .{ .offset = lowest_addr, .data = ro_section } }; + }; + return ro_section; + } + /// Returns the string for a given index into the string table. fn getString(self: Data, off: u32) [:0]const u8 { assert(off < self.strtab.len); @@ -180,7 +320,7 @@ pub const Elf = struct { return std.mem.sliceTo(ptr, 0); } - fn getShdrIndexByName(self: Data, headers: Headers, name: []const u8) ?u32 { + pub fn getShdrIndexByName(self: Data, headers: Headers, name: []const u8) ?u32 { for (headers.shdrs, 0..) |shdr, i| { const shdr_name = self.getString(shdr.sh_name); if (std.mem.eql(u8, shdr_name, name)) { @@ -200,6 +340,7 @@ pub const Elf = struct { allocator: std.mem.Allocator, bytes: []u8, loader: *BuiltinProgram, + config: Config, ) !Elf { const headers = Headers.parse(bytes); const data = try Data.parse(headers); @@ -214,8 +355,8 @@ pub const Elf = struct { else .v1; - if (sbpf_version != .v1) - std.debug.panic("found sbpf version: {s}, support it!", .{@tagName(sbpf_version)}); + if (@intFromEnum(sbpf_version) < @intFromEnum(config.minimum_version)) + return error.VersionUnsupported; var self: Elf = .{ .bytes = bytes, @@ -224,8 +365,16 @@ pub const Elf = struct { .entry_pc = entry_pc, .version = sbpf_version, .function_registry = .{}, + .config = config, + .ro_section = try data.parseRoSections(headers, config, allocator), }; - errdefer self.function_registry.deinit(allocator); + errdefer self.deinit(allocator); + + _ = try self.function_registry.registerHashedLegacy( + allocator, + "entrypoint", + entry_pc, + ); try self.validate(); try self.relocate(allocator, loader); @@ -233,72 +382,6 @@ pub const Elf = struct { return self; } - pub fn parseRoSections(self: *const Elf, allocator: std.mem.Allocator) !Executable.Section { - const ro_names: []const []const u8 = &.{ - ".text", - ".rodata", - ".data.rel.ro", - ".eh_frame", - }; - - var lowest_addr: usize = std.math.maxInt(usize); - var highest_addr: usize = 0; - - var ro_fill_length: usize = 0; - - var ro_slices = try std.ArrayListUnmanaged(struct { - usize, - []const u8, - }).initCapacity(allocator, self.headers.shdrs.len); - defer ro_slices.deinit(allocator); - - for (self.headers.shdrs, 0..) |shdr, i| { - const name = self.data.getString(shdr.sh_name); - for (ro_names) |ro_name| { - if (std.mem.eql(u8, ro_name, name)) break; - } else continue; - - const section_addr = shdr.sh_addr; - - if (section_addr != shdr.sh_offset) { - return error.InvalidOffset; - } - - const vaddr_end = section_addr +| memory.PROGRAM_START; - if (vaddr_end > memory.STACK_START) { - return error.ValueOutOfBounds; - } - - const section_data = self.headers.shdrSlice(@intCast(i)); - lowest_addr = @min(lowest_addr, section_addr); - highest_addr = @max(highest_addr, section_addr +| section_data.len); - ro_fill_length +|= section_data.len; - - ro_slices.appendAssumeCapacity(.{ section_addr, section_data }); - } - - // NOTE: this check isn't valid for SBFv1, just here for sanity. will need to remove for testing. - if (lowest_addr +| ro_fill_length > highest_addr) { - return error.ValueOutOfBounds; - } - - lowest_addr = 0; - const buf_len = highest_addr; - if (buf_len > self.bytes.len) { - return error.ValueOutOfBounds; - } - - const ro_section = try allocator.alloc(u8, buf_len); - @memset(ro_section, 0); - for (ro_slices.items) |ro_slice| { - const section_addr, const slice = ro_slice; - const buf_offset_start = section_addr -| lowest_addr; - @memcpy(ro_section[buf_offset_start..][0..slice.len], slice); - } - - return .{ .owned = .{ .offset = lowest_addr, .data = ro_section } }; - } - /// Validates the Elf. Returns errors for issues encountered. fn validate(self: *Elf) !void { const header = self.headers.header; @@ -368,14 +451,18 @@ pub const Elf = struct { // ensure that the entry point is inside of the ".text" section const entrypoint = header.e_entry; - const text_section = self.getShdrByName(".text") orelse - return error.ShdrNotFound; - - if (entrypoint < text_section.sh_addr or - entrypoint > text_section.sh_addr +| text_section.sh_size) - { + const text_section_index = self.getShdrIndexByName(".text") orelse + return error.NoTextSection; + if (!self.inRangeOfShdrVaddr(text_section_index, entrypoint)) { return error.EntrypointOutsideTextSection; } + const text_section_slice = try self.headers.shdrSlice(text_section_index); + if (text_section_slice.len % @sizeOf(sbpf.Instruction) != 0) + return error.InvalidTextSectionLength; + + if (self.config.minimum_version.enableElfVaddr()) { + if (self.config.optimize_rodata != true) return error.UnsupportedSBPFVersion; + } } fn relocate( @@ -383,15 +470,20 @@ pub const Elf = struct { allocator: std.mem.Allocator, loader: *BuiltinProgram, ) !void { + const config = self.config; + const version = self.version; const text_section_index = self.getShdrIndexByName(".text") orelse return error.ShdrNotFound; const text_section = self.headers.shdrs[text_section_index]; // fixup PC-relative call instructions const text_bytes: []u8 = self.bytes[text_section.sh_offset..][0..text_section.sh_size]; - const instructions = try self.getInstructions(); + const instructions = self.getInstructions(); for (instructions, 0..) |inst, i| { - if (inst.opcode == .call_imm and inst.imm != ~@as(u32, 0)) { + if (inst.opcode == .call_imm and + inst.imm != ~@as(u32, 0) and + !(version.enableStaticSyscalls() and inst.src == .r0)) + { const target_pc = @as(i64, @intCast(i)) +| @as(i32, @bitCast(inst.imm)) +| 1; if (target_pc < 0 or target_pc >= instructions.len) return error.RelativeJumpOutOfBounds; @@ -407,48 +499,69 @@ pub const Elf = struct { } } + var phdr: ?elf.Elf64_Phdr = null; + for (self.data.relocations_table) |reloc| { - if (self.version != .v1) @panic("TODO here"); - const r_offset = reloc.r_offset; + var r_offset = reloc.r_offset; + + if (version.enableElfVaddr()) { + const found = if (phdr) |header| found: { + break :found inRangeOfPhdrVm(header, r_offset); + } else false; + if (!found) { + phdr = for (self.headers.phdrs) |header| { + if (inRangeOfPhdrVm(header, r_offset)) { + break header; + } + } else null; + } + const header = phdr orelse return error.ValueOutOfBounds; + r_offset = r_offset -| header.p_vaddr +| header.p_offset; + } switch (@as(elf.R_X86_64, @enumFromInt(reloc.r_type()))) { .@"64" => { // if the relocation is addressing an instruction inside of the // text section, we'll need to offset it by the offset of the immediate // field into the instruction. - // TODO: in V1 this is by default, but in V2 we check if the offset is inside of the - // section - const imm_offset = r_offset + 4; - - const ref_addr = std.mem.readInt(u32, self.bytes[imm_offset..][0..4], .little); + const in_text_section = self.inRangeOfShdr( + text_section_index, + r_offset, + ) or version == .v1; + const imm_offset = if (in_text_section) r_offset +| 4 else r_offset; + + const addr_slice = try safeSlice(self.bytes, imm_offset, 4); + const ref_addr = std.mem.readInt(u32, addr_slice[0..4], .little); const symbol = self.data.symbol_table[reloc.r_sym()]; - var addr = symbol.st_value +| ref_addr; + if (addr < memory.PROGRAM_START) { addr +|= memory.PROGRAM_START; } - { - const imm_low_offset = imm_offset; - const imm_slice = self.bytes[imm_low_offset..][0..4]; - std.mem.writeInt(u32, imm_slice, @truncate(addr), .little); - } + if (in_text_section or version == .v1) { + { + const imm_low_offset = imm_offset; + const imm_slice = try safeSlice(self.bytes, imm_low_offset, 4); + std.mem.writeInt(u32, imm_slice[0..4], @truncate(addr), .little); + } - { - const imm_high_offset = imm_offset +| 8; - const imm_slice = self.bytes[imm_high_offset..][0..4]; - std.mem.writeInt(u32, imm_slice, @intCast(addr >> 32), .little); + { + const imm_high_offset = imm_offset +| 8; + const imm_slice = try safeSlice(self.bytes, imm_high_offset, 4); + std.mem.writeInt(u32, imm_slice[0..4], @intCast(addr >> 32), .little); + } + } else { + const imm_slice = try safeSlice(self.bytes, imm_offset, 8); + std.mem.writeInt(u64, imm_slice[0..8], addr, .little); } }, .RELATIVE => { const imm_offset = r_offset +| 4; // is the relocation targetting inside of the text section - if (imm_offset >= text_section.sh_offset and - imm_offset < text_section.sh_offset + text_section.sh_size) - { - // the target is a lddw instruction which takes up two instruction - // slots + if (self.inRangeOfShdr(text_section_index, imm_offset)) { + // the target is a lddw instruction which takes up two instruction slots const va_low = val: { const imm_slice = self.bytes[imm_offset..][0..4]; @@ -479,23 +592,27 @@ pub const Elf = struct { std.mem.writeInt(u32, imm_slice, @intCast(ref_addr >> 32), .little); } } else { - switch (self.version) { - .v1 => { - const address = std.mem.readInt( - u32, - self.bytes[imm_offset..][0..4], - .little, - ); - const ref_addr = memory.PROGRAM_START +| address; - std.mem.writeInt( - u64, - self.bytes[r_offset..][0..8], - ref_addr, - .little, + const address: u64 = switch (version) { + .v1 => addr: { + const addr_slice = try safeSlice(self.bytes, imm_offset, 4); + const address = std.mem.readInt(u32, addr_slice[0..4], .little); + break :addr memory.PROGRAM_START +| address; + }, + else => addr: { + const addr_slice = try safeSlice( + self.bytes, + r_offset, + @sizeOf(u64), ); + var address = std.mem.readInt(u64, addr_slice[0..8], .little); + if (address < memory.PROGRAM_START) { + address +|= memory.PROGRAM_START; + } + break :addr address; }, - else => @panic("TODO"), - } + }; + const addr_slice = try safeSlice(self.bytes, r_offset, @sizeOf(u64)); + std.mem.writeInt(u64, addr_slice[0..8], address, .little); } }, .@"32" => { @@ -507,7 +624,7 @@ pub const Elf = struct { const dynstr_index = self.getShdrIndexByName(".dynstr") orelse return error.NoDynStrSection; - const dynstr = self.headers.shdrSlice(dynstr_index); + const dynstr = try self.headers.shdrSlice(dynstr_index); const symbol_name = std.mem.sliceTo(dynstr[symbol.st_name..], 0); // If the symbol is defined, this is a bpf-to-bpf call. @@ -518,16 +635,17 @@ pub const Elf = struct { symbol_name, @intCast(target_pc), ); - const slice = self.bytes[imm_offset..][0..4]; - std.mem.writeInt(u32, slice, key, .little); + const slice = try safeSlice(self.bytes, imm_offset, 4); + std.mem.writeInt(u32, slice[0..4], key, .little); } else { const hash = sbpf.hashSymbolName(symbol_name); - if (loader.functions.lookupKey(hash) == null) { - // return error.UnresolvedSymbol; - @panic(symbol_name); + if (config.reject_broken_elfs and + loader.functions.lookupKey(hash) == null) + { + return error.UnresolvedSymbol; } - const slice = self.bytes[imm_offset..][0..4]; - std.mem.writeInt(u32, slice, hash, .little); + const slice = try safeSlice(self.bytes, imm_offset, 4); + std.mem.writeInt(u32, slice[0..4], hash, .little); } }, else => return error.UnknownRelocation, @@ -535,10 +653,16 @@ pub const Elf = struct { } } - pub fn getInstructions(self: Elf) ![]align(1) const sbpf.Instruction { - const text_section_index = self.getShdrIndexByName(".text") orelse - return error.ShdrNotFound; - const text_bytes: []const u8 = self.headers.shdrSlice(text_section_index); + pub fn deinit(self: *Elf, allocator: std.mem.Allocator) void { + self.function_registry.deinit(allocator); + self.ro_section.deinit(allocator); + } + + /// The function is guarnteed to succeed, since `parse` already checks that + /// the `.text` section exists and it's sized correctly. + pub fn getInstructions(self: Elf) []align(1) const sbpf.Instruction { + const text_section_index = self.getShdrIndexByName(".text").?; + const text_bytes: []const u8 = self.headers.shdrSlice(text_section_index) catch unreachable; return std.mem.bytesAsSlice(sbpf.Instruction, text_bytes); } @@ -549,4 +673,31 @@ pub const Elf = struct { pub fn getShdrByName(self: Elf, name: []const u8) ?elf.Elf64_Shdr { return self.data.getShdrByName(self.headers, name); } + + fn inRangeOfShdr(self: *const Elf, index: usize, addr: usize) bool { + const shdr = self.headers.shdrs[index]; + const sh_offset = shdr.sh_offset; + const sh_size = shdr.sh_size; + return addr >= sh_offset and addr < sh_offset + sh_size; + } + + fn inRangeOfShdrVaddr(self: *const Elf, index: usize, addr: usize) bool { + const shdr = self.headers.shdrs[index]; + const sh_addr = shdr.sh_addr; + const sh_size = shdr.sh_size; + return addr >= sh_addr and addr < sh_addr + sh_size; + } + + fn inRangeOfPhdrVm(phdr: elf.Elf64_Phdr, addr: usize) bool { + const p_vaddr = phdr.p_vaddr; + const p_memsz = phdr.p_memsz; + return addr >= p_vaddr and addr < p_vaddr + p_memsz; + } + + fn safeSlice(base: anytype, start: usize, len: usize) error{OutOfBounds}!@TypeOf(base) { + if (start >= base.len) return error.OutOfBounds; + const end = std.math.add(usize, start, len) catch return error.OutOfBounds; + if (end > base.len) return error.OutOfBounds; + return base[start..][0..len]; + } }; diff --git a/src/svm/executable.zig b/src/svm/executable.zig index 70db6a042..791345f09 100644 --- a/src/svm/executable.zig +++ b/src/svm/executable.zig @@ -13,18 +13,19 @@ pub const Executable = struct { from_elf: bool, ro_section: Section, text_vaddr: u64, - function_registry: Registry(u32), + function_registry: Registry(u64), + config: Config, pub const Section = union(enum) { owned: Owned, - assembly: Assembly, + borrowed: Borrowed, const Owned = struct { offset: u64, data: []const u8, }; - const Assembly = struct { + const Borrowed = struct { offset: u64, start: u64, end: u64, @@ -33,29 +34,28 @@ pub const Executable = struct { pub fn deinit(section: Section, allocator: std.mem.Allocator) void { switch (section) { .owned => |owned| allocator.free(owned.data), - .assembly => {}, + .borrowed => {}, } } }; - pub fn fromElf(allocator: std.mem.Allocator, elf: *const Elf) !Executable { - const ro_section = try elf.parseRoSections(allocator); - errdefer ro_section.deinit(allocator); - + /// Takes ownership of the `Elf`. + pub fn fromElf(elf: Elf) !Executable { return .{ .bytes = elf.bytes, - .ro_section = ro_section, - .instructions = try elf.getInstructions(), + .ro_section = elf.ro_section, + .instructions = elf.getInstructions(), .version = elf.version, .entry_pc = elf.entry_pc, .from_elf = true, - .text_vaddr = elf.getShdrByName(".text").?.sh_addr, + .text_vaddr = elf.getShdrByName(".text").?.sh_addr +| memory.PROGRAM_START, .function_registry = elf.function_registry, + .config = elf.config, }; } - pub fn fromAsm(allocator: std.mem.Allocator, source: []const u8) !Executable { - return Assembler.parse(allocator, source); + pub fn fromAsm(allocator: std.mem.Allocator, source: []const u8, config: Config) !Executable { + return Assembler.parse(allocator, source, config); } /// When the executable comes from the assembler, we need to guarantee that the @@ -75,7 +75,7 @@ pub const Executable = struct { pub fn getProgramRegion(self: *const Executable) memory.Region { const offset, const ro_data = switch (self.ro_section) { .owned => |o| .{ o.offset, o.data }, - .assembly => |a| .{ a.offset, self.bytes[a.start..a.end] }, + .borrowed => |b| .{ b.offset, self.bytes[b.start..b.end] }, }; return memory.Region.init(.constant, ro_data, memory.PROGRAM_START +| offset); } @@ -106,7 +106,11 @@ pub const Assembler = struct { }; }; - fn parse(allocator: std.mem.Allocator, source: []const u8) !Executable { + fn parse( + allocator: std.mem.Allocator, + source: []const u8, + config: Config, + ) !Executable { var assembler: Assembler = .{ .source = source }; const statements = try assembler.tokenize(allocator); defer { @@ -122,7 +126,7 @@ pub const Assembler = struct { var labels: std.StringHashMapUnmanaged(u64) = .{}; defer labels.deinit(allocator); - var function_registry: Registry(u32) = .{}; + var function_registry: Registry(u64) = .{}; try labels.put(allocator, "entrypoint", 0); var inst_ptr: u32 = 0; @@ -334,13 +338,14 @@ pub const Assembler = struct { return .{ .bytes = source, - .ro_section = .{ .assembly = .{ .offset = 0, .start = 0, .end = source.len } }, + .ro_section = .{ .borrowed = .{ .offset = 0, .start = 0, .end = source.len } }, .instructions = try instructions.toOwnedSlice(allocator), - .version = .v1, + .version = config.minimum_version, .entry_pc = entry_pc, .from_elf = false, .text_vaddr = memory.PROGRAM_START, .function_registry = function_registry, + .config = config, }; } @@ -500,3 +505,15 @@ pub const BuiltinProgram = struct { self.functions.deinit(allocator); } }; + +pub const Config = struct { + optimize_rodata: bool = true, + reject_broken_elfs: bool = false, + minimum_version: sbpf.SBPFVersion = .v2, + stack_frame_size: u64 = 4096, + max_call_depth: u64 = 64, + + pub fn stackSize(config: Config) u64 { + return config.stack_frame_size * config.max_call_depth; + } +}; diff --git a/src/svm/lib.zig b/src/svm/lib.zig index 5c1e917cc..769bcd304 100644 --- a/src/svm/lib.zig +++ b/src/svm/lib.zig @@ -10,5 +10,6 @@ pub const Executable = executable.Executable; pub const BuiltinProgram = executable.BuiltinProgram; pub const Registry = executable.Registry; pub const Assembler = executable.Assembler; +pub const Config = executable.Config; pub const Vm = vm.Vm; pub const Elf = elf.Elf; diff --git a/src/svm/main.zig b/src/svm/main.zig index c66974acb..d92f08f17 100644 --- a/src/svm/main.zig +++ b/src/svm/main.zig @@ -9,6 +9,7 @@ const Executable = svm.Executable; const Vm = svm.Vm; const sbpf = svm.sbpf; const syscalls = svm.syscalls; +const Config = svm.Config; const MemoryMap = memory.MemoryMap; @@ -67,11 +68,15 @@ pub fn main() !void { ); } + const config: Config = .{ + .minimum_version = if (assemble) .v2 else .v1, + }; var executable = if (assemble) - try Executable.fromAsm(allocator, bytes) + try Executable.fromAsm(allocator, bytes, config) else exec: { - const elf = try Elf.parse(allocator, bytes, &loader); - break :exec try Executable.fromElf(allocator, &elf); + const elf = try Elf.parse(allocator, bytes, &loader, config); + errdefer elf.deinit(allocator); + break :exec try Executable.fromElf(elf); }; defer executable.deinit(allocator); @@ -79,7 +84,7 @@ pub fn main() !void { defer allocator.free(heap_mem); @memset(heap_mem, 0x00); - const stack_memory = try allocator.alloc(u8, 4096 * 64); + const stack_memory = try allocator.alloc(u8, config.stackSize()); defer allocator.free(stack_memory); @memset(stack_memory, 0); @@ -90,7 +95,7 @@ pub fn main() !void { memory.Region.init(.mutable, &.{}, memory.INPUT_START), }, executable.version); - var vm = try Vm.init(allocator, &executable, m, &loader); + var vm = try Vm.init(allocator, &executable, m, &loader, stack_memory.len); defer vm.deinit(); const result = try vm.run(); diff --git a/src/svm/sbpf.zig b/src/svm/sbpf.zig index 1c98343f0..a94bdbfd5 100644 --- a/src/svm/sbpf.zig +++ b/src/svm/sbpf.zig @@ -20,6 +20,34 @@ pub const SBPFVersion = enum { v2, // v3, // reserved, + + pub fn enableDynamicStackFrames(version: SBPFVersion) bool { + return version != .v1; + } + + pub fn enableElfVaddr(version: SBPFVersion) bool { + return version != .v1; + } + + pub fn enableLDDW(version: SBPFVersion) bool { + return version == .v1; + } + + pub fn enableStaticSyscalls(version: SBPFVersion) bool { + return version != .v1; + } + + pub fn enableNegation(version: SBPFVersion) bool { + return version == .v1; + } + + pub fn enableLe(version: SBPFVersion) bool { + return version == .v1; + } + + pub fn rejectRodataStackOverlap(version: SBPFVersion) bool { + return version != .v1; + } }; pub const Instruction = packed struct(u64) { @@ -174,6 +202,8 @@ pub const Instruction = packed struct(u64) { arsh64_imm = alu64 | k | arsh, /// bpf opcode: `arsh64 dst, src` /// `dst >>= src (arithmetic)`. arsh64_reg = alu64 | x | arsh, + /// bpf opcode: `hor64 dst, imm` /// `dst |= imm << 32`. + hor64_imm = alu64 | k | hor, /// bpf opcode: `ja +off` /// `pc += off`. ja = jmp | 0x0, @@ -229,6 +259,7 @@ pub const Instruction = packed struct(u64) { /// bpf opcode: `exit` /// `return r0`. /// valid only until sbpfv3 exit = jmp | exit_code, + _, pub fn isReg(opcode: OpCode) bool { const is_reg_bit: u1 = @truncate(@intFromEnum(opcode) >> 3); @@ -326,6 +357,8 @@ pub const Instruction = packed struct(u64) { .{ "rsh" , .{ .inst = .alu_binary, .opc = rsh | alu64 } }, .{ "rsh64", .{ .inst = .alu_binary, .opc = rsh | alu64 } }, .{ "rsh32", .{ .inst = .alu_binary, .opc = rsh | alu32 } }, + + .{ "hor64", .{ .inst = .alu_binary, .opc = hor | alu64 } }, .{ "neg" , .{ .inst = .alu_unary, .opc = neg | alu64 } }, .{ "neg64", .{ .inst = .alu_unary, .opc = neg | alu64 } }, @@ -483,6 +516,8 @@ pub const Instruction = packed struct(u64) { pub const arsh: u8 = 0xc0; /// alu/alu64 operation code: endianness conversion. pub const end: u8 = 0xd0; + /// alu/alu64 operation code: high or + pub const hor: u8 = 0xf0; pub const Register = enum(u4) { /// Return Value @@ -525,5 +560,5 @@ pub const Instruction = packed struct(u64) { }; pub fn hashSymbolName(name: []const u8) u32 { - return std.hash.Murmur3_32.hash(name); + return std.hash.Murmur3_32.hashWithSeed(name, 0); } diff --git a/src/svm/tests.zig b/src/svm/tests.zig index 0aeb16d78..7ebff75c2 100644 --- a/src/svm/tests.zig +++ b/src/svm/tests.zig @@ -9,23 +9,29 @@ const Elf = @import("elf.zig").Elf; const Executable = lib.Executable; const BuiltinProgram = lib.BuiltinProgram; +const Config = lib.Config; const Region = memory.Region; const MemoryMap = memory.MemoryMap; const expectEqual = std.testing.expectEqual; -fn testAsm(source: []const u8, expected: anytype) !void { - return testAsmWithMemory(source, &.{}, expected); +fn testAsm(config: Config, source: []const u8, expected: anytype) !void { + return testAsmWithMemory(config, source, &.{}, expected); } -fn testAsmWithMemory(source: []const u8, program_memory: []const u8, expected: anytype) !void { +fn testAsmWithMemory( + config: Config, + source: []const u8, + program_memory: []const u8, + expected: anytype, +) !void { const allocator = std.testing.allocator; - var executable = try Executable.fromAsm(allocator, source); + var executable = try Executable.fromAsm(allocator, source, config); defer executable.deinit(allocator); const mutable = try allocator.dupe(u8, program_memory); defer allocator.free(mutable); - const stack_memory = try allocator.alloc(u8, 4096); + const stack_memory = try allocator.alloc(u8, config.stackSize()); defer allocator.free(stack_memory); const m = try MemoryMap.init(&.{ @@ -36,7 +42,7 @@ fn testAsmWithMemory(source: []const u8, program_memory: []const u8, expected: a }, .v1); var loader: BuiltinProgram = .{}; - var vm = try Vm.init(allocator, &executable, m, &loader); + var vm = try Vm.init(allocator, &executable, m, &loader, stack_memory.len); defer vm.deinit(); const result = vm.run(); @@ -44,7 +50,7 @@ fn testAsmWithMemory(source: []const u8, program_memory: []const u8, expected: a } test "basic mov" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r1, 1 \\ mov r0, r1 @@ -53,7 +59,7 @@ test "basic mov" { } test "mov32 imm large" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov32 r0, -1 \\ exit @@ -61,7 +67,7 @@ test "mov32 imm large" { } test "mov large" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov32 r1, -1 \\ mov32 r0, r1 @@ -70,7 +76,7 @@ test "mov large" { } test "bounce" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r0, 1 \\ mov r6, r0 @@ -83,7 +89,7 @@ test "bounce" { } test "add32" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov32 r0, 0 \\ mov32 r1, 2 @@ -94,7 +100,7 @@ test "add32" { } test "add64" { - try testAsm( + try testAsm(.{ .minimum_version = .v1 }, \\entrypoint: \\ lddw r0, 0x300000fff \\ add r0, -1 @@ -103,7 +109,7 @@ test "add64" { } test "alu32 logic" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov32 r0, 0 \\ mov32 r1, 1 @@ -129,7 +135,7 @@ test "alu32 logic" { , 0x11); } test "alu64 logic" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r0, 0 \\ mov r1, 1 @@ -158,7 +164,7 @@ test "alu64 logic" { } test "mul32 imm" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r0, 3 \\ mul32 r0, 4 @@ -167,7 +173,7 @@ test "mul32 imm" { } test "mul32 reg" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r0, 3 \\ mov r1, 4 @@ -177,7 +183,7 @@ test "mul32 reg" { } test "mul32 overflow" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r0, 0x40000001 \\ mov r1, 4 @@ -187,7 +193,7 @@ test "mul32 overflow" { } test "mul64 imm" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r0, 0x40000001 \\ mul r0, 4 @@ -196,7 +202,7 @@ test "mul64 imm" { } test "mul64 reg" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r0, 0x40000001 \\ mov r1, 4 @@ -206,7 +212,7 @@ test "mul64 reg" { } test "mul32 negative" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r0, -1 \\ mul32 r0, 4 @@ -215,7 +221,7 @@ test "mul32 negative" { } test "div32 imm" { - try testAsm( + try testAsm(.{ .minimum_version = .v1 }, \\entrypoint: \\ lddw r0, 0x10000000c \\ div32 r0, 4 @@ -224,7 +230,7 @@ test "div32 imm" { } test "div32 reg" { - try testAsm( + try testAsm(.{ .minimum_version = .v1 }, \\entrypoint: \\ mov r0, 12 \\ lddw r1, 0x100000004 @@ -234,7 +240,7 @@ test "div32 reg" { } test "div32 small" { - try testAsm( + try testAsm(.{ .minimum_version = .v1 }, \\entrypoint: \\ lddw r0, 0x10000000c \\ mov r1, 4 @@ -244,7 +250,7 @@ test "div32 small" { } test "div64 imm" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r0, 0xc \\ lsh r0, 32 @@ -254,7 +260,7 @@ test "div64 imm" { } test "div64 reg" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r0, 0xc \\ lsh r0, 32 @@ -265,7 +271,7 @@ test "div64 reg" { } test "div division by zero" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov32 r0, 1 \\ mov32 r1, 0 @@ -275,7 +281,7 @@ test "div division by zero" { } test "div32 division by zero" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov32 r0, 1 \\ mov32 r1, 0 @@ -285,7 +291,7 @@ test "div32 division by zero" { } test "neg32" { - try testAsm( + try testAsm(.{ .minimum_version = .v1 }, \\entrypoint: \\ mov32 r0, 2 \\ neg32 r0 @@ -294,7 +300,7 @@ test "neg32" { } test "neg64" { - try testAsm( + try testAsm(.{ .minimum_version = .v1 }, \\entrypoint: \\ mov r0, 2 \\ neg r0 @@ -302,8 +308,22 @@ test "neg64" { , 0xFFFFFFFFFFFFFFFE); } +test "neg invalid on v2" { + try testAsm(.{}, + \\entrypoint: + \\ neg32 r0 + \\ exit + , error.UnknownInstruction); + + try testAsm(.{}, + \\entrypoint: + \\ neg64 r0 + \\ exit + , error.UnknownInstruction); +} + test "sub32 imm" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov32 r0, 3 \\ sub32 r0, 1 @@ -312,7 +332,7 @@ test "sub32 imm" { } test "sub32 reg" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov32 r0, 4 \\ mov32 r1, 2 @@ -322,7 +342,7 @@ test "sub32 reg" { } test "sub64 imm" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r0, 3 \\ sub r0, 1 @@ -331,7 +351,7 @@ test "sub64 imm" { } test "sub64 imm negative" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r0, 3 \\ sub r0, -1 @@ -340,7 +360,7 @@ test "sub64 imm negative" { } test "sub64 reg" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r0, 4 \\ mov r1, 2 @@ -350,31 +370,27 @@ test "sub64 reg" { } test "mod32" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov32 r0, 5748 \\ mod32 r0, 92 \\ mov32 r1, 13 \\ mod32 r0, r1 \\ exit - , - 0x5, - ); + , 0x5); } test "mod32 overflow" { - try testAsm( + try testAsm(.{ .minimum_version = .v1 }, \\entrypoint: \\ lddw r0, 0x100000003 \\ mod32 r0, 3 \\ exit - , - 0x0, - ); + , 0x0); } test "mod32 all" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov32 r0, -1316649930 \\ lsh r0, 32 @@ -385,62 +401,63 @@ test "mod32 all" { \\ mod r0, r1 \\ mod r0, 0x658f1778 \\ exit - , - 0x30ba5a04, - ); + , 0x30ba5a04); } test "mod64 divide by zero" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov32 r0, 1 \\ mov32 r1, 0 \\ mod r0, r1 \\ exit - , - error.DivisionByZero, - ); + , error.DivisionByZero); } test "mod32 divide by zero" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov32 r0, 1 \\ mov32 r1, 0 \\ mod32 r0, r1 \\ exit - , - error.DivisionByZero, - ); + , error.DivisionByZero); +} + +test "arsh32 high shift" { + try testAsm(.{}, + \\entrypoint: + \\ mov r0, 8 + \\ mov32 r1, 0x00000001 + \\ hor64 r1, 0x00000001 + \\ arsh32 r0, r1 + \\ exit + , 0x4); } test "arsh32 imm" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov32 r0, 0xf8 \\ lsh32 r0, 28 \\ arsh32 r0, 16 \\ exit - , - 0xffff8000, - ); + , 0xffff8000); } test "arsh32 reg" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov32 r0, 0xf8 \\ mov32 r1, 16 \\ lsh32 r0, 28 \\ arsh32 r0, r1 \\ exit - , - 0xffff8000, - ); + , 0xffff8000); } test "arsh64" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov32 r0, 1 \\ lsh r0, 63 @@ -448,25 +465,36 @@ test "arsh64" { \\ mov32 r1, 5 \\ arsh r0, r1 \\ exit - , - 0xfffffffffffffff8, - ); + , 0xfffffffffffffff8); +} + +test "hor64" { + try testAsm(.{}, + \\entrypoint: + \\ hor64 r0, 0x10203040 + \\ hor64 r0, 0x01020304 + \\ exit + , 0x1122334400000000); } test "lddw" { - try testAsm( + try testAsm(.{ .minimum_version = .v1 }, \\entrypoint: \\ lddw r0, 0x1122334455667788 \\ exit , 0x1122334455667788); +} - try testAsm( +test "lddw bottom" { + try testAsm(.{ .minimum_version = .v1 }, \\entrypoint: \\ lddw r0, 0x0000000080000000 \\ exit , 0x80000000); +} - try testAsm( +test "lddw logic" { + try testAsm(.{ .minimum_version = .v1 }, \\entrypoint: \\ mov r0, 0 \\ mov r1, 0 @@ -481,38 +509,94 @@ test "lddw" { , 0x2); } -test "lsh64 reg" { - try testAsm( +test "lddw invalid on v2" { + try testAsm(.{}, \\entrypoint: - \\ mov r0, 0x1 - \\ mov r7, 4 - \\ lsh r0, r7 + \\ lddw r0, 0x1122334455667788 \\ exit - , 0x10); + , error.UnknownInstruction); } -test "rhs32 imm" { - try testAsm( - \\entrypoint: - \\ xor r0, r0 - \\ add r0, -1 - \\ rsh32 r0, 8 +test "le16" { + try testAsmWithMemory( + .{ .minimum_version = .v1 }, + \\ ldxh r0, [r1] + \\ le16 r0 \\ exit - , 0x00ffffff); + , + &.{ 0x22, 0x11 }, + 0x1122, + ); } -test "rhs64 reg" { - try testAsm( - \\entrypoint: - \\ mov r0, 0x10 - \\ mov r7, 4 - \\ rsh r0, r7 +test "le16 high" { + try testAsmWithMemory( + .{ .minimum_version = .v1 }, + \\ ldxdw r0, [r1] + \\ le16 r0 \\ exit - , 0x1); + , + &.{ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 }, + 0x2211, + ); +} + +test "le32" { + try testAsmWithMemory( + .{ .minimum_version = .v1 }, + \\ ldxw r0, [r1] + \\ le32 r0 + \\ exit + , + &.{ 0x44, 0x33, 0x22, 0x11 }, + 0x11223344, + ); +} + +test "le32 high" { + try testAsmWithMemory( + .{ .minimum_version = .v1 }, + \\ ldxdw r0, [r1] + \\ le32 r0 + \\ exit + , + &.{ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 }, + 0x44332211, + ); +} + +test "le64" { + try testAsmWithMemory( + .{ .minimum_version = .v1 }, + \\ ldxdw r0, [r1] + \\ le64 r0 + \\ exit + , + &.{ 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11 }, + 0x1122334455667788, + ); +} + +test "le invalid on v2" { + try testAsm(.{}, + \\ le16 r0 + \\ exit + , error.UnknownInstruction); + + try testAsm(.{}, + \\ le32 r0 + \\ exit + , error.UnknownInstruction); + + try testAsm(.{}, + \\ le64 r0 + \\ exit + , error.UnknownInstruction); } test "be16" { try testAsmWithMemory( + .{}, \\entrypoint: \\ ldxh r0, [r1] \\ be16 r0 @@ -525,6 +609,7 @@ test "be16" { test "be16 high" { try testAsmWithMemory( + .{}, \\entrypoint: \\ ldxdw r0, [r1] \\ be16 r0 @@ -537,6 +622,7 @@ test "be16 high" { test "be32" { try testAsmWithMemory( + .{}, \\entrypoint: \\ ldxw r0, [r1] \\ be32 r0 @@ -549,6 +635,7 @@ test "be32" { test "be32 high" { try testAsmWithMemory( + .{}, \\entrypoint: \\ ldxdw r0, [r1] \\ be32 r0 @@ -561,6 +648,7 @@ test "be32 high" { test "be64" { try testAsmWithMemory( + .{}, \\entrypoint: \\ ldxdw r0, [r1] \\ be64 r0 @@ -571,8 +659,39 @@ test "be64" { ); } +test "lsh64 reg" { + try testAsm(.{}, + \\entrypoint: + \\ mov r0, 0x1 + \\ mov r7, 4 + \\ lsh r0, r7 + \\ exit + , 0x10); +} + +test "rhs32 imm" { + try testAsm(.{}, + \\entrypoint: + \\ xor r0, r0 + \\ add r0, -1 + \\ rsh32 r0, 8 + \\ exit + , 0x00ffffff); +} + +test "rhs64 reg" { + try testAsm(.{}, + \\entrypoint: + \\ mov r0, 0x10 + \\ mov r7, 4 + \\ rsh r0, r7 + \\ exit + , 0x1); +} + test "ldxb" { try testAsmWithMemory( + .{}, \\entrypoint: \\ ldxb r0, [r1+2] \\ exit @@ -584,6 +703,7 @@ test "ldxb" { test "ldxh" { try testAsmWithMemory( + .{}, \\entrypoint: \\ ldxh r0, [r1+2] \\ exit @@ -595,6 +715,7 @@ test "ldxh" { test "ldxw" { try testAsmWithMemory( + .{}, \\entrypoint: \\ ldxw r0, [r1+2] \\ exit @@ -606,6 +727,7 @@ test "ldxw" { test "ldxw same reg" { try testAsmWithMemory( + .{}, \\entrypoint: \\ mov r0, r1 \\ sth [r0], 0x1234 @@ -619,6 +741,7 @@ test "ldxw same reg" { test "ldxdw" { try testAsmWithMemory( + .{}, \\entrypoint: \\ ldxdw r0, [r1+2] \\ exit @@ -633,6 +756,7 @@ test "ldxdw" { test "ldxdw oob" { try testAsmWithMemory( + .{}, \\entrypoint: \\ ldxdw r0, [r1+6] \\ exit @@ -647,6 +771,7 @@ test "ldxdw oob" { test "ldxdw oom" { try testAsmWithMemory( + .{}, \\entrypoint: \\ ldxdw r0, [r1+6] \\ exit @@ -658,6 +783,7 @@ test "ldxdw oom" { test "ldxb all" { try testAsmWithMemory( + .{}, \\entrypoint: \\ mov r0, r1 \\ ldxb r9, [r0+0] @@ -701,6 +827,7 @@ test "ldxb all" { test "ldxh all" { try testAsmWithMemory( + .{}, \\entrypoint: \\ mov r0, r1 \\ ldxh r9, [r0+0] @@ -755,6 +882,7 @@ test "ldxh all" { ); try testAsmWithMemory( + .{}, \\entrypoint: \\ mov r0, r1 \\ ldxh r9, [r0+0] @@ -799,6 +927,7 @@ test "ldxh all" { test "ldxw all" { try testAsmWithMemory( + .{}, \\entrypoint: \\ mov r0, r1 \\ ldxw r9, [r0+0] @@ -845,6 +974,7 @@ test "ldxw all" { test "stb" { try testAsmWithMemory( + .{}, \\entrypoint: \\ stb [r1+2], 0x11 \\ ldxb r0, [r1+2] @@ -857,6 +987,7 @@ test "stb" { test "sth" { try testAsmWithMemory( + .{}, \\entrypoint: \\ sth [r1+2], 0x2211 \\ ldxh r0, [r1+2] @@ -872,6 +1003,7 @@ test "sth" { test "stw" { try testAsmWithMemory( + .{}, \\entrypoint: \\ stw [r1+2], 0x44332211 \\ ldxw r0, [r1+2] @@ -887,6 +1019,7 @@ test "stw" { test "stdw" { try testAsmWithMemory( + .{}, \\entrypoint: \\ stdw [r1+2], 0x44332211 \\ ldxdw r0, [r1+2] @@ -902,6 +1035,7 @@ test "stdw" { test "stxb" { try testAsmWithMemory( + .{}, \\entrypoint: \\ mov32 r2, 0x11 \\ stxb [r1+2], r2 @@ -915,6 +1049,7 @@ test "stxb" { test "stxh" { try testAsmWithMemory( + .{}, \\entrypoint: \\ mov32 r2, 0x2211 \\ stxh [r1+2], r2 @@ -928,6 +1063,7 @@ test "stxh" { test "stxw" { try testAsmWithMemory( + .{}, \\entrypoint: \\ mov32 r2, 0x44332211 \\ stxw [r1+2], r2 @@ -941,6 +1077,7 @@ test "stxw" { test "stxdw" { try testAsmWithMemory( + .{}, \\entrypoint: \\ mov r2, -2005440939 \\ lsh r2, 32 @@ -959,6 +1096,7 @@ test "stxdw" { test "stxb all" { try testAsmWithMemory( + .{}, \\entrypoint: \\ mov r0, 0xf0 \\ mov r2, 0xf2 @@ -985,6 +1123,7 @@ test "stxb all" { ); try testAsmWithMemory( + .{}, \\entrypoint: \\ mov r0, r1 \\ mov r1, 0xf1 @@ -1002,6 +1141,7 @@ test "stxb all" { test "stxb chain" { try testAsmWithMemory( + .{}, \\entrypoint: \\ mov r0, r1 \\ ldxb r9, [r0+0] @@ -1035,6 +1175,7 @@ test "stxb chain" { test "exit without value" { try testAsm( + .{}, \\entrypoint: \\ exit , @@ -1043,7 +1184,7 @@ test "exit without value" { } test "exit" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r0, 0 \\ exit @@ -1051,7 +1192,7 @@ test "exit" { } test "early exit" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r0, 3 \\ exit @@ -1062,6 +1203,7 @@ test "early exit" { test "ja" { try testAsm( + .{}, \\entrypoint: \\ mov r0, 1 \\ ja +1 @@ -1074,6 +1216,7 @@ test "ja" { test "jeq imm" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov32 r1, 0xa @@ -1090,6 +1233,7 @@ test "jeq imm" { test "jeq reg" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov32 r1, 0xa @@ -1107,6 +1251,7 @@ test "jeq reg" { test "jge imm" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov32 r1, 0xa @@ -1123,6 +1268,7 @@ test "jge imm" { test "jge reg" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov32 r1, 0xa @@ -1140,6 +1286,7 @@ test "jge reg" { test "jle imm" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov32 r1, 5 @@ -1157,6 +1304,7 @@ test "jle imm" { test "jle reg" { try testAsm( + .{}, \\entrypoint: \\ mov r0, 0 \\ mov r1, 5 @@ -1176,6 +1324,7 @@ test "jle reg" { test "jgt imm" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov32 r1, 5 @@ -1192,6 +1341,7 @@ test "jgt imm" { test "jgt reg" { try testAsm( + .{}, \\entrypoint: \\ mov r0, 0 \\ mov r1, 5 @@ -1210,6 +1360,7 @@ test "jgt reg" { test "jlt imm" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov32 r1, 5 @@ -1226,6 +1377,7 @@ test "jlt imm" { test "jlt reg" { try testAsm( + .{}, \\entrypoint: \\ mov r0, 0 \\ mov r1, 5 @@ -1244,6 +1396,7 @@ test "jlt reg" { test "jlt extend" { try testAsm( + .{}, \\entrypoint: \\ mov r0, 0 \\ add r0, -3 @@ -1259,6 +1412,7 @@ test "jlt extend" { test "jne imm" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov32 r1, 0xb @@ -1275,6 +1429,7 @@ test "jne imm" { test "jne reg" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov32 r1, 0xb @@ -1292,6 +1447,7 @@ test "jne reg" { test "jset imm" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov32 r1, 0x7 @@ -1308,6 +1464,7 @@ test "jset imm" { test "jset reg" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov32 r1, 0x7 @@ -1325,6 +1482,7 @@ test "jset reg" { test "jsge imm" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov r1, -2 @@ -1342,6 +1500,7 @@ test "jsge imm" { test "jsge reg" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov r1, -2 @@ -1361,6 +1520,7 @@ test "jsge reg" { test "jsle imm" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov r1, -2 @@ -1378,6 +1538,7 @@ test "jsle imm" { test "jsle reg" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov r1, -1 @@ -1398,6 +1559,7 @@ test "jsle reg" { test "jsgt imm" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov r1, -2 @@ -1414,6 +1576,7 @@ test "jsgt imm" { test "jsgt reg" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov r1, -2 @@ -1431,6 +1594,7 @@ test "jsgt reg" { test "jslt imm" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov r1, -2 @@ -1447,6 +1611,7 @@ test "jslt imm" { test "jslt reg" { try testAsm( + .{}, \\entrypoint: \\ mov32 r0, 0 \\ mov r1, -2 @@ -1465,6 +1630,7 @@ test "jslt reg" { test "stack1" { try testAsm( + .{}, \\entrypoint: \\ mov r1, 51 \\ stdw [r10-16], 0xab @@ -1481,7 +1647,7 @@ test "stack1" { } test "entrypoint exit" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ call function_foo \\ mov r0, 42 @@ -1493,7 +1659,7 @@ test "entrypoint exit" { } test "call depth in bounds" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r1, 0 \\ mov r2, 63 @@ -1509,7 +1675,7 @@ test "call depth in bounds" { } test "call depth out of bounds" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov r1, 0 \\ mov r2, 64 @@ -1525,7 +1691,7 @@ test "call depth out of bounds" { } test "callx imm" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov64 r0, 0x0 \\ mov64 r8, 0x1 @@ -1540,7 +1706,7 @@ test "callx imm" { } test "callx out of bounds" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov64 r0, 0x3 \\ callx r0 @@ -1549,7 +1715,7 @@ test "callx out of bounds" { } test "call bpf 2 bpf" { - try testAsm( + try testAsm(.{}, \\entrypoint: \\ mov64 r6, 0x11 \\ mov64 r7, 0x22 @@ -1571,18 +1737,57 @@ test "call bpf 2 bpf" { } test "fixed stack out of bounds" { - try testAsm( + try testAsm(.{ .minimum_version = .v1 }, \\entrypoint: \\ stb [r10-0x4000], 0 \\ exit , error.AccessNotMapped); } -fn testElf(path: []const u8, expected: anytype) !void { - return testElfWithSyscalls(path, &.{}, expected); +test "dynamic frame pointer" { + const config: Config = .{}; + try testAsm(config, + \\entrypoint: + \\ add r10, -64 + \\ stxdw [r10+8], r10 + \\ call function_foo + \\ ldxdw r0, [r10+8] + \\ exit + \\function_foo: + \\ exit + , memory.STACK_START + config.stackSize() - 64); + + try testAsm(config, + \\entrypoint: + \\ add r10, -64 + \\ call function_foo + \\ exit + \\function_foo: + \\ mov r0, r10 + \\ exit + , memory.STACK_START + config.stackSize() - 64); + + try testAsm(config, + \\entrypoint: + \\ call function_foo + \\ mov r0, r10 + \\ exit + \\function_foo: + \\ add r10, -64 + \\ exit + , memory.STACK_START + config.stackSize()); +} + +fn testElf( + config: Config, + path: []const u8, + expected: anytype, +) !void { + return testElfWithSyscalls(config, path, &.{}, expected); } fn testElfWithSyscalls( + config: Config, path: []const u8, extra_syscalls: []const syscalls.Syscall, expected: anytype, @@ -1604,12 +1809,14 @@ fn testElfWithSyscalls( ); } - const elf = try Elf.parse(allocator, bytes, &loader); - - var executable = try Executable.fromElf(allocator, &elf); + var executable = exec: { + const elf = try Elf.parse(allocator, bytes, &loader, config); + errdefer elf.deinit(allocator); + break :exec try Executable.fromElf(elf); + }; defer executable.deinit(allocator); - const stack_memory = try allocator.alloc(u8, 4096); + const stack_memory = try allocator.alloc(u8, config.stackSize()); defer allocator.free(stack_memory); const m = try MemoryMap.init(&.{ @@ -1619,7 +1826,7 @@ fn testElfWithSyscalls( Region.init(.mutable, &.{}, memory.INPUT_START), }, .v1); - var vm = try Vm.init(allocator, &executable, m, &loader); + var vm = try Vm.init(allocator, &executable, m, &loader, stack_memory.len); defer vm.deinit(); const result = vm.run(); @@ -1630,23 +1837,43 @@ test "BPF_64_64 sbpfv1" { // [ 1] .text PROGBITS 0000000000000120 000120 000018 00 AX 0 0 8 // prints the address of the first byte in the .text section try testElf( + .{ .minimum_version = .v1 }, sig.ELF_DATA_DIR ++ "reloc_64_64_sbpfv1.so", memory.PROGRAM_START + 0x120, ); } +test "BPF_64_64" { + // 0000000100000000 0000000100000001 R_SBF_64_64 0000000100000000 entrypoint + try testElf( + .{}, + sig.ELF_DATA_DIR ++ "reloc_64_64.so", + memory.PROGRAM_START, + ); +} + test "BPF_64_RELATIVE data sbpv1" { - // [ 1] .text PROGBITS 00000000000000e8 0000e8 000020 00 AX 0 0 8 - // [ 2] .rodata PROGBITS 0000000000000108 000108 000019 01 AMS 0 0 1 - // prints the address of the first byte in the .rodata sections + // 4: 0000000000000140 8 OBJECT LOCAL DEFAULT 3 reloc_64_relative_data.DATA + // 0000000000000140 0000000000000008 R_BPF_64_RELATIVE try testElf( + .{ .minimum_version = .v1 }, sig.ELF_DATA_DIR ++ "reloc_64_relative_data_sbpfv1.so", - memory.PROGRAM_START + 0x108, + memory.PROGRAM_START + 0x140, + ); +} + +test "BPF_64_RELATIVE data" { + // 0000000100000020 0000000000000008 R_SBF_64_RELATIVE + try testElf( + .{}, + sig.ELF_DATA_DIR ++ "reloc_64_relative_data.so", + memory.PROGRAM_START + 0x20, ); } test "BPF_64_RELATIVE sbpv1" { try testElf( + .{ .minimum_version = .v1 }, sig.ELF_DATA_DIR ++ "reloc_64_relative_sbpfv1.so", memory.PROGRAM_START + 0x138, ); @@ -1654,22 +1881,69 @@ test "BPF_64_RELATIVE sbpv1" { test "load elf rodata sbpfv1" { try testElf( + .{ .minimum_version = .v1 }, sig.ELF_DATA_DIR ++ "rodata_section_sbpfv1.so", 42, ); } -test "static internal call sbpv1" { +test "load elf rodata" { try testElf( - sig.ELF_DATA_DIR ++ "static_internal_call_sbpfv1.so", - 10, + .{}, + sig.ELF_DATA_DIR ++ "rodata_section.so", + 42, ); } test "syscall reloc 64_32" { try testElfWithSyscalls( + .{}, sig.ELF_DATA_DIR ++ "syscall_reloc_64_32.so", &.{.{ .name = "log", .builtin_fn = syscalls.printString }}, + error.UnresolvedFunction, + ); +} + +test "static syscall" { + try testElfWithSyscalls( + .{}, + sig.ELF_DATA_DIR ++ "syscall_static.so", + &.{.{ .name = "log", .builtin_fn = syscalls.printString }}, 0, ); } + +test "struct func pointer" { + try testElfWithSyscalls( + .{}, + sig.ELF_DATA_DIR ++ "struct_func_pointer.so", + &.{}, + 0x0102030405060708, + ); +} + +test "data section" { + // [ 6] .data PROGBITS 0000000000000250 000250 000004 00 WA 0 0 4 + try expectEqual( + testElfWithSyscalls( + .{}, + sig.ELF_DATA_DIR ++ "data_section.so", + &.{}, + 0, + ), + error.WritableSectionsNotSupported, + ); +} + +test "bss section" { + // [ 6] .bss NOBITS 0000000000000250 000250 000004 00 WA 0 0 4 + try expectEqual( + testElfWithSyscalls( + .{}, + sig.ELF_DATA_DIR ++ "bss_section.so", + &.{}, + 0, + ), + error.WritableSectionsNotSupported, + ); +} diff --git a/src/svm/vm.zig b/src/svm/vm.zig index c0b88bbac..128cbef57 100644 --- a/src/svm/vm.zig +++ b/src/svm/vm.zig @@ -8,8 +8,6 @@ const Instruction = sbpf.Instruction; const Executable = lib.Executable; const BuiltinProgram = lib.BuiltinProgram; -const assert = std.debug.assert; - pub const Vm = struct { allocator: std.mem.Allocator, executable: *const Executable, @@ -19,7 +17,6 @@ pub const Vm = struct { loader: *const BuiltinProgram, vm_addr: u64, - stack_pointer: u64, call_frames: std.ArrayListUnmanaged(CallFrame), depth: u64, instruction_count: u64, @@ -35,13 +32,18 @@ pub const Vm = struct { executable: *const Executable, memory_map: MemoryMap, loader: *const BuiltinProgram, + stack_len: u64, ) !Vm { + const offset = if (executable.version.enableDynamicStackFrames()) + stack_len + else + executable.config.stack_frame_size; + const stack_pointer = memory.STACK_START +% offset; var self: Vm = .{ .executable = executable, .allocator = allocator, .registers = std.EnumArray(sbpf.Instruction.Register, u64).initFill(0), .memory_map = memory_map, - .stack_pointer = memory.STACK_START + 4096, .depth = 0, .call_frames = try std.ArrayListUnmanaged(CallFrame).initCapacity(allocator, 64), .instruction_count = 0, @@ -49,7 +51,7 @@ pub const Vm = struct { .loader = loader, }; - self.registers.set(.r10, memory.STACK_START + 4096); + self.registers.set(.r10, stack_pointer); self.registers.set(.r1, memory.INPUT_START); self.registers.set(.pc, executable.entry_pc); @@ -68,6 +70,7 @@ pub const Vm = struct { } fn step(self: *Vm) !bool { + const version = self.executable.version; const registers = &self.registers; const pc = registers.get(.pc); var next_pc: u64 = pc + 1; @@ -120,6 +123,7 @@ pub const Vm = struct { .arsh64_imm, .arsh32_reg, .arsh32_imm, + .hor64_imm, .lsh64_reg, .lsh64_imm, .lsh32_reg, @@ -134,22 +138,22 @@ pub const Vm = struct { registers.get(inst.src) else extend(inst.imm); - const lhs = if (opcode.is64()) lhs_large else @as(u32, @truncate(lhs_large)); - const rhs = if (opcode.is64()) rhs_large else @as(u32, @truncate(rhs_large)); + const lhs: u64 = if (opcode.is64()) lhs_large else @as(u32, @truncate(lhs_large)); + const rhs: u64 = if (opcode.is64()) rhs_large else @as(u32, @truncate(rhs_large)); var result: u64 = switch (@intFromEnum(opcode) & 0xF0) { // zig fmt: off - Instruction.add => lhs +% rhs, - Instruction.sub => lhs -% rhs, - Instruction.div => try std.math.divTrunc(u64, lhs, rhs), - Instruction.xor => lhs ^ rhs, - Instruction.@"or" => lhs | rhs, - Instruction.@"and" => lhs & rhs, - Instruction.mod => try std.math.mod(u64, lhs, rhs), - Instruction.lsh => lhs << @truncate(rhs), - Instruction.rsh => lhs >> @truncate(rhs), - Instruction.mov => rhs, - // zig fmt: on + Instruction.add => lhs +% rhs, + Instruction.sub => lhs -% rhs, + Instruction.div => try std.math.divTrunc(u64, lhs, rhs), + Instruction.xor => lhs ^ rhs, + Instruction.@"or" => lhs | rhs, + Instruction.@"and" => lhs & rhs, + Instruction.mod => try std.math.mod(u64, lhs, rhs), + Instruction.lsh => lhs << @truncate(rhs), + Instruction.rsh => lhs >> @truncate(rhs), + Instruction.mov => rhs, + // zig fmt: on Instruction.mul => value: { if (opcode.is64()) break :value lhs *% rhs; const lhs_signed: i32 = @bitCast(@as(u32, @truncate(lhs))); @@ -157,6 +161,7 @@ pub const Vm = struct { break :value @bitCast(@as(i64, lhs_signed *% rhs_signed)); }, Instruction.neg => value: { + if (!version.enableNegation()) return error.UnknownInstruction; const signed: i64 = @bitCast(lhs); const negated: u64 = @bitCast(-signed); break :value if (opcode.is64()) negated else @as(u32, @truncate(negated)); @@ -172,6 +177,9 @@ pub const Vm = struct { break :value shifted; } }, + Instruction.hor => if (!version.enableLDDW()) value: { + break :value lhs_large | @as(u64, inst.imm) << 32; + } else return error.UnknownInstruction, else => unreachable, }; @@ -244,18 +252,21 @@ pub const Vm = struct { .be, .le, - => registers.set(inst.dst, switch (inst.imm) { - inline // - 16, - 32, - 64, - => |size| std.mem.nativeTo( - std.meta.Int(.unsigned, size), - @truncate(registers.get(inst.dst)), - if (opcode == .le) .little else .big, - ), - else => return error.InvalidInstruction, - }), + => { + if (opcode == .le and !version.enableLe()) return error.UnknownInstruction; + registers.set(inst.dst, switch (inst.imm) { + inline // + 16, + 32, + 64, + => |size| std.mem.nativeTo( + std.meta.Int(.unsigned, size), + @truncate(registers.get(inst.dst)), + if (opcode == .le) .little else .big, + ), + else => return error.UnknownInstruction, + }); + }, // branching .ja, @@ -322,17 +333,33 @@ pub const Vm = struct { const frame = self.call_frames.pop(); self.registers.set(.r10, frame.fp); @memcpy(self.registers.values[6..][0..4], &frame.caller_saved_regs); - self.stack_pointer -= 4096; + if (!version.enableDynamicStackFrames()) { + registers.getPtr(.r10).* -= self.executable.config.stack_frame_size; + } next_pc = frame.return_pc; }, .call_imm => { - if (self.executable.function_registry.lookupKey(inst.imm)) |entry| { - try self.pushCallFrame(); - next_pc = entry.value; - } else if (self.loader.functions.lookupKey(inst.imm)) |entry| { - const builtin_fn = entry.value; - try builtin_fn(self); - } else { + var resolved = false; + const external, const internal = if (version.enableStaticSyscalls()) + .{ inst.src == .r0, inst.src != .r0 } + else + .{ true, true }; + if (external) { + if (self.loader.functions.lookupKey(inst.imm)) |entry| { + resolved = true; + const builtin_fn = entry.value; + try builtin_fn(self); + } + } + if (internal and !resolved) { + if (self.executable.function_registry.lookupKey(inst.imm)) |entry| { + resolved = true; + try self.pushCallFrame(); + next_pc = entry.value; + } + } + + if (!resolved) { return error.UnresolvedFunction; } }, @@ -345,11 +372,12 @@ pub const Vm = struct { // other instructions .ld_dw_imm => { - assert(self.executable.version == .v1); + if (!version.enableLDDW()) return error.UnknownInstruction; const value: u64 = (@as(u64, instructions[next_pc].imm) << 32) | inst.imm; registers.set(inst.dst, value); next_pc += 1; }, + else => return error.UnknownInstruction, } if (next_pc >= instructions.len) return error.PcOutOfBounds; @@ -376,12 +404,13 @@ pub const Vm = struct { }; self.depth += 1; - if (self.depth == 64) { + if (self.depth == self.executable.config.max_call_depth) { return error.CallDepthExceeded; } - self.stack_pointer += 4096; - self.registers.set(.r10, self.stack_pointer); + if (!self.executable.version.enableDynamicStackFrames()) { + self.registers.getPtr(.r10).* += self.executable.config.stack_frame_size; + } } /// Performs a i64 sign-extension. This is commonly needed in SBPV1.