G14 — CPython Limited-API / abi3 import-contract conformance¶
Registry: UC-WF-stable-abi-subset (complete)
Effort: M · Risk: low
Status: Done. abicheck/python_ext.py recognises extension modules
(Cython/pybind11/nanobind/C) and captures their imported CPython C-API surface;
abicheck/stable_abi.py classifies each import against the Limited-API
allowlist; abicheck/diff_python.py raises PYTHON_STABLE_ABI_VIOLATION on
compare (a new private _Py* import); and abicheck scan --abi3 3.9
(abicheck/cli_scan.py + diff_python.audit_stable_abi_imports) audits a single
module against a target floor. Interpreter-floor conformance lives in the audit
(not compare), since a bare .abi3.so carries no declared floor. See
Python Extensions (abi3).
Problem¶
Every abi3 wheel promises it uses only the stable CPython C-API subset, so
one binary runs on all Py_LIMITED_API-compatible interpreters. abicheck has
the substrate to verify this (the ELF/PE/Mach-O undefined-symbol table, plus
the appcompat engine) but no driver that checks it. Today compare/appcompat
reason about a binary's exports; for an extension module the exports are
essentially just PyInit_<mod> — the compatibility surface that actually
matters is what the module imports from libpython.
Empirically confirmed (cryptography 42.0.8 → 43.0.3, both cp39-abi3):
abicheck comparereturnsCOMPATIBLE— judged entirely from the export table (1 → 25 exported symbols).- The real contract is invisible: the module's imported CPython symbols grow
from 111 → 118
Py*(e.g.PyByteArray_AsString/Size/Type,PyNumber_And/Multiply/Power,PyObject_GenericSetDict,Py_DecRef/IncRef). If any imported symbol were outside theabi3set, or newer than the wheel's declaredPy_LIMITED_APIfloor, the wheel would fail to import on an older interpreter with anundefined symbolerror — and abicheck would still sayCOMPATIBLE.
Goal & acceptance criteria¶
- [x] A check that enumerates an extension's imported CPython C-API symbols
and classifies each against a stable-ABI allowlist for a target
Py_LIMITED_APIversion. - [x] An imported symbol outside the
abi3set (or newer than the declared floor) is reported as a deployment-RISK/BREAKINGfinding that reaches the verdict and JSON/SARIF — not silentlyCOMPATIBLE. - [x] A clean
abi3extension passes; a--no-limited-apiextension that imports a non-stable symbol is flagged.
Design¶
- Capture undefined/imported symbols already available in the snapshot substrate; expose an imported-symbol view on the model if not present.
- Ship a stable-ABI allowlist (the
abi3symbol set per CPython minor; sourced from CPython'sDoc/data/stable_abi.dat, vendored as data, refreshable). - A driver
abicheck scan --binary <ext.so> --abi3 3.9classifies imports; wires through the existing reporter and a newRISK/BREAKINGChangeKind(added per the rootCLAUDE.mdfour-step procedure). - Cross-platform: the same idea applies to
python3.dll(PE imports) and the macOS.sotwo-level-namespace imports.
Files & surfaces¶
abicheck/model.py(imported-symbol view if missing), a newabicheck/stable_abi.py+ vendored allowlist data (abicheck/stable_abi_data.py, refreshed byscripts/gen_stable_abi_data.py), the--abi3audit mode ofabicheck/cli_scan.py(shared enginediff_python.audit_stable_abi_imports),abicheck/checker_policy.py(new kinds), reuse ofabicheck/reporter.py.
Tests¶
- Unit: a synthetic extension importing only
abi3symbols → pass; one importing a non-stable_Py*/version-newer symbol → flagged. - An example pair under
examples/withground_truth.jsonentry.
Out of scope¶
Verifying a wheel's declared tag against its contents end-to-end (packaging concern); non-CPython stable ABIs.