Skip to content

luci-project/live.so

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

live.so

Implementation of the Luci approach as a preloadable shared object for the glibc RTLD (basically any major Linux distribution): By setting LD_PRELOAD to the absolute path of live.so, file system changes of other loaded shared objects will be tracked and updated at runtime if compatible. The main criteria for compatibility is that the layout of the global writable data is identical and no data structures have changed. Therefore, the focus is on code changes (which covers many bug fixes).

Usage

Build live.so by running

make

It requires the elfutils libraries (Debian/Ubuntu package libelf-dev) to be installed.

Let's assume you have a (glibc) program bin that is linked to shared object libfoo.so.1 (using a symbolic link libfoo.so). You can preload this shared library with

LD_PRELOAD=$(readlink -f live.so) path/to/bin

Now you can modify the library by changing the symlink to point to an updated version libfoo.so.2 during the execution of the program. Within seconds, all library calls will now call code from the second version instead of the first.

Since the global writable data is shared, all variables will retain their contents during the update.

Examples

This project comes with two examples to demonstrate the approach:

  • Hello World exchanges the shared library that provides the "hello world" string with versions in different languages.
  • Fibonacci is identical to the Luci RTLD example and dynamically replaces the algorithms for computing Fibonacci numbers.

Concept (in a nutshell)

When compiling a program that uses shared libraries, the linker cannot determine the address of its variables and functions because this is done at runtime (e.g., for ASLR). Therefore, when a program accesses an external variable or calls an external function, it uses the global offset table (GOT) as an indirection. The dynamic linker/loader prepares this table at program startup with the address of the shared library in the process virtual memory.

And if the shared library is changed, e.g. due to a bug, all programs using it must be restarted. With live.so, this can be omitted:

This library analyzes all shared libraries used by the program before executing the main function and monitors file system changes (via inotify) of the libraries. If a newer version is found, live.so loads it into the process virtual memory (via dlopen) and updates the GOT.

 PROGRAM          GOT             SYMLINK          SHARED LIB

while(1) {      ┌───────┐
  var++; ─────► │var ───┼──┬───► libfoo.so ──┐--- libfoo.so.1
  func(); ────► │func ──┼──┘                 └──► libfoo.so.2
  sleep(1); ──► │sleep ─┼───────────────────────► libc.so.6
}               └───────┘

All subsequent calls to the shared library will now be handled by the newer version.

Neither the program nor the shared library need to be prepared for the update: Since shared libraries use position-independent code, the memory image remains unchanged and is exactly as the compiler & linker intended - no code patching is required. This means that you can change the code in any way you like, as long as the external interface (API) remains the same.

Since shared libraries may have an internal state, the new version of the shared library should use it as well. With this approach not supporting data modification, the data segment is identical to its predecessor. Accordingly, live.so creates a memory alias for this segment, allowing old and new code to work on the same data.

          Process's virtual memory
                 ┌───────┐
    PROGRAM code │█████▀▀│
            data │▄▄▄▄ ▀▀│
                 │  ...  │
libfoo.so.1 code │███▀▀▀▀│
            data │▄▄████▀│ ──┐
                 │  ...  │   │ memory
libfoo.so.2 code │██████▀│   │ alias
            data │▄▄████▀│ ◄─┘
                 └───────┘

No quiescence is required, but the update can happen at any time: If the program is currently executing code in the old shared library, this will not prevent the update. Instead, there will be a concurrent update of the GOT. And except for loading the newer version into virtual memory, there is no runtime overhead.

For further information, have a look at Luci RTLD and our paper and poster.

Limitations

This version has the same conceptual limitations as the Luci RTLD: For example, unless you are using full RELRO, you may not introduce new functions from shared objects (including the glibc) as this would alter the GOT, which is located in the global writable data section.

However, since this version is mostly indented as a proof of concept, it has some additional limitations compared to the Luci RTLD: For example, it cannot update dynamically loaded shared objects (via dlopen). Other missing features include the inability of dealing with direct changes to the shared library and to detect outdated code.

Advantages

So, why bother and creat this project if Luci RTLD seems to be superior? Well, unlike the RTLD this can easily be used on other architectures, since it does not have any CPU-specific instructions or relocations. It should work on any system with a Linux userspace based on a recent Glibc.

Author & License

The Luci project is being developed by Bernhard Heinloth of the Department of Computer Science 4 at Friedrich-Alexander-Universität Erlangen-Nürnberg and is available under the GNU Affero General Public License, Version 3 (AGPL v3).

About

Dynamic updates (Luci approach) via a preloaded shared object [Mirror]

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published