1

I'm trying to load a C shared library within Ruby using Fiddle.

Here is a minimal example:

require 'fiddle'
require 'fiddle/import'

module Era
  extend Fiddle::Importer

  dlload './ServerApi.so'

  extern 'int era_init_lib()'
  extern 'void era_deinit_lib()'
  extern 'int era_process_request(const char* request, char** response)'
  extern 'void era_free(char* response)'
end

Era.era_init_lib
begin
  # ...
ensure
  Era.era_deinit_lib
end

The shared library loads without issues. However when I call Era.era_init_lib it tries to load additional libraries (Network.so and Protobuf.so). I have these file located in the current working directory (in the same directory as ServerApi.so).

However when I try to execute the code above I receive the following error:

! Failed to load library: /home/username/.rvm/rubies/ruby-2.6.5/bin/Network.so, error: /home/username/.rvm/rubies/ruby-2.6.5/bin/Network.so: cannot open shared object file: No such file or directory

If I place the file at the location the error describes everything works fine.

My guess is that the C working directory of fiddle is different from the Ruby working directory. I would like to keep the project files within the project and not in the Ruby installation directory.

How can I use Network.so from my project folder?

All the *.so files are provided by a third-party. I do not have the source and as a result cannot change these files. The function signatures are provided by the documentation.


Searching for Network.so in the strace gives me these results:

readlink("/proc/self/exe", "/home/username/.rvm/rubies/ruby-2."..., 4096) = 44
openat(AT_FDCWD, "/home/username/.rvm/rubies/ruby-2.6.5/bin/Network.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
futex(0x7fcc16666d90, FUTEX_WAKE_PRIVATE, 2147483647) = 0
futex(0x7fcc16b44520, FUTEX_WAKE_PRIVATE, 2147483647) = 0
write(2, "! Failed to load library: ", 26! Failed to load library: ) = 26
write(2, "/home/username/.rvm/rubies/ruby-2."..., 50/home/username/.rvm/rubies/ruby-2.6.5/bin/Network.so) = 50
write(2, ", error: ", 9, error: )                = 9
write(2, "/home/username/.rvm/rubies/ruby-2."..., 109/home/username/.rvm/rubies/ruby-2.6.5/bin/Network.so: cannot open shared object file: No such file or directory) = 109
write(2, "\n", 1)                       = 1

I've also written a C script which does the same thing which works perfectly fine when the files are dropped into the same directory. So it might be the fault of the library, which I assume checks the location of the current running program, then tries to load the library from that folder. This would explain the behavior when ran as a Ruby script (since it runs as part of the Ruby program), whereas a C binary runs standalone.


For those that want to re-create the (Linux) issue. You can download the necessary files from here. Which gives you the server-linux-x86_64.sh file.

Supported distros are: Suse, Ubuntu, Debian, Red Hat and CentOS but others may also work fine.

You can either run the installer, which should place the files in /opt/eset/RemoteAdministrator/Server. Or, assuming most of you don't want to install the full application you can run the following command:

sed '1,/^# Start of TAR\.GZ file #$/d' server-linux-x86_64.sh | sed '1d' > server-linux-x86_64.tar.gz

Which removes all the installer instructions from the .sh file and only leaves the binary .tar.gz data, writing it to server-linux-x86_64.tar.gz.

Copy the files ServerApi.so, Protobuf.so and Network.so into a directory of your liking. Create a Ruby script (with the question code) in the same directory and run the script.

3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
  • 1
    It's hard to say without seeing the source code of ESET. ServerApi.so may be using system functions to try and load Network.so. Working directory may be irrelevant. Try running the whole app with `strace` to try and figure out who/what/how Network.so is being loaded. Also, it would be helpful if you could provide a link to the library (if it is available) so people can run their own tests. – Casper Jan 22 '21 at 20:19
  • I have a possible solution. It's a bit of a hack. The question is are you making this only for yourself, or do you need a portable solution? The solution I have requires the input of root password. – Casper Jan 22 '21 at 22:32
  • @Casper That depends on the solution. This is just an prototype to see if I can work with the C library from within Ruby. The final result will eventually run on a server for which I have sudo rights. My current solution is written in C utilizes a UNIX socket to communicate with Ruby. However the performance currently leaves a lot to be desired so I was wondering if directly working with the C library from within Ruby might speed things up. To answer your question, no it does not have to be portable, but portability is always a plus. – 3limin4t0r Jan 22 '21 at 22:42

2 Answers2

1

Because ServerApi.so checks /proc/self/exe for the location of all subsequent files to load, and it is very difficult to modify this target by normal means, it is easier to just modify ServerApi.so itself so that it uses something else besides proc for the source.

If we run strings ServerApi.so, we can verify that the location to check is stored inside a string in ServerApi.so:

strings ServerApi.so | grep 'proc/self/exe'
B/proc/self/exe

So now all we need to do is modify this string to something else that works for us.

The easiest way to modify the string is to replace it with something that is exactly the same length as the original. This way we do not have to worry about changing the end-of-string zero padding or accidentally changing the total size of ServerApi.so.

Here we can see a suitable candidate could be /tmp/scriptexe:

/proc/self/exe
/tmp/scriptexe   <- same length

So let's do that:

sed -e 's/proc\/self\/exe/tmp\/scriptexe/' ServerApi.so > ServerApi_Mod.so

Now we can verify the change:

strings ServerApi_Mod.so | grep scriptexe
B/tmp/scriptexe

Next we need to create /tmp/scriptexe to actually point to our Ruby script:

ln -s /the/full/path/to/our/ruby/script.rb /tmp/scriptexe

Then we modify our script:

dlload './ServerApi_Mod.so

Now we can run it as normal:

ruby script.rb

And everything should work.

Casper
  • 33,403
  • 4
  • 84
  • 79
0

If we read the strace output we see that the library obtains the current executable location from /proc/self/exe, and then searches subsequent libraries from there.

/proc/self/exe is not easily modifiable, but by using a hard link to a Ruby executable in the current directory we can trick it to point to a new folder.

Problem is making a hard link requires root.

In any case, here is a self-contained solution (note that it will ask for root password the first time you run it, in order to create the hard link).

Put this at the top of your script:

# Obtain path to current executable
exe = File.readlink("/proc/self/exe")

# Check if we are running the hard-liked version
if !exe.match /localruby/
  if !File.exist?('localruby')
    # Create a hard link to the current Ruby exe using sudo
    system("sudo ln #{exe} localruby")
  end

  puts "Restarting..."

  # In order to prevent infinite busy loop in case of some mishap
  sleep 1 

  # Rerun self using the hard-linked Ruby executable.
  # This will make /proc/self/exe point to the hard-link, which then
  # allows the ESET library to search for .so files in current folder.
  exec('./localruby', File.expand_path(__FILE__))
end

require 'fiddle'
require 'fiddle/import'

# ...rest of your script goes here...

A simple solution without any extra Ruby code is to just create the hard link manually, and then always run the script with ./localruby myscript.rb, instead of using the normal ruby myscript.rb.

Casper
  • 33,403
  • 4
  • 84
  • 79
  • It's not needed because `exec` will replace the current running process with whatever process `exec` starts. It's like an in-place process replacement, so the calling program will just go up in smoke. I.e. the `exec` call will never return. – Casper Jan 22 '21 at 23:08
  • Note that you can just create the symbolic link manually too, and run the script as I describe in the last paragraph. Just run `sudo ln /path/to/current/ruby/exe localruby`, and then you can run your script using `localruby myscript.rb`. That works too. – Casper Jan 22 '21 at 23:11
  • There is also one more solution, and that is to edit the `ServerApi.so` binary and change the `/proc/self/exe` string inside it to point somewhere else. It all depends on what you want to do. But there is no "simple" solution to this, because we don't have the source code for the library. – Casper Jan 22 '21 at 23:14
  • Creating a hard link sadly does not solve the problem. It should correctly load the `Network.so` file. However Ruby doesn't load because it also uses `/proc/self/exec` to determine where to load additional libraries from (at leas using RVM). When I use `./localruby` it will result in the following error (without running the script) "./localruby: error while loading shared libraries: libruby.so.2.6: cannot open shared object file: No such file or directory" (It is looking for `../lib/libruby.so.2.6` from the executable location.) – 3limin4t0r Jan 25 '21 at 16:29
  • Ok. I see. I must have a statically linked Ruby exe on my machine and that's why it worked here. If you are OK with modifying the ServerApi.so binary(?), then I could post that as a second solution. – Casper Jan 25 '21 at 22:05
  • Sure. It looks like that may be the only real solution. Since `/proc/self/exec` can only be a single location and Ruby and the loaded library are conflicting in what this location should be. – 3limin4t0r Jan 26 '21 at 14:17
  • See my second answer. – Casper Jan 26 '21 at 19:03