process-bridge snapshots the execution state of a native Linux process and restores that state in an emulator.
The project consists of two main components:
- snapshotter — a native C program that launches and traces a target process, stops it at a breakpoint, captures its CPU state and memory mappings, and writes a snapshot;
- loader — a native C library that restores a snapshot into Unicorn.
The Python package provides a ctypes wrapper around the loader and a helper for creating a Qiling instance from a snapshot.
The main use case is to reach a difficult execution state natively, capture it once, and then reproduce it in an emulator for debugging, analysis, instrumentation, or fuzzing.
x86_64i386
The snapshotter must be built for the architecture of the target process.
process-bridge requires Linux.
Native components:
- Clang
- CMake >= 3.20
- Ninja
Python package:
- Python >= 3.10
unicorn==2.1.4qiling==1.4.6
Qiling is only required when using the Python Qiling helper. The native loader uses Unicorn directly.
For building the 32-bit snapshotter on Debian/Ubuntu:
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install libc6-dev:i386 gcc-multilib g++-multilibA development environment is also provided by Dockerfile.
The Python package builds the native loader automatically through scikit-build-core:
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install .The package exposes:
import process_bridge
process_bridge.restore_snapshot(...)
process_bridge.make_qiling_from_snapshot(...)The snapshotter is built separately with CMake.
Build the x86_64 release version:
cmake --preset snapshotter-x86_64-release
cmake --build --preset snapshotter-x86_64-releaseThe executable is produced at:
build/snapshotter-x86_64-release/snapshotter/pb-snapshotter
For i386:
cmake --preset snapshotter-i386-release
cmake --build --preset snapshotter-i386-releaseDebug presets enable AddressSanitizer and UndefinedBehaviorSanitizer:
cmake --preset snapshotter-x86_64-debug
cmake --build --preset snapshotter-x86_64-debugBuild the loader directly:
cmake --preset loader-release
cmake --build --preset loader-releaseThe resulting library is:
build/loader-release/loader/libpb-loader.so
The Python package builds and installs the loader automatically.
The snapshotter interface is:
pb-snapshotter <breakpoint_offset_hex> <child_path> [child_args...]
Example:
./build/snapshotter-x86_64-release/snapshotter/pb-snapshotter \
0x114d \
./helloThe snapshotter:
- starts the target under
ptrace; - determines the target image base;
- places an
INT3breakpoint atimage_base + breakpoint_offset_hex; - resumes execution until the breakpoint is reached;
- captures the CPU state;
- reads
/proc/<pid>/mapsand the target memory; - writes the snapshot to
ql_snapshot; - terminates the target.
The breakpoint is an offset from the loaded image base, not necessarily an absolute runtime address.
For PIE executables, use the virtual address of the desired instruction in the ELF image:
objdump -d ./hello
nm ./helloA useful snapshot point is often the instruction where execution should resume. For example, placing the breakpoint on a call instruction captures the state immediately before the call, including arguments already prepared by the caller.
See examples/hello and examples/nginx for complete examples.
A snapshot contains:
- integer CPU registers;
- process memory mappings;
- the contents of captured mappings;
- mapping permissions, sharing information, file offset, and path.
For i386, it also contains the TLS descriptors required to restore the captured CPU state.
Special mappings such as [vvar], [vvar_vclock], and [vsyscall] are skipped. If a readable mapping cannot be copied successfully, the affected chunks are replaced with zeroes.
The Python API restores a snapshot into an existing unicorn.Uc instance:
import unicorn
import process_bridge
uc = unicorn.Uc(unicorn.UC_ARCH_X86, unicorn.UC_MODE_64)
entry = process_bridge.restore_snapshot(
uc,
"ql_snapshot",
)
uc.emu_start(
begin=entry,
end=0,
)restore_snapshot() returns the restored instruction pointer. Memory mappings and CPU registers are restored before execution resumes.
The native loader exposes:
uint64_t pb_restore_snapshot(
uc_engine *uc,
const char *snapshot_path
);It also provides:
uint64_t pb_create_from_snapshot(
uc_arch arch,
uc_mode mode,
uc_engine **engine,
const char *snapshot_path
);Supported Unicorn configurations:
UC_ARCH_X86 + UC_MODE_32UC_ARCH_X86 + UC_MODE_64
The Python package includes a Qiling helper:
import process_bridge
from qiling.const import QL_ARCH, QL_VERBOSE
ql, entry = process_bridge.make_qiling_from_snapshot(
"ql_snapshot",
QL_ARCH.X8664,
verbose=QL_VERBOSE.DEBUG,
)
ql.emu_start(
begin=entry,
end=0,
)The helper removes Qiling's initial memory mappings and replaces them with the mappings restored from the snapshot.
Qiling still requires a rootfs. For the current workflow, an empty directory is sufficient:
mkdir -p dummy_rootfsThe restored process may contain CPU instructions that are not supported by the Unicorn version used by the emulator. Applications using AVX2, AVX-512, or instructions such as XSAVEC may therefore fail during emulation.
In such cases, it can be useful to disable CPU features when starting the target process. For example:
GLIBC_TUNABLES=glibc.cpu.hwcaps=-XSAVEC,-AVX,-AVX2,-AVX512 \
./build/snapshotter-x86_64-release/snapshotter/pb-snapshotter \
<breakpoint_offset_hex> \
<path-to-target-binary> \
[target-args...]This makes glibc avoid using the specified CPU features when selecting optimized implementations, which can prevent unsupported instructions from being executed in the restored process.
GLIBC_TUNABLES only affects glibc's CPU feature selection. It does not add support for unsupported instructions to Unicorn.
The native components use PROCESS_BRIDGE_LOG_LEVEL.
Supported levels:
DEBUGINFOWARNERROR
The default is INFO.
Example:
PROCESS_BRIDGE_LOG_LEVEL=DEBUG \
./build/snapshotter-x86_64-release/snapshotter/pb-snapshotter \
0x114d ./hello.
├── common/ Shared Linux data structures
├── snapshotter/ Native process snapshotter
├── loader/ Unicorn-based snapshot loader
├── python/process_bridge/ Python bindings and Qiling helper
├── examples/ Example workflows
├── utilities/ Shared native utilities
├── CMakeLists.txt
├── CMakePresets.json
├── Dockerfile
└── pyproject.toml
Snapshotter tests:
cmake --preset snapshotter-x86_64-debug
cmake --build --preset snapshotter-x86_64-debug
ctest --preset snapshotter-x86_64-debug --output-on-failureLoader tests:
cmake --preset loader-debug
cmake --build --preset loader-debug
ctest --preset loader-debug --output-on-failureThe debug presets enable AddressSanitizer and UndefinedBehaviorSanitizer.
Build the development image:
docker build -t process-bridge .Run it with the project mounted into /workspace:
docker run --rm -it \
-v "$PWD:/workspace" \
process-bridgeThe snapshot is a snapshot of process memory and CPU state, not a complete operating-system checkpoint.
The current implementation does not restore external resources such as:
- file descriptors;
- sockets;
- kernel-backed resources;
- signal state;
- threads other than the traced thread;
- other runtime state not represented by captured memory and registers.
Restored execution therefore has to avoid depending on unsupported external state.
The snapshot format is currently an internal implementation detail and is not intended as a stable interchange format.
process-bridge is released under the MIT License.