diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 29e8898..20ced72 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,3 +31,5 @@ jobs: run: bundle exec rake steep:check - name: Run the default task run: bundle exec rake + - name: Run test using Ruby::Box + run: RUBY_BOX=1 bundle exec rake test diff --git a/Rakefile b/Rakefile index 98f9b71..960a42a 100644 --- a/Rakefile +++ b/Rakefile @@ -7,8 +7,13 @@ require "steep/cli" task default: %i[test] +def supported_ruby_box? = RUBY_VERSION >= "4.0.0" && ENV["RUBY_BOX"] == "1" && defined?(Ruby::Box) + Rake::TestTask.new do |t| t.test_files = FileList['test/**/*_test.rb'] + if supported_ruby_box? + t.test_files = FileList['test/caotral/linker/fiddle_test.rb'] + end end namespace :steep do diff --git a/lib/caotral/binary/elf/reader.rb b/lib/caotral/binary/elf/reader.rb index c59913a..6f716ac 100644 --- a/lib/caotral/binary/elf/reader.rb +++ b/lib/caotral/binary/elf/reader.rb @@ -84,9 +84,9 @@ def read @bin.pos = section.header.offset body_bin = @bin.read(section.header.size) section.body = case type - when :strtab + when :strtab, :dynstr Caotral::Binary::ELF::Section::Strtab.new(body_bin) - when :symtab + when :symtab, :dynsym symtab_entsize = section.header.entsize count = body_bin.bytesize / symtab_entsize count.times.map do |i| @@ -144,6 +144,7 @@ def validate_relocations pt_load = @context.program_headers.find { |ph| ph.type == :LOAD } dynamic = @context.sections.find { |section| section.section_name.to_s == ".dynamic" } rela_plt = @context.sections.find { |section| section.section_name.to_s == ".rela.plt" } + rela_plt_exists = !rela_plt.body.empty? got_plt = @context.sections.find { |s| s.section_name.to_s == ".got.plt" } failed_messages = [] unless rela_dyn && pt_load && dynamic @@ -172,7 +173,7 @@ def validate_relocations failed_messages << "Relocation entries in .rela.dyn exceed LOAD segment range" end - if rela_plt + if rela_plt && rela_plt_exists jump_rel = dynamic.body.find { |dt| dt.jmp_rel? }&.un == rela_plt.header.addr plt_rel_size = dynamic.body.find { |dt| dt.plt_rel_size? }&.un == rela_plt.header.size plt_rel = dynamic.body.find { |dt| dt.plt_rel? }&.un == 7 diff --git a/lib/caotral/binary/elf/section/hash.rb b/lib/caotral/binary/elf/section/hash.rb index 258413d..c591247 100644 --- a/lib/caotral/binary/elf/section/hash.rb +++ b/lib/caotral/binary/elf/section/hash.rb @@ -5,6 +5,7 @@ class ELF class Section class Hash include Caotral::Binary::ELF::Utils + attr_reader :bucket, :chain def initialize(nchain:, nbucket: 1) @nbucket = num2bytes(nbucket, 4) @nchain = num2bytes(nchain, 4) diff --git a/lib/caotral/linker/builder.rb b/lib/caotral/linker/builder.rb index 1a3cf5c..f4f0a18 100644 --- a/lib/caotral/linker/builder.rb +++ b/lib/caotral/linker/builder.rb @@ -6,6 +6,7 @@ class Linker class Builder include Caotral::Binary::ELF::Utils REL_TYPES = Caotral::Binary::ELF::Section::Rel::TYPES + DYNAMIC_TAGS = Caotral::Binary::ELF::Section::Dynamic::TAG_TYPES SYMTAB_BIND = { locals: 0, globals: 1, weaks: 2, }.freeze BIND_BY_VALUE = SYMTAB_BIND.invert.freeze RELOCATION_SECTION_NAMES = [".rela.text", ".rel.text", ".rela.data", ".rel.data"].freeze @@ -18,6 +19,11 @@ class Builder REL_TYPES[:AMD64_GOTPCRELX], REL_TYPES[:AMD64_REX_GOTPCRELX], ].freeze + REJECT_DYNAMIC_TAGS = [ + DYNAMIC_TAGS[:PLTRELSZ], + DYNAMIC_TAGS[:PLTREL], + DYNAMIC_TAGS[:JMPREL], + ].freeze attr_reader :symbols @@ -176,22 +182,22 @@ def build first_insertion = got_plt_offsets[sym].nil? got_plt_offsets[sym] ||= got_plt_offset.tap { got_plt_offset += 8 } if dynamic? && undefined && first_insertion - got_plt_section.body << [0].pack("Q<") - rps = Caotral::Binary::ELF::Section::Rel.new.set!( - offset: got_plt_offsets[sym], - info: ((sym) << 32) | REL_TYPES[:AMD64_JUMP_SLOT] + got_plt_section.body << [0].pack("Q<") + rps = Caotral::Binary::ELF::Section::Rel.new.set!( + offset: got_plt_offsets[sym], + info: ((sym) << 32) | REL_TYPES[:AMD64_JUMP_SLOT] + ) + name = symtab_section.body[sym].name_string + dynstr_index = dynstr.body.offset_of(name) + if dynstr_index.nil? + dynstr.body.names += name + "\0" + dynsym.body << Caotral::Binary::ELF::Section::Symtab.new.set!( + name: dynstr.body.offset_of(name), + info: (1 << 4) | 2, ) - name = symtab_section.body[sym].name_string - dynstr_index = dynstr.body.offset_of(name) - if dynstr_index.nil? - dynstr.body.names += name + "\0" - dynsym.body << Caotral::Binary::ELF::Section::Symtab.new.set!( - name: dynstr.body.offset_of(name), - info: (1 << 4) | 2, - ) - end - rela_plt_section.body << rps - next + end + rela_plt_section.body << rps + next end elsif UNSUPPORTED_REL_TYPES.include?(rel.type) raise Caotral::Binary::ELF::Error, "unsupported relocation type: #{rel.type_name}" @@ -260,13 +266,40 @@ def build if dynamic? sections << dynstr sections << dynsym - sections << build_hash_section if @pie + hash_section = build_hash_section + sections << hash_section sections << rela_dyn_section sections << rela_plt_section sym = sections.index(dynsym) rela_dyn_section.header.set!(link: sym, type: rel_type(rela_dyn_section), entsize: rel_entsize(rela_dyn_section)) rela_plt_section.header.set!(link: sym, type: rel_type(rela_plt_section), info: ref_index(sections, got_plt_section.section_name)) - sections << build_dynamic_section + symtab_section.body.each do |sym| + next unless [SYMTAB_BIND[:globals], SYMTAB_BIND[:weaks]].include?(sym.bind) + next if sym.shndx == 0 + copy_sym = sym.dup + shndx = copy_sym.shndx + name = dynstr.body.offset_of(sym.name_string) + if name.nil? + dynstr.body.names += copy_sym.name_string + "\0" + name = dynstr.body.offset_of(copy_sym.name_string) + end + copy_sym.name_string = sym.name_string + dynsym.body << copy_sym.set!(name:, shndx:, value: sym.value) + end + hash = Caotral::Binary::ELF::Section::Hash.new(nchain: dynsym.body.size) + hash.bucket[0] = num2bytes(1, 4) if dynsym.body.size > 1 + dynsym.body.each_with_index do |sym, i| + next if i == 0 + hash.chain[i] = num2bytes(0, 4) + end + hash_section.body = hash + dynamic_section = build_dynamic_section + if rela_plt_section.body.size == 0 && dynamic? + bodies = dynamic_section.body.reject { |ent| REJECT_DYNAMIC_TAGS.include?(ent.tag) } + dynamic_section.body = bodies + end + + sections << dynamic_section end sections << symtab_section diff --git a/lib/caotral/linker/writer.rb b/lib/caotral/linker/writer.rb index fc24907..cb3e886 100644 --- a/lib/caotral/linker/writer.rb +++ b/lib/caotral/linker/writer.rb @@ -82,7 +82,7 @@ def write rel.header.set!(offset: rel_offset, size: rel_size, entsize:) end - patch_dynamic_sections(file: f) + patch_dynamic_sections(file: f) if dynamic? patch_program_headers(file: f) write_program_headers(file: f) @@ -108,6 +108,21 @@ def patch_dynamic_sections(file:) dyn.header.set!(addr:) end + cur = file.pos + file.seek(dynsym_section.header.offset) + dynsym_section.body.each do |dynsym_body| + if dynsym_body.shndx != 0 + value = dynsym_body.value + secndx = @write_sections[dynsym_body.shndx]&.header&.addr + unless secndx.nil? + value += secndx + dynsym_body.set!(value:) + end + end + file.write(dynsym_body.build) + end + file.seek(cur) + if dynamic? && dynamic_section && rela_dyn_section rdsh = rela_dyn_section&.header bodies = dynamic_section.body @@ -238,23 +253,27 @@ def write_shared_dynamic_sections(file:) interp_section.header.set!(offset: interp_offset, size:, addr: text_addr + (interp_offset - tsh.offset)) end + pad_to_align(file:, align: dynstr_section.header.addralign) dynstr_offset = file.pos file.write(dynstr_section.body.build) size = file.pos - dynstr_offset dynstr_section.header.set!(offset: dynstr_offset, size:, addr: text_addr + (dynstr_offset - tsh.offset)) + pad_to_align(file:, align: dynsym_section.header.addralign) dynsym_offset = file.pos dynsym_section.body.each { |dynsym| file.write(dynsym.build) } size = file.pos - dynsym_offset dynsym_section.header.set!(offset: dynsym_offset, size:, addr: text_addr + (dynsym_offset - tsh.offset)) - if @pie + if dynamic? + pad_to_align(file:, align: hash_section.header.addralign) hash_offset = file.pos file.write(hash_section.body.build) size = file.pos - hash_offset hash_section.header.set!(offset: hash_offset, size:, addr: text_addr + (hash_offset - tsh.offset)) end + pad_to_align(file:, align: dynamic_section.header.addralign) dynamic_offset = file.pos dynamic_section.body.each { |dynamic| file.write(dynamic.build) } size = file.pos - dynamic_offset @@ -316,6 +335,12 @@ def write_section_headers(file:, shoffset:) file.write(@elf_obj.header.build) end + def pad_to_align(file:, align:) + pos = file.pos + padding = (align - (pos % align)) % align + file.write("\0" * padding) + end + def program_header_flags(flag) = Caotral::Binary::ELF::ProgramHeader::PF[flag.to_sym] def elf_type = Caotral::Binary::ELF::Header::TYPE[dynamic? ? :DYN : :EXEC] @@ -343,7 +368,12 @@ def program_headers pph = Caotral::Binary::ELF::ProgramHeader.new pph.set!(type: 6) end - @program_headers = [pph, lph, iph, dph].compact + # ruby's dlopen support + if dynamic? + gsph = Caotral::Binary::ELF::ProgramHeader.new + gsph.set!(type: 0x6474e551, flags: program_header_flags(:RW)) + end + @program_headers = [pph, lph, iph, dph, gsph].compact end def pie_program_header = @pie_program_header ||= program_headers.find { |ph| ph.type == :PHDR } def load_program_header = @load_program_header ||= program_headers.find { |ph| ph.type == :LOAD } @@ -366,7 +396,7 @@ def plt_section = @plt_section ||= @write_sections.find { |s| ".plt" === s.secti def got_plt_section = @got_plt_section ||= @write_sections.find { |s| ".got.plt" === s.section_name.to_s } def rela_plt_section = @rela_plt_section ||= @write_sections.find { |s| ".rela.plt" === s.section_name.to_s } - def dynamic_sections = @dynamic_sections ||= [interp_section, dynstr_section, dynsym_section, dynamic_section, rela_dyn_section, rela_plt_section].compact + def dynamic_sections = @dynamic_sections ||= [interp_section, dynstr_section, dynsym_section, hash_section, dynamic_section, rela_dyn_section, rela_plt_section].compact end end end diff --git a/sample/C/add.c b/sample/C/add.c new file mode 100644 index 0000000..35bf82f --- /dev/null +++ b/sample/C/add.c @@ -0,0 +1,3 @@ +int add(int x, int y) { + return x + y; +} diff --git a/sample/fiddle_add.rb b/sample/fiddle_add.rb new file mode 100644 index 0000000..977ec08 --- /dev/null +++ b/sample/fiddle_add.rb @@ -0,0 +1,7 @@ +require "fiddle/import" + +module X + extend Fiddle::Importer + dlload "./libtmp.so" + extern "int add(int, int)" +end diff --git a/test/caotral/linker/fiddle_test.rb b/test/caotral/linker/fiddle_test.rb new file mode 100644 index 0000000..de404d1 --- /dev/null +++ b/test/caotral/linker/fiddle_test.rb @@ -0,0 +1,40 @@ +require_relative "../../test_suite" + +class Caotral::Linker::FiddleMethodTest < Test::Unit::TestCase + include TestProcessHelper + def setup + @generated = [] + omit("Ruby::Box is not supported in this environment") unless supported_ruby_box? + end + + def teardown + @generated.each do |file| + File.delete(file) if File.exist?(file) + end + end + + def test_sample_call_add_method + @generated = ["libtmp.so", "libtmp.so.o"] + @file = "sample/C/add.c" + IO.popen(["gcc", "-fPIC", "-c", "-o", "libtmp.so.o", "%s" % @file]).close + Caotral::Linker.link!(inputs: ["libtmp.so.o"], output: "libtmp.so", linker: "self", shared: true, executable: false) + elf = Caotral::Binary::ELF::Reader.read!(input: "./libtmp.so") + box = Ruby::Box.new + box.require("./sample/fiddle_add.rb") + assert_equal(10, box::X.add(3, 7)) + dynsym = elf.find_by_name(".dynsym") + rela_plt = elf.find_by_name(".rela.plt") + dynamic = elf.find_by_name(".dynamic") + dynstr = elf.find_by_name(".dynstr") + dynstrs = dynstr.body.names.split("\x00") + assert(dynstrs.include?("add")) + assert_equal(2, dynsym.body.size) + assert_equal("add", dynstr.body.lookup(dynsym.body[1].name_offset)) + assert_equal(0, rela_plt.body.size) + assert_equal(nil, dynamic.body.find { |dt| dt.plt_rel? }) + assert_equal(nil, dynamic.body.find { |dt| dt.plt_rel_size? }) + assert_equal(nil, dynamic.body.find { |dt| dt.jmp_rel? }) + end + + private def supported_ruby_box? = RUBY_VERSION >= "4.0.0" && ENV["RUBY_BOX"] == "1" && defined?(Ruby::Box) +end