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).
- Analyze supply-chain CVE reachability across Python and native code
- Audit Python packages with native extensions
- Quantify dependency bloat for a given client package
PyXray:
- Resolves the dependencies of a target package
- Detects Python-to-native call bridges by analyzing the live object layout of callable Python objects
- Generates static Python call graphs (via PyCG) and native binary call graphs (via Ghidra)
- Stitches the Python and binary graphs together using the identified bridges
- 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.
1. Clone with submodules
git clone --recurse-submodules https://github.com/grgalex/pyxray.gitcd pyxray2. Pull the prebuilt image (or build it locally, see docs/installation.md)
docker pull docker.io/grgalex/pyxray:latestTo 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:latestAll 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.
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.2The 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"
}pyxray xlcg markupsafe:3.0.2This 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 XLCGdata/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.
After an xlcg run, bloat reports how much of each dependency is actually reachable from the client package:
pyxray xlcg thumbor:7.7.4pyxray bloat thumbor:7.7.4 -TThe -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).
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.1grep buildTransform data/bridge_reach/p/pillow/10.0.1/bridge_reach.jsonIf 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.jsonPyXray 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.
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)
- 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.
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.
- Georgios Alexopoulos, Thodoris Sotiropoulos, Georgios Gousios, Zhendong Su, and Dimitris Mitropoulos. PyXray: Practical Cross-Language Call Graph Construction through Object Layout Analysis. In Proceedings of the 48th International Conference on Software Engineering (ICSE '26), April 2026.