DEV Community

Ai2th
Ai2th

Posted on

Pockr | Part 3 — Bundling 50 Native Libraries

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)
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 patchelf on these libraries. The in-place Python script is the only safe modification method. patchelf restructures 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)