1

I am trying to use LD_PRELOAD on linux to wrap calls to system function to add some preprocessing to the argument. Here's my system.cpp:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <string>
#include <iostream>

typedef int (*orig_system_type)(const char *command);

int system(const char *command)
{
    std::string new_cmd = std::string("set -f;") + command;
    // next line is for debuggin only
    std::cout << new_cmd << std::endl;

    orig_system_type orig_system;
    orig_system = (orig_system_type)dlsym(RTLD_NEXT,"system");
    return orig_system(new_cmd.c_str());
}

I build it with

g++ -shared -fPIC -ldl -o libsystem.so system.cpp

which produces the .so object. I then run my program with

$ LD_PRELOAD=/path/to/libsystem.so ./myprogram

I do not get any errors - but seemingly my system function is not called. Running with LD_DEBUG=libs, I can see that my .so is being loaded, however my system function is not being called and the one from the standard library is called instead.

What do I need to change in code/build to get it to work?

Aleks G
  • 56,435
  • 29
  • 168
  • 265

3 Answers3

2

You need

extern "C" int system ...

because it is called by a C function. The C++ version has its name mangled so it is not recognizable.

rici
  • 234,347
  • 28
  • 237
  • 341
  • Yes! that was it. I did wonder about name mangling, as `strings libsystem.so | grep system` had some mangling around the `system` function name. With the `extern "C"` it works correctly now! – Aleks G Jun 15 '17 at 07:34
1

You might also want to consider saving the "orig_system" pointer so that you avoid calling dlsym every time. You can do this in a constructor/init function, so you would have something like

extern "C" {

typedef int (*orig_system_type)(const char *command);

static orig_system_type orig_system;

static void myInit() __attribute__((constructor));

void myInit()
{
    orig_system = (orig_system_type)dlsym(RTLD_NEXT,"system");
}

int system(const char *command)
{
    std::string new_cmd = std::string("set -f;") + command;
    // next line is for debuggin only
    std::cout << new_cmd << std::endl;
    return orig_system(new_cmd.c_str());
}

}

(this code isn't tested, but I have used this technique in the past).

An alternative would be to use GNU ld's --wrap option.

If you compile your shared lib with

-Wl,--wrap system

then in your code you write

extern "C" {

void* __real_system(const char* command);
void* __wrap_system(const char* command)
{
    std::string new_cmd = std::string("set -f;") + command;
    // next line is for debuggin only
    std::cout << new_cmd << std::endl;
    return __real_system(new_cmd.c_str());
}

}

(Note that I've never used this).

Paul Floyd
  • 5,530
  • 5
  • 29
  • 43
0

The code should work perfectly fine. Assuming the driver program is something like this:

#include <cstdlib>

int main() {
    system("find -name *.cpp");
}

Then env LD_PRELOAD=$PWD/libsystem.so ./a.out gives me this output:

set -f;find -name *.cpp
./system.cpp
./test.cpp

Which shows that not only is your debug statement appearing, but that glob is disabled for that command.

  • See @rici answer - you are correct, it would correctly if the driver program is in C, but in my case it's c++ and name mangling gets in the way. – Aleks G Jun 15 '17 at 07:36