6 private links
Detour
On Linux, the traditional divide between statically and dynamically linked executables can feel like a hard wall. Either you bundle everything into your binary, or you accept full dependency on the system's libc and dynamic linker. But Detour, a tiny static library, blows a hole clean through that wall.
Detour lets you build statically linked executables, with no dependency on glibc
or musl while still giving you access to dynamic linking at runtime. You can
dlopen libraries, resolve symbols, and even mix multiple C runtimes in the
same process, all without ever linking against libc directly.
What Is Detour?
At its core, Detour is a minimal bootstrap layer that gives your application access to the system dynamic linker ld-linux.so without requiring libc at all. It allows:
- Dynamically loading libraries without linking libc
- Capturing
libdlfunctionality (e.g.,dlopen,dlsym) inside a fully static executable - Mixing different libcs in one process
- Creating freestanding, zero-libc ELF executables
All while remaining entirely under your control, with no extra dependencies or runtime overhead.
Note: Detour is not limited to freestanding or static use. You can also use it in dynamically linked applications that use an alternative libc such as musl. Detour works in both static and dynamic contexts.
Note: Detour only works with x86_64 Linux currently. Other architectures can be supported but will require writing assembly for system calls, setjmp/longjmp, and the indirect jump into the ELF entry point. See loader.c.
Why Static Linking Alone Is Not Enough
While fully static linking may sound appealing, it comes with major tradeoffs. When you bundle everything into your binary, you lose access to essential system components that rely on dynamic linking. This includes things like:
- GPU drivers (e.g., OpenGL, Vulkan ICDs)
- Window systems (X11, Wayland)
- Audio subsystems
- Input libraries
- PAM modules and NSS services
- Almost any plugin-based runtime
These components expect a working dynamic linker environment. If you statically link a libc, you cannot also have a dynamic linker in the same process. That means dlopen and dlsym will not work, and neither will anything that depends on them.
Detour solves this by letting you statically link your core application while still setting up a dynamic linker for runtime use.
How It Works
To understand Detour, it helps to understand how dynamic executables work under the hood on Linux.
When you run a dynamically linked ELF binary, the kernel does not actually execute your binary. Instead, it reads the ELF Program Header Table to find a segment of type PT_INTERP. This segment specifies the program interpreter to use, typically /lib64/ld-linux-x86-64.so.2. The kernel then executes that interpreter, passing it:
- The full path to your executable
- All command-line arguments
- Environment variables
- Auxiliary vectors
From there, the dynamic linker takes over. It maps your executable into memory,
resolves shared library dependencies, performs relocations, sets up TLS, runs
constructors, and finally jumps to libc's initialization which then jumps to
your binary's main function. In effect, the dynamic
linker is the real program, and your application is just a payload it sets up
and transfers control to after initializing everything.
Detour leverages this system by pretending to be the OS.
It works like this:
- We provide a tiny stub ELF executable that is dynamically linked against the system dynamic linker.
- Your actual program (which Detour bootstraps) loads this stub ELF using a minimal ELF loader.
- Detour reads the stub executable's
PT_INTERPsegment and loads the specified dynamic linker, just like the kernel would. - Before jumping into the dynamic linker, Detour calls
setjmpto capture its current state. - It then jumps into the dynamic linker, forwarding the stub ELF and original arguments as if it were the kernel.
- The dynamic linker maps in and initializes the stub ELF, then calls its
mainfunction. Thatmainreceives a string argument containing a function pointer encoded as a hex string. It decodes the address, casts it to a function pointer, and calls it. - This function captures symbols like
dlopen,dlsym,dlclose,dlerror, and then callslongjmpto return to the original application. - Now, back at your main program's entry point, you have full access to the dynamic linker without ever linking against libc.
It is a trampoline: a short, carefully orchestrated detour through the dynamic linker, giving you just enough of its guts to carry on without ever depending on it directly.
About the Tiny Stub
The helper ELF stub used in the first step is extremely small. It's about 35
lines of C. It is dynamically linked, but uses __asm__(".symver") to
explicitly pin any symbols it calls to the earliest possible version of glibc
that introduced the dynamic linker (around 2002). This ensures maximum forward
compatibility with any glibc-based Linux system in the wild today. Don't believe
me? Look at the code
You can ship this stub alongside your application, compile it at runtime on the
user's system, or even embed it directly into your binary and extract it to a
temporary file at startup. Its only job is to get the dynamic linker to call a
known function pointer. Nothing more.
Included Demo
Included is a demo that uses Detour to render a flashing colored window using SDL2 and OpenGL. The demo is a fully freestanding static executable that dynamically loads the system's libc, libm, libSDL2, and libGL at runtime.
It is compiled with:
-static -nostartfiles -nodefaultlibs -nostdlib -e detour_start
Note: When using Detour in a freestanding way (such as this demo), the ELF entry point must be
detour_start.
Despite being entirely statically linked, the executable dynamically loads
everything it needs at runtime. This includes: graphics drivers, windowing system libraries,
and more without ever linking against glibc or any dynamic libraries at build
time. Provided the system has a libSDL2.so this will work on any Linux install
from 2002 onwards!
Why Use It?
- Create libc-free executables that still load plugins or shared libraries
- Avoid dependency hell when shipping portable tools across Linux distributions
- Experiment with new runtimes that bootstrap their own environment
- Mix musl and glibc in the same process for advanced compatibility or sandboxing
- Access graphics drivers, window systems, and hardware-accelerated APIs without linking glibc
- Maintain compatibility with system components that require a functioning
PT_INTERPchain
Final Thoughts
Detour does not hide how Linux works, it uses how Linux works. By repurposing the exact same mechanism the OS uses to launch dynamic binaries, it gives static executables a back door into the dynamic linker.
Whether you are building minimal tooling, crafting portable binaries, or writing
your own runtime, Detour gives you surgical control over how and when the
dynamic linker shows up.
I've added a new flag to pkill called --require-handler (or -H for short). This flag ensures that signals are only sent to processes that have actually registered a handler for that signal.
Boot time optimization
-
Ensure the system is in a stable state
Make sure no one else is using it and nothing else important is going on. It's probably a good idea to stop service-providing units like httpd or ftpd, just to ensure external connections don't disrupt things in the middle.
systemctl stop httpd
systemctl stop nfs-serverand so on....
Make sure you have lsof installed (lsof -v). And that fuser (fuser -V) in installed too (Debian/Ubuntu package: psmisc).
Unmount all unused filesystems
umount -a
This will print a number of 'Target is busy' warnings, for the root volume itself and for various temporary/system FSs. These can be ignored for the moment. What's important is that no on-disk filesystems remain mounted, except the root filesystem itself. Verify this:
mount alone provides the info, but column makes it possible to read
mount | column -t
If you see any on-disk filesystems still mounted, then something is still running that shouldn't be. Check what it is using fuser:
if necessary:
yum install psmisc
then:
fuser -vm <mountpoint>
systemctl stop <whatever>
umount -arepeat as required...
Make the temporary root Note: if /tmp is a directory on /, we will not be able to unmount / later in this procedure if we use /tmp/tmproot. Thus it may be necessary to use an alternative mountpoint such as /tmproot instead.
mkdir /tmp/tmproot
mount -t tmpfs none /tmp/tmproot
mkdir /tmp/tmproot/{proc,sys,dev,run,usr,var,tmp,oldroot}
cp -ax /{bin,etc,mnt,sbin,lib,lib64} /tmp/tmproot/
cp -ax /usr/{bin,sbin,lib,lib64} /tmp/tmproot/usr/
cp -ax /var/{account,empty,lib,local,lock,nis,opt,preserve,run,spool,tmp,yp} /tmp/tmproot/var/This creates a very minimal root system, which breaks (among other things) manpage viewing (no /usr/share), user-level customizations (no /root or /home) and so forth. This is intentional, as it constitutes encouragement not to stay in such a jury-rigged root system any longer than necessary.
At this point you should also ensure that all the necessary software is installed, as it will also assuredly break the package manager. Glance through all the steps, and make sure you have the necessary executables.
Pivot into the root
mount --make-rprivate / # necessary for pivot_root to work
pivot_root /tmp/tmproot /tmp/tmproot/oldroot
for i in dev proc sys run; do mount --move /oldroot/$i /$i; donesystemd causes mounts to allow subtree sharing by default (as with mount --make-shared), and this causes pivot_root to fail. Hence, we turn this off globally with mount --make-rprivate /. System and temporary filesystems are moved wholesale into the new root. This is necessary to make it work at all; the sockets for communication with systemd, among other things, live in /run, and so there's no way to make running processes close it.
Ensure remote access survived the changeover
systemctl restart sshd
systemctl status sshdAfter restarting sshd, ensure that you can get in, by opening another terminal and connecting to the machine again via ssh. If you can't, fix the problem before moving on.
Once you've verified you can connect in again, exit the shell you're currently using and reconnect. This allows the remaining forked sshd to exit and ensures the new one isn't holding /oldroot.
Close everything still using the old root
fuser -vm /oldroot
This will print a list of processes still holding onto the old root directory. On my system, it looked like this:
USER PID ACCESS COMMAND/oldroot: root kernel mount /oldroot
root 1 ...e. systemd
root 549 ...e. systemd-journal
root 563 ...e. lvmetad
root 581 f..e. systemd-udevd
root 700 F..e. auditd
root 723 ...e. NetworkManager
root 727 ...e. irqbalance
root 730 F..e. tuned
root 736 ...e. smartd
root 737 F..e. rsyslogd
root 741 ...e. abrtd
chrony 742 ...e. chronyd
root 743 ...e. abrt-watch-log
libstoragemgmt 745 ...e. lsmd
root 746 ...e. systemd-logind
dbus 747 ...e. dbus-daemon
root 753 ..ce. atd
root 754 ...e. crond
root 770 ...e. agetty
polkitd 782 ...e. polkitd
root 1682 F.ce. master
postfix 1714 ..ce. qmgr
postfix 12658 ..ce. pickupYou need to deal with each one of these processes before you can unmount /oldroot. The brute-force approach is simply kill $PID for each, but this can break things. To do it more softly:
systemctl | grep running
This creates a list of running services. You should be able to correlate this with the list of processes holding /oldroot, then issue systemctl restart for each of them. Some services will refuse to come up in the temporary root and enter a failed state; these don't really matter for the moment.
If the root drive you want to resize is an LVM drive, you may also need to restart some other running services, even if they do not show up in the list created by fuser -vm /oldroot. You might be unable to to resize an LVM drive under Step 7 because of this Error:
fsadm: Cannot proceed with mounted filesystem "/oldroot"
You can try systemctl restart systemd-udevd and if that fails, you can find the leftover mounts with grep system /proc/*/mounts | column -t
Look for processes that say mounts:none and try restarting these:
PATH BIN FSTYPE
/proc/16395/mounts:tmpfs /run/systemd/timesync tmpfs
/proc/16395/mounts:none /var/lib/systemd/timesync tmpfs
/proc/18485/mounts:tmpfs /run/systemd/inhibit tmpfs
/proc/18485/mounts:tmpfs /run/systemd/seats tmpfs
/proc/18485/mounts:tmpfs /run/systemd/sessions tmpfs
/proc/18485/mounts:tmpfs /run/systemd/shutdown tmpfs
/proc/18485/mounts:tmpfs /run/systemd/users tmpfs
/proc/18485/mounts:none /var/lib/systemd/linger tmpfsSome processes can't be dealt with via simple systemctl restart. For me these included auditd (which doesn't like to be killed via systemctl, and so just wanted a kill -15). These can be dealt with individually.
The last process you'll find, usually, is systemd itself. For this, run systemctl daemon-reexec.
Once you're done, the table should look like this:
USER PID ACCESS COMMAND/oldroot: root kernel mount /oldroot
Unmount the old root
umount /oldroot
At this point, you can carry out whatever manipulations you require. The original question needed a simple resize2fs invocation, but you can do whatever you want here; one other use case is transferring the root filesystem from a simple partition to LVM/RAID/whatever.
Pivot the root back
mount <blockdev> /oldroot
mount --make-rprivate / # again
pivot_root /oldroot /oldroot/tmp/tmproot
for i in dev proc sys run; do mount --move /tmp/tmproot/$i /$i; doneThis is a straightforward reversal of step 4.
Dispose of the temporary root
Repeat steps 5 and 6, except using /tmp/tmproot in place of /oldroot. Then:
umount /tmp/tmproot
rmdir /tmp/tmprootSince it's a tmpfs, at this point the temporary root dissolves into the ether, never to be seen again.
Put things back in their places
Mount filesystems again:
mount -a
At this point, you should also update /etc/fstab and grub.cfg in accordance with any adjustments you made during step 7.
Restart any failed services:
systemctl | grep failed
systemctl restart <whatever>Allow shared subtrees again:
mount --make-rshared /
Start the stopped service units - you can use this single command:
systemctl isolate default.target
And you're done.