Interposing part of a shared object by soname

487 views Asked by At

I’ve written a shared object that modifies the arguments to FreeType’s FT_Load_Glyph and FT_Render_Glyph functions, currently by interposing it with LD_PRELOAD and dlsym.

This works fine, but I’m curious to know whether or not there’s a way to make these changes:

  • to all programs that use FreeType on a given host (running e.g. Debian);
  • without clobbering any programs that aren’t actually linked to FreeType;
  • without simply applying an LD_PRELOAD to all programs on the host;
  • without requiring any maintenance unless FreeType’s soname is changed; and
  • without modifying any of FreeType’s files, nor those of any programs on the host.

The only two “solutions” that I’ve been able to come up with are ugly hacks:

  • to LD_PRELOAD all programs, all of the time, which seems slow and fragile; or
  • to copy e.g. libfreetype.so.6.12.3 to libxxxxtype.so.6.12.3; then
    • patch the soname in libxxxxtype.so.6.12.3 to libxxxxtype.so.6;
    • link the interposing shared object against libxxxxtype.so.6; and
    • install the shared object as e.g. libfreetype.so.6.999.

I’d essentially like to transparently patch a couple of functions in a shared object, while letting the remaining functions through, without necessarily having access to the source of the shared object or the programs that use it, but if I make a fake shared object with the soname libfreetype.so.6, I can’t see a clean way to link it to (or dlopen) the real libfreetype.so.6.

This is my first real experiment with shared libraries, so please bear with me if this question makes some incorrect assumptions, or just makes no sense.

3

There are 3 answers

5
osgx On BEST ANSWER

Can you try to use uprobes to dynamically steal control from some functions?

Check http://www.brendangregg.com/blog/2015-06-28/linux-ftrace-uprobe.html

uprobes: user-level dynamic tracing, which was added to Linux 3.5 and improved in Linux 3.14. It lets you trace user-level functions; for example, the return of the readline() function from all running bash shells, with the returned string:

# ./uprobe 'r:bash:readline +0($retval):string'
Tracing uprobe readline (r:readline /bin/bash:0x8db60 +0($retval):string). Ctrl-C to end.
 bash-11886 [003] d... 19601837.001935: readline: (0x41e876 <- 0x48db60) arg1="ls -l"
 bash-11886 [002] d... 19601851.008409: readline: (0x41e876 <- 0x48db60) arg1="echo "hello world""
 bash-11886 [002] d... 19601854.099730: readline: (0x41e876 <- 0x48db60) arg1="df -h"
 bash-11886 [002] d... 19601858.805740: readline: (0x41e876 <- 0x48db60) arg1="cd .."
 bash-11886 [003] d... 19601898.378753: readline: (0x41e876 <- 0x48db60) arg1="foo bar"
^C
Ending tracing...

And http://www.brendangregg.com/blog/2015-07-03/hacking-linux-usdt-ftrace.html

There were also other solutions of tracing user-space functions, like ftrace, systemtap, dtrace, lttng. Some of them need recompilation and defining tracing points statically in the program; and uprobes are "user-level dynamic tracing".

Some links about uprobes:

There is handler of uprobes which has pt_regs. As said in last link: "Uprobes thus implements a mechanism by which a kernel function can be invoked whenever a process executes a specific instruction location." and it suggests that uprobes may replace some ptrace/gdb based solutions; so there is a possibility to change execution of any program hitting active uprobe, by changing its eip/rip (PC) register.

You may try some other dynamic instrumentation tools, like pin or dyninst; but they are designed for per-process usage.

1
glorpen On

Another solution would be to make system wide "overlay" for lib, with custom libfreetype and then proxying unmodified methods to real lib.

You have to make custom lib compatible with real one. You can to that by using dlopen with absolute path (eg. dlopen("/usr/lib64/libfreetype.so.6")), copying definitions of real, exported functions and proxying them with dlsym. It think that for ease of maintenance you could event replace proxied argument types with simple void*. You would only need to make changes when freetype functions change (arguments count, function names).

To create lib "overlay", you could install custom lib into eg. "/opt/myapp/lib64/libfreetype.so.6", then add this path to dynamic linker run time paths. You may have to create symlinks for other versions or compile new custom lib if original implementation changes. Whatever is needed to shadow real lib and keep other apps working :)

Google says that to change run time loading paths on Debian you have to simply edit /etc/ld.so.conf. Add /opt/myapp/lib64 path at the beginning so it will be checked first. Now any app searching for freetype should load your lib, you can check it with ldd <path to app>.

I can think of just one case when this solution will not work: if app is loading bundled libfreetype or loading it by full path, not by name.

2
James K. Lowden On

to LD_PRELOAD all programs, all of the time, which seems slow and fragile

That's a good solution (for what you want). I don't see a better one.

  • It's not fragile. It provides information to the runtime linker in a documented way. You're not bonking on anything, pretending something isn't what it is. You're just altering the preference hierarchy for function-name resolution.

  • It's not slow. The linker has to do something sometime. It's got to check if LD_PRELOAD is defined, which in any case is a user-space operation. So it will follow that path, and load your library before doing a bunch of other work. I'd be astonished if the time was even measurable under normal circumstances.

There are two concerns I'd have, but they're orthogonal to the technique. The code actually has to work in all cases, and you have to dig into the process-creation framework a bit to make sure LD_PRELOAD really is defined everywhere. Other than that, ld.so defines its environment variables precisely for your intended use. Who's to argue?