Reputation: 280
I have a shared library, say somelib.so
, which uses ioctl
from libc (according to objdump
).
My goal is to write a new library that wraps around somelib.so
and provides a custom ioctl
. I want to avoid preloading a library to ensure that only the calls in somelib.so
use the custom ioctl
.
Here is my current snippet:
typedef int (*entryfunctionFromSomelib_t) (int par, int opt);
typedef int (*ioctl_t) (int fd, int request, void *data);
ioctl_t real_ioctl = NULL;
int ioctl(int fd, int request, void *data )
{
fprintf( stderr, "trying to wrap ioctl\n" );
void *handle = dlopen( "libc.so.6", RTLD_NOW );
if (!handle)
fprintf( stderr, "Error loading libc.so.6: %s\n", strerror(errno) );
real_ioctl = (ioctl_t) dlsym( handle, "ioctl" );
return real_ioctl( fd, request, data);
}
int entryfunctionFromSomelib( int par, int opt ) {
void *handle = dlopen( "/.../somelib.so", RTLD_NOW );
if (!handle)
fprintf( stderr, "Error loading somelib.so: %s\n", strerror(errno) );
real_entryfunctionFromSomelib = entryfunctionFromSomelib_t dlsym( handle, "entryfunctionFromSomelib" );
return real_entryfunctionFromSomelib( par, opt );
}
However, it does not in work in the sense that the calls to ioctl
form somelib.so
are not redirected to my custom ioctl
implementation. How can I enforce that the wrapped somelib.so
does so?
======================
Additional information added after @Nominal Animal post:
Here some information from mylib.so (somelib.so after edit) obtained via readelf -s | grep functionname
:
246: 0000000000000000 121 FUNC GLOBAL DEFAULT UND dlsym@GLIBC_2.2.5 (11)
42427: 0000000000000000 121 FUNC GLOBAL DEFAULT UND dlsym@@GLIBC_2.2.5
184: 0000000000000000 37 FUNC GLOBAL DEFAULT UND ioctl@GLIBC_2.2.5 (6)
42364: 0000000000000000 37 FUNC GLOBAL DEFAULT UND ioctl@@GLIBC_2.2.5
After 'patching' mylib.so it also shows the new function as:
184: 0000000000000000 37 FUNC GLOBAL DEFAULT UND iqct1@GLIBC_2.2.5 (6)
I 'versioned' and exported the symbols from my wrap_mylib library for which readelf
now shows:
25: 0000000000000d15 344 FUNC GLOBAL DEFAULT 12 iqct1@GLIBC_2.2.5
63: 0000000000000d15 344 FUNC GLOBAL DEFAULT 12 iqct1@GLIBC_2.2.5
However, when I try to dlopen
wrap_mylib I get the following error:
symbol iqct1, version GLIBC_2.2.5 not defined in file libc.so.6 with link time reference
Is that maybe because mylib.so tries to dlsym
iqct1
from libc.so.6 ?
Upvotes: 2
Views: 1752
Reputation: 39356
If binutils' objcopy
could modify dynamic symbols, and the mylib.so
is an ELF dynamic library, we could use
mv mylib.so old.mylib.so
objcopy --redefine-sym ioctl=mylib_ioctl old.mylib.so mylib.so
to rename the symbol name in the library from ioctl
to mylib_ioctl
, so we could implement
int mylib_ioctl(int fd, int request, void *data);
in another library or object linked to the final binaries.
Unfortunately, this feature is not implemented (as of early 2017 at least).
We can solve this using an ugly hack, if the replacement symbol name is exactly the same length as the original name. The symbol name is a string (both preceded and followed by a nul byte) in the ELF file, so we can just replace it using e.g. GNU sed:
LANG=C LC_ALL=C sed -e 's|\x00ioctl\x00|\x00iqct1\x00|g' old.mylib.so > mylib.so
This replaces the name from ioctl()
to iqct1()
. It is obviously less than optimal, but it seems the simplest option here.
If you find you need to add version information to the iqct1()
function you implement, with GCC you can simply add a line similar to
__asm__(".symver iqct1,iqct1@GLIBC_2.2.5");
where the version follows the @
character.
Here is a practical example, showing how I tested this in practice.
First, let's create mylib.c, representing the sources for mylib.c (that the OP does not have -- otherwise just altering the sources and recompiling the library would solve the issue):
#include <unistd.h>
#include <errno.h>
int myfunc(const char *message)
{
int retval = 0;
if (message) {
const char *end = message;
int saved_errno;
ssize_t n;
while (*end)
end++;
saved_errno = errno;
while (message < end) {
n = write(STDERR_FILENO, message, (size_t)(end - message));
if (n > 0)
message += n;
else {
if (n == -1)
retval = errno;
else
retval = EIO;
break;
}
}
errno = saved_errno;
}
return retval;
}
The only function exported is myfunc(message)
, as declared in mylib.h:
#ifndef MYLIB_H
#define MYLIB_H
int myfunc(const char *message);
#endif /* MYLIB_H */
Let's compile the mylib.c into a dynamic shared library, mylib.so
:
gcc -Wall -O2 -fPIC -shared mylib.c -Wl,-soname,libmylib.so -o mylib.so
Instead of write()
from the C library (it's a POSIX function just like ioctl()
, not a standard C one), we wish to use mywrt()
of our own design in our own program. The above command saves the original library as mylib.so
(while naming it internally as libmylib.so
), so we can use
sed -e 's|\x00write\x00|\x00mywrt\x00|g' mylib.so > libmylib.so
to alter the symbol name, saving the modified library as libmylib.so
.
Next, we need a test executable, that provides the ssize_t mywrt(int fd, const void *buf, size_t count);
function (the prototype being the same as the write(2)
function it replaces. test.c:
#include <stdlib.h>
#include <stdio.h>
#include "mylib.h"
ssize_t mywrt(int fd, const void *buffer, size_t bytes)
{
printf("write(%d, %p, %zu);\n", fd, buffer, bytes);
return bytes;
}
__asm__(".symver mywrt,mywrt@GLIBC_2.2.5");
int main(void)
{
myfunc("Hello, world!\n");
return EXIT_SUCCESS;
}
The .symver
line specifies version GLIBC_2.2.5
for mywrt
.
The version depends on the C library used. In this case, I ran objdump -T $(locate libc.so) 2>/dev/null | grep -e ' write$'
, which gave me
00000000000f66d0 w DF .text 000000000000005a GLIBC_2.2.5 write
the second to last field of which is the version needed.
Because the mywrt
symbol needs to be exported for the dynamic library to use, I created test.syms:
{
mywrt;
};
To compile the test executable, I used
gcc -Wall -O2 test.c -Wl,-dynamic-list,test.syms -L. -lmylib -o test
Because libmylib.so
is in the current working directory, we need to add current directory to the dynamic library search path:
export LD_LIBRARY_PATH=$PWD:$LD_LIBRARY_PATH
Then, we can run our test binary:
./test
It will output something like
write(2, 0xADDRESS, 14);
because that's what the mywrt()
function does. If we want to check the unmodified output, we can run mv -f mylib.so libmylib.so
and rerun ./test
, which will then output just
Hello, world!
This shows that this approach, although depending on very crude binary modification of the shared library file (using sed
-- but only because objcopy
does not (yet) support --redefine-sym
on dynamic symbols), should work just fine in practice.
This is also a perfect example of how open source is superior to proprietary libraries: the amount of effort already spent in trying to fix this minor issue is at least an order of magnitude higher than it would have been to rename the ioctl
call in the library sources to e.g. mylib_ioctl()
, and recompile it.
Interposing dlsym()
(from <dlfcn.h>
, as standardized in POSIX.1-2001) in the final binary seems necessary in OP's case.
Let's assume the original dynamic library is modified using
sed -e 's|\x00ioctl\x00|\x00iqct1\x00|g;
s|\x00dlsym\x00|\x00d15ym\x00|g;' mylib.so > libmylib.so
and we implement the two custom functions as something like
int iqct1(int fd, unsigned long request, void *data)
{
/* For OP to implement! */
}
__asm__(".symver iqct1,iqct1@GLIBC_2.2.5");
void *d15ym(void *handle, const char *symbol)
{
if (!strcmp(symbol, "ioctl"))
return iqct1;
else
if (!strcmp(symbol, "dlsym"))
return d15ym;
else
return dlsym(handle, symbol);
}
__asm__(".symver d15ym,d15ym@GLIBC_2.2.5");
Do check the versions correspond to the C library you use. The corresponding .syms file for the above would contain just
{ i1ct1; d15ym; };
otherwise the implementation should be as in the practical example shown earlier in this answer.
Because the actual prototype for ioctl()
is int ioctl(int, unsigned long, ...);
, there are no quarantees that this will work for all general uses of ioctl()
. In Linux, the second parameter is of type unsigned long
, and the third parameter is either a pointer or a long or unsigned long -- in all Linux architectures pointers and longs/unsigned longs have the same size --, so it should work, unless the driver implementing the ioctl()
is also closed, in which case you are simply hosed, and limited to either hoping this works, or switching to other hardware with proper Linux support and open-source drivers.
The above special-cases both original symbols, and hard-wires them to the replaced functions. (I call these replaced instead of interposed symbols, because we really do replace the symbols the mylib.so
calls with these ones, rather than interpose calls to ioctl()
and dlsym()
.)
It is a rather brutish approach, but aside from using sed
due to the lack of dynamic symbol redefinition support in objcopy
, it is quite robust and clear as to what is done and what actually happens.
Upvotes: 3