Libraries Without Breaking the Android Linker
Part 3 of 6 — building Pockr, a single APK that runs Docker on non-rooted Android.
← Part 2: Executing Binaries on Android
QEMU Needs Friends
QEMU from Termux doesn't link statically. It depends on a chain of ~50 shared libraries:
libqemu.so
├── libcurl.so → libssl.so, libcrypto.so, libssh.so, libgnutls.so ...
├── libglib-2.0.so → libpcre2-8.so, libffi.so, libandroid-support.so ...
├── libbz2.so
└── libzstd.so
... (44 more)
All of these are bundled in Termux's own prefix at /data/data/com.termux/files/usr/lib/ — which doesn't exist on devices without Termux installed.
Problem 1: RUNPATH Points to Nowhere
Every Termux binary has a hardcoded RUNPATH in its ELF:
readelf -d libqemu.so | grep RUNPATH
(RUNPATH) Library runpath: [/data/data/com.termux/files/usr/lib]
Android's dynamic linker tries this path first. It doesn't exist → symbols not found → crash.
Fix: Zero out the RUNPATH in every .so file.
Why Not patchelf?
The obvious tool is patchelf --remove-rpath. We tried it. It works on a PC, but the resulting .so files crash Android's linker:
bionic/linker/linker_phdr.cpp:168:
Load CHECK 'did_read_' failed
patchelf restructures ELF LOAD segments when it modifies the file — sometimes creating 5 to 50 segments per library. Android 11's linker has strict limits on LOAD segment count.
Fix: An in-place Python script that zeroes only the d_val field of DT_RUNPATH/DT_RPATH entries, leaving the ELF structure completely intact:
import struct, sys
with open(sys.argv[1], 'r+b') as f:
data = bytearray(f.read())
# Parse ELF header, find .dynamic section
# For each DT_RPATH/DT_RUNPATH entry:
# zero out d_val only — leave d_tag intact
# Write back
f.seek(0)
f.write(data)
This preserves every byte of the ELF except the path string offset — exactly what the linker needs.
Problem 2: Versioned Sonames
Termux packages use versioned filenames (libzstd.so.1.5.7) with sonames like libzstd.so.1. Android's linker needs exact filename matches.
Fix: Rename every library to its base soname and update the soname field:
# Before
libzstd.so.1.5.7 (soname: libzstd.so.1)
# After
libzstd.so (soname: libzstd.so)
Problem 3: Namespace Isolation
Android 7+ enforces linker namespace isolation. A library loaded transitively (e.g. libgnutls.so via libcurl.so) is NOT visible to other libraries unless they have a direct DT_NEEDED entry.
This caused errors like:
cannot locate symbol "gnutls_cipher_init"
referenced by libqemu_img.so
Even though libgnutls.so was present and loaded.
Fix: Add explicit DT_NEEDED entries for the minimum set of libs that provide undefined symbols — again using the in-place approach, not patchelf.
The Final Set: 50 Libraries
| Category | Libraries |
|---|---|
| TLS / Crypto | libssl, libcrypto, libgnutls, libnettle, libhogweed, libtasn1, libgmp |
| SSH | libssh, libssh2 |
| HTTP/2-3 | libcurl, libnghttp2, libnghttp3, libngtcp2 |
| Compression | libzstd, libbz2, libz |
| GLib | libglib-2.0, libffi, libpcre2-8, libiconv |
| Android compat | libandroid-support |
| Unicode | libidn2, libunistring |
| Events | libevent |
| ... | 27 more |
Key Rule
Never use
patchelfon these libraries. The in-place Python script is the only safe modification method.patchelfrestructures ELF LOAD segments and breaks Android 11's linker.
Next: Part 4 — Making Docker Start Without Kernel Modules
GitHub: github.com/AI2TH/Pockr
Pockr Series — Docker in Your Pocket
Pockr = Pocket + Docker. A single Android APK that runs real Docker containers in your pocket — no root, no Termux, no PC required.
| # | Post | Topic |
|---|---|---|
| 📖 | Intro | What is Pockr? Start here |
| 1 | Part 1 | The Idea and Architecture |
| 2 | Part 2 | Executing Binaries — The SELinux Problem |
| 3 | Part 3 | Bundling 50 Native Libraries |
| 4 | Part 4 | Docker Without Kernel Modules |
| 5 | Part 5 | Debugging the VM Restart Loop |
| 6 | Part 6 | Test Results and What's Next |
GitHub: github.com/AI2TH/Pockr
Native / Low-Level Engineer: Kalvin Nathan
skalvinnathan@gmail.com · LinkedIn
Top comments (0)