Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions lib/caotral/binary/elf/reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down Expand Up @@ -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?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle missing .rela.plt before checking body

validate_relocations now computes rela_plt_exists = !rela_plt.body.empty? before confirming the section exists, so valid ELF files that have .dynamic/.rela.dyn but no .rela.plt (common when there are no PLT relocations) now raise NoMethodError instead of being validated. This is a regression from the previous behavior where PLT checks were skipped when .rela.plt was absent.

Useful? React with 👍 / 👎.

got_plt = @context.sections.find { |s| s.section_name.to_s == ".got.plt" }
failed_messages = []
unless rela_dyn && pt_load && dynamic
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/caotral/binary/elf/section/hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
67 changes: 50 additions & 17 deletions lib/caotral/linker/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remap dynsym section indexes before emitting symbols

Defined symbols copied into .dynsym keep the original object-file shndx, but section indices are remapped later only for symtab_section symbols, not these dynsym copies. Writer#patch_dynamic_sections then treats dynsym_body.shndx as an index into the output section list when adjusting st_value, so symbols from sections whose index changed (notably .data) get incorrect runtime addresses in .dynsym.

Useful? React with 👍 / 👎.

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
Comment on lines +290 to +294

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Build SysV hash chains for all dynamic symbols

The new .hash table generation hard-codes bucket[0] to index 1 and sets every chain entry to 0, which means SysV hash lookup can only inspect dynsym[1]. Any export not at that slot (for example libraries with multiple exports or with an undefined PLT symbol inserted first) becomes unresolvable via dlsym, so dlopen symbol resolution fails for common inputs.

Useful? React with 👍 / 👎.

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

Expand Down
38 changes: 34 additions & 4 deletions lib/caotral/linker/writer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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 }
Expand All @@ -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
3 changes: 3 additions & 0 deletions sample/C/add.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
int add(int x, int y) {
return x + y;
}
7 changes: 7 additions & 0 deletions sample/fiddle_add.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require "fiddle/import"

module X
extend Fiddle::Importer
dlload "./libtmp.so"
extern "int add(int, int)"
end
40 changes: 40 additions & 0 deletions test/caotral/linker/fiddle_test.rb
Original file line number Diff line number Diff line change
@@ -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