Skip to content

Commit

Permalink
Test symbol visibility in the static archive and shared objects
Browse files Browse the repository at this point in the history
The tests check that the static archive librubyparser.a hides all of
the YARP functions, to ensure the symbols do not end up externally
visible in a dynamic symbol table and therefore not susceptible to
being replaced by the dynamic linker. We expect something like:

```
$ objdump --syms build/librubyparser.a | grep " yp_parse$"
000000000001dbf0 g     F .text	0000000000000009 .hidden yp_parse
```

The `g` means the symbol is global, but `.hidden` means that the
symbol should not be made external if linked into a shared
object (like `yarp.so`).

The tests also check that the shared object librubyparser.so contains
global versions of the public methods like `yp_parse`:

```
$ nm build/librubyparser.so | grep " yp_parse$"
0000000000021a50 T yp_parse
```

Finally, the tests verify that there is exactly one dynamic symbol in
`yarp.so`, which is the initializing function `Init_yarp`. NOTE that
we add `-fvisibility=hidden` to the C extension compiler flags to
ensure this test passes.
  • Loading branch information
flavorjones committed Jul 9, 2023
1 parent 83061ae commit c87d91a
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 5 deletions.
9 changes: 4 additions & 5 deletions ext/yarp/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@ module Yarp
module ExtConf
class << self
def configure
configure_debug
configure_c_extension
configure_rubyparser

create_makefile("yarp")
end

def configure_debug
if debug_mode_build?
append_cflags("-DYARP_DEBUG_MODE_BUILD")
end
def configure_c_extension
append_cflags("-DYARP_DEBUG_MODE_BUILD") if debug_mode_build?
append_cflags("-fvisibility=hidden")
end

def configure_rubyparser
Expand Down
101 changes: 101 additions & 0 deletions test/library_symbols_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# frozen_string_literal: true

require "yarp_test_helper"

if RUBY_PLATFORM =~ /linux/
#
# examine a yarp dll or static archive for expected external symbols.
# these tests only work on a linux system right now.
#
class LibrarySymbolsTest < Test::Unit::TestCase
def setup
super

@librubyparser_a = File.expand_path(File.join(__dir__, "..", "build", "librubyparser.a"))
@librubyparser_so = File.expand_path(File.join(__dir__, "..", "build", "librubyparser.so"))
@yarp_so = File.expand_path(File.join(__dir__, "..", "lib", "yarp.so"))
end

# objdump runner and helpers
def objdump(path)
assert_path_exist(path)
%x(objdump --section=.text --syms #{path}).split("\n")
end

def global_objdump_symbols(path)
objdump(path).select { |line| line[17] == "g" }
end

def hidden_global_objdump_symbols(path)
global_objdump_symbols(path).select { |line| line =~ / \.hidden / }
end

def visible_global_objdump_symbols(path)
global_objdump_symbols(path).reject { |line| line =~ / \.hidden / }
end

# nm runner and helpers
def nm(path)
assert_path_exist(path)
%x(nm #{path}).split("\n")
end

def global_nm_symbols(path)
nm(path).select { |line| line[17] == "T" }
end

def local_nm_symbols(path)
nm(path).select { |line| line[17] == "t" }
end

# dig the symbol name out of each line. works for both `objdump` and `nm` output.
def names(symbol_lines)
symbol_lines.map { |line| line.split(/\s+/).last }
end

#
# static archive - librubyparser.a
#
def test_librubyparser_a_contains_nothing_globally_visible
omit("librubyparser.a is not built") unless File.exist?(@librubyparser_a)

assert_empty(names(visible_global_objdump_symbols(@librubyparser_a)))
end

def test_librubyparser_a_contains_hidden_yp_symbols
omit("librubyparser.a is not built") unless File.exist?(@librubyparser_a)

names(hidden_global_objdump_symbols(@librubyparser_a)).tap do |symbols|
assert_includes(symbols, "yp_parse")
assert_includes(symbols, "yp_version")
end
end

#
# shared object - librubyparser.so
#
def test_librubyparser_so_exports_only_the_necessary_functions
omit("librubyparser.so is not built") unless File.exist?(@librubyparser_so)

names(global_nm_symbols(@librubyparser_so)).tap do |symbols|
assert_includes(symbols, "yp_parse")
assert_includes(symbols, "yp_version")
end
names(local_nm_symbols(@librubyparser_so)).tap do |symbols|
assert_includes(symbols, "yp_encoding_shift_jis_isupper_char")
end
# TODO: someone who uses this library needs to finish this test
end

#
# shared object - yarp.so
#
def test_yarp_so_exports_only_the_C_extension_init_function
omit("yarp.so is not built") unless File.exist?(@yarp_so)

names(global_nm_symbols(@yarp_so)).tap do |symbols|
assert_equal(["Init_yarp"], symbols)
end
end
end
end

0 comments on commit c87d91a

Please sign in to comment.