From 242eaa822fe0b5033ee34222c53c79f31913932e Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Sat, 28 Feb 2026 10:11:24 +0900 Subject: [PATCH 1/6] emit .hash and PT_GNU_STACK for dynamic outputs --- lib/caotral/linker/builder.rb | 2 +- lib/caotral/linker/writer.rb | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/caotral/linker/builder.rb b/lib/caotral/linker/builder.rb index 1a3cf5c..9b9188c 100644 --- a/lib/caotral/linker/builder.rb +++ b/lib/caotral/linker/builder.rb @@ -260,7 +260,7 @@ def build if dynamic? sections << dynstr sections << dynsym - sections << build_hash_section if @pie + sections << build_hash_section sections << rela_dyn_section sections << rela_plt_section sym = sections.index(dynsym) diff --git a/lib/caotral/linker/writer.rb b/lib/caotral/linker/writer.rb index fc24907..213e635 100644 --- a/lib/caotral/linker/writer.rb +++ b/lib/caotral/linker/writer.rb @@ -343,7 +343,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 } From c93eef4d78552e1c43c51bb8e248d21bf5851f2c Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Sat, 28 Feb 2026 11:09:23 +0900 Subject: [PATCH 2/6] export dynsym for shared objects --- lib/caotral/linker/builder.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/caotral/linker/builder.rb b/lib/caotral/linker/builder.rb index 9b9188c..305cdf0 100644 --- a/lib/caotral/linker/builder.rb +++ b/lib/caotral/linker/builder.rb @@ -266,6 +266,15 @@ def build 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)) + 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) + dynstr.body.names += copy_sym.name_string + "\0" if name.nil? + dynsym.body << copy_sym.set!(name:, shndx:) + end sections << build_dynamic_section end sections << symtab_section From 1b63bf791bd803838b3fcf17a7f3a73b1e6b083e Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Sun, 1 Mar 2026 01:05:47 +0900 Subject: [PATCH 3/6] guard dynamic patching and empty .rela.plt is empty --- lib/caotral/binary/elf/section/hash.rb | 1 + lib/caotral/linker/builder.rb | 62 ++++++++++++++++++-------- lib/caotral/linker/writer.rb | 31 +++++++++++-- 3 files changed, 72 insertions(+), 22 deletions(-) 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 305cdf0..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,7 +266,8 @@ def build if dynamic? sections << dynstr sections << dynsym - sections << build_hash_section + hash_section = build_hash_section + sections << hash_section sections << rela_dyn_section sections << rela_plt_section sym = sections.index(dynsym) @@ -272,10 +279,27 @@ def build copy_sym = sym.dup shndx = copy_sym.shndx name = dynstr.body.offset_of(sym.name_string) - dynstr.body.names += copy_sym.name_string + "\0" if name.nil? - dynsym.body << copy_sym.set!(name:, shndx:) + 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 - sections << build_dynamic_section + 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 213e635..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] @@ -371,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 From 02f81f4a5916c07a990197869c9ce440d29f8a68 Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Sun, 1 Mar 2026 01:06:13 +0900 Subject: [PATCH 4/6] skip DT_PLT checks when .rela.plt is empty --- lib/caotral/binary/elf/reader.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/caotral/binary/elf/reader.rb b/lib/caotral/binary/elf/reader.rb index c59913a..3c85073 100644 --- a/lib/caotral/binary/elf/reader.rb +++ b/lib/caotral/binary/elf/reader.rb @@ -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 From 046a0f64514e174492a836f64281ec8be60f4223 Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Sun, 1 Mar 2026 01:06:51 +0900 Subject: [PATCH 5/6] isolate fiddle test under RUBY_BOX mode --- .github/workflows/main.yml | 2 ++ Rakefile | 5 +++++ sample/C/add.c | 3 +++ sample/fiddle_add.rb | 7 +++++++ test/caotral/linker/fiddle_test.rb | 27 +++++++++++++++++++++++++++ 5 files changed, 44 insertions(+) create mode 100644 sample/C/add.c create mode 100644 sample/fiddle_add.rb create mode 100644 test/caotral/linker/fiddle_test.rb 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/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..bf7a38d --- /dev/null +++ b/test/caotral/linker/fiddle_test.rb @@ -0,0 +1,27 @@ +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 + linker = Caotral::Linker.link!(inputs: ["libtmp.so.o"], output: "libtmp.so", linker: "self", shared: true, executable: false) + box = Ruby::Box.new + box.require("./sample/fiddle_add.rb") + assert_equal(10, box::X.add(3, 7)) + end + + private def supported_ruby_box? = RUBY_VERSION >= "4.0.0" && ENV["RUBY_BOX"] == "1" && defined?(Ruby::Box) +end From ae3df6128c93146b5cde90074e92ac22d10117f5 Mon Sep 17 00:00:00 2001 From: "MATSUMOTO, Katsuyoshi" Date: Mon, 2 Mar 2026 16:36:20 +0900 Subject: [PATCH 6/6] read .dynsym and .dynstr and assert dyn tags in fiddle test --- lib/caotral/binary/elf/reader.rb | 4 ++-- test/caotral/linker/fiddle_test.rb | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/caotral/binary/elf/reader.rb b/lib/caotral/binary/elf/reader.rb index 3c85073..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| diff --git a/test/caotral/linker/fiddle_test.rb b/test/caotral/linker/fiddle_test.rb index bf7a38d..de404d1 100644 --- a/test/caotral/linker/fiddle_test.rb +++ b/test/caotral/linker/fiddle_test.rb @@ -17,10 +17,23 @@ 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 - linker = Caotral::Linker.link!(inputs: ["libtmp.so.o"], output: "libtmp.so", linker: "self", shared: true, executable: false) + 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)