Skip to content

grgalex/pyxray

Repository files navigation

DOI Zenodo License: AGPL v3 Docker Image

PyXray: Practical Python Cross-Language Call Graphs

PyXray is a hybrid (static/dynamic) analysis tool that constructs cross-language call graphs (XLCGs) of Python packages with native extensions.

It accompanies the ICSE'26 paper PyXray: Practical Cross-Language Call Graph Construction through Object Layout Analysis (PDF).

PyXray cross-language call graph architecture diagram

Use Cases

  • Analyze supply-chain CVE reachability across Python and native code
  • Audit Python packages with native extensions
  • Quantify dependency bloat for a given client package

How It Works

PyXray:

  1. Resolves the dependencies of a target package
  2. Detects Python-to-native call bridges by analyzing the live object layout of callable Python objects
  3. Generates static Python call graphs (via PyCG) and native binary call graphs (via Ghidra)
  4. Stitches the Python and binary graphs together using the identified bridges
  5. Produces a unified cross-language call graph (XLCG) for the target package and all of its dependencies

Bridges and call graphs are cached per package version and reused across analyses, so the cost of computing XLCGs drops as the cache grows.

Note that the XLCG inherits potential imprecision and incompleteness from the underlying call graph generators (PyCG for Python code, Ghidra for binaries), so it may contain false positives and false negatives.

Quickstart

1. Clone with submodules

git clone --recurse-submodules https://github.com/grgalex/pyxray.git
cd pyxray

2. Pull the prebuilt image (or build it locally, see docs/installation.md)

docker pull docker.io/grgalex/pyxray:latest

To maximize bridge-detection accuracy, PyXray builds the package under analysis (and its dependencies) from source with debug symbols. The default image bundles the compilers, system headers, and Python build backends this needs. If you prefer a smaller image, grgalex/pyxray:slim is enough for wheel-only (-W) analysis.

3. Start a container

docker run --rm -it --name pyxray \
    -e GHIDRA_INSTALL_DIR=/ghidra --cap-add=SYS_PTRACE \
    --security-opt seccomp=unconfined \
    -v ${PWD}:/pyxray -w /pyxray \
    grgalex/pyxray:latest

All commands below run inside the container. Results land under data/, which is bind-mounted from your host. The container runs as a non-root user named pyxray (UID 1000, with passwordless sudo), so the results are owned by your host user.

Usage

1. Find Python-to-native bridges

A bridge is a Python-visible callable that is actually implemented by a C function inside a native library. PyXray discovers bridges through object layout analysis, without any prior knowledge of the package:

pyxray bridges markupsafe:3.0.2

The result is written to data/star_bridges/m/markupsafe/3.0.2/starbridges.json. Each bridge records the Python name, the C function, and the native library that contains it:

{
  "pyname": "markupsafe._speedups._escape_inner",
  "cfunc": "escape_unicode",
  "library": "markupsafe___3.0.2/markupsafe/_speedups.cpython-312-x86_64-linux-gnu.so"
}

2. Build the cross-language call graph

pyxray xlcg markupsafe:3.0.2

This runs the full pipeline: dependency resolution, Python call graph construction, bridge detection, binary call graph extraction with Ghidra, and stitching. Outputs:

  • data/unified_cg/pypi/m/markupsafe/3.0.2/unified.json, the full XLCG
  • data/reached_cg/pypi/m/markupsafe/3.0.2/reached.json, the subgraph reachable from the package entry points

An XLCG is a single graph that contains both Python functions and native functions. Every node carries its owning package, native nodes also carry their library, and edges are plain caller-to-callee pairs that cross the language boundary at bridges:

{
  "nodes": {
    "31": {"name": "markupsafe._speedups._escape_inner", "package": "markupsafe:3.0.2"},
    "43": {"name": "escape_unicode",
           "library": "markupsafe/_speedups.cpython-312-x86_64-linux-gnu.so",
           "package": "markupsafe:3.0.2"}
  },
  "edges": [[31, 43]]
}

In this fragment the Python callable markupsafe._speedups._escape_inner calls the C function escape_unicode inside the package's native extension.

3. Quantify dependency bloat

After an xlcg run, bloat reports how much of each dependency is actually reachable from the client package:

pyxray xlcg thumbor:7.7.4
pyxray bloat thumbor:7.7.4 -T

The -T flag prints a summary table. For thumbor, three direct dependencies turn out to be entirely unused:

Unreachable (3 packages):
  pytz:2023.4  (6 py files, 107 py funcs, 0 bin files, 0 bin funcs, 69.9K)
  webcolors:1.13  (6 py files, 31 py funcs, 0 bin files, 0 bin funcs, 39.4K)
  thumbor-plugins-gifv:0.1.5  (2 py files, 7 py funcs, 0 bin files, 0 bin funcs, 3.5K)

The full per-package metrics are written to data/bloat/t/thumbor/7.7.4/bloat.json.

We reported the unused webcolors dependency to the thumbor maintainers and it was fixed (thumbor#1808).

4. Check CVE reachability

CVE-2024-28219 affects the native function buildTransform in Pillow versions before 10.3.0.

Before running any reachability analysis, first determine whether PyXray can see the vulnerable symbol at all. bridge-reach computes every native function reachable from any Python-to-native bridge of the package, so search its output for the symbol:

pyxray bridge-reach pillow:10.0.1
grep buildTransform data/bridge_reach/p/pillow/10.0.1/bridge_reach.json

If there is no match, PyXray cannot observe the vulnerable symbol (for example, the symbol may be inlined or stripped) and reachability results for it would be meaningless. If there is a match, checking client reachability makes sense: to determine whether a specific application is transitively affected, search its reached XLCG (thumbor 7.7.4 depends on the vulnerable Pillow, and we built its XLCG in the previous step):

grep buildTransform data/reached_cg/pypi/t/thumbor/7.7.4/reached.json

PyXray can then extract the concrete cross-language call chains from the client to the vulnerable function:

python3 -m pyxray.call_chain -i data/reached_cg/pypi/t/thumbor/7.7.4/reached.json -s buildTransform
[
  "thumbor.engines.pil.Engine.read",
  "thumbor.utils.ensure_srgb",
  "PIL.ImageCms.ImageCmsTransform.__init__",
  "PIL._imagingcms.buildTransform",
  "buildTransform"
]

The chain starts at a Python function of thumbor, crosses into Pillow's Python layer, and ends inside the vulnerable native code.

CLI Overview

pyxray <command> [PACKAGE...] [options]

Commands:
  bridges       Find Python-to-native bridges of the package itself (deps excluded)
  bridge-reach  Compute native reachability from ANY bridge (baseline for CVE reachability)
  xlcg          Compute cross-language call graph (XLCG)
  bloat         Calculate dependency bloat (requires prior xlcg)

Package sources:
  PACKAGE              One or more packages as name:version (e.g. numpy:1.26.4)
                       or just name (e.g. pillow) to use the latest PyPI version
  -i, --input-file     CSV file with name:version entries (one per line)
  --source-dir DIR     Source project directory (auto-detect & install)
  --install-dir DIR    Pre-installed site-packages directory
  --requirements FILE  Requirements file (name:version per line) for --source-dir deps

Options:
  --version            Show version and exit
  -j, --jobs N         Number of parallel workers (default: 1)
  -A, --always         Force regeneration of all artifacts
  -B, --binary-always  Force binary CG regeneration via Ghidra (xlcg/bridge-reach)
  -W, --wheel-only     Use pre-built wheels instead of source builds
  -T, --table          Print ASCII summary table (bloat only)
  -v, --verbose        Stream subprocess output in real time
  --log LEVEL          Logging level (debug, info, warning, error)

Documentation

  • docs/installation.md: installing via Docker (pull or build locally) or manual setup without Docker.
  • docs/advanced-usage.md: full CLI reference, all subcommands, batch processing, parallelism, wheel-only mode, source-dir and install-dir analysis, architecture, data layout, development and tests.

License

Copyright (C) 2026 Georgios Alexopoulos

This project is licensed under the GNU Affero General Public License v3.0 (AGPLv3). See the LICENSE file for details.

Related Publications

About

Build cross-language call graphs for Python packages. Perform vulnerability reachability analysis, detect unused code (bloat)

Topics

Resources

License

Stars

5 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors